Public
Edited
Dec 8, 2023
Insert cell
Insert cell
Insert cell
width = 900
Insert cell
Insert cell
highlight = ["United States", "Australia", "Germany", "South Korea", "Spain"]
Insert cell
Insert cell
highlight[2]
Insert cell
Insert cell
margin = ({ left: 100, right: 100, top: 20, bottom: 60 })
Insert cell
margin.right
Insert cell
Insert cell
chartWidth = width - margin.left - margin.right;
Insert cell
Insert cell
bump-chart.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
data = d3
.csvParse(await FileAttachment("bump-chart.csv").text(), d3.autoType)
.filter((d) => d.measure != "GDP at PPP* per person in labour force")
Insert cell
Insert cell
measures = data.map((d) => d.measure)
Insert cell
Insert cell
measuresUnique = [...new Set(measures)]
Insert cell
Insert cell
x = d3.scaleBand()
.domain(measuresUnique)
.range([0, chartWidth]);
Insert cell
Insert cell
colour = d3
.scaleOrdinal()
.domain(highlight)
.range(["#f6423c", "#fbaaa7", "#3dbbd1", "#1f5c99", "#66cccc"]);
Insert cell
Insert cell
ranks = d3.range(
...d3.extent(data, (d) => d.rank).map((d, i) => (i === 0 ? d : d + 1))
)
Insert cell
Insert cell
viewof rowHeight = Inputs.range([10,30])
Insert cell
chartHeight = ranks.length * rowHeight
Insert cell
y = d3.scaleBand()
.domain(ranks)
.range([0, chartHeight]);
Insert cell
Insert cell
nested = d3.groups(data, (d) => d.country)
Insert cell
line = d3
.line()
.x((d) => x(d.measure) + x.bandwidth() / 2) // adding half of the bandwidth to the x-position centres the datapoints
.y((d) => y(d.rank))
.curve(d3.curveBumpX) // sets the type of line interpolation
Insert cell
Insert cell
Insert cell
Insert cell
chart = {

// Variables inside brackets are local variables. They can't be referenced in other parts of the notebook
// Create an svg and give in the correct dimensions. Again we are chaining functions together
const svg = d3
.create("svg")
.attr("width", chartWidth + margin.left + margin.right)
.attr("height", chartHeight + margin.top + margin.bottom);

svg.append("style").html(style);

// declare an svg group <g> element named "g" and translate it to the correct position
// in html coordinates start in the top-left hence why we have to translate by margin.top rather than margin.bottom
const g = svg
.append("g")
.attr("transform", `translate(${[margin.left, margin.top]})`);

// add another group element to "g" and call the x-axis within it
g.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${chartHeight})`)
.call(d3.axisBottom(x));

// add lines
g.append("g")
.attr("class", "links")
.selectAll("path") // confusingly in d3 you have to select the path before you actually draw it
.data(nested)
// for every element in the nested array it will create a path
.join("path")
.attr("fill", "none")
.attr("stroke", (d) =>
// this gives it a colour if it's in the array "highlight" otherwise it defaults to stone
// we use d[0] to access the country name from the nested array
highlight.includes(d[0]) ? colour(d[0]) : "#b3b09e"
)
.attr("stroke-width", 1.5)
// we use d[1] to access the data from the nested array
.attr("d", (d) => line(d[1]));

// add circles
g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data) // join to ungrouped data because we want a circle for every datapoint
.join("circle")
.attr("fill", (d) =>
highlight.includes(d.country) ? colour(d.country) : "#b3b09e"
)
.attr("stroke", "white")
.attr(
"transform",
(d) => `translate(${x(d.measure) + x.bandwidth() / 2},${y(d.rank)})`
)
.attr("r", 4); // set radius

// add country labels
g.append("g")
.attr("class", "labels")
.selectAll("text")
.data(
//only to final value of the x-scale
data.filter((d) => d.measure == measuresUnique[measuresUnique.length-1])
)
.join("text")

.attr(
"transform",
(d) => `translate(${x(d.measure) + x.bandwidth() / 2},${y(d.rank)})`
)
.text((d) => d.country)
.attr("dy", 4)
.attr("dx", 8);

// add rank labels
g.append("g")
.attr("class", "labels")
.selectAll("text")
.data(
data
.filter((d) => d.measure == measuresUnique[0])
.filter((d) => d.rank % 5 === 0 || d.rank == 1) //only to ranks divisible by five, or to the first rank
)
.join("text")

.attr(
"transform",
(d) => `translate(${x(d.measure) + x.bandwidth() / 2},${y(d.rank)})`
)
.text((d) => d.rank)
.attr("dy", 4)
.attr("dx", -8)
.attr("text-anchor", "end");

// return the outer SVG element so it can be rendered
return svg.node();
}
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