Last active
March 30, 2020 04:36
-
-
Save MaxMotovilov/5be98a8d63a9e1a35bda4e074de10cdb to your computer and use it in GitHub Desktop.
Covid-19 positive cases & hospitalization vs. testing density per 1M (uses covidtracking.com API)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>COVID-19 testing progress based on covidtracking.com</title> | |
<style type="text/css"> | |
body { | |
font-family: sans-serif; | |
} | |
.pane { | |
position: relative; | |
width: 1200px; | |
height: 1200px; | |
} | |
.pane>.legend { | |
position: absolute; | |
left: 200px; | |
} | |
.pane>.legend span { | |
color: blue; | |
} | |
.pane>.legend.bottom { | |
bottom: 80px; | |
} | |
.popup { | |
display: none; | |
position: absolute; | |
width: 440px; | |
height: 440px; | |
left: 250px; | |
bottom: 200px; | |
background-color: #f0f0ff; | |
} | |
body.show .popup { | |
display: block; | |
} | |
svg { | |
position: absolute; | |
} | |
.pane>svg.grid, .pane>svg.data { | |
width: 1000px; | |
height: 1000px; | |
left: 200px; | |
top: 50px; | |
} | |
.pane>svg.grid line.major { | |
stroke: #808080; | |
} | |
.pane>svg.grid line.minor { | |
stroke: #c0c0c0; | |
} | |
.pane>svg.vertical { | |
width: 180px; | |
height: 1100px; | |
top: 6px; | |
} | |
.pane>svg.vertical text { | |
text-anchor: end; | |
} | |
.pane>svg.horizontal { | |
width: 1140px; | |
height: 180px; | |
top: 1000px; | |
left: 95px; | |
} | |
.pane>svg.horizontal text { | |
text-anchor: middle; | |
} | |
.pane>svg.axis text { | |
font-size: 16px; | |
} | |
.pane>svg.data g.trail:hover, .pane>svg.data g.trail.hilite { | |
stroke: blue !important; | |
} | |
.pane>svg.data g.trail polyline { | |
stroke: none; | |
} | |
.pane>svg.data g.trail text { | |
visibility: hidden; | |
font-size: 12px; | |
fill: blue; | |
transform: translate(3px,4px); | |
bottom: 6px; | |
} | |
.pane>svg.data g.trail:hover polyline, .pane>svg.data g.trail.hilite polyline { | |
stroke: blue; | |
} | |
.pane>svg.data g.trail:hover text, .pane>svg.data g.trail.hilite text { | |
visibility: visible; | |
} | |
.pane>svg.map { | |
right: 20px; | |
bottom: 170px; | |
width: 321px; | |
height: 194px; | |
} | |
.pane>svg.map * { | |
cursor: pointer; | |
} | |
.pane>svg.map path:hover { | |
stroke: blue !important; | |
stroke-width: 3 !important; | |
} | |
.popup>div#stateLabel { | |
font-size: 36px; | |
font-weight: bold; | |
color: blue; | |
padding: 20px; | |
} | |
.popup>svg { | |
text-anchor: middle; | |
font-size: 12px; | |
} | |
.popup>svg.axis { | |
width: 400px; | |
height: 400px; | |
top: 20px; | |
left: 20px; | |
} | |
.popup>svg.data { | |
width: 440px; | |
height: 440px; | |
} | |
.popup>svg.axis line { | |
stroke: #808080; | |
} | |
.popup>svg.data g#upper text { | |
transform: translate(0,-4px); | |
} | |
.popup>svg.data g#lower text { | |
transform: translate(0,16px); | |
} | |
.popup svg .tested { | |
stroke: #608060; | |
fill: #80e080; | |
} | |
.popup svg .positive { | |
stroke: #c0c040; | |
fill: #f0f040; | |
} | |
.popup svg .hospitalized { | |
stroke: #b08020; | |
fill: #ffb030; | |
} | |
.popup svg .dead { | |
stroke: #b02010; | |
fill: #f03020; | |
} | |
.popup>.legend { | |
position: absolute; | |
left: 20px; | |
top: 160px; | |
} | |
.popup>.legend>svg { | |
position: absolute; | |
width: 22px; | |
height: 80px; | |
left: 90px; | |
top: 13px; | |
} | |
</style> | |
<script src="./states.js"></script> | |
</head> | |
<body> | |
<div class="pane"> | |
<svg class="vertical axis" viewBox="0 0 180 1000"> | |
<g transform="translate(180 0)"> | |
<text x="0" y="0">100%</text> | |
<text x="0" y="75.26">50%</text> | |
<text x="0" y="174.74">20%</text> | |
<text x="0" y="250">10%</text> | |
<text x="0" y="325.26">5%</text> | |
<text x="0" y="424.74">2%</text> | |
<text x="0" y="500">1%</text> | |
<text x="0" y="575.26">0.5%</text> | |
<text x="0" y="674.74">0.2%</text> | |
<text x="0" y="750">0.1%</text> | |
<text x="0" y="825.26">0.05%</text> | |
<text x="0" y="924.74">0.02%</text> | |
<text x="0" y="1000">0.01%</text> | |
</g> | |
</svg> | |
<svg class="horizontal axis" viewBox="-70 0 1070 180"> | |
<g transform="translate(0 80)"> | |
<text y="0" x="0">1</text> | |
<text y="0" x="200">10</text> | |
<text y="0" x="400">100</text> | |
<text y="0" x="600">1,000</text> | |
<text y="0" x="800">10,000</text> | |
<text y="0" x="1000">100,000</text> | |
</g> | |
</svg> | |
<svg class="grid" viewBox="0 0 1000 1000"> | |
<g stroke-width="0.001px" transform="scale(1000 1000)"> | |
<line class="major" x1="0" y1="0" x2="1" y2="0" /> | |
<line class="minor" x1="0" y1="0.07526" x2="1" y2="0.07526" /> | |
<line class="minor" x1="0" y1="0.17474" x2="1" y2="0.17474" /> | |
<line class="major" x1="0" y1="0.25" x2="1" y2="0.25" /> | |
<line class="minor" x1="0" y1="0.32526" x2="1" y2="0.32526" /> | |
<line class="minor" x1="0" y1="0.42474" x2="1" y2="0.42474" /> | |
<line class="major" x1="0" y1="0.5" x2="1" y2="0.5" /> | |
<line class="minor" x1="0" y1="0.57526" x2="1" y2="0.57526" /> | |
<line class="minor" x1="0" y1="0.67474" x2="1" y2="0.67474" /> | |
<line class="major" x1="0" y1="0.75" x2="1" y2="0.75" /> | |
<line class="minor" x1="0" y1="0.82526" x2="1" y2="0.82526" /> | |
<line class="minor" x1="0" y1="0.92474" x2="1" y2="0.92474" /> | |
<line class="major" x1="0" y1="1" x2="1" y2="1" /> | |
</g> | |
<g stroke-width="0.001px" transform="scale(1000 1000)"> | |
<line class="major" x1="0" y1="0" x2="0" y2="1" /> | |
<line class="major" x1="0.2" y1="0" x2="0.2" y2="1" /> | |
<line class="major" x1="0.4" y1="0" x2="0.4" y2="1" /> | |
<line class="major" x1="0.6" y1="0" x2="0.6" y2="1" /> | |
<line class="major" x1="0.8" y1="0" x2="0.8" y2="1" /> | |
<line class="major" x1="1" y1="0" x2="1" y2="1" /> | |
</g> | |
</svg> | |
<svg class="data" viewBox="0 0 1000 1000"> | |
<defs> | |
<g id="pos-rate"> | |
<line x1="-3" y1="0" x2="3" y2="0" stroke-width="2" /> | |
<line y1="-3" x1="0" y2="3" x2="0" stroke-width="2" /> | |
</g> | |
<g id="hsp-rate"> | |
<line x1="-3" y1="-3" x2="3" y2="3" stroke-width="2" /> | |
<line y1="-3" x1="3" y2="3" x2="-3" stroke-width="2" /> | |
</g> | |
</defs> | |
<g id="data" /> | |
</svg> | |
<svg class="map" id="map" viewBox="0 0 962 583"></svg> | |
<div class="popup"> | |
<svg class="axis" viewBox="0 0 400 400"> | |
<g transform="translate(0 300)"> | |
<line x1="0" x2="400" y1="0 " y2="0" stroke-width="3" /> | |
</g> | |
</svg> | |
<svg class="data" viewBox="-20 -20 440 440"> | |
<g id="upper" transform="translate(0 300)" stroke-width="1" /> | |
<g id="lower" transform="translate(0 300)" stroke-width="1" /> | |
</svg> | |
<div id="stateLabel"></div> | |
<div class="legend"> | |
<svg viewBox="-1 0 22 80"> | |
<rect class="tested" x="0" width="20" y="7" height="7" /> | |
<rect class="positive" x="0" width="20" y="27" height="7" /> | |
<rect class="hospitalized" x="0" width="20" y="47" height="7" /> | |
<rect class="dead" x="0" width="20" y="67" height="7" /> | |
</svg> | |
<div><b>Daily values</b></div> | |
<div>Tested</div> | |
<div>Positive</div> | |
<div>Hospitalized</div> | |
<div>Died</div> | |
</div> | |
</div> | |
<div class="legend top"><span>+</span> % tested positive <span>⨉</span> % hospitalized <span>hover over markers or click on the map to highlight by state</span></div> | |
<div class="legend bottom">Number of people tested per 1,000,000 of the state population</div> | |
</div> | |
<script src="./chart.js"></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
const population = { | |
AL: 4903185, AK: 731545, AZ: 7278717, AR: 3017804, CA: 39512223, CO: 5758736, CT: 3565287, DE: 973764, DC: 705749, FL: 21477737, GA: 10617423, | |
HI: 1415872, ID: 1787065, IL: 12671821, IN: 6732219, IA: 3155070, KS: 2913314, KY: 4467673, LA: 4648794, ME: 1344212, MD: 6045680, MA: 6892503, | |
MI: 9986857, MN: 5639632, MS: 2976149, MO: 6137428, MT: 1068778, NE: 1934408, NV: 3080156, NH: 1359711, NJ: 8882190, NM: 2096829, NY: 19453561, | |
NC: 10488084, ND: 762062, OH: 11689100, OK: 3956971, OR: 4217737, PA: 12801989, RI: 1059361, SC: 5148714, SD: 884659, TN: 6829174, TX: 28995881, | |
UT: 3205958, VT: 623989, VA: 8535519, WA: 7614893, WV: 1792147, WI: 5822434, WY: 578759 | |
} | |
function logScale(lo, hi) { | |
lo = Math.log(lo); | |
hi = Math.log(hi) - lo; | |
return v => Math.round( 1000 * (Math.log(v) - lo) / hi ); | |
} | |
const vScale = logScale(1, 0.0001), hScale = logScale(1, 100000); | |
async function dataset() { | |
const rsp = await fetch('https://covidtracking.com/api/states/daily'); | |
if(!rsp.ok) { | |
console.error(await rsp.text()); | |
throw Error(`${rsp.status} ${rsp.statusText}`); | |
} | |
return rsp.json(); | |
} | |
const highlight = ( | |
(fadeout, highlighted) => state => { | |
if(state && fadeout) { | |
clearTimeout(fadeout); | |
fadeout = null; | |
} | |
if(highlighted!=state) { | |
if(highlighted) { | |
if(state) { | |
fillPopup(state); | |
highlightTrail(highlighted = state); | |
} else if(!fadeout) { | |
fadeout = setTimeout( () => { | |
document.body.className = ''; | |
highlightTrail(fadeout = highlighted = null); | |
}, 2000 ); | |
} | |
} else { | |
fillPopup(state); | |
highlightTrail(highlighted = state); | |
document.body.className = 'show'; | |
} | |
} | |
} | |
)(); | |
async function loadMap() { | |
const | |
rsp = await fetch('https://upload.wikimedia.org/wikipedia/commons/2/2a/Blank_US_Map_With_Labels.svg'), | |
text = await rsp.text(); | |
if(!rsp.ok) { | |
console.error(text); | |
throw Error(`${rsp.status} ${rsp.statusText}`); | |
} | |
const svg = (new DOMParser()).parseFromString(text, 'image/svg+xml'); | |
document.getElementById('map').innerHTML = svg.documentElement.innerHTML; | |
} | |
function loadCss() { | |
const | |
{stateColors} = window, | |
style = document.createElement('style'); | |
style.type = 'text/css'; | |
style.innerHTML = Object.keys(stateColors).map( | |
state => ` | |
.pane>svg.data .${state} { stroke: #${stateColors[state]}; } | |
.pane>svg.map path#${state}, .pane>svg.map path#${state}- { fill: #${stateColors[state]} !important; }` | |
).join(''); | |
document.head.appendChild(style); | |
} | |
loadCss(); | |
let all; | |
dataset().then( ds => { | |
all = ds.reduce( | |
(all, {state, positive, negative, pending, hospitalized, deathIncrease, hospitalizedIncrease, positiveIncrease, totalTestResultsIncrease}) => { | |
const | |
{pos=[], hsp=[], raw=[]} = all[state] || {}, | |
tested = (positive||0) + (negative||0) + (pending||0); | |
if(positive && population[state]) { | |
const rate = hScale( 1000000 * tested / population[state] ); | |
raw.push({ | |
tested: totalTestResultsIncrease, | |
dead: deathIncrease, | |
positive: positiveIncrease, | |
hospitalized: hospitalizedIncrease | |
}); | |
if(rate>=0) { | |
pos.unshift( [rate, vScale(positive/tested)] ); | |
if(hospitalized) | |
hsp.unshift( [rate, vScale(hospitalized/tested)] ); | |
} | |
all[state] = {pos, hsp, raw} | |
} | |
return all; | |
}, {} | |
); | |
document.getElementById('data').innerHTML = Object.keys(all).map( | |
state => `<g class="trail ${state}">` + markers(all[state].pos, 'pos-rate', state) + markers(all[state].hsp, 'hsp-rate', state) + '</g>' | |
).join(''); | |
} ); | |
document.getElementById('data').addEventListener('mouseover', ({target}) => highlight(findParentWithClass(target, /\btrail\s*/))); | |
document.getElementById('data').addEventListener('mouseout', ({target}) => { if(findParentWithClass(target, /\btrail\s*/)) highlight(null); }); | |
loadMap().then( () => { | |
document.getElementById('map').addEventListener('click', ({target}) => highlight(mapStateId(target))); | |
document.getElementById('map').addEventListener('mouseout', ({target: {tagName}}) => { if(tagName.toLowerCase()=='svg') highlight(null); }); | |
} ); | |
function markers(pts, mk, state) { | |
return pts.length ? | |
pts.map(([x,y]) => `<use href="#${mk}" x="${x}" y="${y}" />`).join('') | |
+ `<polyline fill="none" points="${pts.map(xy => xy.join(',')).join(' ')}" />` | |
+ text(pts[pts.length-1], state) | |
: ''; | |
} | |
function text([x, y], str) { | |
return `<text x="${x}" y="${y}">${str}</text>`; | |
} | |
function findParentWithClass(elt, cls_regex) { | |
while(elt && elt.tagName.toLowerCase() != 'svg') | |
if(cls_regex.test(elt.className.baseVal)) | |
return elt.className.baseVal.replace(cls_regex, '').substr(0, 2); | |
else | |
elt = elt.parentNode; | |
return null; | |
} | |
function fillPopup(state) { | |
document.getElementById('stateLabel').innerHTML = state; | |
document.getElementById('upper').innerHTML = barChart(all[state].raw, -300, 'tested', 'positive'); | |
document.getElementById('lower').innerHTML = barChart(all[state].raw, 100, 'hospitalized', 'dead'); | |
} | |
function barChart(data, height, ...fields) { | |
const | |
maxValue = fields.reduce( (sofar, field) => Math.max(sofar, Math.max(...data.map(x => x[field]))), 0 ), | |
width = Math.min( Math.floor(400/data.length), 9 ) - 2; | |
return maxValue ? fields.map( | |
field => data.map( ({[field]: value}, i) => { | |
const h = height * value/maxValue; | |
return `<rect class="${field}" x="${400 - (i+1)*width}" y="${Math.min(0, h)}" width="${width-2}" height="${Math.abs(h)}" />`; | |
} ).join('') | |
).join('') + fields.map( | |
field => { | |
let | |
maxFieldValue = Math.max(...data.map(x => x[field])), | |
h = height * maxFieldValue/maxValue; | |
return maxFieldValue ? data.map( ({[field]: value}, i) => { | |
if(value==maxFieldValue) { | |
maxFieldValue = -1; | |
return `<text x="${400 - i*width - (width+2)/2}" y="${h}">${value}</text>`; | |
} else return ''; | |
} ).join('') : ''; | |
} | |
).join('') : ''; | |
} | |
function mapStateId({id}) { | |
const [, state] = /^([A-Z]{2})-?$/.exec(id) || []; | |
return state; | |
} | |
function highlightTrail(setState) { | |
document.getElementById('data').querySelectorAll('g.trail').forEach( | |
elt => { | |
const [, state, hilite] = /^trail (..)( hilite)?$/.exec(elt.className.baseVal); | |
if(hilite && setState!=state) | |
elt.className.baseVal = `trail ${state}`; | |
else if(!hilite && setState==state) | |
elt.className.baseVal = `trail ${state} hilite`; | |
} | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var stateColors = { | |
"AL": "b0c0e0", | |
"AK": "b0c0e0", | |
"AZ": "b0c0e0", | |
"AR": "b0c0e0", | |
"CA": "f08080", | |
"CO": "b0c0e0", | |
"CT": "f08080", | |
"DE": "b0c0e0", | |
"DC": "b0c0e0", | |
"FL": "b0c0e0", | |
"GA": "b0c0e0", | |
"HI": "b0c0e0", | |
"ID": "b0c0e0", | |
"IL": "f08080", | |
"IN": "b0c0e0", | |
"IA": "b0c0e0", | |
"KS": "b0c0e0", | |
"KY": "b0c0e0", | |
"LA": "f08080", | |
"ME": "b0c0e0", | |
"MD": "b0c0e0", | |
"MA": "b0c0e0", | |
"MI": "f08080", | |
"MN": "b0c0e0", | |
"MS": "b0c0e0", | |
"MO": "b0c0e0", | |
"MT": "b0c0e0", | |
"NE": "b0c0e0", | |
"NV": "b0c0e0", | |
"NH": "b0c0e0", | |
"NJ": "f08080", | |
"NM": "b0c0e0", | |
"NY": "f08080", | |
"NC": "b0c0e0", | |
"ND": "b0c0e0", | |
"OH": "b0c0e0", | |
"OK": "b0c0e0", | |
"OR": "b0c0e0", | |
"PA": "b0c0e0", | |
"RI": "b0c0e0", | |
"SC": "b0c0e0", | |
"SD": "b0c0e0", | |
"TN": "b0c0e0", | |
"TX": "b0c0e0", | |
"UT": "b0c0e0", | |
"VA": "b0c0e0", | |
"VT": "b0c0e0", | |
"WA": "b0c0e0", | |
"WV": "b0c0e0", | |
"WI": "b0c0e0", | |
"WY": "b0c0e0" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment