Published
Edited
May 28, 2020
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart({ axisX, axisY, groupBy, useLog })
Insert cell
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"),
//data.filter(d => d.ymd <= maxDate),
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, // where to put our zeros on a log scale
zeroAxis = .4; // where to put the axis

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);
// 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(",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));
}
Insert cell
Insert cell
chart()
Insert cell
Insert cell
Insert cell
Insert cell
viewof axisY = select({
options: ["confirmed", "deaths", "recovered"],
value: "confirmed",
description: "Y dimension"
})
Insert cell
viewof axisX = select({
options: ["date", "confirmed", "deaths", "recovered"],
value: "date",
description: "X dimension"
})
Insert cell
viewof groupBy = select({
options: ["country", "province"],
description: "group by"
})
Insert cell
viewof useLog = checkbox({
options: ["Use log scales"],
value: ["Use log scales"]
})
Insert cell
viewof showPoints= checkbox({
options: ["Show points"],
value: ["Show points"]
})
Insert cell
viewof useRegression = checkbox({
options: ["Trend lines"],
value: []
})
Insert cell
viewof regressionPeriod = slider({
min: 3,
max: 20,
value: 0,
step: 1,
description: "number of days past"
})
Insert cell
viewof regressionDelay = Scrubber(d3.range(21), {
autoplay: false,
loop: false,
alternate: true,
delay: 700,
format: () => "delay"
})
Insert cell
Insert cell
color = (groupBy,
d3.scaleOrdinal(
d3.schemeCategory10.concat(d3.schemeAccent).concat(d3.schemeDark2)
))
Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
d3 = require("d3@5", "d3-array@2", "d3-delaunay@5", "d3-regression")
Insert cell
import { checkbox, select, slider } from "@jashkenas/inputs"
Insert cell
import { occlusion } with { d3 } from "@fil/occlusion"
Insert cell
html`<style>
svg.chart { overflow: visible; }
svg.chart text.occluded { display: none; }
svg.chart text:not(.occluded) { text-shadow: 1px 1px 1px #fff; pointer-events: none; font-family: sans-serif; }
</style>`
Insert cell
import { callout } from "@d3/line-chart-with-tooltip"
Insert cell
import { Scrubber } from "@mbostock/scrubber"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more