Published
Edited
Mar 31, 2020
Insert cell
md`# US Covid-19`
Insert cell
groupBy = 'total'
Insert cell
cumulative = true
Insert cell
axisX = 'date'
Insert cell
axisY = 'cases'
Insert cell
logScale = false
Insert cell
showPoints = true
Insert cell
filter = d => d.state === 'New York' && d.county === 'Saratoga'
Insert cell
chart = {
const zero = 0.6;
const zeroAxis = 0.4;
const svg = d3
.create('svg')
.attr('class', 'chart')
.attr('viewBox', [0, 0, width, height]);

yield svg.node();
const line = d3.line().curve(cumulative ? d3.curveMonotoneY : d3.curveLinear);

const seriesGroups = svg
.selectAll('g')
.data(series)
.join('g');
const lines = seriesGroups
.append('path')
.attr('d', d => {
let points = d[1].map(d => [
x(d[1][axisX] || zero),
y(d[1][axisY] || zero)
]);
return line(points);
})
.style('fill', 'none')
.style('stroke-width', 1.5)
.style('stroke', d => color(groupBy === 'total' ? 1 : d[1][0][1][groupBy]));

const circles = seriesGroups
.style('fill', d => color(groupBy === 'total' ? 1 : d[1][0][1][groupBy]))
.selectAll('circle')
.data(d => (showPoints ? d[1] : []))
.join('circle');
circles
.attr('r', 2.5)
.attr('cx', d => x(d[1][axisX]))
.attr('cy', d => y(d[1][axisY]));
const axis = svg.append('g');
const axisB = d3.axisBottom(x),
axisL = d3.axisLeft(y);

const formatN = d3.format(","),
logTickFormat = d => ((Math.log10(d) - Math.floor(Math.log10(d))) === 0 ? formatN(d) : "");
if (logScale) {
axisL.tickFormat(logTickFormat);
if (axisX !== "date") axisB.tickFormat(logTickFormat);
}

axis
.append('g')
.call(axisB)
.attr('transform', `translate(0, ${y(zeroAxis)})`)
.append('text')
.text(axisX === "date" ? axisX : `${cumulative ? 'cumulative' : 'daily'} ${axisX}`)
.attr('fill', `currentColor`)
.attr('text-anchor', 'end')
.attr('transform', `translate(${x.range()[1]}, ${-2})`);
axis
.append("g")
.call(axisL)
.attr('transform', `translate(${x.range()[0]}, 0)`)
.append("text")
.text(`${cumulative ? 'cumulative' : 'daily'} ${axisY}`)
.attr("fill", `currentColor`)
.attr("text-anchor", `start`)
.attr("transform", `translate(${2}, ${y.range()[1]})`);

const tooltip = svg.append('g');
const xy = [];
circles.each(function() {
xy.push([+this.getAttribute("cx"), +this.getAttribute("cy") + 2]);
});
const delaunay = d3.Delaunay.from(xy);
const numformat = d3.format(',d');
svg.on('touchmove mousemove', function() {
const m = d3.mouse(this),
i = delaunay.find(...m),
dist = Math.hypot(m[0] - xy[i][0], m[1] - xy[i][1]),
d = circles.data()[i];
if (dist < 10)
tooltip.attr("transform", `translate(${xy[i]})`).call(
callout,
`${groupBy === 'total' ? 'Total' : d[1][groupBy]}
${d[0]}
cases: ${numformat(d[1].cases)}
deaths: ${numformat(d[1].deaths)}`
);
else tooltip.call(callout, null);
});

svg.on('touchend mouseleave', () => tooltip.call(callout, null));
}
Insert cell
callout = (g, value) => {
if (!value) return g.style("display", "none");
g
.style("display", null)
.style("pointer-events", "none")
.style("font", "10px sans-serif");

const path = g.selectAll("path")
.data([null])
.join("path")
.attr("fill", "white")
.attr("stroke", "black");

const text = g.selectAll("text")
.data([null])
.join("text")
.call(text => text
.selectAll("tspan")
.data((value + "").split(/\n/))
.join("tspan")
.attr("x", 0)
.attr("y", (d, i) => `${i * 1.1}em`)
.style("font-weight", (_, i) => i ? null : "bold")
.text(d => d));

const {x, y, width: w, height: h} = text.node().getBBox();

text.attr("transform", `translate(${-w / 2},${15 - y})`);
path.attr("d", `M${-w / 2 - 10},5H-5l5,-5l5,5H${w / 2 + 10}v${h + 20}h-${w + 20}z`);
}
Insert cell
x = {
let scale;
if (axisX === 'date') {
scale = d3.scaleTime().domain(d3.extent(data, d => d.date))
} else {
scale = numericScale().domain([
1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisX]))
])
}
scale.range([120, width - 100]);
return scale;
}
Insert cell
y = numericScale().domain([
1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisY]))
]).range([height - 90, 40])
Insert cell
color = (
groupBy,
d3.scaleOrdinal(
d3.schemeCategory10.concat(d3.schemeSet2).concat(d3.schemeDark2)
)
)
Insert cell
numericScale = d3[logScale ? 'scaleLog' : 'scaleLinear']
Insert cell
height = width * 0.6
Insert cell
series = d3.rollups(
filteredData,
v => ({
cases: d3.sum(v, d => d.cases),
deaths: d3.sum(v, d => d.deaths),
state: v[0].state,
county: v[0].county,
fips: v[0].fips,
date: v[0].date,
}),
d => groupBy === 'total' ? 'Total' : d[groupBy],
d => d.dateString,
).reverse();
Insert cell
filteredData = data.filter(filter);
Insert cell
data = d3.csv(
'https://raw.githubusercontent.com/nytimes/covid-19-data/master/us-counties.csv',
d => {
d.dateString = d.date;
d.date = parseDate(d.date);
d.cases = +d.cases;
d.deaths = +d.deaths;
return d;
},
)
Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
d3 = require('d3@5', 'd3-array@2', 'd3-delaunay@5')
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more