barebones sim done

This commit is contained in:
Nicky Case 2020-03-31 15:02:59 -04:00
commit 368bd06733
3 changed files with 471 additions and 0 deletions

115
sim.css Normal file
View File

@ -0,0 +1,115 @@
body{
font-family: Helvetica, Arial, sans-serif;
}
#controls{
width:300px;
/*height:500px;*/
float:left;
position: relative;
}
#controls > div{
overflow: hidden;
margin-bottom: 1em;
position: relative;
border-left: 5px solid #ffffff;
padding-left: 10px;
}
#controls > div > div{
width: 150px;
float: left;
position: relative;
}
#controls > div > div.bar{
height:10px;
background: #eeeeee;
position: absolute;
bottom:0; left:150px;
overflow: hidden;
}
#controls > div > div.bar > div{
height: 100%;
float: left;
}
#controls > div > div.bar > div:nth-child(1){
background: #ff4040;
}
#controls > div > div.bar > div:nth-child(2){
background: #cc4040;
}
#controls > div > div.bar > div:nth-child(3){
background: #994040;
}
#controls > div > div.bar > div:nth-child(4){
background: #664040;
}
#controls > div > #r_end{
height: 30px;
}
#controls > #containment_line{
display: block;
width:1px;
height: 100%;
background: #000;
position: absolute;
top: 0;
}
#controls .cap{
background: #fff;
opacity: 0.8;
position: absolute;
bottom: 0;
right: 0;
width: 50px;
height: 20px;
}
#controls > div[locked]{
opacity: 0.2;
pointer-events: none;
}
#graph{
float:left;
margin-left: 1em;
width: 500px;
}
#graphCanvas{
width:500px;
height:500px;
border: 1px solid #eeeeee;
}
#graph > #months{
color: #bbb;
font-size: 10px;
width: 562.5px;
height: 50px;
text-align: right;
position: relative;
left: -70.5px;
top: 2px;
}
#graph > #months > div{
color: #bbb;
font-size: 10px;
width: 62.5px;
float: left;
transform-origin: top right;
transform: rotate(-30deg);
}
#graph > #month_ticks{
width: 562.5px;
height: 5px;
position: relative;
left: -61.5px;
top: -50px;
}
#graph > #month_ticks > div{
width: 61.5px;
height: 100%;
border-right: 1px solid #bbb;
float: left;
}

244
sim.js Normal file
View File

