Last active
April 13, 2020 17:48
-
-
Save femto113/eb1aecac0d711141e64239ccc51b70e4 to your computer and use it in GitHub Desktop.
Plot of COVID-19 cases by state in the style evocative of Joy Division's Unknown Pleasures album cover
This file contains hidden or 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> | |
<style> | |
/* so body text will match the chart */ | |
ul li { font-family: sans-serif; } | |
svg { | |
display: block; | |
margin: 0 auto; | |
} | |
.axis .domain { | |
display: none; | |
} | |
.axis--x text { | |
fill: #999; | |
} | |
.axis--x line { | |
stroke: #aaa; | |
} | |
.axis--activity .tick line { | |
display: none; | |
} | |
.axis--activity text { | |
font-size: 12px; | |
fill: #777; | |
} | |
.axis--activity .tick:nth-child(odd) text { | |
fill: #222; | |
} | |
.line { | |
fill: none; | |
stroke: #fff; | |
} | |
.area { | |
fill: #448cab; | |
} | |
.activity:nth-child(odd) .area { | |
fill: #5ca3c1; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script> | |
var margin = { top: 30, right: 10, bottom: 30, left: 300 }, | |
width = 700 - margin.left - margin.right, | |
height = 600 - margin.top - margin.bottom; | |
// Percent two area charts can overlap | |
var overlap = 0.7; | |
// var formatTime = d3.timeFormat('%I %p'); | |
var formatTime = d3.timeFormat('%b-%d'); | |
var svg = d3.select('body').append('svg') | |
.attr('width', width + margin.left + margin.right) | |
.attr('height', height + margin.top + margin.bottom) | |
.append('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
var x = function(d) { return d.time; }, | |
xScale = d3.scaleTime().range([0, width]), | |
xValue = function(d) { return xScale(x(d)); }, | |
xAxis = d3.axisBottom(xScale).tickFormat(formatTime); | |
var y = function(d) { return d.value; }, | |
yScale = d3.scaleLinear(), | |
yValue = function(d) { return yScale(y(d)); }; | |
var activity = function(d) { return d.key; }, | |
activityScale = d3.scaleBand().range([0, height]), | |
activityValue = function(d) { return activityScale(activity(d)); }, | |
activityAxis = d3.axisLeft(activityScale); | |
var area = d3.area() | |
.x(xValue) | |
.y1(yValue); | |
var line = area.lineY1(); | |
function parseTime(ds) { | |
// date comes in as "yyyymmdd" string | |
let [year, month, day] = [ds.slice(0, 4), ds.slice(4,6), ds.slice(6)].map(s => parseInt(s, 10)); | |
let date = new Date(year, month, day); | |
return d3.timeMinute.offset(date, 0); | |
} | |
// daily state format | |
// | |
// date,state,positive,negative,pending,hospitalizedCurrently,hospitalizedCumulative,inIcuCurrently,inIcuCumulative,onVentilatorCurrently,onVentilatorCumulative,recovered,hash,dateChecked,death,hospitalized,total,totalTestResults,posNeg,fips,deathIncrease,hospitalizedIncrease,negativeIncrease,positiveIncrease,totalTestResultsIncrease | |
// 20200412,AK,272,7766,,,31,,,,,66,5f686eece203e247c3bfb219c664517920d7c141,2020-04-12T20:00:00Z,8,31,8038,8038,8038,02,0,0,291,15,306 | |
function row(d) { | |
return { | |
activity: d.state, | |
time: parseTime(d.date), | |
value: parseInt(d.positiveIncrease || "0", 10) | |
}; | |
} | |
d3.csv('https://covidtracking.com/api/v1/states/daily.csv', row, function(error, dataFlat) { | |
if (error) throw error; | |
dataFlat.forEach(d => d.value = d.value/10); | |
console.log(dataFlat) | |
// Sort by time | |
dataFlat.sort(function(a, b) { return a.time - b.time; }); | |
var data = d3.nest() | |
.key(function(d) { return d.activity; }) | |
.entries(dataFlat); | |
// Sort activities by peak value | |
function peakTime(d) { | |
var i = d3.scan(d.values, function(a, b) { return y(b) - y(a); }); | |
return d.values[i].value; | |
}; | |
function total(d) { | |
return d.values.reduce((s, d) => s + y(d), 0); | |
} | |
data.sort(function(a, b) { return total(b) - total(a); }); | |
data.splice(-(data.length-15)); // keep top 15 | |
xScale.domain(d3.extent(dataFlat, x)); | |
activityScale.domain(data.map(function(d) { return d.key; })); | |
var areaChartHeight = (1 + overlap) * (height / activityScale.domain().length); | |
console.log(areaChartHeight) | |
yScale | |
.domain(d3.extent(dataFlat, y)) | |
.range([areaChartHeight, 0]); | |
area.y0(yScale(0)); | |
svg.append('g').attr('class', 'axis axis--x') | |
.attr('transform', 'translate(0,' + height + ')') | |
.call(xAxis); | |
svg.append('g').attr('class', 'axis axis--activity') | |
.call(activityAxis); | |
var gActivity = svg.append('g').attr('class', 'activities') | |
.selectAll('.activity').data(data) | |
.enter().append('g') | |
.attr('class', function(d) { return 'activity activity--' + d.key; }) | |
.attr('transform', d => 'translate(0,' + (activityValue(d) - activityScale.bandwidth() + 5) + ')') | |
; | |
gActivity.append('path').attr('class', 'area') | |
.datum(function(d) { return d.values; }) | |
.attr('d', area); | |
gActivity.append('path').attr('class', 'line') | |
.datum(function(d) { return d.values; }) | |
.attr('d', line); | |
}); | |
</script> | |
<ul> | |
<li> | |
Data from <a href="https://covidtracking.com/">The COVID Tracking Project</a> | |
</li> | |
<li> | |
Visualized using <a href="https://d3js.org/">D3.js</a> based on a chart design by | |
<a href="https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3">Andrew Mollica</a>. | |
</li> | |
</ul> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment