Public
Edited
Mar 1, 2024
5 forks
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// compute min and max years
Insert cell
// filter to initial year
Insert cell
Insert cell
Insert cell
Insert cell
x = d3
// TODO: create scale for x axis
Insert cell
y = d3
// TODO: create scale for y axis
Insert cell
Insert cell
color = d3

Insert cell
Insert cell
size = d3
// TODO: create size encoding
Insert cell
Insert cell
{
// create the container SVG element
const svg = d3.create("svg").attr("viewBox", viewBox);

// position and populate the x-axis
svg
.append("g")

// position and populate the y-axis
svg
.append("g")

// TODO: add circle elements for each country
// use scales to set fill color, x, y, and radius

return svg.node();
}
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("width", width).attr("height", height);

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x))
// Add x-axis title 'text' element.

svg
.append("g")
.call(d3.axisLeft(y))
.attr("transform", `translate(${margin.left}, 0)`)
// TODO: Add y-axis title 'text' element.

// TODO: Add a background label for the current year.
svg.append("text")


const countries = svg
.selectAll("circle")
.data(dataInitial)
.join("circle")
// TODO: sort so smaller circles are drawn last
.sort((a, b) => b.pop - a.pop)
.attr("opacity", 0.75)
.attr("fill", (d) => color(d.cluster))
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));
// TODO: add a tooltip
// TODO: Add mouse hover interactions, using D3 to update attributes directly.
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof yearFilter = Scrubber(
// TODO(tukey): construct scrubber
)
Insert cell
{
const svg = d3.create("svg").attr("viewBox", viewBox);

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("x", width)
.attr("y", -10)
.text("Fertility");

svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "translate(20, 0) rotate(-90)")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Life Expectancy");

const yearLabel = svg
.append("text")
.attr("class", "year")
.attr("x", 40)
.attr("y", height - 20)
.attr("fill", "#ccc")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-weight", 500)
.attr("font-size", 80)
.text(years[0]); // <-- TODO(tukey): update to use yearFilter

const countries = svg
.selectAll("circle")
.data() // <-- TODO(tukey): update to use yearFilter
.join("circle")
.sort((a, b) => b.pop - a.pop)
.attr("class", "country")
.attr("opacity", 0.75)
.attr("fill", (d) => color(d.cluster));

countries
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));

countries.append("title").text((d) => d.country);

countries
.on("mouseover", function () {
d3.select(this).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
viewof yearAnimate = Scrubber(
d3.range(years[0], years[1] + 1, 5),
{ autoplay: false, delay: 1000, loop: false }
)
Insert cell
chartAnimate.changeYear(yearAnimate)
Insert cell
chartAnimate = {
const svg = d3.create("svg").attr("viewBox", viewBox);

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("x", width)
.attr("y", -10)
.text("Fertility");

svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "translate(20, 0) rotate(-90)")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Life Expectancy");

const yearLabel = svg
.append("text")
.attr("class", "year")
.attr("x", 40)
.attr("y", height - 20)
.attr("fill", "#ccc")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-weight", 500)
.attr("font-size", 80)
.text(years[0]); // <-- simply use the minimum year, as updates occur elsewhere

const countries = svg
.selectAll("circle.country")
// TODO(tukey): add key function
.data(dataInitial)
.join("circle")
.sort((a, b) => b.pop - a.pop)
.attr("class", "country")
.attr("opacity", 0.75)
.attr("fill", (d) => color(d.cluster))
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));

countries.append("title").text((d) => d.country);

countries
.on("mouseover", function () {
d3.select(this).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
});

// TODO(tukey): update function

function changeYear(year) {
// (1) data to the given year
// (2) locations, size
// (3) sort
// (4) yearLabel

}

