Public
Edited
Mar 1, 2024
Importers
Insert cell
Insert cell
chart = {
// const width = maxWidth;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, maxWidth, height])
.attr("width", maxWidth)
.attr("height", height)
.style("overflow", "visible")
.style("font-family", "sans-serif")
.style("margin", "10px 0px");

svg.append("g").attr("class", "lines");
svg.append("g").attr("class", "labels").style("pointer-events", "none");
const dot = svg.append("g")
.attr("display", "none")
.style("pointer-events", "none")
.attr("id", "dot");
dot.append("circle")
.attr("r", 3.5);

dot.append("text")
.style("font", "10px sans-serif")
.attr("text-anchor", "middle")
.style("pointer-events", "none")
.attr("y", -8);
svg.append("g").attr("class", "x--axis")
.attr("transform", `translate(0,${height - margin.bottom})`);

svg.append("text")
.attr("class", "x--axis--label")
.style("pointer-events", "none")
.text(xLabel)
.attr("text-anchor", "start")
// .attr("font-weight", "bold")
.style("text-anchor", "end")
.attr("x", maxWidth - margin.right )
.attr("y", height+7 );
svg.append("g").attr("class", "y--axis")
.attr("transform", `translate(${margin.left},0)`);

svg.append("text")
.attr("class", "y--axis--label")
.style("pointer-events", "none")
.text(yLabel)
.attr("text-anchor", "start")
// .attr("font-weight", "bold")
.style("text-anchor", "start")
.attr("x", 5 )
.attr("y", 10 );
svg.append("g")
.append("text")
.attr("transform", `translate(${margin.left+10}, ${margin.top+70})`)
.style("pointer-events", "none")
.attr("class", "daysPassed")
.style("font-size", "48pt")
.style("fill", "#333")
.style("opacity", 0.3)
.text("");
const resEle = html`
<style>* { font-family: sans-serif }</style>
<br>
${header}
${viewof animDate} ${viewof focusName} ${viewof useLinearScale}
${svg.node()}
<div>${footer}
<br>
<a href="https://johnguerra.co">John Alexis Guerra Gómez</a> &nbsp;&nbsp; <a href="https://twitter.com/duto_guerra"> @duto_guerra</a> &nbsp;<a href="https://twitter.com/guerravis"> @guerravis</a>
<br>
<a href="https://johnguerra.co/coronavirus">https://johnguerra.co/coronavirus</a>
<br>
<a rel="license" href="http://creativecommons.org/licenses/by/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/4.0/88x31.png" /></a>
</div>
`;


return resEle;
}
Insert cell
updateChart(d3.select(chart), data);
Insert cell
chartByDate = chart
Insert cell
updateChart = (selection, data) => {
const svg = selection.select("svg");

svg.select(".x--axis")
.call(xAxis);

// ---------- Path -------------
const path = svg.select(".lines")
.attr("fill", "none")
.attr("stroke-width", 3.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("path")
.data(data.series, d => d.name)
.join("path")
.style("opacity", d => d.name===focusName ? 1 : 0.3)
// .style("-webkit-filter", "drop-shadow( 1px 1px 1px rgba(0, 0, 0, .3))")
// .style("filter", "drop-shadow( 1px 1px 1px rgba(0, 0, 0, .3))")
.attr("stroke", d => color(d.name));
path.filter(d => d.name ===focusName)
.raise();
// ------------- Label ---------------
const label = svg.select("g.labels")
.style("font-size", "12pt")
.style("font-family", "sans-serif")
.selectAll(".dataLabel")
.data(data.series
.filter(d => d.values.length)
.sort((a,b ) =>
a.name === focusName ? -1 :
b.name === focusName ? 1 :
d3.descending(a.values[a.values.length-1], b.values[b.values.length-1]) )
.slice(0, MAX_LABELS),
d => d.name)
.join((enter)=> {
const text = enter
.append("text")
.attr("class", "dataLabel")
.style("opacity", 0)
.attr("transform", d => `translate(${x(data.dates[d.values.length-1])}, ${y(d.values[d.values.length-1])})`);
text.append("tspan")
.attr("y", 0)
.attr("x", 10)
.text(d => d.name);
text.append("tspan")
.attr("y", 15)
.attr("x", 10)
.attr("class", "count")
.text(d => format(d.values[d.values.length-1]));
return text;
})
.attr("class", "dataLabel")
.attr("fill", "black")
.style("opacity", d => d.name===focusName ? 1 : 0.5);

svg.call(hover, path, label);
// Transitions
const transitionFull = d3.transition().duration(delay);
const transition1 = d3.transition().duration(delay/2)
.end().then(() => {
const transition2 = d3.transition().duration(delay/2)
y.domain([0, d3.max(data.series, d => d3.max(d.values))+1]).nice();
svg.select(".y--axis")
.transition(transition2)
.call(yAxis);
path.transition(transition2)
.attr("d", d => line(d.values));
label
.transition(transition2)
.attr("transform", d =>
`translate(${x(data.dates[d.values.length-1])}, ${y(d.values[d.values.length-1])})`);

});
path.transition(transition1)
.attr("d", d => line(d.values))

label
.transition(transition1)
.attr("transform", d =>
`translate(${x(data.dates[d.values.length-1])}, ${y(d.values[d.values.length-1])})`);
label
.transition(transitionFull)
.select("tspan.count")
.textTween(function(d) {
const i = d3.interpolate(this._current, format(d.values[d.values.length-1]));
return function(t) {
return this._current = format(i(t));
}
});

svg.select(".daysPassed")
.text(`${animDate}`);
return svg.node();
}
Insert cell
Insert cell
Insert cell
format = d3.format("d");
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(data.dates))
.range([margin.left, maxWidth - margin.right]);

