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

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