Public
Edited
Sep 20, 2021
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
gapminder = FileAttachment("gapminder.min.json").json()
Insert cell
Insert cell
years = d3.extent(gapminder, d => d.year)
Insert cell
Insert cell
initData = 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
### Create chart scales
Insert cell
Insert cell
x = d3.scaleLinear()
.domain([0, d3.max(gapminder, d => d.gdpPercap)])
.range([margin.left, width - margin.right])
.nice()
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(gapminder, d => d.lifeExp)])
.range([height - margin.bottom, margin.top])
.nice()
Insert cell
Insert cell
color = d3.scaleOrdinal()
.domain(gapminder.map(d => d.continent))
.range(d3.schemeTableau10)
Insert cell
Insert cell
size = d3.scaleSqrt()
.domain(d3.extent(gapminder, d => d.pop))
.range([5, 40]) // output radius range from 5 to 40 pixels
Insert cell
{
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

//apply x scale
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('Gross Domestic Product (GDP) per capita');

//apply y scale
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]);

//create bubbles
const countries = svg
.selectAll('circle')
.data(initData)
.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.continent)) //apply color scale
.attr('cx', d => x(d.gdpPercap))
.attr('cy', d => y(d.lifeExp))
.attr('r', d => size(d.pop)); //apply size scale
// add a tooltip with country name
const tooltip = d3.select("body").append('div')
.attr('class', 'tooltip')
.style("opacity", 0)
.style("position", "absolute")
.style("background-color", "black")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white")

//event to make tooltip follow mouse - comment out if using fixed tooltip
window.onmousemove = function (e) {
const x = (e.clientX + 10) + 'px', y = (e.clientY - 10) + 'px';
tooltip.style("left", x).style("top", y);
};
// 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(event,d){
d3.select(this).attr('stroke', '#333').attr('stroke-width', 2);

tooltip
.transition()
.duration(200)
.style("opacity", 1);
tooltip.html("Country: " + d.country)

})
.on('mouseout', function(){
// Setting the stroke color to null removes it entirely.
d3.select(this).attr('stroke', null);

tooltip
.transition()
.duration(200)
.style("opacity", 0)
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
viewof yearsFilter = Scrubber(
d3.range(years[0], years[1]+1, 5), //1952 min to 2007 max , show every 5
{ autoplay: false, delay: 500, loop: false } // experiment with these settings
)
Insert cell
{
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

//apply x scale
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('Gross Domestic Product (GDP) per capita');

//apply y scale
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(yearsFilter); //<-- Update to use yearFilter

//create bubbles
const countries = svg
.selectAll('circle')
.data(gapminder.filter(d => d.year === yearsFilter)) // <-- Update to use yearFilter
.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.continent)) //apply color scale
.attr('cx', d => x(d.gdpPercap))
.attr('cy', d => y(d.lifeExp))
.attr('r', d => size(d.pop)); //apply size scale
// add a tooltip with country name
const tooltip = d3.select("body").append('div')
.attr('class', 'tooltip')
.style("opacity", 0)
.style("position", "absolute")
.style("background-color", "black")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white")

//event to make tooltip follow mouse - comment out if using fixed tooltip
window.onmousemove = function (e) {
const x = (e.clientX + 10) + 'px', y = (e.clientY - 10) + 'px';
tooltip.style("left", x).style("top", y);
};
// 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(event,d){
d3.select(this).attr('stroke', '#333').attr('stroke-width', 2);

tooltip
.transition()
.duration(200)
.style("opacity", 1);
tooltip.html("Country: " + d.country)

})
.on('mouseout', function(){
// Setting the stroke color to null removes it entirely.
d3.select(this).attr('stroke', null);

tooltip
.transition()
.duration(200)
.style("opacity", 0)
});
return svg.node();
}
Insert cell
Insert cell
viewof yearsAnimate = Scrubber(
d3.range(years[0], years[1]+1, 5), //every 5 years
{ autoplay: false, delay: 1000, loop: false }
)
Insert cell
chart.setYear(yearsAnimate)
Insert cell
chart = {
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

//apply x scale
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('Gross Domestic Product (GDP) per capita');

//apply y scale
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]); //<-- set as initial year

