covid-19/sim/js/Model.js

598 lines
13 KiB
JavaScript

/////////////////////////////////////
// UPDATE ///////////////////////////
/////////////////////////////////////
let int = {
non_s: 0,
hygiene: 0,
distancing: 0,
isolate: 0,
quarantine: 0,
masks: 0,
summer: 0,
vaccines: 0
};
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
let r0, re;
//let r0_dom = $('#p_r0');
let s_dom = $('#p_s');
//let re_dom = $('#p_re');
let interventionStrengths = [
['non_s', 1.0],
['hygiene', 0.25],
['distancing', 0.7],
['isolate', 0.4],
['quarantine', 0.5],
['masks', 0.35*0.5], // 3.4 fold reduction (70%) (what CI?), subtract points for... improper usage? https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3591312/ // cloth masks...
['summer', 0.333] // 15°C diff * 0.0225 (Wang et al)
];
let updateModel = (days, fake)=>{
let real_S=S, real_E=E, real_I=I, real_R=R;
// Susceptible & Re
/*if(fake && s_dom.disabled){
s_dom.value = 1 - S;
}*/
if(fake && !s_dom.disabled){
//s_dom.value = 1 - S;
S = 1 - parseFloat(s_dom.value);
}
let transmissionRate = 1/params.p_transmission,
incubationRate = 1/params.p_exposed,
recoveryRate = 1/params.p_recovery,
immunityLossRate = 1/(params.p_waning*365);
// R0
r0 = transmissionRate/recoveryRate;
/*r0_dom.value = transmissionRate/recoveryRate;
if(r0_dom.oninput) r0_dom.oninput();*/
// Transmission affected by interventions
int.non_s = 1 - S;
int.hygiene = params.p_hygiene;
int.distancing = params.p_distancing;
int.isolate = params.p_isolate;
int.quarantine = params.p_quarantine;
int.masks = params.p_masks;
int.summer = (1 - Math.cos((daysCurrent-30)/365 * Math.TAU))/2;
int.summer *= params.p_summer;
interventionStrengths.forEach((isPair,i)=>{
if(i==0) return; // DON'T double count S
transmissionRate *= 1 - (int[isPair[0]] * isPair[1]);
});
// Vaccination...
if(S > 1-params.p_vaccines){
let newlyVaccinated = S - (1-params.p_vaccines);
S -= newlyVaccinated;
R += newlyVaccinated;
}
int.vaccines = params.p_vaccines;
// S...
if(!s_dom.disabled){
S = 1 - parseFloat(s_dom.value);
}
// Update Model
let newlyExposed;
if(params.EXPONENTIAL){
newlyExposed = I*transmissionRate*days;
}else{
newlyExposed = S*I*transmissionRate*days;
}
let newlyInfectious = params.c_exposed ? (E*incubationRate*days) : 0;
let newlyRecovered = params.c_recovery ? (I*recoveryRate*days) : 0;
let newlySusceptible = params.c_waning ? (R*immunityLossRate*days) : 0;
S -= newlyExposed;
E += newlyExposed;
if(params.c_exposed){
E -= newlyInfectious;
I += newlyInfectious;
}else{
I += E; // instant transfer
E = 0;
}
I -= newlyRecovered;
R += newlyRecovered;
R -= newlySusceptible;
S += newlySusceptible;
// bounds
if(S<0) S=0;
if(I>1) I=1;
// Susceptible & Re
if((!fake || params.FROZEN_IN_TIME) && s_dom.disabled){
s_dom.value = 1 - S;
}
re = newlyExposed/newlyRecovered;
/*re_dom.value = newlyExposed/newlyRecovered;
if(re_dom.oninput) re_dom.oninput();*/
// IF FAKE, UNDO EVERYTHING
if(fake){
S = real_S;
E = real_E;
I = real_I;
R = real_R;
}
}
/////////////////////////////////////
// DRAW /////////////////////////////
/////////////////////////////////////
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 interventionColors = [
['non_s', '#bbbbbb'],
['hygiene', '#40AEFF', 0.1],
['distancing', '#405CFF', 0.2],
['isolate', '#8FD68A', 0.2],
['quarantine', '#75AD6F', 0.2],
['masks', '#9240FF', 0.2],
['summer', '#FF8142', 0.3],
['vaccines', '#FFDF40', 0.6],
];
// SUPER HACK SLIDER COLORS
// I hate browsers (thx https://stackoverflow.com/a/13348618 )
let isThisFrikkinChrome = false;
{
var isChromium = window.chrome;
var winNav = window.navigator;
var vendorName = winNav.vendor;
var isOpera = typeof window.opr !== "undefined";
var isIEedge = winNav.userAgent.indexOf("Edge") > -1;
var isIOSChrome = winNav.userAgent.match("CriOS");
if (isIOSChrome) {
// is Google Chrome on IOS
isThisFrikkinChrome = true;
} else if(
isChromium !== null &&
typeof isChromium !== "undefined" &&
vendorName === "Google Inc." &&
isOpera === false &&
isIEedge === false
) {
// is Google Chrome
isThisFrikkinChrome = true;
} else {
// not Google Chrome
isThisFrikkinChrome = false;
}
}
let sliderColors = JSON.parse(JSON.stringify(interventionColors));
sliderColors.shift();
sliderColors.push([ 'hospital', '#000' ]);
let hackStyle = '';
sliderColors.forEach((icPair, i)=>{
if(i==0) return;
let [name,color] = icPair;
// Huge thanks to this person https://stackoverflow.com/a/38163892
if(isThisFrikkinChrome){
hackStyle += `
@media screen and (-webkit-min-device-pixel-ratio:0) {
input#p_${name} {
overflow: hidden;
-webkit-appearance: none;
background-color: #dddddd;
}
input#p_${name}::-webkit-slider-runnable-track {
height: 10px;
-webkit-appearance: none;
color: ${color};
margin-top: -1px;
}
input#p_${name}::-webkit-slider-thumb {
width: 10px;
-webkit-appearance: none;
height: 9px;
cursor: ew-resize;
background: ${color};
color: ${color};
border:1px solid rgba(0,0,0,0.5);
position:relative;
top:1px;
cursor:grab;
box-shadow: -250px 0 0 250px;
}
}
`;
}else{
hackStyle += `
input#p_${name}::-moz-range-progress {
background-color: ${color};
}
input#p_${name}::-moz-range-track {
background-color: #dddddd;
}
input#p_${name}::-moz-range-thumb {
background: ${color};
border-color: ${color};
cursor: grab;
}
input#p_${name}::-ms-fill-lower {
background-color: ${color};
}
input#p_${name}::-ms-fill-upper {
background-color: #dddddd;
}
input#p_${name}::-ms-thumb {
background: ${color};
border-color: ${color};
cursor: grab;
}
`;
}
});
let hackStyleDOM = document.createElement('style');
hackStyleDOM.innerHTML = hackStyle;
document.head.appendChild(hackStyleDOM);
let _isItPastHerd = false;
let label_p_r0 = $('#label_p_r0');
let canvas_r0 = $('#canvas_r0');
let label_p_re = $('#label_p_re');
let canvas_re = $('#canvas_re');
canvas_r0.width = 540;
canvas_r0.height = 40;
canvas_r0.style.width = (canvas_r0.width/2)+"px";
canvas_r0.style.height = (canvas_r0.height/2)+"px";
canvas_re.width = 540;
canvas_re.height = 40;
canvas_re.style.width = (canvas_re.width/2)+"px";
canvas_re.style.height = (canvas_re.height/2)+"px";
let updateRBar = (label, canvas, number, THIS_IS_RE)=>{
label.innerHTML = number.toFixed(2);
let ctx = canvas.getContext('2d');
ctx.scale(2,2);
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.fillStyle = "#ff4040";
ctx.fillRect(0, 3, 250*number/3, 14);
if(THIS_IS_RE && re<r0-0.01){
// interventionColors & interventionStrengths
let diff = r0-re;
/*
let rReductions = [];
let fakeR = r0;
for(let i=0; i<interventionStrengths.length; i++){
let isPair = interventionStrengths[i],
name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
let newFakeR = fakeR*(1 - myStrength);
rReductions.push(fakeR - newFakeR);
fakeR = newFakeR;
}
*/
let interventionsAdded = interventionStrengths.reduce((sum, isPair)=>{
let name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
return sum+myStrength;
},0);
let interventionsRelative = interventionStrengths.map((isPair)=>{
let name = isPair[0],
maxStrength = isPair[1],
myStrength = int[name] * maxStrength;
return myStrength/interventionsAdded;
});
// Go from right to left
ctx.translate( (250/3)*r0, 10 );
interventionColors.forEach((icPair, i)=>{
let name = icPair[0],
color = icPair[1],
myDiff = interventionsRelative[i] * diff,
dx = -myDiff*(250/3);
if(dx<-8){
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(dx+4, 0);
ctx.lineTo(dx+4+4, -4);
ctx.lineTo(dx+4, 0);
ctx.lineTo(dx+4+4, 4);
ctx.stroke();
}
ctx.translate(dx,0);
});
}
ctx.setTransform(1,0,0,1,0,0);
ctx.scale(2,2);
// Herd Immunity
ctx.fillStyle = "#000";
ctx.fillRect(250*1/3, 0, 1, ctx.canvas.height);
ctx.setTransform(1,0,0,1,0,0);
};
let IS_PLAYING_RECORDING = false;
let recordedHistory = [];
let show_percent_s = $('#show_percent_s'),
show_percent_e = $('#show_percent_e'),
show_percent_i = $('#show_percent_i'),
show_percent_r = $('#show_percent_r'),
herdDOM = $('.herd');
let draw = ()=>{
// Redraw
requestAnimationFrame(draw);
// SUCH A HACK
if(!CURRENT_STAGE) return;
if(CURRENT_STAGE._HACK_MAKE_TIME_KEEP_GOING){
daysTotal = Infinity;
daysCurrent += 1;
S = 0.999;
E = 0;
I = 0.001;
R = 0;
updateModel(1);
updateRBar(label_p_r0, canvas_r0, r0);
updateRBar(label_p_re, canvas_re, re, true);
}
// Update SI
if((IS_PLAYING || INPUTS_WERE_CHANGED) && params._HACK_SHOW_SI_PERCENTS){
let digits = 3; //(params._HACK_SHOW_SI_PERCENTS===true) ? 5 : params._HACK_SHOW_SI_PERCENTS;
show_percent_s.innerHTML = ': '+(S*100).toFixed(digits)+'%';
show_percent_e.innerHTML = ': '+(I*100).toFixed(digits)+'%';
show_percent_i.innerHTML = ': '+(I*100).toFixed(digits)+'%';
show_percent_r.innerHTML = ': '+(R*100).toFixed(digits)+'%';
}
// Update R0 & Re
if(IS_PLAYING || INPUTS_WERE_CHANGED){
updateRBar(label_p_r0, canvas_r0, r0);
updateRBar(label_p_re, canvas_re, re, true);
// Herd Immunity
herdDOM.style.left = (1-(1/r0))*250 + 'px';
}
INPUTS_WERE_CHANGED = false;
// Paused? At the end?
if(!IS_PLAYING) return; // STOP
if(params.FROZEN_IN_TIME) return; // STOP
if(daysCurrent > daysTotal){
IS_PLAYING = false;
_updateButtons();
if(params._HACK_RESET_WHEN_R_100=="ready"){
params._HACK_RESET_WHEN_R_100 = "go";
if(CURRENT_STAGE.SHOW_HAND=="tutorial_1" && handTutorial==1){
if(!HAND_IS_VISIBLE){
showHand('start');
}
}
}
return;
}
// HACK
if(params._HACK_RESET_WHEN_I_100=="ready" && I>=0.999999){
params._HACK_RESET_WHEN_I_100 = "go";
bbDOM.setAttribute('label','reset');
sbDOM.setAttribute('label','');
if(CURRENT_STAGE.SHOW_HAND=="tutorial_0" && handTutorial==1){
if(!HAND_IS_VISIBLE){
setTimeout(()=>{
showHand('start');
},2500);
}
}
if(CURRENT_STAGE.SHOW_HAND=="tutorial_1" && handTutorial==1){
if(!HAND_IS_VISIBLE){
showHand('start');
}
}
}
// Replay History!
if(IS_PLAYING_RECORDING){
let keepFindingNewEntries = true;
while(keepFindingNewEntries){
let record = recordedHistory[0];
if(!record || daysCurrent<record[2]){
// Not its time yet
keepFindingNewEntries = false;
}else{
// It is!
recordedHistory.shift(); // remove first element
// Set that slider
let slider = $('#'+record[0]);
slider.value = record[1];
//DONT_RECORD_HISTORY = true;
slider.oninput();
//DONT_RECORD_HISTORY = false;
}
}
}
/*
if(IS_REPLAYING_HISTORY){
let keepLooping = true;
while(keepLooping){
let record = recordedHistory[0];
if(!record || daysCurrent<record[2]){
keepLooping = false;
}else{
recordedHistory.shift(); // remove first element
let slider = $('#'+record[0]);
slider.value = record[1];
DONT_RECORD_HISTORY = true;
slider.oninput();
DONT_RECORD_HISTORY = false;
}
}
}
*/
// For each new day, draw a new pixel
daysPerFrame = (daysTotal / params.p_speed) / 60; // (days/second) / (frames/second) = (days/frame)
daysCurrent += daysPerFrame;
let timeDelta = params.TIME_DELTA || 1;
while(daysDrawn < daysCurrent && daysDrawn<=daysTotal){
updateModel(timeDelta); // one day
// DRAWING TIME
let ctx = context;
let y=0, h;
let pixelsPerDay = canvas.width/daysTotal;
let w = Math.ceil(pixelsPerDay * timeDelta);
ctx.translate(Math.floor(daysDrawn * pixelsPerDay),0);
// S
h = S * canvas.height;
ctx.fillStyle = "#eeeeee";
ctx.fillRect(0,y,w,h);
// R
y += h;
h = R * canvas.height;
ctx.fillStyle = "#cccccc";
ctx.fillRect(0,y,w,h);
// E
y += h;
h = E * canvas.height;
ctx.fillStyle = "#FF9393";
ctx.fillRect(0,y,w,h);
// I
y += h;
h = I * canvas.height;
ctx.fillStyle = "#ff4040";
ctx.fillRect(0,y,w,h);
// INTERVENTIONS
y = 0;
h = canvas.height;
interventionColors.forEach((ic)=>{
if(ic[0]=="non_s") return; // EXCEPT Non-Susceptibles
ctx.fillStyle = ic[1];
ctx.globalAlpha = int[ic[0]] * ic[2];
ctx.fillRect(0,y,w,h);
ctx.globalAlpha = 1;
});
// ICU bed capacity
// 0.6%
if(params.p_hospital){
y = (1-((params.p_hospital/100)*0.006))*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,w,h);
}
// Herd Immunity
if(params.c_recovery && !params.DO_NOT_SHOW_HERD_IMMUNITY){
let herdImmunity = 1 - (1/r0);
// Dashed
if((daysDrawn/daysTotal)*100 % 1 < 0.5){
y = (1-herdImmunity)*canvas.height;
h = 2;
ctx.fillStyle = "#000000";
ctx.fillRect(0,y,w,h);
}
// Line when it passes!
let isItCurrentlyAboveHerd = S<(1/r0);
if(!_isItPastHerd && isItCurrentlyAboveHerd){
h = canvas.height;
ctx.fillStyle = "rgba(0,0,0,0.2)";
ctx.fillRect(0,0,w,h);
}
_isItPastHerd = isItCurrentlyAboveHerd;
}
// RESET
ctx.setTransform(1,0,0,1,0,0);
daysDrawn += timeDelta;
}
};