Public
Edited
Jan 29
1 fork
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)
Insert cell
dataInitial = gapminder.filter(d => d.year === years[0])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
x = d3.scaleLinear()
.domain([0, d3.max(gapminder, d => d.fertility)])
.range([margin.left, width - margin.right])
.nice()
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(gapminder, d => d.life_expect)])
.range([height - margin.bottom, margin.top])
.nice()
Insert cell
Insert cell
color = d3.scaleOrdinal()
.domain(gapminder.map(d => d.cluster))
.range(d3.schemeTableau10) // try other schemes, too!
Insert cell
Insert cell
size = d3.scaleSqrt()
.domain(d3.extent(gapminder, d => d.pop))
.range([4, 35]) // output radii range from 4 to 35 pixels
Insert cell
Insert cell
{
// create the container SVG element
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

// position and populate the x-axis
svg.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x));

// position and populate the y-axis
svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y));

// 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('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));

// return the SVG DOM element for display
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.
.append('text')
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('x', width - margin.right)
.attr('y', -10)
.text('Fertility');

svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
// Add y-axis title 'text' element.
.append('text')
.attr('transform', `translate(20, ${margin.top}) rotate(-90)`)
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.text('Life Expectancy');
// Add a background label for the current year.
const yearLabel = svg.append('text')
.attr('class', 'year')
.attr('x', 40)
.attr('y', height - margin.bottom - 20)
.attr('fill', '#ccc')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 500)
.attr('font-size', 80)
.text(years[0]);

const countries = svg
.selectAll('circle')
.data(dataInitial)
.join('circle')
.sort((a, b) => b.pop - a.pop) // <-- sort so smaller circles are drawn last
.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));
// add a tooltip
countries
.append('title')
.text(d => d.country);
// Add mouse hover interactions, using D3 to update attributes directly.
// In a stand-alone context, we could also use stylesheets with 'circle:hover'.
countries
// The 'on()' method registers an event listener function
.on('mouseover', function() {
// The 'this' variable refers to the underlying SVG element.
// We can select it directly, then use D3 attribute setters.
// (Note that 'this' is set when using "function() {}" definitions,
// but *not* when using arrow function "() => {}" definitions.)
d3.select(this).attr('stroke', '#333').attr('stroke-width', 2);
})
.on('mouseout', function() {
// Setting the stroke color to null removes it entirely.
d3.select(this).attr('stroke', null);
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof yearFilter = Scrubber(
d3.range(years[0], years[1] + 1, 5), // min to max years in 5 year increments
{ autoplay: false, delay: 500, loop: false } // experiment with these settings!
)
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))
.append('text')
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('x', width - margin.right)
.attr('y', -10)
.text('Fertility');

svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(20, ${margin.top}) 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 - margin.bottom - 20)
.attr('fill', '#ccc')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 500)
.attr('font-size', 80)
.text(yearFilter); // <-- update to use yearFilter

const countries = svg
.selectAll('circle')
.data(gapminder.filter(d => d.year === yearFilter)) // <-- 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))
.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('width', width)
.attr('height', height);

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 - margin.right)
.attr('y', -10)
.text('Fertility');

svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(20, ${margin.top}) 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 - margin.bottom - 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')
// Bind to the filtered initial data, we will perform updates elsewhere
// Provide a 🔑 key function for joining data to SVG elements
.data(dataInitial, d => d.country) // <-- Add key function!!
.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);
});

