Published
Edited
Sep 6, 2020
1 fork
Importers
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const nodes = data;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])

//create background
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.attr("class", "svgBackground");
svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);
//Create the datapoints as circles with radius and colour corresponding to population size
const bubble = svg.append("g")
.attr("stroke-width", 1)
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("opacity", 0.75)
.attr("cx", d => x(d.birthRate))
.attr("cy", d => y(d.deathRate))
.attr("r", d => r(d.population))
.attr("stroke", d => color(d.population))
.attr("fill", d => color(d.population))
bubble.append("title")
.text(tooltip)

//Create the ISO country codes as text elements
const label = svg.append("g")
.attr("font-family", "Yanone Kaffeesatz")
.attr("font-weight", 700)
.attr("text-anchor", "middle")
.selectAll("text")
.data(data)
.join("text")
.attr("id", "isoCode")
.attr("opacity", 0)
.attr("dy", "0.35em")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("font-size", d => r(d.population)*1.5)
.attr("fill", d => color(d.population))
.text(d => d.code);
//add a title to act as a mousover tooltip, function tooltip() defined in a cell bleow
label.append("title")
.text(tooltip);
const legend1 = svg.append("g")
.attr("transform", `translate(${width - 340} ${height - 90})`)
.append(() => legend({
color: color, // <= this is the scale "color" being passed into field "color"
title: "Population (in millions)",
ticks: 4,
tickFormat: d => d3.format(",.0f")(d / 1000000)
}))
// sort of hack-y collision here - we are approximating the text labels as "square" (only possible due to the short 2 char ISO codes) with a maximum radius aprox. equal to the radius of the corresponding bubble (bubble_rad*0.7) and running collision on that. This is not true rectangular text element collision detection - see my Pokemon Force Graph for that.
const simulation = d3.forceSimulation(nodes)
.force("collide", d3.forceCollide(d => d.radius * 0.7))
.force("x", d3.forceX(d => d.x0))
.force("y", d3.forceY(d => d.y0));
simulation.on("tick", () => {
label.attr("x", d => d.x)
.attr("y", d => d.y);
});

invalidation.then(() => simulation.stop());
return svg.node();
}
Insert cell
Insert cell
Insert cell
data = Object.assign(d3.csvParse(await FileAttachment("BirthDeath_pop_sort.csv").text(),
({country, code, population, birth_rate, death_rate}) =>
({country, code, population: +population, birthRate: +birth_rate,
deathRate: +death_rate})),
{xLabel: "Birth Rate (per 1000 people) →",
yLabel: "↑ Death Rate (per 1000 people)"})
Insert cell
// script that adds some extra attributes to the data for use with the force simulation. Unfortunately this script cannot be in the same cell as the other data parsing due to dependencies. Setting values data.x0, y0, radius => depends on scales x(), y(), r() => based on data.extent(d.variable). Declaring these before the scales are created would be a circular reference.
{
data.forEach(function(d){
d.x0 = x(d.birthRate) //home x-position
d.y0 = y(d.deathRate) //home y-position
d.radius = r(d.population) // radius (const)
})
}
Insert cell
// function for the text formating for mouseover tooltips
tooltip = function(d){
return d.code + ": " + d.country +
"\nBirth Rate: " + d.birthRate +
"\nDeath Rate: " + d.deathRate +
"\nPopulation: " + d3.format(",.3f")(d.population / 1000000) + "m";}
Insert cell
// function that updates the svg elements when the checkbox is clicked (hide circle fill and show country codes)
{
d3.selectAll("circle")
.transition()
.duration(200)
.style("fill-opacity", (state === "toggle")? 0:1)
d3.selectAll("text#isoCode")
.transition()
.duration(200)
.style("opacity", (state === "toggle")? 1:0)
}
Insert cell
Insert cell
x = d3.scaleLinear()
.domain([d3.min(data, d => d.birthRate) * 0.9, d3.max(data, d => d.birthRate)])
.range([margin.left, width - margin.right])
Insert cell
y = d3.scaleSqrt()
.domain([d3.min(data, d => d.deathRate) * 0.9, d3.max(data, d => d.deathRate)])
.range([height - margin.bottom, margin.top])
Insert cell
// note, the lower bount on the range is 9: we want a minimum size so that small countries aren't too small to read
r = d3.scaleSqrt()
.domain(d3.extent(data, d => d.population))
.range([9,25])
Insert cell
color = {
// only want a subset of the Viridis color scale. Want perceptually uniform color but high values of Viridis blend into the background too much and text is no longer legible. Piecewise CIELAB interpolation preserves Viridis' uniform colorscale as close as possible. If only 2 values of Viridis were given then it would directly interpolate between them giving something that looks nothing like Viridis. Instead we have to use a number of smaller increments to get aprox. Viridis

//Using a sequential Quantile scale because the color of the data is showing relative spread in population ranking (ie. China 1st, India 2nd, ...) not a direct comparison of population size (ie. China - 1.2bil vs.Latvia 2.4mil). Since China and India are so much larger than most other nations they act as outliers. Power / Sqrt scales could also be used but they require messing with the scaling factor.
const clr = d => d3.interpolateViridis(d)
return d3.scaleSequentialQuantile(d3.piecewise(d3.interpolateLab,
[clr(0), clr(0.1), clr(0.2), clr(0.3), clr(0.4), clr(0.5), clr(0.6), clr(0.7), clr(0.8), clr(0.9)]))
.domain(data.map(d => d.population))
}
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width/100))
.call(g => g.append("text")
.attr("x", width)
.attr("y", margin.bottom - 4)
.attr("fill", "#263c54")
.attr("text-anchor", "end")
.text(data.xLabel))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 15)
.attr("fill", "#263c54")
.attr("text-anchor", "start")
.attr("font-family", "Lato")
.attr("font-weight", 400)
.text(data.yLabel))
Insert cell
Insert cell
d3 = require("d3@5")
Insert cell
import {legend} from "@d3/color-legend"
Insert cell
import {checkbox} from "@jashkenas/inputs"
Insert cell
Insert cell
Insert cell
html`
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Yanone+Kaffeesatz:wght@700&display=swap" rel="stylesheet">
<style>

.svgBackground {fill: #fffbeb;}

.title {
font: 24px "Lato", sans-serif;
fill: #263c54;
font-weight: 700;
}

.other {
font: 20px "Lato", sans-serif;
fill: #263c54;
font-weight: 400;
}
</style>`
Insert cell
Insert cell
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