Insert cell
y ={
const y = useLinearScale ?
d3.scaleLinear() :
d3.scalePow()
.exponent(0.5);
return y
.domain([0,10])
.range([height - margin.bottom, margin.top]);
}
Insert cell
xAxis = g => g
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
Insert cell
yAxis = g => g
.call(d3.axisLeft(y).tickFormat(d3.format("d")))
// .call(g => g.select(".domain").remove());

Insert cell
yLabel = "Casos confirmados"
Insert cell
xLabel = "Días desde el primer caso"
Insert cell
line = d3.line()
.defined(d => !isNaN(d))
.x((d, i) => x(data.dates[i]))
.y(d => y(d));

Insert cell
color = (d) => d===focusName ? "steelblue" : "#ddd";
// const color = d3.scaleOrdinal(d3.schemeCategory10);
// .range(d3.range(0, data.series.length).map(i => d3.interpolateRainbow(i/data.series.length)));

Insert cell
function hover(svg, path, label) {
if ("ontouchstart" in document) svg
.style("-webkit-tap-highlight-color", "transparent")
.on("touchmove", moved)
.on("touchstart", entered)
.on("touchend", left)
else svg
.on("mousemove", moved)
.on("mouseenter", entered)
.on("mouseleave", left);

const dot = svg.select("#dot")
.attr("display", "none");

function moved() {
d3.event.preventDefault();
const ym = y.invert(d3.event.offsetY);
const xm = x.invert(d3.event.offsetX);
const i1 = d3.bisectLeft(data.dates, xm, 1);
const i0 = i1 - 1;
const i = xm - data.dates[i0] > data.dates[i1] - xm ? i1 : i0;
let s = data.series
.filter(d => d.values.length>i)
.reduce((a, b) =>
(Math.abs(a.values[i] - ym) > Math.abs(b.values[i] - ym)) ? b : a
);
path.attr("stroke",
d => d === s ||
d.name===focusName ?
color(d.name) : "#ccc"
).style("opacity",
d => d.name === focusName ? 1 :
d === s ? 0.9 : 0.3)
.filter(d => d === s)
.raise();
label.style("opacity", d => d === s ? 1.0 : d.name === focusName ? 0.7 : 0.1)
.filter(d => d === s).raise();
dot.attr("transform", `translate(${x(data.dates[i])},${y(s.values[i])})`);
dot.select("text").text(`${s.name}: ${s.values[i]}` );
}

function entered() {
path
// .style("mix-blend-mode", null)
.attr("stroke", "#ccc");
dot.attr("display", null);
}

function left() {
console.log("left");
path
// .style("mix-blend-mode", "multiply")
.style("opacity", d => d.name === focusName ? 1 : 0.3)
.attr("stroke", d => color(d.name));
dot.attr("display", "none");
label.style("opacity", d => d.name===focusName ? 1 : 0.3)
}
}
Insert cell
Insert cell
maxWidth = Math.min(800, width)
Insert cell
Insert cell
rawData = {
const confirmedPromise = d3.csv("https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv");
const continentsPromise = FileAttachment("country-and-continent-codes-list-csv.csv")
.text().then(d3.csvParse);

return Promise.all([confirmedPromise, continentsPromise])
.then(([confirmedData, continentsData]) => {

// US has different names in both datasets
continentsData
.forEach(row =>
row.Country_Name = (row.Country_Name === "United States of America") ?
"US" : row.Country_Name
);

// Add continents data
confirmedData.forEach((row) => {
row.Continent = null;
for (let country of continentsData) {
if (country.Country_Name.indexOf(row["Country/Region"])!==-1) {
row.Continent = country.Continent_Name;
break;
}
}
});
const columns = confirmedData.columns;
const filtered = confirmedData.filter(rowFilter);
filtered.columns = columns;
return filtered;
})
}
Insert cell
rowFilter = (row) => {
return (row.Continent==="South America" || row.Continent==="North America"
&& row["Country/Region"]!=="US" && row["Country/Region"]!=="Canada");
}
Insert cell
dates = rawData.columns.slice(4, rawData.columns.length+1)
Insert cell
// Aggregate data by country
nestedData = d3.nest()
.key(d => d["Country/Region"])
.rollup(v => ({ values : v,
sumsByDate:
dates.map(c =>
({ date: d3.utcParse("%m/%d/%y")(c),
sum : v.map(d =>
+d[c]).reduce((p,n) => p+n)
})
)
}))
.entries(rawData);


