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

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more