sandbox very modified wow
This commit is contained in:
parent
769ee54b93
commit
fe74ab9f56
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -69,7 +69,7 @@
|
|||
<p><strong>Click "Start" play the simulation! You can then change the "generation time", and see how that changes the simulation:</strong></p>
|
||||
|
||||
<div class="sim">
|
||||
<iframe src="sim?stage=01" width="800" height="520"></iframe>
|
||||
<iframe src="sim?stage=01" width="800" height="540"></iframe>
|
||||
</div>
|
||||
|
||||
<p>Starts small ("it's just a flu"), then explodes ("oh right, flus don't break hospitals in rich countries"). This is the "J-shaped" <strong>exponential growth curve</strong>.</p>
|
||||
|
|
296
sim/index.html
296
sim/index.html
|
@ -11,166 +11,200 @@
|
|||
|
||||
<div id="controls">
|
||||
|
||||
<div id="section_dynamics">
|
||||
<div id="hide_on_first_playthrough">
|
||||
<div id="section_dynamics">
|
||||
|
||||
On <i>average</i>,
|
||||
each <icon i></icon>...
|
||||
<br><br>
|
||||
|
||||
<div id="label_transmission">
|
||||
Infects a <icon s></icon> every <span id="label_p_transmission">N</span> days
|
||||
<br>
|
||||
(when almost everyone's still <icon s></icon>)
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_transmission" min="1" max="28" step="1" value="4">
|
||||
</div>
|
||||
|
||||
<div id="label_c_exposed">
|
||||
<input class="sim_checkbox" type="checkbox" id="c_exposed">
|
||||
Takes <span id="label_p_exposed">N</span> days to go from <icon e></icon> to <icon i></icon>
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_exposed" min="1" max="28" step="1" value="3">
|
||||
</div>
|
||||
|
||||
<div id="label_c_recovery">
|
||||
<input class="sim_checkbox" type="checkbox" id="c_recovery">
|
||||
Recovers <icon r></icon> in <span id="label_p_recovery">N</span> days
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_recovery" min="1" max="28" step="1" value="14">
|
||||
</div>
|
||||
|
||||
<div id="label_c_waning">
|
||||
<input class="sim_checkbox" type="checkbox" id="c_waning">
|
||||
Loses immunity <icon s></icon> in <span id="label_p_waning">N</span> years
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_waning" min="0.5" max="5" step="0.5" value="1">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
On <i>average</i>,
|
||||
each infected person...
|
||||
<br><br>
|
||||
|
||||
<div id="label_transmission">
|
||||
Infects a new person every <span id="label_p_transmission">N</span> days
|
||||
(in the beginning)
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_transmission" min="1" max="28" step="1" value="4">
|
||||
</div>
|
||||
|
||||
<div id="label_c_recovery">
|
||||
<input class="sim_checkbox" type="checkbox" id="c_recovery">
|
||||
Recovers in <span id="label_p_recovery">N</span> days
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_recovery" min="1" max="28" step="1" value="14">
|
||||
</div>
|
||||
|
||||
<div id="label_c_waning">
|
||||
<input class="sim_checkbox" type="checkbox" id="c_waning">
|
||||
Loses immunity in <span id="label_p_waning">N</span> years
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_waning" min="0.5" max="5" step="0.5" value="1">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div id="section_r">
|
||||
|
||||
</div>
|
||||
R0 is <span id="label_p_r0" toFixed="2"></span>
|
||||
<input class="sim_input" type="range" id="p_r0" min="0" max="6" step="0.01" disabled>
|
||||
<br>
|
||||
|
||||
<div id="section_r">
|
||||
<span id="label_s">
|
||||
But R is changed by...
|
||||
<br>
|
||||
Susceptible population
|
||||
<input class="sim_input" type="range" id="p_s" min="0" max="1" step="0.001" value="1" disabled>
|
||||
<br>
|
||||
</span>
|
||||
|
||||
R0 is <span id="label_p_r0" toFixed="2"></span>
|
||||
<input class="sim_input" type="range" id="p_r0" min="0" max="6" step="0.01" disabled>
|
||||
<br>
|
||||
<span id="int_block_0">
|
||||
Personal Hygiene
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_hygiene" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_1">
|
||||
Physical Distancing
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_distancing" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_2">
|
||||
Isolating Cases
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_isolate" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
Quarantining Contacts
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_quarantine" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_3">
|
||||
Deep Cleaning
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_cleaning" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
Face Masks
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_masks" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_4">
|
||||
Summer
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_summer" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_5">
|
||||
Vaccinations
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_vaccines" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
|
||||
<span id="label_s">
|
||||
But R is changed by...
|
||||
<br>
|
||||
Susceptible population
|
||||
<input class="sim_input" type="range" id="p_s" min="0" max="1" step="0.001" disabled>
|
||||
<br>
|
||||
</span>
|
||||
|
||||
<span id="int_block_0">
|
||||
Personal Hygiene
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_hygiene" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_1">
|
||||
Physical Distancing
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_distancing" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_2">
|
||||
Isolating Cases
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_isolate" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
Quarantining Contacts
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_quarantine" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_3">
|
||||
Deep Cleaning
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_cleaning" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
Face Masks
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_masks" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_4">
|
||||
Summer
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_summer" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="int_block_5">
|
||||
Vaccinations
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_vaccines" min="0" max="1" step="0.001" value="0">
|
||||
<br>
|
||||
</span>
|
||||
<span id="label_re">
|
||||
R is now <span id="label_p_re" toFixed="2"></span>
|
||||
<input class="sim_input" type="range" id="p_re" min="0" max="6" step="0.01" disabled>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
<span id="hospital_capacity">
|
||||
Hospital capacity at <span id="label_p_hospital">N</span>%
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_hospital" min="0" max="500" step="1" value="100">
|
||||
</span>
|
||||
|
||||
<span id="label_re">
|
||||
R is now <span id="label_p_re" toFixed="2"></span>
|
||||
<input class="sim_input" type="range" id="p_re" min="0" max="6" step="0.01" disabled>
|
||||
</span>
|
||||
<hr>
|
||||
|
||||
<span id="hospital_capacity">
|
||||
Hospital capacity at <span id="label_p_hospital">N</span>%
|
||||
</div>
|
||||
|
||||
<div id="section_meta">
|
||||
|
||||
Simulate <span id="label_p_years" toFixed="1">N</span> years
|
||||
<span id="section_meta_years">
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_years" min="0.5" max="10" step="0.5" value="2">
|
||||
<br>
|
||||
</span>
|
||||
in <span id="label_p_speed">N</span> seconds
|
||||
<br>
|
||||
<input class="sim_input recordable" type="range" id="p_hospital" min="0" max="500" step="1" value="100">
|
||||
</span>
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="section_meta">
|
||||
|
||||
Simulate <span id="label_p_years">N</span> years...
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_years" min="0.5" max="10" step="0.5" value="2">
|
||||
<br>
|
||||
...in <span id="label_p_speed">N</span> seconds
|
||||
<br>
|
||||
<input class="sim_input" type="range" id="p_speed" min="1" max="60" step="1" value="30">
|
||||
|
||||
<hr>
|
||||
<input class="sim_input" type="range" id="p_speed" min="1" max="60" step="1" value="30">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sim_controls">
|
||||
<button id="start">start/stop</button>
|
||||
<button id="restart">restart</button>
|
||||
<button id="replay">replay recording</button>
|
||||
<div class="big_button">
|
||||
<div id="bb_start">▶ Start</div>
|
||||
<div id="bb_pause">❙❙ Pause</div>
|
||||
<div id="bb_continue">▶ Continue</div>
|
||||
<div id="bb_reset">↺ Reset</div>
|
||||
</div>
|
||||
<div class="small_button">
|
||||
<div id="sb_reset">↺ Reset</div>
|
||||
<div id="sb_replay">↺ Replay what you just did</div>
|
||||
<div id="sb_params">↺ Reset all sliders</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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><span>2020</span></div>
|
||||
<div><span>2021</span></div>
|
||||
-->
|
||||
</div>
|
||||
<div id="legend">
|
||||
|
||||
<icon s></icon> Susceptible<span id="show_percent_s"></span>
|
||||
<span id="label_exposed"><br><icon e></icon> Exposed</span>
|
||||
<br><icon i></icon> Infectious<span id="show_percent_i"></span>
|
||||
<span id="label_removed"><br><icon r></icon> Removed</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span id="label_herd_immunity">
|
||||
- - - Herd Immunity
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span id="label_capacity">
|
||||
––– Healthcare Capacity
|
||||
</span>
|
||||
|
||||
</div>
|
||||
-->
|
||||
<img src="legend.png" width="500" height="500" style="position: absolute;top:0; left:0"></img>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<span id="month_names">
|
||||
<span>jan</span>
|
||||
<span>feb</span>
|
||||
<span>mar</span>
|
||||
<span>apr</span>
|
||||
<span>may</span>
|
||||
<span>jun</span>
|
||||
<span>jul</span>
|
||||
<span>aug</span>
|
||||
<span>sep</span>
|
||||
<span>oct</span>
|
||||
<span>nov</span>
|
||||
<span>dec</span>
|
||||
</span>
|
||||
|
||||
</body>
|
||||
|
||||
<script src="sim.js"></script>
|
164
sim/sim.css
164
sim/sim.css
|
@ -2,12 +2,19 @@ body{
|
|||
margin: 0;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
background: #ddd;
|
||||
}
|
||||
div{
|
||||
-webkit-user-select: none; /* Chrome all / Safari all */
|
||||
-moz-user-select: none; /* Firefox all */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none; /* Likely future */
|
||||
}
|
||||
|
||||
#sandbox{
|
||||
width: 800px;
|
||||
height: 520px;
|
||||
height: 540px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
|
@ -24,6 +31,77 @@ body{
|
|||
float:left;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#sim_controls{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.big_button{
|
||||
font-weight: 100;
|
||||
background: #ff4040;
|
||||
color: #fff;
|
||||
padding:10px;
|
||||
border-radius: 10px;
|
||||
border-bottom: 4px solid rgba(0,0,0,0.2);
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.big_button:hover{
|
||||
background: #ff6060;
|
||||
}
|
||||
.big_button > div{
|
||||
display: none;
|
||||
}
|
||||
.big_button[label='start'] > #bb_start{
|
||||
display: block;
|
||||
}
|
||||
.big_button:not([label='start']){
|
||||
background: #dddddd;
|
||||
color: #555;
|
||||
}
|
||||
.big_button:not([label='start']):hover{
|
||||
background: #dddddd;
|
||||
}
|
||||
.big_button[label='continue'] > #bb_continue{
|
||||
display: block;
|
||||
}
|
||||
.big_button[label='pause'] > #bb_pause{
|
||||
display: block;
|
||||
}
|
||||
.big_button[label='reset'] > #bb_reset{
|
||||
display: block;
|
||||
}
|
||||
.small_button{
|
||||
text-align: right;
|
||||
margin-top: 3px;
|
||||
color: #999;
|
||||
font-size: 17px;
|
||||
font-weight: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
.small_button:hover{
|
||||
color: #aaa;
|
||||
}
|
||||
.small_button > div{
|
||||
display: none;
|
||||
}
|
||||
.small_button[label='reset'] > #sb_reset{
|
||||
display: block;
|
||||
}
|
||||
.small_button[label='replay'] > #sb_replay{
|
||||
display: block;
|
||||
}
|
||||
.small_button[label='params'] > #sb_params{
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr{
|
||||
border:none;
|
||||
border-bottom: 2px dashed #ddd;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
#controls > div{
|
||||
overflow: hidden;
|
||||
|
@ -111,34 +189,66 @@ body{
|
|||
height:500px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#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;
|
||||
width: 500px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 501px;
|
||||
color: #bbb;
|
||||
font-size: 14px;
|
||||
font-weight: 100;
|
||||
}
|
||||
#graph > #month_ticks > div{
|
||||
width: 61.5px;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
border-right: 1px solid #bbb;
|
||||
float: left;
|
||||
border-left: 1px solid #bbb;
|
||||
position: absolute;
|
||||
}
|
||||
#graph > #month_ticks > div > span{
|
||||
position: absolute;
|
||||
left:7px;
|
||||
top:5px;
|
||||
}
|
||||
|
||||
icon{
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
top:0.1em;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
icon[s]{
|
||||
background-image: url(../icons/s.png);
|
||||
}
|
||||
icon[e]{
|
||||
background-image: url(../icons/e.png);
|
||||
}
|
||||
icon[i]{
|
||||
background-image: url(../icons/i.png);
|
||||
}
|
||||
icon[r]{
|
||||
background-image: url(../icons/r.png);
|
||||
}
|
||||
|
||||
#legend{
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
/* font-size: 17px; */
|
||||
font-weight: 100;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
#hide_on_first_playthrough{
|
||||
transition: height 0.5s ease-in-out;
|
||||
overflow: hidden;
|
||||
height:0px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
#month_names{
|
||||
display:none;
|
||||
}
|
528
sim/sim.js
528
sim/sim.js
|
@ -8,61 +8,6 @@ window.$all = (query,el=document)=>{
|
|||
return [...el.querySelectorAll(query)];
|
||||
};
|
||||
|
||||
/////////////////////////////////////
|
||||
// PARAMETERS ///////////////////////
|
||||
/////////////////////////////////////
|
||||
|
||||
let S,I,R;
|
||||
|
||||
let params = {};
|
||||
|
||||
// Sliders
|
||||
let DONT_RECORD_HISTORY = true;
|
||||
$all('.sim_input').forEach((slider)=>{
|
||||
let id = slider.id;
|
||||
let label = $('#label_'+id);
|
||||
let isRecordable = slider.classList.contains('recordable');
|
||||
let onChange = ()=>{
|
||||
|
||||
// Change label & param
|
||||
let val = parseFloat(slider.value);
|
||||
if(label){
|
||||
let digits = parseInt(label.getAttribute('toFixed'));
|
||||
label.innerHTML = digits ? val.toFixed(digits) : val;
|
||||
}
|
||||
params[id] = val;
|
||||
|
||||
// Record history (@ this day)
|
||||
if(isRecordable){
|
||||
if(!DONT_RECORD_HISTORY){
|
||||
recordedHistory.push([id, val, Math.round(daysCurrent)]);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
slider.oninput = onChange;
|
||||
onChange();
|
||||
});
|
||||
DONT_RECORD_HISTORY = false;
|
||||
|
||||
// Checkboxes
|
||||
$all('.sim_checkbox').forEach((checkbox)=>{
|
||||
let id = checkbox.id;
|
||||
let label = $('#label_'+id);
|
||||
let onChange = ()=>{
|
||||
if(label){
|
||||
if(!checkbox.checked){
|
||||
label.setAttribute("strikethrough","yes");
|
||||
}else{
|
||||
label.removeAttribute("strikethrough");
|
||||
}
|
||||
}
|
||||
params[id] = checkbox.checked;
|
||||
};
|
||||
checkbox.oninput = onChange;
|
||||
onChange();
|
||||
});
|
||||
|
||||
/////////////////////////////////////
|
||||
// UPDATE ///////////////////////////
|
||||
/////////////////////////////////////
|
||||
|
@ -78,18 +23,23 @@ let int = {
|
|||
vaccines: 0
|
||||
};
|
||||
|
||||
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
|
||||
let r0_dom = $('#p_r0');
|
||||
let s_dom = $('#p_s');
|
||||
let re_dom = $('#p_re');
|
||||
|
||||
let updateModel = (days, fake)=>{
|
||||
|
||||
let real_S=S, real_I=I, real_R=R;
|
||||
let real_S=S, real_E=E, real_I=I, real_R=R;
|
||||
|
||||
let transmissionRate = 1/params.p_transmission,
|
||||
incubationRate = 1/params.p_exposed,
|
||||
recoveryRate = 1/params.p_recovery,
|
||||
immunityLossRate = 1/(params.p_waning*365);
|
||||
|
||||
// R0
|
||||
let r0_dom = $('#p_r0');
|
||||
r0_dom.value = transmissionRate/recoveryRate;
|
||||
r0_dom.oninput();
|
||||
if(r0_dom.oninput) r0_dom.oninput();
|
||||
|
||||
// Transmission affected by interventions
|
||||
|
||||
|
@ -125,23 +75,32 @@ let updateModel = (days, fake)=>{
|
|||
int.vaccines = params.p_vaccines;
|
||||
|
||||
// S...
|
||||
let s_dom = $('#p_s');
|
||||
if(!s_dom.disabled){
|
||||
S = parseFloat(s_dom.value);
|
||||
}
|
||||
|
||||
// Update Model
|
||||
let newlyInfected;
|
||||
let newlyExposed;
|
||||
if(params.EXPONENTIAL){
|
||||
newlyInfected = I*transmissionRate*days;
|
||||
newlyExposed = I*transmissionRate*days;
|
||||
}else{
|
||||
newlyInfected = S*I*transmissionRate*days;
|
||||
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 -= newlyInfected;
|
||||
I += newlyInfected;
|
||||
S -= newlyExposed;
|
||||
E += newlyExposed;
|
||||
|
||||
if(params.c_exposed){
|
||||
E -= newlyInfectious;
|
||||
I += newlyInfectious;
|
||||
}else{
|
||||
I += E; // instant transfer
|
||||
E = 0;
|
||||
}
|
||||
|
||||
I -= newlyRecovered;
|
||||
R += newlyRecovered;
|
||||
|
@ -157,19 +116,160 @@ let updateModel = (days, fake)=>{
|
|||
if(s_dom.disabled){
|
||||
s_dom.value = S;
|
||||
}
|
||||
let re_dom = $('#p_re');
|
||||
re_dom.value = newlyInfected/newlyRecovered;
|
||||
re_dom.oninput();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// PARAMETERS ///////////////////////
|
||||
/////////////////////////////////////
|
||||
|
||||
let S,E,I,R;
|
||||
|
||||
let params = {};
|
||||
|
||||
// Update Month Ticks!
|
||||
let mn = [...$('#month_names').children].map((dom)=>{
|
||||
return dom.innerHTML;
|
||||
});
|
||||
let updateMonthTicks = ()=>{
|
||||
|
||||
let ticks;
|
||||
|
||||
switch(params.p_years){
|
||||
case 0.5:
|
||||
ticks = [
|
||||
mn[0]+" 2020",
|
||||
mn[1]+" 2020",
|
||||
mn[2]+" 2020",
|
||||
mn[3]+" 2020",
|
||||
mn[4]+" 2020",
|
||||
mn[5]+" 2020",
|
||||
null
|
||||
];
|
||||
break;
|
||||
case 1.0:
|
||||
ticks = [
|
||||
mn[0]+" 2020",
|
||||
mn[3]+" 2020",
|
||||
mn[6]+" 2020",
|
||||
mn[9]+" 2020",
|
||||
null
|
||||
];
|
||||
break;
|
||||
default:
|
||||
ticks = [];
|
||||
for(let time=0; time<=params.p_years; time+=0.5){
|
||||
if(time%1 != 0){
|
||||
ticks.push(null);
|
||||
}else{
|
||||
ticks.push(time+2020);
|
||||
}
|
||||
}
|
||||
ticks[ticks.length-1] = null;
|
||||
break;
|
||||
}
|
||||
|
||||
let monthTicksDOM = $('#month_ticks');
|
||||
monthTicksDOM.innerHTML = '';
|
||||
|
||||
ticks.forEach((tick,i)=>{
|
||||
|
||||
if(i==ticks.length-1 || !tick) return;
|
||||
|
||||
let tickDOM = document.createElement('div');
|
||||
let tickSpanDOM = document.createElement('span');
|
||||
tickSpanDOM.innerHTML = tick;
|
||||
tickDOM.style.left = ((i/(ticks.length-1))*500)+"px";
|
||||
tickDOM.appendChild(tickSpanDOM);
|
||||
monthTicksDOM.appendChild(tickDOM);
|
||||
|
||||
});
|
||||
|
||||
console.log(JSON.stringify(ticks));
|
||||
|
||||
}
|
||||
|
||||
// Sliders
|
||||
let DONT_RECORD_HISTORY = true;
|
||||
let _DO_NOT_RECURSE = true;
|
||||
let yearsSlider = $('#p_years');
|
||||
$all('.sim_input').forEach((slider)=>{
|
||||
let id = slider.id;
|
||||
let label = $('#label_'+id);
|
||||
let isRecordable = slider.classList.contains('recordable');
|
||||
let onChange = ()=>{
|
||||
|
||||
// Change label & param
|
||||
let val = parseFloat(slider.value);
|
||||
if(label){
|
||||
let digits = parseInt(label.getAttribute('toFixed'));
|
||||
label.innerHTML = digits ? val.toFixed(digits) : val;
|
||||
}
|
||||
params[id] = val;
|
||||
|
||||
// Record history (@ this day)
|
||||
if(isRecordable){
|
||||
if(!DONT_RECORD_HISTORY){
|
||||
recordedHistory.push([id, val, Math.round(daysCurrent)]);
|
||||
}
|
||||
}
|
||||
|
||||
// Just to update other random crap
|
||||
if(!_DO_NOT_RECURSE){
|
||||
_DO_NOT_RECURSE = true;
|
||||
updateModel(1, true); // one day
|
||||
_DO_NOT_RECURSE = false;
|
||||
}
|
||||
|
||||
// HACK: If this is years, RESET SIM & CHANGE THE MONTHS
|
||||
if(slider==yearsSlider){
|
||||
if(daysCurrent>0){
|
||||
_resetTheSim();
|
||||
_updateButtons();
|
||||
}
|
||||
updateMonthTicks();
|
||||
}
|
||||
|
||||
// MORE HAX
|
||||
if(daysCurrent==0){
|
||||
sbDOM.setAttribute('label','params');
|
||||
}
|
||||
|
||||
};
|
||||
slider.oninput = onChange;
|
||||
onChange();
|
||||
});
|
||||
DONT_RECORD_HISTORY = false;
|
||||
_DO_NOT_RECURSE = false;
|
||||
|
||||
// Checkboxes
|
||||
$all('.sim_checkbox').forEach((checkbox)=>{
|
||||
let id = checkbox.id;
|
||||
let label = $('#label_'+id);
|
||||
let onChange = ()=>{
|
||||
if(label){
|
||||
if(!checkbox.checked){
|
||||
label.setAttribute("strikethrough","yes");
|
||||
}else{
|
||||
label.removeAttribute("strikethrough");
|
||||
}
|
||||
}
|
||||
params[id] = checkbox.checked;
|
||||
};
|
||||
checkbox.oninput = onChange;
|
||||
onChange();
|
||||
});
|
||||
|
||||
/////////////////////////////////////
|
||||
// DRAW /////////////////////////////
|
||||
/////////////////////////////////////
|
||||
|
@ -181,7 +281,6 @@ canvas.height = 1000;
|
|||
canvas.style.width = (canvas.width/2)+"px";
|
||||
canvas.style.height = (canvas.height/2)+"px";
|
||||
|
||||
let daysCurrent, daysDrawn, daysTotal, daysPerFrame;
|
||||
|
||||
let interventionColors = [
|
||||
['hygiene', 'hsl(230,100%,63%)', 0.1],
|
||||
|
@ -198,12 +297,28 @@ let draw = ()=>{
|
|||
|
||||
// Redraw
|
||||
requestAnimationFrame(draw);
|
||||
updateModel(1, true); // one day
|
||||
|
||||
// Update SI
|
||||
if(params._HACK_SHOW_SI_PERCENTS){
|
||||
$('#show_percent_s').innerHTML = ': '+(S*100).toFixed(5)+'%';
|
||||
$('#show_percent_i').innerHTML = ': '+(I*100).toFixed(5)+'%';
|
||||
}
|
||||
|
||||
// Paused? At the end?
|
||||
if(!IS_PLAYING) return;
|
||||
if(params.FROZEN_IN_TIME) return;
|
||||
if(daysCurrent > daysTotal) return;
|
||||
if(daysCurrent > daysTotal){
|
||||
IS_PLAYING = false;
|
||||
_updateButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
// HACK
|
||||
if(params._HACK_RESET_WHEN_I_100=="ready" && I>=0.99999999){
|
||||
params._HACK_RESET_WHEN_I_100 = "go";
|
||||
bbDOM.setAttribute('label','reset');
|
||||
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
|
||||
}
|
||||
|
||||
// Replay History!
|
||||
if(IS_REPLAYING_HISTORY){
|
||||
|
@ -254,6 +369,12 @@ let draw = ()=>{
|
|||
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;
|
||||
|
@ -271,10 +392,26 @@ let draw = ()=>{
|
|||
});
|
||||
|
||||
// ICU bed capacity
|
||||
y = (1-((params.p_hospital/100)*0.02))*canvas.height;
|
||||
h = 2;
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillRect(0,y,w,h);
|
||||
if(params.p_hospital){
|
||||
y = (1-((params.p_hospital/100)*0.02))*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){
|
||||
if((daysDrawn/daysTotal)*100 % 1 < 0.5){
|
||||
let transmissionRate = 1/params.p_transmission,
|
||||
recoveryRate = 1/params.p_recovery,
|
||||
r0 = transmissionRate/recoveryRate;
|
||||
let herdImmunity = 1 - (1/r0);
|
||||
y = (1-herdImmunity)*canvas.height;
|
||||
h = 2;
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillRect(0,y,w,h);
|
||||
}
|
||||
}
|
||||
|
||||
// RESET
|
||||
ctx.setTransform(1,0,0,1,0,0);
|
||||
|
@ -291,15 +428,12 @@ requestAnimationFrame(draw);
|
|||
/////////////////////////////////////
|
||||
|
||||
let IS_PLAYING = false;
|
||||
let togglePlaying = ()=>{
|
||||
IS_PLAYING = !IS_PLAYING;
|
||||
};
|
||||
$('#start').onclick = togglePlaying;
|
||||
|
||||
let recordedHistory = [];
|
||||
let IS_REPLAYING_HISTORY = false;
|
||||
|
||||
let START_S = 0.999,
|
||||
START_E = 0.001,
|
||||
START_I = 0.001,
|
||||
START_R = 0.000;
|
||||
|
||||
|
@ -307,6 +441,7 @@ let restart = ()=>{
|
|||
|
||||
// Model
|
||||
S = START_S;
|
||||
E = START_E;
|
||||
I = START_I;
|
||||
R = START_R;
|
||||
|
||||
|
@ -320,6 +455,119 @@ let restart = ()=>{
|
|||
ctx.clearRect(0,0,canvas.width,canvas.height);
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
// BIG & SMALL BUTTONS //////////////
|
||||
/////////////////////////////////////
|
||||
|
||||
let bbDOM = $('.big_button');
|
||||
let sbDOM = $('.small_button');
|
||||
|
||||
bbDOM.onclick = ()=>{
|
||||
if(daysCurrent>daysTotal || params._HACK_RESET_WHEN_I_100=="go"){
|
||||
_resetTheSim();
|
||||
params._HACK_RESET_WHEN_I_100 = undefined;
|
||||
}else{
|
||||
if(daysCurrent==0){
|
||||
restart();
|
||||
}
|
||||
IS_PLAYING = !IS_PLAYING;
|
||||
}
|
||||
_updateButtons();
|
||||
};
|
||||
|
||||
let defaultParams = [
|
||||
["p_transmission", 4],
|
||||
["p_exposed", 3],
|
||||
["p_recovery", 14],
|
||||
["p_waning", 1],
|
||||
["p_hospital", 100],
|
||||
["p_years", 2],
|
||||
["p_speed", 30],
|
||||
];
|
||||
|
||||
sbDOM.onclick = ()=>{
|
||||
if(daysCurrent==0){
|
||||
|
||||
changeSliders(defaultParams);
|
||||
changeSliders(CURRENT_STAGE.inputs);
|
||||
|
||||
}else if(daysCurrent>daysTotal){
|
||||
_replayTheSim();
|
||||
}else{
|
||||
_resetTheSim();
|
||||
}
|
||||
_updateButtons();
|
||||
};
|
||||
|
||||
let _updateButtons = ()=>{
|
||||
|
||||
if(daysCurrent > daysTotal){
|
||||
|
||||
bbDOM.setAttribute('label','reset');
|
||||
sbDOM.setAttribute('label',params.CANNOT_REPLAY_HISTORY ? '' : 'replay');
|
||||
|
||||
}else if(IS_PLAYING){
|
||||
|
||||
bbDOM.setAttribute('label','pause');
|
||||
sbDOM.setAttribute('label','reset');
|
||||
|
||||
}else{
|
||||
|
||||
if(daysCurrent==0){
|
||||
bbDOM.setAttribute('label','start');
|
||||
sbDOM.setAttribute('label','NONE');
|
||||
}else{
|
||||
bbDOM.setAttribute('label','continue');
|
||||
sbDOM.setAttribute('label','reset');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let hofp = $('#hide_on_first_playthrough');
|
||||
let _showAllControls = ()=>{
|
||||
|
||||
let originalHeight = hofp.getBoundingClientRect().height;
|
||||
if(originalHeight>0) return;
|
||||
|
||||
hofp.style.height = "auto";
|
||||
hofp.style.position = "absolute";
|
||||
hofp.style.top = "-1000px";
|
||||
setTimeout(()=>{
|
||||
let newHeight = hofp.getBoundingClientRect().height;
|
||||
hofp.style.position = "";
|
||||
hofp.style.top = "";
|
||||
hofp.style.height = originalHeight+"px";
|
||||
setTimeout(()=>{
|
||||
hofp.style.marginTop = "";
|
||||
hofp.style.height = newHeight+"px";
|
||||
},20);
|
||||
},20);
|
||||
};
|
||||
let _hideAllControls = ()=>{
|
||||
hofp.style.height = "0px";
|
||||
};
|
||||
|
||||
let _resetTheSim = ()=>{
|
||||
_showAllControls();
|
||||
IS_REPLAYING_HISTORY = false;
|
||||
recordedHistory = [];
|
||||
restart();
|
||||
IS_PLAYING = false;
|
||||
};
|
||||
let _replayTheSim = ()=>{
|
||||
_hideAllControls();
|
||||
IS_REPLAYING_HISTORY = true;
|
||||
restart();
|
||||
IS_PLAYING = true;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
$('#restart').onclick = ()=>{
|
||||
IS_REPLAYING_HISTORY = false;
|
||||
recordedHistory = [];
|
||||
|
@ -329,6 +577,7 @@ $('#replay').onclick = ()=>{
|
|||
IS_REPLAYING_HISTORY = true;
|
||||
restart();
|
||||
};
|
||||
*/
|
||||
|
||||
/////////////////////////////////////
|
||||
// SIM STAGES ///////////////////////
|
||||
|
@ -361,34 +610,71 @@ SB Full Sandbox
|
|||
|
||||
const STAGES = {
|
||||
|
||||
"00": {
|
||||
hide: [
|
||||
"section_dynamics",
|
||||
"c_recovery","label_c_waning","section_meta_years","c_exposed",
|
||||
"int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity"
|
||||
],
|
||||
inputs: [
|
||||
["p_transmission",4],
|
||||
["p_years",1],
|
||||
["p_speed",5],
|
||||
["TIME_DELTA", 0.5],
|
||||
//["EXPONENTIAL",true],
|
||||
["CANNOT_REPLAY_HISTORY", true]
|
||||
],
|
||||
checkboxes: [
|
||||
["c_recovery", true],
|
||||
["c_exposed",true]
|
||||
],
|
||||
},
|
||||
|
||||
//////////////////////////////////////////
|
||||
// THE SIMULATION ////////////////////////
|
||||
//////////////////////////////////////////
|
||||
|
||||
"01": {
|
||||
hide: ["section_r","section_meta","label_c_recovery","label_c_waning"],
|
||||
hide: [
|
||||
"section_r","label_c_recovery","label_c_waning","section_meta_years","label_c_exposed",
|
||||
"label_exposed","label_removed",
|
||||
"label_herd_immunity","label_capacity"
|
||||
],
|
||||
inputs: [
|
||||
["p_years",0.5],
|
||||
["p_speed",10],
|
||||
["p_speed",20],
|
||||
["p_hospital", 0],
|
||||
["TIME_DELTA", 0.1],
|
||||
["EXPONENTIAL",true]
|
||||
]
|
||||
["EXPONENTIAL",true],
|
||||
["CANNOT_REPLAY_HISTORY", true],
|
||||
["_HACK_RESET_WHEN_I_100","ready"],
|
||||
["_HACK_SHOW_SI_PERCENTS",true]
|
||||
],
|
||||
SIR: [0.9999999,0.0000001,0]
|
||||
},
|
||||
|
||||
"02": {
|
||||
hide: ["section_r","section_meta","label_c_recovery","label_c_waning"],
|
||||
hide: [
|
||||
"section_r","section_meta","label_c_recovery","label_c_waning",
|
||||
"label_exposed","label_removed",
|
||||
"label_herd_immunity","label_capacity"
|
||||
],
|
||||
inputs: [
|
||||
["p_years",0.5],
|
||||
["p_speed",10],
|
||||
["p_speed",20],
|
||||
["TIME_DELTA", 0.1],
|
||||
]
|
||||
["p_hospital", 0],
|
||||
["_HACK_RESET_WHEN_I_100","ready"],
|
||||
["_HACK_SHOW_SI_PERCENTS",true]
|
||||
],
|
||||
SIR: [0.9999999,0.0000001,0]
|
||||
},
|
||||
|
||||
"03": {
|
||||
hide: ["section_r","section_meta","label_transmission","label_c_waning","c_recovery"],
|
||||
inputs: [
|
||||
["p_years",0.5],
|
||||
["p_speed",10],
|
||||
["p_speed",20],
|
||||
["TIME_DELTA", 0.1],
|
||||
],
|
||||
checkboxes: [
|
||||
|
@ -398,15 +684,25 @@ const STAGES = {
|
|||
},
|
||||
|
||||
"04": {
|
||||
hide: ["section_r","section_meta","label_c_waning","c_recovery"],
|
||||
hide: [
|
||||
"section_r","label_c_waning","c_recovery","label_c_exposed","section_meta_years",
|
||||
//"section_meta",
|
||||
"label_exposed",
|
||||
"label_herd_immunity","label_capacity"
|
||||
],
|
||||
inputs: [
|
||||
["p_years",0.5],
|
||||
["p_speed",10],
|
||||
["TIME_DELTA", 0.1],
|
||||
["p_speed",20],
|
||||
["p_hospital", 0],
|
||||
//["TIME_DELTA", 0.1],
|
||||
["CANNOT_REPLAY_HISTORY", true],
|
||||
["DO_NOT_SHOW_HERD_IMMUNITY", true]
|
||||
],
|
||||
checkboxes: [
|
||||
["c_recovery", true]
|
||||
]
|
||||
],
|
||||
SIR: [0.9999999,0.0000001,0],
|
||||
//SHOW_ALL_AT_START: true,
|
||||
},
|
||||
|
||||
"05a": {
|
||||
|
@ -427,7 +723,7 @@ const STAGES = {
|
|||
|
||||
"05b": {
|
||||
hide: [
|
||||
"section_meta","label_c_waning","c_recovery",
|
||||
"section_meta","label_c_waning","c_recovery","label_c_exposed",
|
||||
"int_block_0","int_block_1","int_block_2","int_block_3","int_block_4","int_block_5","hospital_capacity",
|
||||
"graph",
|
||||
"sim_controls"
|
||||
|
@ -661,9 +957,32 @@ const STAGES = {
|
|||
|
||||
};
|
||||
|
||||
let changeSliders = (idValPair)=>{
|
||||
|
||||
DONT_RECORD_HISTORY = true;
|
||||
_DO_NOT_RECURSE = true;
|
||||
|
||||
idValPair.forEach((idValPair)=>{
|
||||
let [id,val] = idValPair;
|
||||
let slider = $('#'+id);
|
||||
if(slider){
|
||||
slider.value = val;
|
||||
slider.oninput();
|
||||
}
|
||||
params[id] = val;
|
||||
});
|
||||
|
||||
DONT_RECORD_HISTORY = false;
|
||||
_DO_NOT_RECURSE = false;
|
||||
|
||||
};
|
||||
|
||||
let CURRENT_STAGE;
|
||||
|
||||
let setStage = (stageID)=>{
|
||||
|
||||
let stage = STAGES[stageID];
|
||||
CURRENT_STAGE = stage;
|
||||
|
||||
// Hide what
|
||||
stage.hide = stage.hide || [];
|
||||
|
@ -673,17 +992,7 @@ let setStage = (stageID)=>{
|
|||
|
||||
// Sliders
|
||||
stage.inputs = stage.inputs || [];
|
||||
DONT_RECORD_HISTORY = true;
|
||||
stage.inputs.forEach((idValPair)=>{
|
||||
let [id,val] = idValPair;
|
||||
let slider = $('#'+id);
|
||||
if(slider){
|
||||
slider.value = val;
|
||||
slider.oninput();
|
||||
}
|
||||
params[id] = val;
|
||||
});
|
||||
DONT_RECORD_HISTORY = false;
|
||||
changeSliders(stage.inputs);
|
||||
|
||||
// Checkboxes
|
||||
stage.checkboxes = stage.checkboxes || [];
|
||||
|
@ -706,13 +1015,28 @@ let setStage = (stageID)=>{
|
|||
// Start SIR
|
||||
if(stage.SIR){
|
||||
START_S = stage.SIR[0];
|
||||
START_E = 0;
|
||||
START_I = stage.SIR[1];
|
||||
START_R = stage.SIR[2];
|
||||
}
|
||||
|
||||
// Show all?
|
||||
if(stage.SHOW_ALL_AT_START){
|
||||
_showAllControls();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let stageParams = new URLSearchParams(location.search);
|
||||
if(stageParams.has('stage')) setStage(stageParams.get('stage'));
|
||||
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
// INIT! ////////////////////////////
|
||||
/////////////////////////////////////
|
||||
|
||||
restart();
|
||||
s_dom.oninput();
|
||||
_updateButtons();
|
Loading…
Reference in New Issue