Public
Edited
Apr 12, 2023
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@7')
Insert cell
gapminder = datasets['gapminder.json']()
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
height = 500
Insert cell
margin = ({top: 10, right: 10, bottom: 20, left: 20})
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
dataInitial
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));


svg.append("text")
.attr("x", margin.left)
.attr("y", margin.top)
.text("Title")

// add circle elements for each country
// use scales to set fill color, x, y, and radius
// sort circles to draw smaller circles on top
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))
.attr("class", d => "continent_" + d.cluster)
.on("click", function(s) {
var select_class = d3.select(this).attr("class");
d3.selectAll("." + select_class)
.style("stroke", "black");
});

// 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', d => 'country continent_' + d.cluster)
.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);
var select_cluster = d3.select(this).data()[0]["cluster"];
d3.selectAll(".continent_" + select_cluster)
.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);
var select_cluster = d3.select(this).data()[0]["cluster"];
d3.selectAll(".continent_" + select_cluster).attr('stroke', null);
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
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
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
.text(yearFilter);

const countries = svg
.selectAll('circle')
//.data(gapminder.filter(d => d.year === years[0])) // <-- Update to use yearFilter
.data(gapminder.filter(d => d.year === 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
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
// Our selection model wraps two components:
// - A JavaScript Set for tracking the selected elements
// - A D3 dispatch helper for registering and invoking listener callbacks upon changes
function SelectionModel(values) {
const dispatch = d3.dispatch('change');
const state = new Set(values);
const api = {
on: (type, fn) => (dispatch.on(type, fn), api),
clear: () => (clear(), api),
has: value => !state.size || state.has(value),
set: value => (update(value, true), api),
toggle: value => (update(value, !state.has(value)), api)
};
function clear() {
if (state.size) {
state.clear();
dispatch.call('change', api, api);
}
}
function update(value, add) {
if (add && !state.has(value)) {
state.add(value);
dispatch.call('change', api, api);
} else if (!add && state.has(value)) {
state.delete(value);
dispatch.call('change', api, api);
}
}

return api;
}
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

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