@ -0,0 +1,244 @@
window.$ = (query,el=document)=>{
return el.querySelector(query);
};
window.$all = (query,el=document)=>{
return [...el.querySelectorAll(query)];
};
let canvas = $('#graphCanvas');
let context = canvas.getContext('2d');
canvas.width = 1000;
canvas.height = 1000;
canvas.style.width = (canvas.width/2)+"px";
canvas.style.height = (canvas.height/2)+"px";
let r_0 = $('#r_0');
let slider_susceptible = $('#slider_susceptible');
let r_susceptible = $('#r_susceptible');
let slider_distancing = $('#slider_distancing');
let r_distancing = $('#r_distancing');
let slider_cases = $('#slider_cases');
let r_cases = $('#r_cases');
let slider_contacts = $('#slider_contacts');
let r_contacts = $('#r_contacts');
let slider_vax = $('#slider_vax');
let r_vax = $('#r_vax');
let r_end = $('#r_end');
let r_now = $('#r_now');
let N = 100;
let S,I,R;
let I_FLOOR = 0.013;
//let R0 = [0.8,0.9,0.1,0.2];
let R0 = [0.8*1.25, 0.9*1.25, 0.1*1.25, 0.2*1.25];
let Rt = R0.slice();
let update = (delta)=>{
let R_total = Rt.reduce(add);
let newlyInfected = R_total * I * delta;
S -= newlyInfected;
I += newlyInfected;
let newlyRecovered = 1 * I * delta;
I -= newlyRecovered;
R += newlyRecovered;
if(I<I_FLOOR) I=I_FLOOR; // Floor
//////////////////////////////////////////
// DOMs //////////////////////////////////
//////////////////////////////////////////
// R0
updateBar(r_0, R0);
Rt = R0;
// Susceptible
let change = S/N;
Rt = Rt.map(r => r * change);
slider_susceptible.value = change;
updateBar(r_susceptible, Rt);
// Distancing
change = 1 - parseFloat(slider_distancing.value);
Rt = Rt.map(r => r * change);
updateBar(r_distancing, Rt);
// Cases
change = 1 - parseFloat(slider_cases.value);
Rt[0] = Rt[0] * change;
updateBar(r_cases, Rt);
// Contacts
change = 1 - parseFloat(slider_contacts.value);
Rt[1] = Rt[1] * change;
Rt[2] = Rt[2] * change;
updateBar(r_contacts, Rt);
// Vax
change = 1 - parseFloat(slider_vax.value);
Rt = Rt.map(r => r * change);
updateBar(r_vax, Rt);
// R, current growth
updateBar(r_end, Rt);
// What's the current R?
R_total = Rt.reduce(add);
r_now.innerHTML = R_total.toFixed(2) + "<br>" + ((R_total>1) ? "uncontained" : "contained");
};
let BAR_WIDTH_R = 2.5;
let updateBar = (bar, Rt)=>{
[...bar.children].forEach((sub_bar,i)=>{
let r = Rt[i];
sub_bar.style.width = (r/BAR_WIDTH_R)*150 + "px";
});
};
$('#containment_line').style.left = 150 + (150*(1/BAR_WIDTH_R)) + 5 + "px";
const add = (a,b)=>a+b;
//let x = 0;
let SIM_TIME = 0;
let X_SPEED = 0.5;
let UPDATE_PER_PIXEL = 0.025;
const INTERVENTION_COLORS = [
[slider_distancing, "#71c0ff", 0.5],
[slider_cases, "#70ff70", 0.25],
[slider_contacts, "#b8ff70", 0.25],
[slider_vax, "#ffe770", 0.5],
];
let PHASE = 0;
let draw = ()=>{
// Redraw
requestAnimationFrame(draw);
// Caps
[slider_distancing, slider_cases, slider_contacts, slider_vax].forEach((slider)=>{
let cap = parseFloat(slider.getAttribute("CAP"));
if(parseFloat(slider.value) > cap){
slider.value = cap;
}
});
// SIM TIME: Phase unlock or stop
if(SIM_TIME>canvas.width) return;
if(SIM_TIME>canvas.width * 1/8 && PHASE<1){
PHASE = 1;
$all('.phase_i').forEach((control)=>{
control.removeAttribute('locked');
});
}
if(SIM_TIME>canvas.width * 2/8 && PHASE<2){
PHASE = 2;
$all('.phase_ii').forEach((control)=>{
control.removeAttribute('locked');
});
}
if(SIM_TIME>canvas.width * 7/8 && PHASE<3){
PHASE = 3;
$all('.phase_iii').forEach((control)=>{
control.removeAttribute('locked');
});
}
// Update!
update(UPDATE_PER_PIXEL*X_SPEED);
////////////////////////////////////////
// DRAWWWWWWW //////////////////////////
////////////////////////////////////////
let ctx = context;
let y=0, h;
// S
h = (S/N)*canvas.height;
ctx.fillStyle = "#eeeeee";
ctx.fillRect(0,y,1,h);
// R
y += h;
h = (R/N)*canvas.height;
ctx.fillStyle = "#cccccc";
ctx.fillRect(0,y,1,h);
// I
y += h;
h = (I/N)*canvas.height;
ctx.fillStyle = "#ff4040";
ctx.fillRect(0,y,1,h);
// ICU bed capacity
y = (1-0.02)*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,1,h);
// Interventions
INTERVENTION_COLORS.forEach((ic)=>{
let [slider,color,alpha] = ic;
ctx.globalAlpha = parseFloat(slider.value) * alpha;
ctx.fillStyle = color;
ctx.fillRect(0,0,1,canvas.height);
ctx.globalAlpha = 1;
});
// Next!
ctx.translate(X_SPEED,0);
SIM_TIME += X_SPEED;
};
requestAnimationFrame(draw);
// Caps
[slider_distancing, slider_cases, slider_contacts, slider_vax].forEach((slider)=>{
let cap = parseFloat(slider.getAttribute("CAP"));
let capDOM = document.createElement('div');
capDOM.className = 'cap';
capDOM.style.left = (cap*133)+"px";
capDOM.style.width = ((1-cap)*133)+"px";
slider.parentNode.appendChild(capDOM);
});
///////////////
// RESTART ////
///////////////
const restart = ()=>{
let ctx = context;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0,0,canvas.width,canvas.height);
SIM_TIME = 0;
PHASE = 0;
$all('.phase').forEach((control)=>{
control.setAttribute('locked','yup');
});
S = N;
I = I_FLOOR;
R = 0;
Rt = R0.slice();
slider_susceptible.value = 1;
slider_distancing.value = 0;
slider_cases.value = 0;
slider_contacts.value = 0;
slider_vax.value = 0;
};
$('#restart').onclick = restart;
restart();

112
wip.html Normal file
View File

@ -0,0 +1,112 @@
<link rel="stylesheet" type="text/css" href="sim.css" />
<div id="controls">
<div>
<div>
Growth in the beginning:
<br>
<b>R<sub>0</sub> = 2.50</b>
</div>
<div class='bar' id="r_0">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div>
<div>
Ratio of folks still Susceptible:
<input type="range" id="slider_susceptible" min="0" max="1" step="0.001" disabled>
</div>
<div class='bar' id="r_susceptible">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div class="phase_i phase" locked style="border-color:#71c0ff;">
<div>
Social Distancing:
<input type="range" id="slider_distancing" min="0" max="1" step="0.001" value="0" CAP="0.9">
</div>
<div class='bar' id="r_distancing">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div class="phase_ii phase" locked style="border-color:#70ff70;">
<div>
Cases isolated:
<input type="range" id="slider_cases" min="0" max="1" step="0.001" value="0" CAP="0.9">
</div>
<div class='bar' id="r_cases">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div class="phase_ii phase" locked style="border-color:#b8ff70;">
<div>
Contacts quarantined:
<input type="range" id="slider_contacts" min="0" max="1" step="0.001" value="0" CAP="0.7">
</div>
<div class='bar' id="r_contacts">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div class="phase_iii phase" locked style="border-color:#ffe770;">
<div>
Folks vaccinated:
<input type="range" id="slider_vax" min="0" max="1" step="0.001" value="0" CAP="0.9">
</div>
<div class='bar' id="r_vax">
<div></div><div></div><div></div><div></div>
</div>
</div>
<div>
<div>
Growth right now:
<br>
<b>
R = <span id="r_now">0.1</span>
</b>
</div>
<div class='bar' id="r_end">
<div></div><div></div><div></div><div></div>
</div>
</div>
<span id="containment_line"></span>
<button id="restart">restart</button>
</div>
<div id="graph">
<canvas id="graphCanvas"></canvas>
<div id="months">
<div>dec 2019</div>
<div>mar 2020</div>
<div>jun 2020</div>
<div>sep 2020</div>
<div>dec 2020</div>
<div>mar 2021</div>
<div>jun 2021</div>
<div>sep 2021</div>
<div>dec 2021</div>
</div>
<div id="month_ticks">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<script src="sim.js"></script>