Published
Edited
Sep 23, 2020
1 fork
7 stars
Insert cell
Insert cell
Insert cell
world_covid_csv = {
const text = await FileAttachment("covid-19-worldwide@1.csv").text();
return d3.csvParse(text, (d) => ({
country : d.countriesAndTerritories,
cumulative: +d.Cumulative_number_for_14_days_of_COVID_19_cases_per_100000
}));
}
Insert cell
world = FileAttachment("countries-110m.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
d3Legend = require("d3-svg-legend")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colorScale = d3.scaleSequential([0, d3.max(world_covid_csv, d => d.cumulative)], d3.interpolateYlOrRd)
Insert cell
projectionOrthographic = d3.geoOrthographic()
.scale(scale)
.translate([ width/2, height/2 ])
.rotate([0, config.verticalTilt, config.horizontalTilt]).center([0, 0]);
Insert cell
projectionMercator = d3.geoEquirectangular()
.scale(scale)
.translate([width / 2, height / 2])
.rotate([0, 0])
Insert cell
Insert cell
globe = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
//.style("overflow", "visible")
.attr("class", "globe");
var path = d3.geoPath()
.projection(projectionOrthographic);
// note: this (linearGradient) can be initialized through css
// I used d3 to put lineargradient because I can't
// seem to make css work with the Observable notebook
var linearGradient = svg.append("defs").append("linearGradient")
.attr("id", "mygrad") //id of the gradient
.attr("x1", "0%")
.attr("x2", "50%")
.attr("y1", "0%")
.attr("y2", "100%");
linearGradient.append("stop")
.attr("offset", "0%")
.style("stop-color", linear_gradient_colors[0])
.style("stop-opacity", 1)

linearGradient.append("stop")
.attr("offset", "100%")
.style("stop-color", linear_gradient_colors[1]) // start in linear_gradient_colors[1]
.style("stop-opacity", 1)
// add the background (sphere when projection is orthographic,
// rectangle when projection is mercator)
var globe_bg = svg.append("rect")
.attr("rx",1200)
.attr("ry",1200)
.attr("x", (width/2) - projectionOrthographic.scale())
.attr("y", (height/2) - projectionOrthographic.scale())
.attr("width", projectionOrthographic.scale()*2)
.attr("height", projectionOrthographic.scale()*2)
.style("fill", "url(#mygrad)")
.attr("class", "globe-bg");

// make dictionary of country to cumulative value
// This is done for convenience access to cumulative value
var world_covid_dict_csv = {};
world_covid_csv.map((e,i) => { world_covid_dict_csv[e.country]=e.cumulative});
/**
// uncomment to add graticule to Orthographic projection
var graticule = d3.geoGraticule().step([20, 20]);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", "#fff")
.style("stroke", "#ccc");
**/
// add paths of the world map to svg
svg.selectAll(".country")
.data(countries.filter(function(d){return d.properties.name != "Antarctica";}))
.enter().append("path")
.attr('stroke-linejoin', 'round')
.style("fill", function(d) {
if (world_covid_dict_csv[d.properties.name.split(' ').join('_')] != undefined){
return colorScale(world_covid_dict_csv[d.properties.name.split(' ').join('_')]);
}
else {
return "white"
}
})
.attr("class", "country")
.style("stroke", "orange")
.style("stroke-width", "0.3px")
.attr("d", path);
// add legend to svg
svg.append("g")
.attr("class", "legend_auto")
.style('font-size', 12)
.style('font-family', 'sans-serif')
.attr("transform", `translate(10,${height-120})`)
.call(legend_svg)
svg.selectAll('.label')
.attr("transform", "translate(20, 13)")
var timer;
var isOrthographic = true;
svg.append("text")
.attr("x", width/2)
.attr("y", 50)
.text("CHANGE PROJECTION TO MERCATOR")
.attr("text-anchor", "middle")
.style("font-family", "sans-serif")
.on("mouseover", function() { d3.select(this).style("text-decoration", "underline") })
.on("mouseout", function() { d3.select(this).style("text-decoration", "none") })
.on("click", function() {
d3.select(this)
.text(isOrthographic ?
"CHANGE PROJECTION TO MERCATOR" : "CHANGE PROJECTION TO ORTHOGRAPHIC")
.attr("fill", "black");
if (isOrthographic == false){
svg.select('.legendTitle')
.attr("fill", "black");
svg.selectAll('.label')
.attr("fill", "black");
transition_projection("orthographic");
}
else{
timer.stop();
transition_projection("mercator");
d3.select(this)
.attr("fill", "white");
svg.select('.legendTitle')
.attr("fill", "white");
svg.selectAll('.label')
.attr("fill", "white");
}
isOrthographic = !isOrthographic;
})
// enable rotation
enableRotation();
function enableRotation() {
if (isOrthographic == true){
transition_projection("orthographic");
timer = d3.timer(timerFunc);
}
else {
timer.stop();
projectionOrthographic.rotate();
svg.selectAll("path").attr("d", path);
transition_projection("mercator");
}
}
// function that rotates the projection
function timerFunc(elapsed) {
if (isOrthographic){
mutable elapsedTime = (config.speed * elapsed) % 360;
projectionOrthographic.rotate([(config.speed * elapsed)%360, config.verticalTilt, config.horizontalTilt]);
svg.selectAll("path").attr("d", path);
}
}

function transition_projection(projectionToTransition){
if (projectionToTransition == "mercator"){
projectionOrthographic.rotate([360, config.verticalTilt, config.horizontalTilt]);
// first transition = rotate (reset) orthographic projection to its default position
// why do this: the rotation of the Orthographic projection affects the position (coordinate position // [x,y]) of the Mercator projection
// you can comment out the first transition and .attr("d", path) too see what I'm saying
var first_transition = d3.transition()
.duration(500)
.ease(d3.easeLinear);
var second_transition = d3.transition()
.duration(500)
.ease(d3.easeCubicInOut);
svg.selectAll("path")
.transition(first_transition)
.attr("d", path)
.transition(second_transition)
.attrTween("d", projectionTween(projectionOrthographic, projectionMercator));
svg.select(".globe-bg")
.transition(second_transition)
.delay(500)
.attr("rx",0)
.attr("ry",0)
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
}
else
{
svg.selectAll("path").transition("mercator-to-ortho")
.duration(1000).ease(d3.easeCubicInOut)
.attrTween("d", projectionTween(projectionMercator, projectionOrthographic));
svg.select(".globe-bg")
.transition("mercator-to-ortho")
.duration(750)
.ease(d3.easeCubicInOut)
.attr("rx",200)
.attr("ry",200)
.attr("x", (width/2) - projectionOrthographic.scale())
.attr("y", (height/2) - projectionOrthographic.scale())
.attr("width",projectionOrthographic.scale()*2)
.attr("height",projectionOrthographic.scale()*2)
.style("fill", "url(#mygrad)")
.on("end", timerRestart);
}
}
function timerRestart(){
timer.restart(timerFunc);
}
yield svg.node();
}
Insert cell
mutable elapsedTime = 0
Insert cell
function projectionTween(projection0, projection1) {
return function(d) {
var t = 0;
var projection = d3.geoProjection(project)
.scale(1)
.translate([width / 2, height / 2]);

var path = d3.geoPath()
.projection(projection);

function project(λ, φ) {
λ *= 180 / Math.PI, φ *= 180 / Math.PI;
var p0 = projection0([λ, φ]), p1 = projection1([λ, φ]);
return [(1 - t) * p0[0] + t * p1[0], (1 - t) * -p0[1] + t * -p1[1]];
}

return function(_) {
t = _;
//console.log(path(d));
return path(d);
};
};
}

