Published
Edited
Jul 12, 2021
6 forks
Importers
30 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof goBack = {
const s = slider({
title: "Go back to:",
min: 0,
max: goBackSpan,
step: 1,
value: goBackSpan,
format: date => formatDate(d3.timeDay.offset(startDate, date)),
description: `You can view the trend in the past by going back. The starting point of the slider is ${formatDate(startDate)}, and the end point is ${formatDate(goBackEnd)}.`
})
s.input.style.width = "40%";
return s;
}
Insert cell
Insert cell
function onMouseOver(selected) {
const country_code = d3.select(this).attr("class");
d3.selectAll(`.${country_code}`).attr("fill-opacity", 1).attr("stroke-opacity", 1).attr("stroke-width", 3)
.attr("r", 5);
d3.select("#fullname").text(code_to_name(country_code))
.style("text-anchor", "end")
.attr("font-size", 50).attr("fill-opacity", 0.1).attr("fill", "black")
.attr("font-style", "bold")
.attr("font-family", "Helvetica, sans-serif")
.attr("x", x(params.xmax)).attr("y", y(params.ymin)-20);
const circle = d3.select(`circle.${country_code}`);
//d3.select("#guides").attr("transform", `translate(${circle.attr("cx")} ${-circle.attr("cy")})`)
}
Insert cell
function onMouseOut() {
const country_code = d3.select(this).attr("class");
d3.selectAll(`.${country_code}`)
.attr("fill-opacity", params.opac_weak).attr("stroke-opacity", params.opac_weak).attr("stroke-width", 1)
.attr("r", 3);
d3.select("#fullname").attr("fill-opacity", 0);
d3.select("#guides").attr("transform", `translate(0 0)`)
}
Insert cell
function draw_axes(svg) {
svg.append("g").call(xAxis);
svg.append("g").call(yAxis);
}
Insert cell
function draw_trajectories(svg) {
svg.append("g").selectAll("path").data(viz_data).join("path")
.attr("class", d => `${d.country_code}`)
.attr("stroke", d => color(d.region)).attr("fill", "none")
.attr("stroke-opacity", params.opac_weak)
.attr("d", d => get_path(d.shifted))
.on("mouseover", onMouseOver).on("mouseout", onMouseOut)
svg.append("g").selectAll("circle").data(viz_data).join("circle")
.attr("class", d => `${d.country_code}`)
.attr("fill", d => color(d.region)).attr("fill-opacity", params.opac_weak)
.attr("cx", d => x(d.shifted.length-1)).attr("cy", d => y(d.shifted[d.shifted.length - 1])).attr("r", 3)
.on("mouseover", onMouseOver).on("mouseout", onMouseOut);
svg.append("g").selectAll("text").data(viz_data).join("text").text(d => d.country_code)
.attr("class", d => d.country_code)
.attr("fill", d => color(d.region)).attr("fill-opacity", params.opac_weak)
.attr("x", d => d3.min( [x(d.shifted.length-1)+4, x(params.xmax)-10] ))
.attr("y", d => y(d.shifted[d.shifted.length - 1]))
.on("mouseover", onMouseOver).on("mouseout", onMouseOut);
}
Insert cell
function draw_doubling_lines(svg) {
// (0, params.ymin) -> (params.xmax, Math.pow(2, params.ymin*params.xmax/period))
// every day doubling -> (1, params.ymin*2), (2, params.ymin*2*2), ...
svg.append("g").attr("id", "guides").selectAll("path").data(doubling_periods).join("path")
.attr("class", "guidelines")
.attr("stroke", "orange").attr("stroke-opacity", 0.8).attr("stroke-dasharray", 2)
.attr("d", d => d3.line()([
[x(0), y(params.ymin)],
[x(params.xmax), y(params.ymin*Math.pow(2, params.xmax/d))]
]));
svg.append("g").selectAll("text").data(doubling_period_labels).join("text")
.text(d => d.period > 1 ? `Doubling every ${d.period} days`: "Doubling every day")
.attr("font-size", 12)
.style("text-anchor", "end")
.attr("fill", "orange")
.attr("transform", d => `rotate(${get_text_rotation_angle(d.period)} ${x(d.x)} ${y(d.y)})`)
.attr("x", d => x(d.x))
.attr("y", d => y(d.y));
}
Insert cell
doubling_period_labels = doubling_periods.map(function(d) {
d = Number(d);
const candidate_n = params.xmax / d;
const candidate_y = Math.pow(2, candidate_n) * params.ymin;
if (candidate_y < params.ymax) {
return { 'period': d, 'x': params.xmax, 'y': candidate_y };
} else {
return { 'period': d, 'x': Math.log2(params.ymax/params.ymin) * d, 'y': params.ymax };
}
});
Insert cell
function draw_date(svg) {
svg.append("g") // Date
.append("text").text(d3.timeFormat("%b %d %Y")(d3.timeDay.offset(startDate, goBack)))
.style("text-anchor", "end")
.attr("font-size", 50).attr("fill-opacity", 0.2)
.attr("font-style", "bold")
.attr("font-family", "Helvetica, sans-serif")
.attr("x", x(params.xmax) + params.margin.right)
.attr("y", y(params.ymax) + params.margin.top);
}
Insert cell
function get_path(d) {
const l = d3.line();
return l(d.map(function(e, i) { return [x(i), y(e)] }));
}
Insert cell
Insert cell
color = d3.scaleOrdinal(data.map(d => d.region), ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#17becf"])
// color = d3.scaleOrdinal(data.map(d => d.region), d3.schemeTableau10).unknown("black")
//d3.schemeCategory10, d3.schemeTableau10
Insert cell
Insert cell
Insert cell
yAxis = g => g
.attr("transform", `translate(${params.margin.left},0)`)
.call(d3.axisLeft(y).ticks(params.height/80, ","))
.call(g => g.append("text")
.attr("font-size", 15)
.attr("x", -params.margin.left)
.attr("y", params.margin.top-10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(`↑ Number of ${y_type}`))
Insert cell
y = d3.scaleLog([params.ymin, params.ymax], [params.height - params.margin.bottom, params.margin.top])
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${params.height - params.margin.bottom})`)
.call(d3.axisBottom(x).ticks(width/80))
.call(g => g.append("text")
.attr("x", params.width)
.attr("y", params.margin.bottom - 4)
.attr("font-size", 15)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(`Days since ${pivot_values[pivot_n]} ${y_type}→`))
Insert cell
x = d3.scaleLinear().domain([params.xmin, params.xmax])
.range([params.margin.left, params.width - params.margin.right]);
Insert cell
Insert cell
function get_text_rotation_angle(doubling_period) {
const radians = 0.0174532925;
return -Math.atan( (y(params.ymin)-y(params.ymin*2))/(x(doubling_period)-x(0)) ) / radians;
}
Insert cell
function code_to_name(country_code) {
return data.find(d => d.country_code === country_code).country_name;
}
Insert cell
function name_to_code(country_name) {
return data.find(d => d.country_name === country_name).country_code;
}
Insert cell
viz_data = filtered_data.filter(d => selected_regions.includes(d.region)).filter(d => d[y_type][last_index][1] > pivot).map(function(d) {
const start_idx = pivot_bisector.right(d[y_type], pivot);
d['shifted'] = d[y_type].slice(start_idx).map(d => d[1]);
return d;
})
Insert cell
pivot_bisector = d3.bisector(d => d[1])
Insert cell
Insert cell
countryNameList = {
let list = [];
for (let i = 0; i < data.length; i++) {
list.push(data[i].country_name)
}
return list;
}
Insert cell
pivot = pivot_values[pivot_n]
Insert cell
pivot_values = [1, 5, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 13000, 15000, 20000, 50000, 100000]
Insert cell
today = all_dates[all_dates.length - 1];
Insert cell
parseTime = d3.timeParse("%Y-%m-%d");
Insert cell
all_dates = data[0]['confirmed'].map(x => parseTime(x[0]));
Insert cell
// startDate = d3.timeDay(new Date(2019, 11, 31))
startDate = parseTime(data[0].confirmed[0][0])
Insert cell
d3.timeDay.count(startDate, parseTime("2020-01-02"))
Insert cell
d3.timeDay.count(startDate, today)
Insert cell
d3.timeDay(parseTime(data[0].confirmed[10][0]))
Insert cell
d3.timeDay.count(d3.timeDay(parseTime(data[0].confirmed[0][0])), today)
Insert cell
formatDate = d3.timeFormat("%Y-%m-%d")
Insert cell
last_index = goBack
Insert cell
filtered_data = data.map(function(d) {
return {
'country_code': d.country_code,
'country_name': d.country_name,
'population': d.population,
'region': d.region,
'confirmed': d.confirmed.filter(e => d3.timeDay.count(startDate, parseTime(e[0])) <= goBackSpan),
'deaths': d.deaths.filter(e => d3.timeDay.count(startDate, parseTime(e[0])) <= goBackSpan),
'confirmed_fraction': d.confirmed_fraction.filter(e => d3.timeDay.count(startDate, parseTime(e[0])) <= goBackSpan),
'confirmed_lastweek': d.confirmed_lastweek.filter(e => d3.timeDay.count(startDate, parseTime(e[0])) <= goBackSpan),
'deaths_lastweek': d.deaths_lastweek.filter(e => d3.timeDay.count(startDate, parseTime(e[0])) <= goBackSpan),
}
})
Insert cell
data = data_raw.filter(d => d.country_code != 'OWID_WRL' && d.region !== null).map(function(d) {
d.confirmed_fraction = d.confirmed.map(x => [x[0], x[1] / d.population]);
d.confirmed_lastweek = past_week(d.confirmed);
d.deaths_lastweek = past_week(d.deaths);
return d;
})
Insert cell
function past_week(a) {
var new_array = [[a[0][0], a[0][1]]];
var curr_sum = 0;
var min_idx = 0;
for (var i = 1; i < a.length; i++) {
min_idx = d3.max([0, i-7]);
if (min_idx != 0) {
curr_sum = a[i][1] - a[min_idx][1];
} else {
curr_sum = a[i][1];
}
new_array.push([ a[i][0], curr_sum ])
}
return new_array;
}
Insert cell
data_raw = d3.json("https://raw.githubusercontent.com/hongtaoh/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
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