Published
Edited
Jul 19, 2021
8 forks
Importers
154 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Fatality_chart = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
svg.append("g").call(xAxis);
svg.append("g").call(yAxis);
svg.append("g").call(grid);
svg.append("g").call(annotations);
svg.append("g").call(legends);
function onMouseOver(selected) {
d3.select("#p-circles").selectAll("circle").attr("fill-opacity", 0.2).attr("stroke-opacity", 0.2);
d3.select("#country_labels").selectAll("text").attr("fill-opacity", 0.2);
d3.select("#cfr_labels").selectAll("text").attr("fill-opacity", 0);
d3.select(".shade").attr("fill-opacity", 0.1);
const selected_circle = d3.select(this);
const country_code = selected_circle.attr("class");
const cfr_labels = (selected.deaths/selected.confirmed*100).toFixed(1);
selected_circle.attr("fill-opacity", 1.0);
const temp_path = svg.append("g").attr("id", "temp-path").style("pointer-events", "none")
.selectAll("path")
.data(data.filter(
d => d.country_code === country_code)
).join("path").attr("class", `${country_code}`)
.attr("stroke", d => color(d.region)).attr("fill", "none")
.attr("stroke-opacity", 1).attr("stroke-width", 2)
.attr("d", d => country_trajectory(d.country_code));
const temp_circles = svg.append("g").attr("id", "temp-circles").style("pointer-events", "none")
.selectAll("circle").data(dataOfCountry_new(country_code)).join("circle")
.attr("fill", selected_circle.attr("fill"))
.attr("fill-opacity", 1)
.attr("stroke", selected_circle.attr("fill")).attr("stroke-opacity", 1)
.attr("cx", d => x(d.confirmed)).attr("cy", d => y(d.deaths)).attr("r", 2);
const temp_label = svg.append("text").attr("id", "temp-label").style("pointer-events", "none")
.text(country_code_to_name(country_code) + ": " + cfr_labels + "%").attr("font-size", 50)
.style("text-anchor", "middle")
.attr("x", selected_circle.attr("cx") - 60).attr("y", selected_circle.attr("cy") - 50)
.attr("font-style", "bold")
.attr("font-family", "Helvetica, sans-serif");
}
function onMouseMove() {
}
function onMouseOut() {
d3.select("#p-circles").selectAll("circle").attr("fill-opacity", 0.5).attr("stroke-opacity", 0.5);
d3.select("#country_labels").selectAll("text").attr("fill-opacity", 1.0);
d3.select("#cfr_labels").selectAll("text").attr("fill-opacity", 0);
d3.select(this).attr("fill-opacity", 0.5);
d3.select(".shade").attr("fill-opacity", 0.2);
d3.select("#temp-circles").remove();
d3.select("#temp-label").remove();
d3.select("#temp-path").remove();
}

const country_labels = svg.append("g").attr("id", "country_labels").selectAll("text")
.data(dataAt(today).filter(d => d.population > 1e7), d => d.country)
.join("text");

const p_circles = svg.append("g").attr("id", "p-circles")
.selectAll("circle").data(dataAt(today), d => d.country).join("circle");
const c_circles = svg.append("g").attr("id", "c-circles")
.selectAll("circle").data(dataAt(today), d=>d.country).join("circle");
const d_circles = svg.append("g").attr("id", "d-circles")
.selectAll("circle").data(dataAt(today), d => d.country).join("circle");

const cfr_labels = svg.append("g").attr("id", "cfr_labels").selectAll("text")
.data(dataAt(today).filter(d => d.deaths > 5), d => d.country)
.join("text");

