Public
Edited
Jul 21, 2023
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
viewof con = controls({speed: 50})
Insert cell
con
Insert cell
{
con;
let simulation = this;
if (con.reset) {
simulation = htl.html`<div>`;
simulation.cycle = 0;
} else {
simulation.cycle++;
}
simulation.innerText = `Cycles: ${simulation.cycle}`;
return simulation;
}
Insert cell
Insert cell
viewof conSkip = controls({skip: true, speed: 75})
Insert cell
conSkip
Insert cell
{
conSkip;
const epochLength = 20;
let simulation = this;
if (conSkip.reset) {
simulation = htl.html`<div>`;
simulation.cycle = 0;
} else {
simulation.cycle++;
if (conSkip.skip && (simulation.cycle % epochLength) === 0) {
conSkip.stopSkip();
}
}
simulation.innerText = `Cycles: ${simulation.cycle}, Epoch: ${Math.floor((simulation.cycle - 1) / epochLength + 1)}`;
return simulation;
}
Insert cell
Insert cell
viewof conRandom = controls({speed: 25, random: true})
Insert cell
conRandom
Insert cell
{
conRandom;
let simulation = this;
if (conRandom.reset) {
simulation = htl.html`<div>`;
simulation.cycle = 0;
simulation.values = [];
} else {
simulation.cycle++;
simulation.values.push(conRandom.random());
}
simulation.innerText = `Cycles: ${simulation.cycle}, Values: ${simulation.values}`;
return simulation;
}
Insert cell
Insert cell
viewof conResample = controls({run: false, random: true})
Insert cell
conResample
Insert cell
{
return htl.html`Samples: ${d3.range(5).map(() => { return htl.html`<br>${conResample.random()}`; })}`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
controls = function({running = false, speed = 25, min = 0, max = 1000, run = true, skip = false, random = false} = {}) {
// Layout the controls
const newSeed = 'Generate';
const startLabel = htl.html`<div class=content>
<i class="bi bi-play-fill"></i>
<span>Start</span>
</div>`;
const stopLabel = htl.html`<div class=content>
<i class="bi bi-stop-fill"></i>
<span>Stop</span>
</div>`;
const form = htl.html`<form class="controls ${run ? 'run' : ''} ${skip ? 'skip' : ''}">
${icons.cloneNode(true)}
${controlsStyle.cloneNode(true)}
<div class=container>
<div class=buttons>
<button type=button name=reset value=reset>
${run
? htl.html`<div class=content>
<i class="bi bi-skip-backward-fill"></i>
<span>Reset</span>
</div>`
: htl.html`<div class=content>
<i class="bi bi-arrow-clockwise"></i>
<span>Resample</span>
</div>`}
</button>
${run
? htl.html`<button type=button name=step value=step>
<div class=content>
<i class="bi bi-skip-end-fill" ></i>
<span>Step</span>
</div>
</button>`
: ``
}
${(run && skip)
? htl.html`<button type=button name=skip value=skip>
<div class=content>
<i class="bi bi-skip-forward-fill" ></i>
<span>Skip</span>
</div>
</button>`
: ``
}
${run
? htl.html`<button type=button name=startStop value=startStop>
${startLabel}
</button>`
: ``
}
</div>
${run
? htl.html`<label class=speed>
<div class=content>
<input type=range name=speed min=0 max=100 step=1 value=${speed}>
<span>Speed</span>
</div>
</label>`
: ``
}
${random
? htl.html`<div class=seeds>
<label>
<div class=content>
<select name=nextSeedSelect>
<option value=${newSeed} selected>${newSeed}</option>
</select>
<input type=text name=nextSeed value=${newSeed}>
<span>Next Seed</span>
</div>
</label>
<label>
<div class=content>
<input type=text name=currentSeed readonly>
<span>Current Seed</span>
</div>
</label>
</div>`
: ``
}
</div>
</form>`;
// Initialize state
let state = {
running,
skipping: false,
};
// Stop skip mode function
const stopSkip = () => {
if (state.running && state.skipping) {
form.update({stop: true});
}
};

// Event handlers
form.reset.onclick = (event) => {
form.update({reset: true});
event.stopImmediatePropagation();
}
if (run) {
form.step.onclick = (event) => {
form.update({step: true});
event.stopImmediatePropagation();
}
form.startStop.onclick = (event) => {
if (state.running) {
form.update({stop: true});
} else {
form.update({start: true});
}
event.stopImmediatePropagation();
}
form.speed.oninput = (event) => {
event.stopImmediatePropagation();
}
}
if (run && skip) {
form.skip.onclick = (event) => {
form.update({skip: true});
event.stopImmediatePropagation();
}
}
if (random) {
form.nextSeedSelect.oninput = (event) => {
form.nextSeed.value = form.nextSeedSelect.value;
event.stopImmediatePropagation();
}
form.nextSeed.oninput = (event) => {
event.stopImmediatePropagation();
}
form.currentSeed.onclick = (event) => {
navigator.clipboard.writeText(form.currentSeed.value);
form.currentSeed.nextElementSibling.classList.remove('highlight');
void form.currentSeed.nextElementSibling.offsetWidth;
form.currentSeed.nextElementSibling.classList.add('highlight');
event.stopImmediatePropagation();
}
}

// Random seed handling
const seedScale = 1000000000;
const createSeed = () => {
return Math.floor(Math.random() * seedScale).toString().padStart(9, '0');
}
const createRNG = (seed) => {
return d3.randomLcg(seed / seedScale)
}
// Process input
form.update = (action = {}) => {
// Update random seed
if (random && action.reset) {
form.currentSeed.value = (form.nextSeed.value === newSeed)
? createSeed()
: form.nextSeed.value;

if (!form.nextSeedSelect.querySelector(`[value="${form.currentSeed.value}"]`)) {
form.nextSeedSelect.children[0].after(htl.html`<option value=${form.currentSeed.value}>${form.currentSeed.value}</option>`);
}
}

// Update internal state
state = {
running: (action.start || action.skip)
? true
: action.stop
? false
: state.running,
skipping: action.skip
? true
: (action.start || action.stop)
? false
: state.skipping,
delay: run
? max - (form.speed.valueAsNumber * ((max - min) / 100))
: NaN,
random: random
? action.reset
? createRNG(form.currentSeed.value)
: state.random
: null,
seed: random
? action.reset
? form.currentSeed.value
: state.seed
:null,
};
// Update exposed value
form.value = {
...(run && skip && state.skipping ? {stopSkip} : {}),
...(run && skip ? {skip: state.skipping} : {}),
reset: action.reset
? true
: false,
...(random ? {random: state.random, seed: state.seed} : {}),
};
// Update Start/Stop button
if (action.stop) {
form.startStop.replaceChild(startLabel, stopLabel);
} else if (action.start || action.skip) {
form.startStop.replaceChild(stopLabel, startLabel);
}
// Short circuit the timer on Reset, Step, Skip, Start, and Stop
if (form.timeout && (action.reset || action.step || action.skip || action.start || action.stop)) {
clearTimeout(form.timeout);
form.timeout = null;
}

// Step the simulation on Reset, Step, or when running
if (action.reset || action.step || state.running) {
form.dispatchEvent(new CustomEvent('input', {bubbles: true}));
}

// Keep running
const resetSafety = 20;
if (state.running) {
form.timeout = setTimeout(
form.update,
(action.reset && (state.delay < resetSafety)) ? resetSafety : state.delay,
);
} else {
form.timeout = null;
}
}

// Init
Inputs.disposal(form).then(() => {
clearTimeout(form.timeout);
form.timeout = null;
});
form.update({reset: true});
return form;
}
Insert cell
controlsStyle = htl.html.fragment`<style>
.controls .container {
display: inline-flex;
flex-flow: row wrap;
align-items: center;
}

.controls .buttons {
display: inline-grid;
grid: auto-flow auto / 1fr;
align-items: center;
margin-right: 1px;
}

.controls.run .buttons {
grid: auto-flow auto / 1fr 1fr 1fr;
}

.controls.run.skip .buttons {
grid: auto-flow auto / 1fr 1fr 1fr 1fr;
}

.controls .speed {
margin-right: 1px;
margin-left: 1px;
}

.controls .seeds {
display: inline-flex;
flex-flow: row nowrap;
align-items: center;
margin-left: 1px;
}

.controls button,
.controls label {
font: inherit;
margin: 2px;
}

.controls .content {
display: inline-flex;
flex-flow: column nowrap;
align-items: center;
font: inherit;
}

.controls i {
font-size: x-large;
line-height: 0.8;
}

.controls span {
font-size: small;
line-height: 1.2;
}

.controls input[name="nextSeed"] {
position: absolute;
align-self: flex-start;
width: 9ch;
margin-bottom: 2px;
font-family: var(--monospace);
}

.controls select[name="nextSeedSelect"] {
width: calc(9ch + 1.5rem);
margin-bottom: 2px;
padding: 1px 0;
font-family: var(--monospace);
}

.controls input[name="currentSeed"] {
width: 9ch;
margin-bottom: 2px;
font-family: var(--monospace);
opacity: 0.65;
}

.controls input[name="currentSeed"] + span.highlight {
animation: 0.2s cubic-bezier(0.075, 0.75, 0.75, 0.075) 1 highlight;
}
@keyframes highlight {
from {
background-color: white;
}
50% {
background-color: yellow;
}
to {
background-color: white;
}
}
</style>`
Insert cell
Insert cell
Insert cell
icons = htl.html.fragment`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more