Published
Edited
Nov 4, 2021
7 forks
Importers
33 stars
Andy's Walgreens COVID-19 Tracker TrackerBelgium Vaccination Tracker - Progress of the vaccination campaignVaccination against covid-19 in France500,000 COVID-19 DeathsCovid-19 vaccinations: BubblesTime Spiral with a COVID DemoCOVID MasksGrid Cartogram component with live COVID demos (plus a MapEditor)The US COVID SyringeCOVID-19 DKCOVID-19 World Community Mobility Report by GoogleCOVID 19Chicago COVID ZIP SparklinesThe COVID Syringeభారతదేశంలో కోవిడ్-19SVG DataGrid with many features and a live COVID Dashboard demoThe spread of Covid-19 in New MexicoHeatmap of COVID-19 Confirmed Cases by Age over Time in JapanThe CoViD-19 ReportCovid19 WorldwideMassachusetts Coronavirus Cases by TownChoropleth map about Covid19 in FranceWell ordered coronavirus heatmaps for US and the WorldCOVID-19 Racial/Ethnic Mortality AnalysisProPublica's COVID Arrow MapClustering students to slow virus spread inside schoolsCOVID-19 in the USAWho Is Wearing Masks in the U.S.Covid-19 Viz RoundupCoronavirus StatsThe Covid-19 Crisis' Impact on the Number of US Flight PassengersCOVID-19 Daily New CasesCOVID-19 CasesCOVID–19 Bubble Chart with D3 RenderCoronavirus Deaths by Race / EthnicityHow many SARS-CoV-2 tests are we running in the U.S.?COVID-19 Onset vs. ConfirmationPeaks in confirmed daily deaths due to COVID-19 so farCOVID-19 in the U.S.Recreating John Burn-Murdoch’s Coronavirus trackerTracking COVID-19 Cases in VietnamCOVID-19 in NYC by Zip Code & IncomeVisualizing the Network Meta-Analysis of Covid-19 Studiesxkcd COVID-19 spread sketchCOVID-19's deaths in EuropeCovid-19 (corona virus) deaths per 1,000,000 peopleCOVID-19 Bubble map or spike map? (Twitter debate)A Timeline of Shelter-in-PlaceWhere’s that $2 trillion going?Estimating SARS-COV-2 infectionsCODAVIM - CountySARS-CoV-2 Epi CurveCOVID-19 Curves (U.S.)COVID-19 Cases by CountyCOVID-19 world growth rateA graphical experiment of exponential spreadCOVID-19 by US countyCOVID-19 Confirmed vs. New cases"Live" Logistic Coronavirus Death CounterInfografiche: COVID-19 in ItaliaCoronavirus (COVID-19) GlobeBar Chart Race, COVID-19 outbreak Worldwide to 24th March 2020US Coronavirus testing by statesUnited States Coronavirus Daily Cases Map (COVID-19)COVID-19 Numbers by State, Side by SideRecreating NYT U.S. Cases MapCOVID-19 in Washington stateCOVID-19 outbreak in maps and chartsCOVID-19 Spreading trendsRestaurants during COVID-19 social distancingCOVID-19 Countries Trajectories in 3DStates that aren't reporting aspects of their COVID-19 testing processNextstrain Prototyping - Issue 817Reviewing COVID-19 SARS-CoV-2 preprints from medRxiv and bioRxivCoronavirus worldwide evolutionCovid-19 New Cases PunchcardCovid-19 cases per district in Germany.COVID-19 Cases, Deaths, and Recoveries (Select Country)Quarantine NowEmissions in WuhanCOVID-19(nCOV-2019) Outbreak in S.KoreaMovement of population between provinces in 2019-nCoVComparing COVID-19 Growth
Covid-19 derived chart
Coronavirus Trends (COVID-19)Netherlands Coronavirus Daily Cases Map (COVID-19)Map and timeline of Corona outbreakSARS-CoV-2 Phylogenetic TreeCoronavirus data (covid-19)Visualizing the Logic of Exponential Viral SpreadItaly Coronavirus Daily Cases Map (COVID-19)COVID-19 Fatality Rate
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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 dateDomain = d3.extent(
series
.map(d => d[1])
.flat()
.filter(d => d[1][axisX] >= -10 * DAY),
d => d[1][axisX]
);

// return yield { dateDomain, series };

const isDateX = ["date", "daterelative"].includes(axisX),
isDateY = ["date", "daterelative"].includes(axisY),
per100kX = axisX.match(/100/),
per100kY = axisY.match(/100/);
const x = isDateX
? axisX === "date"
? d3.scaleTime().domain(dateDomain)
: d3
.scaleLinear()
.domain(dateDomain)
.clamp(true)
: d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
per100kX ? 0.0001 : 1,
d3.max(series, ([, s]) =>
Math.max(10, d3.max(s, ([, d]) => d[axisX]))
)
]),
y = d3[useLog ? "scaleLog" : "scaleLinear"]().domain([
per100kY ? 0.0001 : 1,
Math.max(10, d3.max(series, ([, s]) => d3.max(s, ([, d]) => d[axisY])))
]);

