Public
Edited
Jan 26, 2023
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
years = d3.extent(gapminder, d => d.year); // gives the lowest and highest value
Insert cell
dataInitial = gapminder.filter(d => d.year === years[0]); // filter the data
Insert cell
Insert cell
width
Insert cell
Insert cell
margin = ({top: 10, right: 10, bottom: 20, left: 20}); // define margin
Insert cell
viewBox = [-20, -10, width + 30, height + 30]; // define viewBox -> translates to coordinates
Insert cell
Insert cell
x = d3
.scaleLinear()
.domain([0, d3.max(gapminder, d => d.fertility)])
.range([0, width])
.nice(); // rounds and behaves "nicer"
// create scale for x axis (fertility)
Insert cell
y = d3
// create scale for y axis (life expectancy)
.scaleLinear()
.domain([0, d3.max(gapminder, d => d.life_expect)])
.range([height, 0]) // you need to start from the height and then go to 0 for the y-axis
.nice();
Insert cell
Insert cell
color = d3
.scaleOrdinal()
.domain(gapminder.map(d => d.cluster))
.range(d3.schemeTableau10);
// create scale for color
Insert cell
Insert cell
size = d3
.scaleSqrt()
// create size encoding
.domain(d3.extent(gapminder, d => d.pop)) // population doesn't actually start at 0
.range([4, 35]) // when we are encoding with areas, we don't want to have 0 (but use small values like 4) so that you can see it
Insert cell
Insert cell
{
// create the container SVG element
const svg = d3.create("svg").attr("viewBox", viewBox);

// TODO(Yu): position and populate the x-axis
svg.append("g").call(d3.axisBottom(x)).attr("transform", `translate(0, ${height})`); // translate evaluates at 0, height

// TODO(Yu): position and populate the y-axis
svg.append("g").call(d3.axisLeft(y));

// TODO(Yu): add circle elements for each country
// use scales to set fill color, x, y, and radius
const countries = svg.selectAll("circle")
.data(dataInitial)
.join("circle")
.attr("cx", d => x(d.fertility))
.attr("cy", d => y(d.life_expect))
.attr("r", d => size(d.pop)) // size of the circle
.attr("fill", d => color(d.cluster)) // add color
.attr("opacity", 0.75) // opacity

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

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

svg
.append("g")
.call(d3.axisLeft(y))
// TODO(Yu): Add y-axis title 'text' element.
.append("text")
.text("Life Expectancy")
.attr("transform", "translate(20, 0) rotate(-90)")
.attr("fill", "black")
.attr("font-size", "12px")
.attr("font-weight", "bold")
.attr("text-anchor", "end")

// TODO(Yu): Add a background label for the current year.
svg.append("text")
.text(years[0])
.attr("x", 40)
.attr("y", height - 20)
.attr("fill", "#ccc")
.attr("font-size", 80)
.attr("font-weight", 500)
.attr("gont-family", "Helvetica neue, Arial")

const countries = svg
.selectAll("circle")
.data(dataInitial)
.join("circle")
.sort((a, b) => b.pop - a.pop)
.attr("cx", d => x(d.fertility))
.attr("cy", d => y(d.life_expect))
.attr("r", d => size(d.pop)) // size of the circle
.attr("fill", d => color(d.cluster)) // add color
.attr("opacity", 0.75) // opacity

// TODO(Yu): add a tooltip
countries.append("title").text(d => d.country)
// TODO(Yu): Add mouse hover interactions, using D3 to update attributes directly.
countries
.on("mouseover", function (evt) {
d3.select(evt.target).attr("stroke", "#333").attr("stroke-width", 2);
})
.on("mouseout", function (evt) {
d3.select(evt.target).attr("stroke", null);
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
years
Insert cell
viewof yearFilter = Scrubber(
// TODO(tukey): construct scrubber
d3.range(years[0], years[1] + 1, 5),
{autoplay: false, delay: 500, loop: false}
)
Insert cell
yearFilter
Insert cell
{
const svg = d3.create("svg").attr("viewBox", viewBox);

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]); // <-- TODO(tukey): update to use yearFilter

const countries = svg
.selectAll("circle")
.data(gapminder.filter(d => d.yearFilter)) // <-- 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 = {
const svg = d3.create("svg").attr("viewBox", viewBox);

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(1955); // <-- 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("cx", (d) => x(d.fertility))
.attr("cy", (d) => y(d.life_expect))
.attr("r", (d) => size(d.pop))
.attr("fill", (d) => color(d.cluster))
.attr("opacity", 0.75)

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
// update countries and animate the transition
// step 1: change the data to filter to the given year
// step 2: re-sort elements to ensure the smallest remain on top of the visualization, as pop values may have changed
// step 3: update position and radius, 1000 ms
// step 4: update the year label

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))
.attr("fill", (d) => color(d.cluster))
.attr("opacity", 0.75)
}

// TODO(tukey): extend SVG node
// return svg.node();
return Object.assign(svg.node(), {changeYear})
}
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[123]
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
.text("Region")
.attr("x", 0)
.attr("y", 0)
.attr("fill", "black")
.attr("font-family", "Arial")
.attr("font-weight", "bold")
.attr("font-size", "12px")

const entries = container
.selectAll("g")
// TODO(tukey): create and entry for each label
.data(regions)
.join("g")

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

const labels = entries

// 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})`)
.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(1955);

// Add and position the legend; place in the upper right corner.
// TODO(tukey): add legend
svg
.append("g")

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));
}

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