chart = function*(options) {
options = options || {};
const w = options.width || width,
height = options.height || w * 0.7,
axisX = options.axisX || "date",
axisY = options.axisY || "confirmed",
perMillionX = axisX.indexOf("per million") >= 0,
perMillionY = axisY.indexOf("per million") >= 0,
groupBy = ((perMillionX || perMillionY) && options.groupBy !== "country" && options.groupBy !== "province") ? "country" : (options.groupBy || "country"),
useLog = options.useLog || false;
const svg = d3
.create("svg")
.attr("class", "chart")
.attr("viewBox", [0, 0, w, height]);
yield svg.node();
const maxDate = d3.timeFormat("%Y-%m-%d")(
Date.now()
);
const series = d3
.rollups(
data.filter(d => d.ymd <= maxDate),
v => ({
confirmed:
d3.sum(v.filter(d => d.type === "confirmed"), d => d[valueField]) || 0,
"confirmed per million":
d3.sum(v.filter(d => d.type === `confirmed per million (${groupBy})`), d => d[valueField]) || 0,
"deaths per million":
d3.sum(v.filter(d => d.type === `death per million (${groupBy})`), d => d[valueField]) || 0,
deaths: d3.sum(v.filter(d => d.type === "death"), d => d[valueField]) || 0,
recovered:
d3.sum(v.filter(d => d.type === "recovered"), d => d[valueField]) || 0,
country: v[0].country,
population: v[0].population,
province: v[0].province,
region: v[0].region,
continent: v[0].continent,
ymd: v[0].ymd,
date: parseDate(v[0].ymd)
}),
d => groupBy === 'total' ? 'Total' : d[groupBy],
d => d.ymd
)
.reverse();
const x =
axisX === "date"
? d3.scaleTime().domain(d3.extent(data, d => d.date))
: d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
perMillionX ? 0.001 : 1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisX]))
]),
y = d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
perMillionY ? 0.001 : 1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisY]))
]);
x.range([120, w - 100]);
y.range([height - 90, 40]);
const zero = (perMillionX || perMillionY) ? 0.0006 : .6, // where to put our zeros on a log scale
zeroAxis = (perMillionX || perMillionY) ? 0.0004 : .4; // where to put the axis
const line = d3.line().curve(cumulative ? d3.curveMonotoneY : d3.curveLinear);
const s = svg
.selectAll("g")
.data(series)
.join("g");
const lines = s
.append("path")
.attr("d", d => {
let points = d[1].map(d => [
x(d[1][axisX] || zero),
y(d[1][axisY] || zero)
]);
let transform = useLog ? Math.log10 : d => d;
let pointsRaw = d[1].map(d => [
axisX === "date" ? Math.floor(d[1].date/8.64e7) : transform(d[1][axisX] || zero),
axisY === "date" ? Math.floor(d[1].date/8.64e7) : transform(d[1][axisY] || zero)
]);
if (useRegression)
points = d3.regressionLinear()(
points.slice(
Math.max(0, points.length - 1 - regressionDelay - regressionPeriod),
Math.max(0, points.length -regressionDelay)
)
);
d.regression = d3.regressionLinear()(
pointsRaw.slice(
Math.max(0, pointsRaw.length - 1 - regressionDelay - regressionPeriod),
Math.max(0, pointsRaw.length - regressionDelay)
)
);
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 = s
.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] || zero))
.attr("cy", d => y(d[1][axisY] || zero));
const labels = svg.append("g");
labels
.selectAll("text")
.data(series)
.join("text")
.text(d => {
return `${
d[0].replace(/Mainland China:/, "").replace(/:$/, "")
} ${useRegression ? `${
(axisX === 'date' && useLog) ? (1/d.regression.a).toFixed(3) : d.regression.a.toFixed(3)
} ${
useLog
? axisX === 'date' ? `days per 10x ${axisY}` : `10x ${axisY} per 10x ${axisX}`
: axisX === 'date' ? `${axisY} per day` : `${axisY} per ${axisX}`
}` : ''}`;
})
.attr(
"transform",
d =>
`translate(${x(d[1][d[1].length - 1][1][axisX] || zero)},${y(
d[1][d[1].length - 1][1][axisY] || zero
) - 5})`
)
.attr("text-anchor", "middle")
.attr("dx", -6)
.attr("dy", -4)
.style("fill", d => color(groupBy === 'total' ? 1 : d[1][0][1][groupBy]));
labels.call(occlusion);
// try a few times to move occluded labels down their lines
// gives them a second chance at finding a spot
for (let i = 1; i < 14; i++) {
labels
.selectAll(".occluded")
.classed("occluded", false)
.attr("transform", d => {
const b = d[1].slice(-i)[0];
if (b)
return `translate(${x(b[1][axisX] || zero)},${y(
b[1][axisY] || zero
)})`;
});
labels.call(occlusion);
}
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 (useLog) {
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] - 30}, 0)`)
.append("text")
.text(`${cumulative ? 'cumulative' : 'daily'} ${axisY}`)
.attr("fill", `currentColor`)
.attr("text-anchor", `start`)
.attr("transform", `translate(${2}, ${y.range()[1]})`);
const grid = svg.insert("g", "g");
grid
.append("g")
.call(axisL)
.call(g =>
g
.selectAll(".tick line")
.attr("x2", x.range()[0])
.attr("x1", x.range()[1])
.style("stroke", "#ddd")
.style("stroke-width", .5)
)
.call(g => g.selectAll(".domain,text").remove());
grid
.append("g")
.call(axisB)
.call(g =>
g
.selectAll(".tick line")
.attr("y2", y.range()[0])
.attr("y1", y.range()[1])
.style("stroke", "#ddd")
.style("stroke-width", .5)
)
.call(g => g.selectAll(".domain,text").remove());
const eventLines = svg.append('g');
eventLines
.selectAll('line')
.data(events)
.join('line')
.attr('x1', d => x(d.date))
.attr('x2', d => x(d.date))
.attr('y1', d => y(zeroAxis))
.attr('y2', d => y(y.domain()[1]))
.style('visibility', axisX === 'date' ? 'visible' : 'hidden')
.style('stroke', '#aaa')
.style('stroke-width', 1);
const eventLabels = svg.append('g');
eventLabels
.selectAll('text')
.data(events)
.join('text')
.text(d => d.name)
.style('visibility', axisX === 'date' ? 'visible' : 'hidden')
.attr('x', 4)
.attr('y', -4)
.attr('transform', d => `translate(${x(d.date)},${y(zeroAxis)})rotate(-90)`)
.style('fill', '#aaa');
const tooltip = svg.append("g"),
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]}
c: ${numformat(d[1].confirmed)}
d: ${numformat(d[1].deaths)}
r: ${numformat(d[1].recovered)}
cpm: ${numformat(d[1]['confirmed per million'])}
dpm: ${numformat(d[1]['deaths per million'])}`
);
else tooltip.call(callout, null);
});
svg.on("touchend mouseleave", () => tooltip.call(callout, null));
}