return Object.assign(svg.node(), {
update(data) {
if (radius_type == 'population') {
p_circles.data(data, d => d.country)
.attr("stroke", "black").attr("fill", d => color(d.region)).attr("fill-opacity", 0.5)
.attr("cx", d => x(d.confirmed)).attr("cy", d => y(d.deaths)).attr("r", d => radius(d.population))
.attr("class", d => `${d.country_code}`)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
};

c_circles.data(data, d => d.country)
.attr("fill", "black").attr("fill-opacity", 0.5)
.attr("cx", d => x(d.confirmed)).attr("cy", d => y(d.deaths)).attr("r", d => radius(d.confirmed));
d_circles.data(data, d => d.country)
.attr("fill", "black").attr("fill-opacity", 0.75)
.attr("cx", d => x(d.confirmed)).attr("cy", d => y(d.deaths)).attr("r", d => radius(d.deaths));
country_labels.data(data, d => d.country)
.text(d => d.country)
.attr("font-size", 12)
.style("text-anchor", "middle")
.attr("x", d => x(d.confirmed))
.attr("y", d => y(d.deaths) - radius(d[radius_type]) - 2);
cfr_labels.data(data, d => d.country)
.attr("font-size", 9)
.attr("fill-opacity", 0)
.style("text-anchor", "start")
.text(d => 5)
.text(d => (d.deaths/d.confirmed*100).toFixed(1) + "%")
.attr("x", d => x(d.confirmed) + radius(d[radius_type]) + 2)
.attr("y", d => y(d.deaths));

}
});
}
Insert cell
Insert cell
Insert cell
legends = g => g
.call(g => g.append("g")
.append("rect") // rectangular background
.attr("fill", "white")
.attr("stroke", "lightgrey")
.attr("x", x(2)).attr("y", y(10e6))
.attr("width", x(200)-x(2)).attr("height", y(5e1)-y(20e3)))
.call(g => g.append("g")
.selectAll("circle")
.data(legend_circles)
.join("circle")
.attr("cx", d => d.cx).attr("cy", d => d.cy).attr("r", d => d.r)
.attr("fill", d => d.color)
.attr("fill-opacity", d => d.opacity)
.attr("stroke", d => d.stroke))
.call(g => g.append("g")
.selectAll("text")
.data(legend_labels)
.join("text")
.text(d => d.text)
.style("text-anchor", d => d.anchor)
.attr("font-size", d => d.size)
.attr("x", d => d.x).attr("y", d => d.y))
.call(g => g.append("g")
.selectAll("line")
.data(legend_lines)
.join("line")
.attr("stroke", "black")
.attr("x1", d => d.x1).attr("y1", d => d.y1).attr("x2", d => d.x2).attr("y2", d => d.y2));
Insert cell
legend_lines = ([
{x1: x(40), x2: x(15), y1: y(3.1e6), y2: y(10.5e5)},
{x1: x(16.5), x2: x(10), y1: y(3e5), y2: y(6.9e5)},
{x1: x(10), x2: x(8), y1: y(1e5), y2: y(8e5)},
]);
Insert cell
legend_labels = {
const cx = legend_base_params.cx;
const cy = legend_base_params.cy;
const r = legend_base_params.r;
const labels = ([
{text: "Country name", size: 12, anchor: "middle", x: cx, y: cy - r - 5},
{text: "Fatality rate", size: 10, anchor: "start", x: cx + r + 5, y: cy},
{text: "Population", size: 12, anchor: "start", x: x(40), y: y(3e6)},
{text: "Confirmed cases", size: 12, anchor: "start", x: x(18), y: y(2.5e5)},
{text: "Deaths", size: 12, anchor: "start", x: x(8), y: y(0.7e5)},
]);
return labels;
};
Insert cell
legend_circles = {
const cx = legend_base_params.cx;
const cy = legend_base_params.cy;
const r = legend_base_params.r;
const circles = ([
{cx: cx, cy: cy, r: r, color: color("East Asia & Pacific"), opacity: 0.5, stroke: "black"},
{cx: cx, cy: cy, r: r/2, color: "black", opacity: 0.5, stroke: "none"},
{cx: cx, cy: cy, r: r/5, color: "black", opacity: 0.75, stroke: "none"},
]);
return circles;
};
Insert cell
legend_base_params = ({cx: x(8), cy: y(8e5), r: (x(380)-x(2))/6});
Insert cell
annotations = g => g
.call(g => g.append("g") // Date
.append("text").text(dateOnlyFormat(currDateF))
.style("text-anchor", "end")
.attr("font-size", 70).attr("fill-opacity", 0.1)
.attr("font-style", "bold")
.attr("font-family", "Helvetica, sans-serif")
.attr("x", x(params.xmax)).attr("y", y(1)))
.call(g => g.append("g") // shade
.append("polygon").attr("class", "shade")
.attr("fill","black").attr("fill-opacity","0.2").attr("points", shade))
.call(fatality_lines).call(fatality_labels) // fatality lines and labels
.call(g => g.append("g") // Fatality rate label
.append("text").attr("stroke", "none").attr("fill", "darkred")
.attr("font-weight", "bold").attr("font-size", 18).style("text-anchor", "end")
.text("Fatality Rates").attr("x", x(params.xmax)).attr("y", y(params.ymax)-5))
/* .call(g => g.append("g")
.append("text").text("Likely under-tested or under stress ← → Likely better response")
.style("text-anchor", "middle")
.attr("font-size", 15).attr("fill-opacity", "0.3").attr("style","white-space:pre")
.attr("x", x(2.5e3)).attr("y", y(7e4))
.attr("transform", "rotate(" + (text_rotation_angle+90) + " " + x(2.5e3) + " " + y(7e4) +")"));
*/
Insert cell
fatality_labels = g => g
.attr("stroke", "none")
.attr("font-size", 12)
.attr("stroke-opacity", 0.4)
.call(g => g.append("g")
.selectAll("text")
.data(fatality_data)
.join("text")
.style("text-anchor", "end")
.attr("fill", d => d.color)
.text(d => d.name + " (" + (d.fatality*100).toFixed(1) + "%)" )
.attr("transform", d => "rotate(" + text_rotation_angle + " " + x(d.x) + " " + y(d.x*d.fatality) +")")
.attr("x", d => x(d.x))
.attr("y", d => y(d.x * d.fatality)));
Insert cell
fatality_lines = g => g
.attr("stroke-opacity", 0.3)
.call(g => g.append("g")
.selectAll("line")
.data(fatality_data)
.join("line")
.attr("stroke", d=> d.color)
.attr("x1", d => x(1.0/d.fatality))
.attr("x2", d => x(d3.min([params.xmax, params.ymax/d.fatality])))
.attr("y1", y(1.0))
.attr("y2", d => y(d3.min([params.xmax * d.fatality, params.ymax]))));
Insert cell
text_rotation_angle = - Math.atan( (y(1)-y(2))/(x(2)-x(1)) ) / radians;
Insert cell
grid = g => g
.attr("stroke", "currentColor")
.attr("stroke-opacity", 0.1)
.call(g => g.append("g")
.selectAll("line")
.data(x.ticks())
.join("line")
.attr("x1", d => 0.5 + x(d))
.attr("x2", d => 0.5 + x(d))
.attr("y1", params.margin.top)
.attr("y2", height - params.margin.bottom))
.call(g => g.append("g")
.selectAll("line")
.data(y.ticks())
.join("line")
.attr("y1", d => 0.5 + y(d))
.attr("y2", d => 0.5 + y(d))
.attr("x1", params.margin.left)
.attr("x2", width - params.margin.right));
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - params.margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80, ","))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", width)
.attr("y", params.margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
//.text(`Confirmed cases up until ${timeFormat(d3.timeDay.offset(today, -delay_parameter))} →`));
.text(`Confirmed cases up until ${delay_parameter} day(s) ago →`));
Insert cell
yAxis = g => g
.attr("transform", `translate(${params.margin.left},0)`)
.call(d3.axisLeft(y).ticks(height/80, ","))
.call(g => g.select(".domain").remove())
.call(g => g.append("text")
.attr("x", -params.margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Number of deaths"))
Insert cell
color = d3.scaleOrdinal(data.map(d => d.region), ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#17becf"])
Insert cell
radius = {
const factor = 15;
if (radius_type == 'population') {
return d3.scaleSqrt([1, max_pop], [1, width / factor])
} else if (radius_type == 'confirmed') {
return d3.scaleSqrt([1, max_confirmed], [1, width / factor])
} else {
return d3.scaleSqrt([1, max_confirmed_fraction], [1, width / factor])
}
}
Insert cell
y = d3.scaleLog([params.ymin, params.ymax], [height - params.margin.bottom, params.margin.top])
Insert cell
x = d3.scaleLog([params.xmin, params.xmax], [params.margin.left, width - params.margin.right])
Insert cell
Insert cell
width = 800;
Insert cell
height = 550;
Insert cell
params = ({
margin: ({top: 20, right: 35, bottom: 35, left: 66}),
xmin: 1,
xmax: 1e9,
ymin: 1,
ymax: 1e8,
});
Insert cell
startDate = d3.timeDay(new Date(2019, 11, 31))
Insert cell
formatDate = d3.timeFormat("%Y-%m-%d")
Insert cell
Insert cell
Insert cell
function country_code_to_name(country_code) {
const country_data = data.find(d => d.country_code === country_code);
return country_data.country_name;
}
Insert cell
function country_trajectory(country_code) {
const l = d3.line();
let countryData = dataOfCountry(country_code);
let trajectoryData = [];
for (let i = 0; i < countryData.length; i++) {
if (i < delay_parameter) {
trajectoryData.push([x(0.01), y(countryData[i].deaths)])} else {
trajectoryData.push([x(countryData[i - delay_parameter].confirmed), y(countryData[i].deaths)])
}
}
return l(trajectoryData.filter(d => d[0] > x(0.5) && d[1] < y(0.5)));
}
Insert cell
function dataOfCountry(country_code) {
const country_data = data.find(d => d.country_code === country_code);
return country_data['confirmed'].map(function(e, i) {
return {'country_code': country_code,
'date': new Date(e[0]),
'confirmed': d3.max([e[1], 0.01]),
'deaths': d3.max([country_data['deaths'][i][1], 0.01])};
});
}
Insert cell
function dataOfCountry_new(country_code) {
const country_data = data.find(d => d.country_code === country_code);
return country_data['deaths'].map(function(e, i) {
return {'country_code': country_code,
'date': new Date(e[0]),
'confirmed': dataOfCountry_confirmed_calculation(country_code)[i],
'deaths': d3.max([e[1],0.01])};
});
}
Insert cell
function dataOfCountry_confirmed_calculation(country_code) {
const country_data_confirmed = data.find(d => d.country_code === country_code).confirmed;
let confirmed_data = [];
for (let i = 0; i < country_data_confirmed.length; i++) {
if (i < delay_parameter) {confirmed_data.push(0.01)} else {
confirmed_data.push(country_data_confirmed[i - delay_parameter][1])
}
}
return confirmed_data;
}
Insert cell
function dataAt(date) {
return data.map(d => ({
country_code: d.country_code,
country: d.country_name,
population: d.population,
region: d.region,
confirmed: valueAtForConfirmed(d.confirmed, date),
deaths: valueAt(d.deaths, date),
}));
}
Insert cell
dataAt(today)
Insert cell
function valueAt(values, date) {
const i = bisectDate(values, date, 0, values.length - 1);
const a = values[i];
if (a[1] > 0) {return a[1]} else {return 0.01};
}
Insert cell
function valueAtForConfirmed (values, date) {
const i = bisectDate(values, date, 0, values.length - 1);
if (i < delay_parameter) {return 0.01} else {return values[i - delay_parameter][1]}
}
Insert cell
bisectDate = d3.bisector(([date]) => parseTime(date)).left
Insert cell
dateOnlyFormat = d3.timeFormat("%b %d %Y");
Insert cell
timeFormat = d3.timeFormat("%Y-%m-%d");
Insert cell
parseTime = d3.timeParse("%Y-%m-%d");
Insert cell
Insert cell
radians = 0.0174532925;
Insert cell
shade = `${x(1.0/0.04)},${y(1.0)} ${x(1.0/0.01)},${y(1)} ${x(params.xmax)},${y(params.xmax*0.01)} ${x(params.xmax)},${y(params.xmax*0.04)}`;
Insert cell
fatality_data = ([
{name: 'Smallpox (malignant)', fatality: 0.95, color: "red", x: params.ymax/0.95 /1.5},
{name: "Bubonic plague", fatality: 0.55, color: "red", x: params.ymax/0.55 /1.5},
{name: "SARS", fatality: 0.11, color: "red", x: params.xmax/2},
{name: "", fatality: 0.04, color: "black", x: params.xmax},
{name: "Spanish (1918) flu", fatality: 0.07, color: "red", x:params.xmax},
{name: "", fatality: 0.01, color: "black", x: params.xmax},
{name: "Influenza A (typical)", fatality: 0.001, color: "red", x:params.xmax},
{name: "Minimum observed (10+ deaths)", fatality: min_fatality, color: "black", x:params.xmax},
]);
Insert cell
min_fatality = d3.min(dataAt(new Date).filter(d => d.deaths > 10), d => (d.deaths / d.confirmed));
Insert cell
max_confirmed = d3.max(dataAt(new Date), d => d.confirmed);
Insert cell
max_confirmed_fraction = d3.max(dataAt(new Date).map( d => d.confirmed / d.population))
Insert cell
max_pop = d3.max(data, d => d.population);
Insert cell
today = all_dates[all_dates.length - 1];
Insert cell
all_dates = data[0]['confirmed'].map(x => parseTime(x[0]));
Insert cell
data = data_raw_region_null_deleted.map(function(d) {
d.confirmed_fraction = d.confirmed.map(x => [x[0], x[1] / d.population]);
return d;
})
Insert cell
data_raw_region_null_deleted = data_raw.filter(d => d.region !== null)
// The updated data from OWID included Continent data as country data. Need to exclude these before data processing.
Insert cell
data_raw = d3.json("https://raw.githubusercontent.com/covid19-data/covid19-data/master/output/cntry_stat_owid.json")
Insert cell
Insert cell
Insert cell
Insert cell
import { rangeSlider as rangeSlider } from '@mootari/range-slider'
Insert cell
Insert cell
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