Skip to content

Instantly share code, notes, and snippets.

@MaxMotovilov
Last active March 30, 2020 04:36
Show Gist options
  • Save MaxMotovilov/5be98a8d63a9e1a35bda4e074de10cdb to your computer and use it in GitHub Desktop.
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)
<!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&emsp;<span>⨉</span> % hospitalized&emsp;<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>
"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`;
}
);
}
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