//create bubbles
const countries = svg
.selectAll('circle')
.data(initData) // <-- keep as initData
.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.continent)) //apply color scale
.attr('cx', d => x(d.gdpPercap))
.attr('cy', d => y(d.lifeExp))
.attr('r', d => size(d.pop)); //apply size scale
// add a tooltip with country name
const tooltip = d3.select("body").append('div')
.attr('class', 'tooltip')
.style("opacity", 0)
.style("position", "absolute")
.style("background-color", "black")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white")

//event to make tooltip follow mouse - comment out if using fixed tooltip
window.onmousemove = function (e) {
const x = (e.clientX + 10) + 'px', y = (e.clientY - 10) + 'px';
tooltip.style("left", x).style("top", y);
};
// 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(event,d){
d3.select(this).attr('stroke', '#333').attr('stroke-width', 2);

tooltip
.transition()
.duration(200)
.style("opacity", 1);
tooltip.html("Country: " + d.country)

})
.on('mouseout', function(){
// Setting the stroke color to null removes it entirely.
d3.select(this).attr('stroke', null);

tooltip
.transition()
.duration(200)
.style("opacity", 0)
});

// 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, interpolated across a 1 sec (1000ms) animation
countries
.data(gapminder.filter(d => d.year === year), d => d.country) // <-- filter data by year and update bubble for each country transition
.sort((a, b) => b.pop - a.pop)
.transition() // <-- akin to a D3 selection, but interpolates values
.duration(2000) // <-- 1000 ms === 1 sec
.ease(d3.easeExpOut) // <-- sets pacing; cubic is the default, try some others!
.attr('cx', d => x(d.gdpPercap))
.attr('cy', d => y(d.lifeExp))
.attr('r', d => size(d.pop));
}

//return object containing node and setYear function
return Object.assign( svg.node(), {setYear} );
}
Insert cell
Insert cell
ChartJS = require('chart.js');
Insert cell
viewof slide = Inputs.range(years, {value: years[0], step: 5, label: "year"})
Insert cell
bubblechartjs.update(slide)
Insert cell
bubblechartjs = {

//find unique regions
const regions = [... new Set(gapminder.map(d => d.continent))]
//create color palette for each continent
let colors = [];
regions.forEach(region => {
const r = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
const color = `rgb(${r}, ${g}, ${b})`;
colors.push({"region": region, "color": color })
})

const data = {
datasets: [
{
data: initData,
parsing: {
xAxisKey: 'gdpPercap',
yAxisKey: 'lifeExp',
},
radius: function(context) { //size of the bubble
var index = context.dataIndex;
var data = context.dataset.data[index];

//find min and max pop and rema to 5 - 40
const minmax = d3.extent(gapminder, d=>d.pop);
const map = function(val,in_min, in_max, out_min, out_max){
return (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

return map(data.pop, minmax[0], minmax[1], 5, 40);
},
backgroundColor: function(data){
const findC = colors.filter(c => c.region == data.raw.continent); //find match
return findC[0].color; //return color for match
},
},
]
};

const config = {
type: 'bubble',
data: data,
options: {
plugins: { //extra customization
legend: false, //default is true
title: { //default false
display: false,
},
tooltip: {
callbacks:{
//https://www.chartjs.org/docs/2.9.4/configuration/tooltip.html#label-callback
label: function(tooltipItem) { //customize bubble label
var index = tooltipItem.dataIndex;
var data = tooltipItem.dataset.data[index];
return data.country
},
}
}
},
scales: {
y: {
beginAtZero: true, //set at 0
ticks: {
// Include measurement type on ticks
callback: function(value, index, values) {
return value + ' yrs';
}
},
title: {
display: true,
text: 'Life expectancy (yrs)',
fontSize: 16,
}
},
x: {
beginAtZero: true, //set at 0
ticks: {
// Include measurement type on ticks
callback: function(value, index, values) {
return '$ '+value;
}
},
title: {
display: true,
text: 'GDP Per Capita',
fontSize: 16,
}
},
},
},
};
const canvas = document.createElement('canvas');
const chart = new ChartJS(canvas, config);

function update(year){
const filteredYear = gapminder.filter(d => d.year === year);//apply filter
chart.data.datasets[0].data = filteredYear; //update dataset
chart.update(); //update chart
}

return Object.assign(canvas, {update});
}
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