chart = function*(options) {
options = options || {};
const w = options.width || width,
height = options.height || w * 0.7,
axisX = options.axisX || "date",
axisY = options.axisY || "confirmed",
groupBy = 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")(
new Date(Date.now() - 24 * 3600 * 1000 * regressionDelay)
);
const series = d3
.rollups(
data.filter(d => d.ymd <= maxDate).filter(d => d.country == "Italy" || d.country == "US" || d.country == "Sweden" || d.country == "Russia" || d.country == "Argentina" || d.country == "India" || d.country == "Brazil"),
v => ({
confirmed:
d3.sum(v.filter(d => d.type === "confirmed"), d => d.total) || 0,
deaths: d3.sum(v.filter(d => d.type === "death"), d => d.total) || 0,
recovered:
d3.sum(v.filter(d => d.type === "recovered"), d => d.total) || 0,
country: v[0].country,
province: v[0].province,
ymd: v[0].ymd,
date: parseDate(v[0].ymd)
}),
d => d[groupBy],
d => d.ymd
)
.reverse();
const x =
axisX === "date"
? d3.scaleTime().domain(d3.extent(data, d => d.date))
: d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisX]))
]),
y = d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
1,
d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisY]))
]);
x.range([120, w - 100]);
y.range([height - 90, 40]);
const zero = .6,
zeroAxis = .4;
const line = d3.line().curve(d3.curveMonotoneY);
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)
]);
if (useRegression)
points = d3.regressionLinear()(
points.slice(
Math.max(0, points.length - 1 - regressionPeriod),
Math.max(0, points.length)
)
);
return line(points);
})
.style("fill", "none")
.style("stroke-width", 1.5)
.style("stroke", d => color(d[1][0][1].country));
const circles = s
.style("fill", d => color(d[1][0][1].country))
.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 => d[0].replace(/Mainland China:/, "").replace(/:$/, ""))
.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(d[1][0][1].country));
labels.call(occlusion);
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(",d"),
logTickFormat = d => (String(d)[0] === "1" ? 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 ${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 ${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 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,
`${d[1][groupBy]} ${d[0]}
c: ${numformat(d[1].confirmed)} d: ${numformat(d[1].deaths)} r: ${numformat(
d[1].recovered
)}`
);
else tooltip.call(callout, null);
});
svg.on("touchend mouseleave", () => tooltip.call(callout, null));
}