x.range([120, w - 100]);
y.range([height - 90, 40]);

const zero = y.invert(height - 50), // where to put our zeros on a log scale
zeroAxis = y.invert(height - 30); // 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)
]);
let transform = useLog ? Math.log10 : d => d;
let pointsRaw = d[1].map(d => [
isDateX ? d[1][axisX] / DAY : transform(d[1][axisX] || zero),
isDateY ? d[1][axisY] / DAY : 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)
)
);
else if (smooth) {
points = points.filter(d => d[1] < y(zero));
if (points.length > 4) {
points = smoothPoints(points);
line.curve(d3.curveMonotoneY);
}
}

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 => {
const base =
d[0] !== "Total"
? d[0].replace(/Mainland China:/, "").replace(/:$/, "")
: label_region();
return axisY.match(/_daily/)
? base
: `${base} ${
useRegression
? `${
isDateX && useLog
? (Math.log(2) / Math.log(10) / d.regression.a).toFixed(1)
: d.regression.a.toFixed(1)
} ${
useLog
? isDateX
? `days per 2x ${axisY}`
: `10x ${axisY} per 10x ${axisX}`
: isDateX
? `${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 (!isDateX) axisB.tickFormat(logTickFormat);
}

if (axisX === "daterelative") axisB.tickFormat(d => Math.round(+d / DAY));

axis
.append("g")
.call(axisB)
.attr("transform", `translate(0, ${y(zeroAxis)})`)
.append("text")
.text(isDateX ? 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(axisY.match(/_daily/) ? axisY : `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());

if (isDateX) {
const eventsG = svg
.append("g")
.selectAll("g")
.data(axisX === "daterelative" ? relativeevents : events)
.join("g");
eventsG
.append('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('stroke', '#aaa')
.style('stroke-width', 1);
eventsG
.append('text')
.text(d => d.name)
.attr('x', 4)
.attr('y', -4)
.attr(
'transform',
d => `translate(${x(d.date)},${y(zeroAxis)})rotate(-90)`
)
.style('fill', '#aaa');
}

// tooltip
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(event) {
const m = d3.pointer(event),
i = delaunay.find(...m),
dist = Math.hypot(m[0] - xy[i][0], m[1] - xy[i][1]),
d = circles.data()[i];
if (dist < 30)
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)}`
);
else tooltip.call(callout, null);
});

svg.on("touchend mouseleave", () => tooltip.call(callout, null));
}
Insert cell
Insert cell
Insert cell
data = rawData
.filter(
d =>
countryFilter.includes("All") ||
countryFilter.length === 0 ||
(countryFilter.includes("Outside China") && d.country !== 'China') ||
countryFilter.includes(d.country) ||
(population.has(d.country) &&
(countryFilter.includes(population.get(d.country).continentalRegion) ||
countryFilter.includes(population.get(d.country).statisticalRegion)))
)
.filter(
d =>
!(+populationThreshold > 0) ||
(population.has(d.country) &&
population.get(d.country).pop2019 > +populationThreshold * 1e6)
)
Insert cell
countries = rawData.map(d => d.country).filter((value, index, self) => self.indexOf(value) === index).sort();
Insert cell
Insert cell
population = FileAttachment("population.csv")
.text()
.then(d3.csvParse)
.then(data => new Map(data.map(d => [d.country, d])))
Insert cell
regions = [
...new Set(
[...population.values()]
.map(d => [d.continentalRegion, d.statisticalRegion])
.flat()
)
].filter(d => d)
Insert cell
allSeries = {
const maxDate = d3.timeFormat("%Y-%m-%d")(Date.now());

return d3
.rollups(
data.filter(d => d.ymd <= maxDate),
v => {
const confirmed_daily =
d3.sum(v.filter(d => d.type === "confirmed"), d => d.cases) || 0,
deaths_daily =
d3.sum(v.filter(d => d.type === "death"), d => d.cases) || 0,
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,
pop100k =
groupBy === 'country'
? (population.has(v[0].country)
? population.get(v[0].country).pop2019
: 1e8) /* arbitrary value for bad data */ / 100000
: Infinity;
return {
confirmed,
deaths,
recovered,
confirmed_daily,
deaths_daily,
"confirmed per 100k": confirmed / pop100k,
"deaths per 100k": deaths / pop100k,
country: v[0].country,
province: v[0].province,
ymd: v[0].ymd,
date: parseDate(v[0].ymd)
};
},
d => (groupBy === 'total' ? 'Total' : d[groupBy]),
d => d.ymd
)
.reverse();
}
Insert cell
dateThreshold = new Map(
allSeries
.map(d => [
d[0],
d3.min(d[1].filter(e => e[1].deaths >= threshold), e => e[1].date)
])
.filter(d => d[1])
)
Insert cell
series = {
const series = allSeries.filter(d => dateThreshold.has(d[0]));

for (const d of series) {
d[1] = d[1].map(e => {
e[1].daterelative = e[1].date - +dateThreshold.get(d[0]);
return e;
});
}

return series;
}
Insert cell
events = [
{
name: 'WHO declares pandemic',
date: parseDate('2020-03-11')
}
/*,
{
name: 'US declares national emergency',
date: parseDate('2020-03-13'),
},*/
]
Insert cell
relativeevents = [
{
name: 'Relative date reference',
date: 0
}
/*,
{
name: 'US declares national emergency',
date: parseDate('2020-03-13'),
},*/
]
Insert cell
// Total
function label_region() {
return countryFilter.includes("All") ? "Total" : countryFilter.join(", ");
}
Insert cell
Insert cell
viewof countryFilter = select({
options: [
"All",
/* "Outside China", */
"——————",
...regions,
"——————",
...countries
],
value: preset.countryFilter || ["All"],
description: "Countries",
multiple: true,
size: 10
})
Insert cell
viewof axisX = select({
options: [
"date",
"daterelative",
"confirmed",
"deaths",
"recovered",
"confirmed per 100k",
"deaths per 100k"
],
value: preset.axisX || (threshold ? "daterelative" : "date"),
description: "X dimension"
})
Insert cell
viewof axisY = select({
options: [
"confirmed",
"deaths",
"recovered",
"confirmed_daily",
"deaths_daily",
"confirmed per 100k",
"deaths per 100k"
],
value: preset.axisY || "deaths",
description: "Y dimension"
})
Insert cell
viewof groupBy = select({
options: ["country", "province", "total"],
value: preset.groupBy || "country",
description: "group by"
})
Insert cell
viewof useLog = checkbox({
options: ["Use log scales"],
value: preset.useLog || false
})
Insert cell
viewof showPoints = checkbox({
options: ["Show points"],
value: preset.showPoints === false ? false : ["Show points"]
})
Insert cell
viewof useRegression = checkbox({
options: ["Trend lines"],
value: preset.useRegression ? ["Trend lines"] : []
})
Insert cell
viewof regressionPeriod = slider({
min: 3,
max: 30,
value: preset.regressionPeriod || 0,
step: 1,
description: "Days"
})
Insert cell
viewof regressionDelay = slider({
min: 0,
max: 30,
value: preset.regressionDelay||0,
step: 1,
description: "Days from latest"
})
Insert cell
viewof thresholdLog10 = slider({
min: -.5,
max: Math.log10(300),
value: Math.log10(preset.deathsThreshold) || -.5,
step: 0.01,
format: v => `${Math.round(10 ** v)}`,
description: "align on deaths #"
})
Insert cell
viewof populationThreshold = select({
options: ["—", "100", "50", "20", "10", "5", "1"],
value: preset.populationThreshold || "—",
description: "Population threshold (millions)"
})
Insert cell
Insert cell
Insert cell
viewof smoothGaussian = checkbox({
options: ["gaussian kernel"],
value: preset.smoothGaussian && ["gaussian kernel"]
})
Insert cell
Insert cell
import { bookmark } from "@severo/bookmark"
Insert cell
preset = bookmark.read()
Insert cell
Insert cell
color = (groupBy,
d3.scaleOrdinal(
d3.schemeCategory10.concat(d3.schemeSet2).concat(d3.schemeDark2)
))
Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
DAY = 8.64e7
Insert cell
d3 = require("d3@6", "d3-regression@1")
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; font-size: 10px }
</style>`
Insert cell
// import { callout } from "@d3/line-chart-with-tooltip"
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
import { Scrubber } from "@mbostock/scrubber"
Insert cell
import { movingAverage } from "@d3/moving-average"
Insert cell
precision = 1e-4
Insert cell
function applyKernel(points, w) {
const values = new Float64Array(points.length).fill(0),
total = new Float64Array(points.length).fill(0);
let p = 1;
for (let d = 0; p > precision; d++) {
p = w(d);
for (let i = 0; i < points.length; i++) {
if (i + d < points.length) {
values[i + d] += p * points[i];
total[i + d] += p;
}
if (i - d >= 0) {
values[i - d] += p * points[i];
total[i - d] += p;
}
}
}
for (let i = 0; i < values.length; i++) {
values[i] /= total[i];
}
return values;
}
Insert cell
function smoothPoints(points) {
if (smoothGaussian) {
const w = d => Math.exp(-((d / smooth) ** 2));
const mm = applyKernel(points.map(d => d[1]), w);
points = points.map((d, i) => [d[0], mm[i]]).filter(d => d[1]);
} else {
const mm = movingAverage(points.map(d => d[1]), 2 * smooth + 1);
points = points.map((d, i) => [d[0], mm[i + smooth]]).filter(d => d[1]);
}
return points;
}
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