Public
Edited
Oct 18
83 forks
Importers
49 stars
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
d3.extent(gapminder, d => d.cluster)
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 => 'black' /* update code here */)
.attr('cx', d => 0 /* update code here */)
.attr('cy', d => 0 /* update code here */)
.attr('r', d => 0 /* update code here */);

// 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('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));
// 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(years[0]); // <-- Update to use yearFilter

const countries = svg
.selectAll('circle')
.data(gapminder.filter(d => d.year === years[0])) // <-- 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
Insert cell
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')
.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')
.data(regions)
.join('g')
.attr('transform', d => `translate(0, ${titlePadding + d.index * entrySpacing})`);

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

const labels = entries.append('text')
.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
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', () => {
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'),
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.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('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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more