Insert cell
dateFirstCase = {
const firstCases = new Map();

for (let d of nestedData) {
let minDateI = 0;
for (let i = 0;
i < d.value.sumsByDate.length && d.value.sumsByDate[i].sum <= ALIGN_CASES;
i+=1
) {
minDateI = i;
}
// console.log(d.key, d.value.sumsByDate, minDateI);
const firstDate = d.value.sumsByDate[minDateI].date ;
firstCases.set(d.key, {
i: minDateI,
firstDate
});
}
return firstCases;
}
Insert cell
nestedDataFirstCase = nestedData.map(k => {
k.value.sumsByDate = k.value.sumsByDate.map((s,i, sumsByDate) => {
s.daysSinceFirstCase = (s.date - dateFirstCase.get(k.key).firstDate)/(1000*24*3600);
s.diff = (i > 0 && sumsByDate[i-1].sum > 0) ?
(s.sum - sumsByDate[i-1].sum)/sumsByDate[i-1].sum : 0;
return s;
});
return k;
});
Insert cell
data = {
const animDateParsed = d3.utcParse("%m/%d/%y")(animDate);
const filteredData = nestedDataFirstCase.map(d => {
return {
...d,
value:{
...d.value,
sumsByDate: d.value.sumsByDate.filter(d => d.date <= animDateParsed),
filteredLength: d.value.sumsByDate.filter(d => d.date <= animDateParsed).length
}
};
});
return {
y: "Confirmed cases",
series: filteredData.map(d => ({
name: d.key.replace(/, ([\w-]+).*/, " $1"),
values: d.value.sumsByDate
.map(s => s.sum)
.filter(d => d > 0),
filteredLength: d.value.filteredLength
})),
dates: d3.range(0, d3.max(nestedDataFirstCase, d => d3.max(d.value.sumsByDate, e => e.daysSinceFirstCase)))
};
}
Insert cell
ALIGN_CASES=0
Insert cell
html`<style>*, h1 {font-family: sans-serif;}</style>`
Insert cell
a = dateFirstCase.values()
Insert cell
viewof animDate = Scrubber(dates.slice(d3.min(Array.from(dateFirstCase.values()), d => d.i)), {delay:delay*1.2, loop: false})
Insert cell
countryToFocus = "Colombia"
Insert cell
viewof focusName = select({
title: "País a comparar",
options: nestedData.map(d => d.key),
value: countryToFocus
})
Insert cell
viewof useLinearScale = checkbox({
options: [{label: "Escala Lineal", value:true}],
value:["Escala Lineal"]
})
Insert cell
MAX_LABELS = 5
Insert cell
// d3 = require("d3@5", "d3-interpolate-path")
d3 = require("d3@5")
Insert cell
Insert cell
delay = 200
Insert cell
import {Scrubber} from "@mbostock/scrubber"

Insert cell
import {select, checkbox} from "@jashkenas/inputs"

Insert cell
import {vl} from '@vega/vega-lite-api'

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