Insert cell
ease = d3.easeCubicInOut
Insert cell
function interpolateProjection(raw0, raw1) {
const {scale: scale0, translate: translate0} = fit(raw0);
const {scale: scale1, translate: translate1} = fit(raw1);
return t => d3.geoProjection((x, y) => lerp2(raw0(x, y), raw1(x, y), t))
.scale(lerp1(scale0, scale1, t))
.translate(lerp2(translate0, translate1, t))
.precision(0.1);
}
Insert cell
function lerp1(x0, x1, t) {
return (1 - t) * x0 + t * x1;
}
Insert cell
function lerp2([x0, y0], [x1, y1], t) {
return [(1 - t) * x0 + t * x1, (1 - t) * y0 + t * y1];
}
Insert cell
function fit(raw) {
const outline = ({type: "Sphere"});
const p = d3.geoProjection(raw).fitExtent([[0.5, 0.5], [width - 0.5, height - 0.5]], outline);
return {scale: p.scale(), translate: p.translate()};
}
Insert cell
legend_svg = {
const svg = d3.select(DOM.svg(500, 500))

var cases_format = d3.format(".2s")

var legendSequential = d3Legend.legendColor()
.shapeHeight(15)
.shapePadding(20)
.orient("vertical")
.labelOffset(20)
.cells(5)
.labelFormat(cases_format)
.scale(colorScale)
.title("Cumulative number for 14 days of COVID 19 cases per 100,000 [Aug 28]")
return legendSequential
}
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