// Update function: given a year value, update the chart.
function setYear(year) {
// Update the year label by simply setting it to the new value.
yearLabel.text(year);

// Update countries and animate the transition:
// 1. Change the data to filter to the given year, keyed by country
// 2. Re-sort elements to ensure smallest remain on top, as pop. values may have changed
// 3. Update position and radius, interploated across a 1 sec (1000 ms) animation
countries
.data(gapminder.filter(d => d.year === year), d => d.country)
.sort((a, b) => b.pop - a.pop)
.transition() // <-- akin to a D3 selection, but interpolates values
.duration(1000) // <-- 1000 ms === 1 sec
.ease(d3.easeCubic) // <-- sets pacing; cubic is the default, try some others!
.attr('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('r', d => size(d.pop));
}
// Extend SVG node, export setYear as a property thereof
return Object.assign(svg.node(), { setYear });
}
Insert cell
chartAnimate.setYear(yearAnimate),
md`Average Fertility & Life Expectancy by Country in ${yearAnimate}`
Insert cell
Insert cell
Insert cell
Insert cell
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
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')
// create legend's title
.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');

const entries = container.selectAll('g')
// create and entry for each label
.data(regions)
.join('g')
.attr('transform', d => `translate(0, ${titlePadding + d.index * entrySpacing})`);

const symbols = entries.append('circle')
// add color indication for each label
.attr('cx', entryRadius) // <-- offset symbol x-position by radius
.attr('r', entryRadius)
.attr('fill', d => color(d.index));

const labels = entries.append('text')
// add text labels
.attr('x', 2 * entryRadius + labelOffset) // <-- place labels to the left of symbols
.attr('y', baselineOffset) // <-- adjust label y-position for proper alignment
.attr('fill', 'black')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-size', '11px')
.style('user-select', 'none') // <-- disallow selectable text
.text(d => d.label);
}
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
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')
// create an entry for each label + handle interaction
.data(regions)
.join('g')
.attr('transform', d => `translate(0, ${titlePadding + d.index * entrySpacing})`)
.on('click', (e, d) => selmodel.toggle(d.index)) // <-- respond to clicks
.on('dblclick', () => selmodel.clear()); // <-- respond to double clicks
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', () => {
// add effects from legend's interaction
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
Insert cell
Insert cell
chartLegend = {
const selmodel = SelectionModel(); // <-- Instantiate a selection model
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))
.append('text')
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('x', width - margin.right)
.attr('y', -10)
.text('Fertility');

svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(20, ${margin.top}) 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 - margin.bottom - 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.
svg.append('g')
.attr('transform', `translate(${width - margin.right - 150}, 10)`)
// Add legend content; call with both container element and selection model.
.call(container => legend(container, selmodel)); // <-- invoke legend helper
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 setYear(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)
.attr('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('r', d => size(d.pop));
}

// Add a selection model listener that updates country colors
// We name the event 'change.chart' as this is the chart's own internal listener.
// We do not want the name to collide with the listener defined in the legend component.
selmodel.on('change.chart', (t) => {
// add effects from legend's interaction
countries.attr('fill', d => selmodel.has(d.cluster) ? color(d.cluster) : '#ccc');
});

return Object.assign(svg.node(), { setYear });
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chartDynamic = {
const selmodel = SelectionModel();
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))
.append('text')
.attr('text-anchor', 'end')
.attr('fill', 'black')
.attr('font-size', '12px')
.attr('font-weight', 'bold')
.attr('x', width - margin.right)
.attr('y', -10)
.text('Fertility');

svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(20, ${margin.top}) 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 - margin.bottom - 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 - margin.right - 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 setYear(year) {
yearLabel.text(year);

countries = countries
.data(dataDynamic.filter(d => d.year === year), d => d.country)
.join(
// 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('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('r', 0)
.attr('opacity', 0)
.attr('fill', d => selmodel.has(d.cluster) ? color(d.cluster) : '#ccc'),
update => update,
// 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()
);
// Animate enter + update countries to current position and size
// Hint: If you modify opacity above, you probably want to update it here!
countries.transition()
.duration(1000)
.attr('opacity', 0.75)
.attr('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('r', d => size(d.pop));
}

selmodel.on('change.chart', () => {
countries.attr('fill', d => selmodel.has(d.cluster) ? color(d.cluster) : '#ccc');
});

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