// TODO(tukey): extend SVG node
return svg.node()
}
Insert cell
(chartAnimate.changeYear(yearAnimate),
md`Average Fertility & Life Expectancy by Country in ${yearAnimate}`)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gapminder
Insert cell
regions = [
{ index: 0, label: 'South Asia' },
{ index: 1, label: 'Europe & Central Asia' },
{ index: 2, label: 'Sub-Saharan Africa' },
{ index: 3, label: 'America' },
{ index: 4, label: 'East Asia & Pacific' },
{ index: 5, label: 'Middle East & North Africa' }
];
Insert cell
Insert cell
Insert cell
{
const svg = d3.create('svg')
.attr('width', 200)
.attr('height', 110);
const legend = svg.append('g')
.attr('transform', 'translate(0, 10)')
.call(colorLegend); // <-- our legend helper is invoked just like an axis generator

return svg.node();
}
Insert cell
function colorLegend(container) {
const titlePadding = 14; // padding between title and entries
const entrySpacing = 16; // spacing between legend entries
const entryRadius = 5; // radius of legend entry marks
const labelOffset = 4; // additional horizontal offset of text labels
const baselineOffset = 4; // text baseline offset, depends on radius and font size

const title = container
.append("text")
// TODO(tukey): create legend's title
const entries = container
.selectAll("g")
// TODO(tukey): create and entry for each label

const symbols = entries
.append("circle")
// TODO(tukey): add color indication for each label

const labels = entries
.append("text")
// TODO(tukey): add text labels
}
Insert cell
Insert cell
Insert cell
chartLegend = {
const svg = d3.create("svg").attr("viewBox", viewBox);

svg
.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x))
.append("text")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("x", width)
.attr("y", -10)
.text("Fertility");

svg
.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "translate(20, 0) rotate(-90)")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Life Expectancy");

const yearLabel = svg
.append("text")
.attr("class", "year")
.attr("x", 40)
.attr("y", height - 20)
.attr("fill", "#ccc")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-weight", 500)
.attr("font-size", 80)
.text(1955);

// Add and position the legend; place in the upper right corner.
// TODO(tukey): add legend
svg
.append("g")
.attr("transform", `translate(${(width - 150)}, 10)`)
const countries = svg
.selectAll("circle.country")
.data(dataInitial, (d) => d.country)
.join("circle")
.sort((a, b) => b.pop - a.pop)
.attr("class", "country")
.attr("opacity", 0.75)
.attr("fill", (d) => color(d.cluster))
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));

countries.append("title").text((d) => d.country);

countries
.on("mouseover", function () {
d3.select(this).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
});

function changeYear(year) {
yearLabel.text(year);
countries
.data(
gapminder.filter((d) => d.year === year),
(d) => d.country
)
.sort((a, b) => b.pop - a.pop)
.transition()
.duration(1000)
.ease(d3.easeCubic)
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));
}

