Published
Edited
May 8, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
proxiedApiUrl = {
const demoProxyUrl = "https://cors-anywhere.herokuapp.com"; // enables cross-origin requests to anywhere
const apiUrl = "https://api.stateofjs.com/graphql";
return `${demoProxyUrl}/${apiUrl}`;
}
Insert cell
createQuery = (survey, year) => `{
survey(survey: ${survey}) {
surveyName
demographics {
country {
year(year: ${year}) {
year
buckets {
id
count
percentage
}
}
}
}
}
}`
Insert cell
createOptions = (query) => {
return {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({query})
};
}
Insert cell
fetchParticipationByCountry = async (survey, year) => {
const query = createQuery(survey, year);
const options = createOptions(query);
const response = await d3.json(proxiedApiUrl, options);
return response.data.survey.demographics.country.year;
}
Insert cell
participationByCountry = {
if (source === "graphql-api") {
return fetchParticipationByCountry(survey, year);
}

return restoreParticipationByCountry(survey, +year);
}
Insert cell
restoreParticipationByCountry = (survey, year) => {
const resultsByYear = resultsBackup.filter(d => d.surveyName === survey);
const annualResults = resultsByYear.find(d => d.demographics.country.year.year === year);
return annualResults.demographics.country.year;
}
Insert cell
resultsBackup = FileAttachment("results-backup.json").json()
Insert cell
Insert cell
// see: https://github.com/topojson/world-atlas#countries-110m.json
topology = d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
Insert cell
topojson = require("topojson-client@3") // converts TopoJSON to GeoJSON (required by D3)
Insert cell
countries = {
const countries = topojson.feature(topology, topology.objects.countries);
const usa = countries.features.find(d => d.properties.name === "United States of America");
usa.properties.name = "United States"; // shortening for the tooltip
return countries;
}
Insert cell
Insert cell
countries.features[0].id
Insert cell
Insert cell
participationByCountry.buckets[0].id
Insert cell
getParticipation = (numericCountryCode) => {
const country = countryCodes.find(d => d["country-code"] === numericCountryCode);
if (country === undefined) {
return undefined;
}
const threeLetterCode = country["alpha-3"];
return participationByCountry.buckets.find(d => d.id === threeLetterCode);
}
Insert cell
// see: https://github.com/lukes/ISO-3166-Countries-with-Regional-Codes#slim-3json
countryCodes = d3.json("https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/master/slim-3/slim-3.json")
Insert cell
Insert cell
colorSchemes = [
{
survey: "state_of_js",
backgroundColor: "#2a2d33", // closely matches page background color, mismatching Observable's background color
defaultCountryColor: "#222429",
participatingCountryColors: [
"#41c7c7", "#65e0e0", "#94eeee", "#f8a8a8", "#fe6a6a", "#ec5555", "#be3737"
],
legendTextColor: "#41c7c7" // matches original navigation bar text color
},
{
survey: "state_of_css",
backgroundColor: "#1a1f35",
defaultCountryColor: "#303652",
participatingCountryColors: [
"#3c52d1", "#808ee1", "#b2bbee", "#d3bbf2", "#d68df0", "#ec75cb", "#f649a7"
],
legendTextColor: "#a3cacd" // matches page text color
}
]
Insert cell
colorScheme = colorSchemes.find(d => d.survey === survey)
Insert cell
Insert cell
createTooltip = () => {
const tooltip = d3.create("svg:g")
.attr("id", "tooltip")
.attr("visibility", "hidden");
let tooltipWidth = 200;
const tooltipHeight = 32;
const shadowOffset = 9;
const shadow = tooltip.append("rect")
.attr("id", "tooltip-shadow")
.attr("x", shadowOffset)
.attr("y", shadowOffset)
.attr("height", tooltipHeight)
const background = tooltip.append("rect")
.attr("id", "tooltip-background")
.attr("height", tooltipHeight)
.attr("fill", "gray")
const markSideLength = 12;
const markMargin = {
top: (tooltipHeight - markSideLength) / 2,
right: 7,
left: shadowOffset
};
const mark = tooltip.append("rect")
.attr("id", "tooltip-mark")
.attr("x", markMargin.left)
.attr("y", markMargin.top)
.attr("width", markSideLength)
.attr("height", markSideLength)
const textOffset = {
x: markMargin.left + markSideLength + markMargin.right,
y: tooltipHeight / 2
};
const text = tooltip.append("text")
.attr("dx", textOffset.x)
.attr("dy", textOffset.y);
tooltip.show = (country, participation, color) => {
mark.attr("fill", color);
text.text(`${country.properties.name}: `);
text.append("tspan")
.attr("id", "participation")
.text(`${format(participation.percentage)} (${participation.count})`);
const textWidth = Math.ceil(text.node().getBBox().width);
tooltipWidth = textOffset.x + textWidth + markMargin.right;
background.attr("width", tooltipWidth);
shadow.attr("width", tooltipWidth);
tooltip.attr("visibility", "visible");
};
tooltip.move = (x, y) => {
const dx = x - (tooltipWidth / 2);
const dy = y - tooltipHeight - (shadowOffset * 2);
tooltip.attr("transform", `translate(${dx},${dy})`);
};
tooltip.hide = () => {
tooltip.attr("visibility", "hidden");
};
return tooltip;
}
Insert cell
Insert cell
Insert cell
choropleth = () => {
const svg = createCanvas()
const tooltip = createTooltip();
addCountries(svg, tooltip);
addLegend(svg);
svg.append(() => tooltip.node()); // rendering order is based on the document order
return svg.node()
}
Insert cell
createCanvas = () =>
d3.create("svg")
.attr("class", "choropleth")
.attr("width", canvasWidth)
.attr("height", canvasHeight)
.call(addBackground)
Insert cell
d3 = require("d3@6")
Insert cell
canvasWidth = Math.max(800, width) // see: https://github.com/observablehq/stdlib#width
Insert cell
Insert cell
addBackground = (parent) =>
parent.append("rect")
.attr("id", "background")
.attr("width", canvasWidth)
.attr("height", canvasHeight)
Insert cell
addCountries = (parent, tooltip) => {
const container = parent.append("g").attr("id", "countries");
countries.features.forEach(country => addCountry(container, country, tooltip));
return container;
}
Insert cell
addCountry = (parent, country, tooltip) => {
const container = parent.append("g")
.attr("class", "country")
.attr("id", country.id);
const participation = getParticipation(country.id);
const color = getCountryColor(participation);
const surface = addSurface(container, country, color);
if (participation !== undefined) {
surface.on("mouseover", e => tooltip.show(country, participation, color));
surface.on("mousemove", e => tooltip.move(e.offsetX, e.offsetY));
surface.on("mouseout", e => tooltip.hide());
}
return container;
}
Insert cell
addSurface = (parent, country, color) =>
parent.append("path")
.attr("fill", color)
.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 colorScheme.defaultCountryColor;
}
Insert cell
scale = d3.scaleThreshold()
.domain(participationPercentageSlices)
.range(colorScheme.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", colorScheme.participatingCountryColors[i]);
mark.append("text")
.attr("dx", legendMarkSideLength + 10)
.attr("dy", (legendMarkSideLength * i) + (legendMarkSideLength / 2))
.text(`${format(percentageSliceLowerBound)} - ${format(percentageSliceUpperBound)}`)
percentageSliceLowerBound = percentageSliceUpperBound;
}
}
Insert cell
Insert cell
Insert cell
format = (percentage) => `${d3.format(".1f")(percentage)}%` // 0 as '0.0%'
Insert cell
html`<style>
#background {
fill: ${colorScheme.backgroundColor};
}

.country {
stroke: ${colorScheme.backgroundColor};
stroke-width: 0.5;
}

#tooltip,
#legend {
dominant-baseline: central;
font-family: "IBM Plex Mono", monospace;
font-weight: 300;
}

#tooltip text {
fill: #273aa2;
font-size: 14px;
}

#tooltip text #participation {
font-weight: 600;
}

#tooltip-background {
fill: #e0e4e4;
}

#tooltip-shadow {
fill-opacity: .15;
}

#legend {
fill: ${colorScheme.legendTextColor};
font-size: 11px;
}
</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