|
|
|
// SARS-CoV-2-Viz
|
|
|
|
// Animated COVID case count visualization
|
|
|
|
// Copyright 2022 Edward L. Platt <ed@elplatt.com>
|
|
|
|
|
|
|
|
async function runInTimeout(f) {
|
|
|
|
let p = new Promise((resolve, reject) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
resolve( f() );
|
|
|
|
}, 0);
|
|
|
|
});
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function sleep(ms) {
|
|
|
|
return new Promise((resolve, reject) => setTimeout(resolve, ms));
|
|
|
|
}
|
|
|
|
|
|
|
|
function UI() {
|
|
|
|
|
|
|
|
let frames = [];
|
|
|
|
let currentFrame = 0;
|
|
|
|
let msPerFrame = 500;
|
|
|
|
let playing = false;
|
|
|
|
let onTick = null;
|
|
|
|
let onNormalize = () => null;
|
|
|
|
|
|
|
|
let animate = function () {
|
|
|
|
if (playing) {
|
|
|
|
setTimeout(animate, msPerFrame);
|
|
|
|
}
|
|
|
|
if (onTick) {
|
|
|
|
onTick(currentFrame);
|
|
|
|
}
|
|
|
|
currentFrame = (currentFrame + 7) % frames.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
let node = (
|
|
|
|
document.getElementById('controls-template').content
|
|
|
|
.querySelector('div').cloneNode(true));
|
|
|
|
let controls = node.querySelector('.controls');
|
|
|
|
let normalizeAll = node.querySelector('input.all');
|
|
|
|
let normalizeToDate = node.querySelector('input.to-date');
|
|
|
|
let play = node.querySelector('button');
|
|
|
|
let display = node.querySelector('.display');
|
|
|
|
let mainDisplay = node.querySelector('.main-display');
|
|
|
|
|
|
|
|
let secondaryDisplays = {};
|
|
|
|
|
|
|
|
play.addEventListener(
|
|
|
|
"click", () => {
|
|
|
|
if (playing) {
|
|
|
|
play.innerText = 'play';
|
|
|
|
playing = false;
|
|
|
|
} else {
|
|
|
|
play.innerText = 'pause';
|
|
|
|
playing = true;
|
|
|
|
animate();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let onNormalizeChange = () => {
|
|
|
|
if (normalizeAll.checked) {
|
|
|
|
onNormalize('all');
|
|
|
|
} else {
|
|
|
|
onNormalize('toDate');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
normalizeAll.addEventListener('change', onNormalizeChange);
|
|
|
|
normalizeToDate.addEventListener('change', onNormalizeChange);
|
|
|
|
|
|
|
|
return {
|
|
|
|
node: node,
|
|
|
|
display: (s, id=null) => {
|
|
|
|
if (id === null) {
|
|
|
|
mainDisplay.textContent = s;
|
|
|
|
} else {
|
|
|
|
secondaryDisplays[id].textContent = s;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
addDisplay: async (id) => {
|
|
|
|
let secondary = document.createElement("div");
|
|
|
|
secondary.id = `display-${id}`;
|
|
|
|
display.appendChild(secondary);
|
|
|
|
secondaryDisplays[id] = secondary;
|
|
|
|
},
|
|
|
|
showControls: (show=true) => {
|
|
|
|
if (show) {
|
|
|
|
controls.classList.remove("hidden");
|
|
|
|
} else {
|
|
|
|
controls.classList.add("hidden");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
clearDisplays: () => {
|
|
|
|
mainDisplay.innerText = '';
|
|
|
|
for (const [id, d] of Object.entries(secondaryDisplays)) {
|
|
|
|
d.remove();
|
|
|
|
}
|
|
|
|
secondaryDisplays = {};
|
|
|
|
},
|
|
|
|
onTick: (f) => { onTick = f; },
|
|
|
|
setFrames: (newFrames, current) => {
|
|
|
|
frames = newFrames;
|
|
|
|
currentFrame = current;
|
|
|
|
},
|
|
|
|
onNormalize: (f) => { onNormalize = f; },
|
|
|
|
setMsPerFrame: (newMsPerFrame) => { msPerFrame = newMsPerFrame; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|