// TODO(tukey): consider using a legend library like https://observablehq.com/@d3/color-legend
return Object.assign(svg.node())
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// container is a d3 selection for the container group (<g>) element
// selmodel is a selection model instance for tracking selected legend entries
function legend(container, selmodel) {
const titlePadding = 14;
const entrySpacing = 16;
const entryRadius = 5;
const labelOffset = 4;
const baselineOffset = 4;

const title = container
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("fill", "black")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-weight", "bold")
.attr("font-size", "12px")
.text("Region");

// The "on" method registers event listeners
// We update the selection model in response
const entries = container
.selectAll("g")
.data(regions)
.join("g")
.attr(
"transform",
(d) => `translate(0, ${titlePadding + d.index * entrySpacing})`
)
// TODO(vishal): handle interaction
.on("click", (evt, d) => selmodel.toggle(d.index))
.on("dblclick", (evt) => selmodel.clear());

const symbols = entries
.append("circle")
.attr("cx", entryRadius)
.attr("r", entryRadius)
.attr("fill", (d) => color(d.index));

const labels = entries
.append("text")
.attr("x", 2 * entryRadius + labelOffset)
.attr("y", baselineOffset)
.attr("fill", "black")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-size", "11px")
.style("user-select", "none")
.text((d) => d.label);

// Listen to selection model, update symbol and labels upon changes
selmodel.on("change.legend", () => {
// TODO(vishal): add effects from legend's interaction
// This is an event listener that executes when the legend selection is changed, so when something is toggled on or off.
// symbols.attr("fill", (d) => {
// if (selmodel.has(d.index)) {
// return color(d.index);
// } else {
// return "#ccc";
// }
// We can simplify this with a JavaScript ternary operator, which I really like since they're so concise
symbols.attr("fill", (d) =>
selmodel.has(d.index) ? color(d.index) : "#ccc"
);
labels.attr("fill", (d) => (selmodel.has(d.index) ? "black" : "#bbb"));
});
}
Insert cell
Insert cell
{
const selmodel = SelectionModel(); // <-- Instantiate a selection model
const svg = d3.create("svg").attr("width", 200).attr("height", 110);

svg
.append("g")
.attr("transform", "translate(0, 10)")
.call((container) => legend(container, selmodel));

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gapminder
Insert cell
// We need a messy data set for testing.
// Edit the data so that it's missing some countries for some years.
dataDynamic = gapminder
.filter((d) => d.cluster !== 4 || d.year >= 1980)
.filter((d) => d.cluster !== 3 || d.year <= 1960)
Insert cell
viewof yearDynamic = Scrubber(d3.range(years[0], years[1] + 1, 5), {
autoplay: false,
delay: 1000,
loop: false
})
Insert cell
Insert cell
chartDynamic = {
const svg = d3.create("svg").attr("viewBox", viewBox);

const selmodel = SelectionModel(); // <-- Instantiate a selection model

svg
.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x))
.append("text")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("x", width)
.attr("y", -10)
.text("Fertility");

svg
.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "translate(20, 0) rotate(-90)")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text("Life Expectancy");

const yearLabel = svg
.append("text")
.attr("class", "year")
.attr("x", 40)
.attr("y", height - 20)
.attr("fill", "#ccc")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-weight", 500)
.attr("font-size", 80)
.text(years[0]);

svg
.append("g")
.attr("transform", `translate(${width - 150}, 10)`)
.call((container) => legend(container, selmodel));

let countries = svg
.selectAll("circle.country")
.data(
dataDynamic.filter((d) => d.year === years[0]),
(d) => d.country
)
.join("circle")
.attr("class", "country")
.sort((a, b) => b.pop - a.pop)
.attr("opacity", 0.75)
.attr("fill", (d) => color(d.cluster))
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop));

countries.append("title").text((d) => d.country);

countries
.on("mouseover", function () {
d3.select(this).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
});

function changeYear(year) {
yearLabel.text(year);

countries = countries
.data(
dataDynamic.filter((d) => d.year === year),
(d) => d.country
)
// what we had before
// .join("circle")

.join(
// TODO: Add code to customize how countries enter the scene.
// Idea: fade in from transparent and grow from zero size
// Make sure new elements have their properties properly initialized!
(enter) =>
enter
.append("circle")
.attr("class", "country")
.attr("fill", (d) => color(d.cluster))
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => 0),

(update) => update, // countries that still exist

// TODO: Add code to customize how countries exit the scene.
// Idea: fade out to transparent and shrink to zero size before removal
(exit) =>
exit
.transition()
.duration(1000)
.attr("r", 0)
.attr("opacity", 0)
.remove()
);

// TODO: Animate enter + update countries to current position and size
// Hint: If you modify opacity above, you probably want to update it here!
countries
.sort((a, b) => b.pop - a.pop)
.transition()
.duration(1000)
.attr("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop))
.attr("opacity", 0.75);

countries
.on("mouseover", function () {
d3.select(this).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function () {
d3.select(this).attr("stroke", null);
});
}

return Object.assign(svg.node(), { changeYear });
}
Insert cell
Insert cell
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