Published
Edited
May 8, 2021
1 fork
Importers
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// see: https://2020.stateofcss.com/en-US/demographics/country
participationByCountry = FileAttachment("participation-by-country.json").json()
Insert cell
countries = {
const shallowCopy = filterProperties(naturalEarthGeo);
adjustNames(shallowCopy);
return d3.geoStitch(shallowCopy);
}
Insert cell
// see: https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/
naturalEarthGeo = {
const compressedFileBuffer = await (await FileAttachment("ne_110m_admin_0_countries.zip")).arrayBuffer();
const archive = await jszip.loadAsync(compressedFileBuffer)
const shp = await archive.file("ne_110m_admin_0_countries.shp").async("arraybuffer"); // shapes
const dbf = await archive.file("ne_110m_admin_0_countries.dbf").async("arraybuffer"); // metadata
const options = {encoding: "utf-8"}; // defaults to "windows-1252" otherwise
return shapefile.read(shp, dbf, options);
}
Insert cell
// see: https://observablehq.com/@mbostock/hello-jszip
jszip = require("jszip@3/dist/jszip.min.js")
Insert cell
shapefile = require("shapefile@0.6")
Insert cell
filterProperties = (geo) => {
const shallowCopy = {
type: geo.type,
features: []
};
for (const feature of geo.features) {
shallowCopy.features.push({
type: feature.type,
id: feature.properties.ADM0_A3, // ISO 3166-1 alpha-3 code
properties: {
commonName: feature.properties.NAME_LONG
},
geometry: feature.geometry
});
}
return shallowCopy;
}
Insert cell
adjustNames = (geo) => {
const properties = geo.features.map(d => d.properties);
for (let country of properties) {
if (commonNameAdjustments.has(country.commonName)) {
country.commonName = commonNameAdjustments.get(country.commonName);
}
}
}
Insert cell
commonNameAdjustments = new Map([
["Brunei Darussalam", "Brunei"],
["Timor-Leste", "East Timor"],
["eSwatini", "Eswatini"],
["The Gambia", "Gambia"],
["Côte d'Ivoire", "Ivory Coast"],
["Lao PDR", "Laos"],
["Dem. Rep. Korea", "North Korea"],
["Russian Federation", "Russia"],
["Republic of Korea", "South Korea"],
["Macedonia", "North Macedonia"],
])
Insert cell
d3 = require("d3@6", "d3-geo-projection@3")
Insert cell
Insert cell
choropleth = () =>
createCanvas()
.call(addBackground)
.call(addCountries)
.call(addLegend)
.node()
Insert cell
createCanvas = () =>
d3.create("svg")
.attr("class", "choropleth")
.attr("width", canvasWidth)
.attr("height", canvasHeight)
Insert cell
canvasWidth = Math.max(800, width) // see: https://github.com/observablehq/stdlib#width
Insert cell
canvasHeight = 500
Insert cell
addBackground = (container) =>
container.append("rect")
.attr("id", "background")
.attr("width", canvasWidth)
.attr("height", canvasHeight)
Insert cell
addCountries = (container) => {
const group = container.append("g")
.attr("id", "countries");
for (const country of countries.features) {
const participation = participationByCountry.buckets.find(d => d.id === country.id);
addCountry(group, country, participation);
}
}
Insert cell
addCountry = (container, country, participation) =>
container.append("g")
.attr("class", "country")
.attr("id", country.id)
.call(addAnnotation, country, participation)
.call(addSurface, country, participation)
Insert cell
addAnnotation = (container, country, participation) => {
const participationPercentage = participation?.percentage ?? 0;
const participantCount = participation?.count ?? 0;
const annotation = `${country.properties.commonName}: ${format(participationPercentage)} (${participantCount})`;
container.append("title")
.text(annotation);
}
Insert cell
addSurface = (container, country, participation) =>
container.append("path")
.attr("fill", getCountryColor(participation))
.attr("d", path(country))
Insert cell
path = d3.geoPath(projection)
Insert cell
// for projection parameters, see: https://github.com/StateOfJS/StateOfCSS-2020/blob/master/src/core/charts/demographics/ParticipationByCountryChart.js
projection = d3.geoMercator()
.rotate([-11, 0])
.scale(118)
.translate([0.5 * canvasWidth, 0.7 * canvasHeight])
Insert cell
getCountryColor = (participation) => {
if (participation !== undefined) {
return scale(participation.percentage);
}
return defaultCountryColor;
}
Insert cell
scale = d3.scaleThreshold()
.domain(participationPercentageSlices)
.range(participatingCountryColors)
Insert cell
participationPercentageSlices = [1.1, 2.3, 3.4, 4.6, 5.7, 6.9, 8]
Insert cell
addLegend = (container) => {
const dx = legendMargin.left;
const dy = canvasHeight - ((participationPercentageSlices.length * legendMarkSideLength) + legendMargin.bottom);
const legend = container.append("g")
.attr("id", "legend")
.attr("transform", `translate(${dx},${dy})`);
let percentageSliceLowerBound = 0;
for (let i = 0; i < participationPercentageSlices.length; i++) {
const percentageSliceUpperBound = participationPercentageSlices[i];
const mark = legend.append("g")
.attr("class", "mark");
mark.append("rect")
.attr("y", legendMarkSideLength * i)
.attr("width", legendMarkSideLength)
.attr("height", legendMarkSideLength)
.attr("fill", participatingCountryColors[i]);
mark.append("text")
.attr("dx", legendMarkSideLength + 10)
.attr("dy", (legendMarkSideLength * i) + (legendMarkSideLength / 2))
.text(`${format(percentageSliceLowerBound)} - ${format(percentageSliceUpperBound)}`)
percentageSliceLowerBound = percentageSliceUpperBound;
}
}
Insert cell
legendMargin = ({top: 0, right: 0, bottom: 30, left: 30})
Insert cell
Insert cell
format = (percentage) => `${d3.format(".1f")(percentage)}%` // 0 as '0.0%'
Insert cell
// see: https://observablehq.com/@d3/color-schemes
shadesOfGrey = {
const participatingCountryShadeCount = participationPercentageSlices.length;
const additionalShadeCount = 7; // background; hovered / default country / border; annotation / text / link colors
const totalShadeCount = participatingCountryShadeCount + additionalShadeCount;
const shadesOfGrey = []; // Britisch spelling for consistency reasons
for (let i = 0; i < totalShadeCount; i++) {
const discreteValue = i / (totalShadeCount - 1); // a number in the range [0, 1]
const rgbSpecifier = d3.interpolateGreys(discreteValue); // CSS color specifier such as 'rgb(247, 234, 186)'
const hexColorCode = d3.rgb(rgbSpecifier).hex();
shadesOfGrey.push(hexColorCode);
}
return shadesOfGrey;
}
Insert cell
hoveredCountryColor = shadesOfGrey[0] // white
Insert cell
// lightest grey after white (in our greyscale / monochromatic / sequential single-hue color scheme)
backgroundColor = shadesOfGrey[1]
Insert cell
defaultCountryColor = shadesOfGrey[2]
Insert cell
countryBorderColor = shadesOfGrey[3]
Insert cell
participatingCountryColors = shadesOfGrey.slice(4, 4 + participationPercentageSlices.length);
Insert cell
legendTextColor = shadesOfGrey[shadesOfGrey.length - 1] // black
Insert cell
// darkest grey before black; considered to be more pleasant to the eye than pure black (as to your toner cartridge)
textColor = shadesOfGrey[shadesOfGrey.length - 2]
Insert cell
// slightly brighter than the text color; compensating contrast shortage with bolder font-weight
linkColor = shadesOfGrey[shadesOfGrey.length - 3]
Insert cell
html`<style>
.h2,
.h3,
.lead,
#legend {
font-family: "IBM Plex Mono", monospace;
}

.h2,
.h3,
.lead {
color: ${textColor};
max-width: unset;
}

.h2,
.h3 {
font-weight: 600;
}

.h2 {
font-size: 2rem;
margin: 0 0 20px 0;
}

.h3 {
border-bottom: 1px dashed;
font-size: 1.17em;
padding: 20px 0 10px 0;
}

.lead,
#legend {
font-weight: 300;
}

.lead a[href] {
color: ${linkColor};
font-weight: 600;
}

#background {
fill: ${backgroundColor};
}

.country {
fill: ${defaultCountryColor};
stroke: ${countryBorderColor};
}

.country:hover > path {
fill: ${hoveredCountryColor};
}

#legend {
dominant-baseline: central;
fill: ${legendTextColor};
font-size: 12px;
}
</style>`
Insert cell
html`<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;600&display=swap" rel="stylesheet">`
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