Published
Edited
Feb 25, 2021
3 forks
Insert cell
Insert cell
Insert cell
chart = {
const zoom = d3.zoom()
.scaleExtent([1, 4])
.on("zoom", zoomed);

const svg = d3.create("svg")
.attr("viewBox", [-80, -20, width + 50, height + 50]);
const clip = svg
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("x", -20)
.attr("y", -20);
const calculateGdpPerCapita = (d) => d.gdp / d.population_population_number_of_people;
const calculateWasteGenerationPerCapita = (d) => d.total_msw_total_msw_generated_tons_year / d.population_population_number_of_people / 365 * 1000;
const gx = svg.append("g");
const gy = svg.append("g");
// bubbles
const gBubbles = svg.append("g")
.attr('clip-path', 'url(#clip)')
.selectAll(".country")
.data(data.filter((d) => {
return d.gdp && d.population_population_number_of_people && d.total_msw_total_msw_generated_tons_year;
}))
.enter()
.append("circle")
.attr("class", (d) => `bubble bubble-${d.region_id}`)
.attr("cx", (d) => x(calculateGdpPerCapita(d)))
.attr("cy", (d) => y(calculateWasteGenerationPerCapita(d)))
.attr("r", (d) => z(d.population_population_number_of_people))
.attr("stroke", "white")
.style("fill", (d) => color(d.region_id))
.on("mouseover", showTooltip)
.on("mouseleave", hideTooltip)
const tooltip = svg.append("g")
.style("opacity", 1)
.attr("class", "tooltip")
// region legends
const gLegendBox = svg.append("g")
.append("rect")
.attr("x", width - 250)
.attr("y", 0)
.attr("width", 210)
.attr("height", 185)
.style("fill", "#F5F5F5")
.attr("stroke", "#D4D4D4")
const gLegendRect = svg.append("g")
.selectAll("regionLegendRect")
.data(regionIds)
.enter()
.append("rect")
.attr("x", width - 240)
.attr("y", (d, i) => 10 + i * (25))
.attr("width", 15)
.attr("height", 15)
.style("fill", (d, i) => colorCodes[i])
.on("mouseover", highlight)
.on("mouseleave", noHighlight)
const gLegendLabel = svg.append("g")
.selectAll("regionLegendLabel")
.data(regionIds)
.enter()
.append("text")
.attr("x", width - 220)
.attr("y", (d, i) => 18 + i * (25))
.attr("text-anchor", "left")
.style("cursor", "default")
.style("fill", (d, i) => colorCodes[i])
.style("alignment-baseline", "middle")
.style("font-size", 12)
.style("font-family", "sans-serif")
.text((d, i) => regions[i])
.on("mouseover", highlight)
.on("mouseleave", noHighlight)

// population legends
const populationLegendX = width - 150;
const populationLegendY = height - 60;
const populationLegendCircle = svg.append("g");
const populationLegendLabel = svg.append("g");
populationLegendCircle.selectAll("legend")
.data([10e7, 50e7, 10e8])
.enter()
.append("circle")
.attr("cx", populationLegendX)
.attr("cy", (d) => populationLegendY - z(d))
.attr("r", (d) => z(d))
.style("fill", "none")
.attr("stroke", "black");
populationLegendLabel.selectAll("legend")
.data([10e7, 50e7, 10e8])
.enter()
.append("text")
.attr("x", populationLegendX)
.attr("y", (d) => populationLegendY - 2 - 2 * z(d))
.text((d) => `${d/10e5}M`)
.style("font-size", 10)
.style("font-family", "sans-serif")
.style("text-anchor", "middle");
const populationLegendTitle = svg.append("text")
.attr("x", populationLegendX)
.attr("y", (d) => populationLegendY + 15)
.text("Population")
.style("font-size", 12)
.style("font-family", "sans-serif")
.style("text-anchor", "middle");

svg.call(xTitle).call(yTitle).call(zoom).call(zoom.transform, d3.zoomIdentity);

function zoomed({ transform }) {
const newX = transform.rescaleX(x);
const newY = transform.rescaleY(y);
gx.call(xAxis, newX);
gy.call(yAxis, newY);
gBubbles.attr("transform", transform).attr("stroke-width", 1.5 / transform.k);
tooltip.attr("transform", transform)
populationLegendCircle
.selectAll("circle")
.attr("cy", (d) => populationLegendY - z(d) * transform.k)
.attr("r", (d) => z(d) * transform.k);
populationLegendLabel
.selectAll("text")
.attr("y", (d) => populationLegendY - 2 - 2 * z(d) * transform.k);
}
// show country name
function showTooltip(e, d) {
tooltip
.selectAll("*")
.remove();
tooltip.append("text")
.attr("class", `tooltip-${d.iso3c}`)
.attr("x", x(calculateGdpPerCapita(d)))
.attr("y", y(calculateWasteGenerationPerCapita(d)))
.attr("pointer-events", "none")
.style("font-size", 12)
.style("font-family", "sans-serif")
.style("alignment-baseline", "middle")
.style("text-anchor", "middle")
.text(d.country_name)
.clone(true)
.lower()
.attr("class", `tooltip-${d.iso3c}`)
.attr("aria-hidden", "true")
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 2)
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round");
}
// hide country name
function hideTooltip(e, d) {
tooltip
.selectAll(`.tooltip-${d.iso3c}`)
.transition()
.duration(500)
.style("opacity", 0)
.remove();
}
// add highlight to a region
function highlight(e, d){
svg.selectAll(".bubble").style("opacity", 0.1).style("saturate", "20%")
svg.selectAll(`.bubble-${d}`).style("opacity", 1).style("saturate", "100%").attr("stroke", "#181818")
}
// remove highlight of a region
function noHighlight(){
svg.selectAll(".bubble").style("opacity", 1).style("saturate", "100%").attr("stroke", "white")
}

return Object.assign(svg.node(), {
reset() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
});
}
Insert cell
legend = d3Legend
.legendColor()
.shape("path", d3.symbol().type(d3.symbolCircle).size(60)())
.shapePadding(20)
.labelOffset(10)
.scale(d3.scaleOrdinal(regions, colorCodes))
.labels(regions);
Insert cell
regions = [
"North America",
"Latin America & the Caribbean",
"East Asia and Pacific",
"South Asia",
"Europe and Central Asia",
"Middle East and North Africa",
"Sub-Saharan Africa"];
Insert cell
regionIds = ["NAC", "LCN", "EAS", "SAS", "ECS", "MEA", "SSF"];
Insert cell
data = d3.csvParse(await FileAttachment("country_level_data_0.csv").text())
Insert cell
// scale x-axis (GDP per capita)
x = d3.scaleLinear()
.domain([0, 70000])
.range([0, size.width])
Insert cell
// scale y-axis (waste generation per capita)
y = d3.scaleLinear()
.domain([0, 3])
.range([size.height, 0])
Insert cell
// scale population
z = d3.scaleSqrt()
.domain([200000, 1310000000])
.range([2, 30])
Insert cell
color = d3.scaleOrdinal()
.domain(regionIds)
.range(colorCodes);
Insert cell
// use blind safe colors
colorCodes = ["#DC7C7C", "#C69FDF", "#D2B279", "#E5D27D", "#8285CA", "#A1D774", "#66CCB3"]
Insert cell
xAxis = (g, x) => g
.attr("transform", `translate(0, ${size.height + 20})`)
.call(d3.axisBottom(x).ticks(10))
// axis label
.append("text")
.attr("x", 0)
.attr("y", 0)
.text("GDP per Capita (USD)")
Insert cell
xTitle = (g) => g
.append("text")
.attr("x", width / 2)
.attr("y", height + 20)
.attr("class", "axis-label")
.style("font-size", 14)
.style("font-family", "sans-serif")
.style("text-anchor", "middle")
.text("GDP per Capita (USD)")
Insert cell
yAxis = (g, y) => g
.attr("transform", `translate(-20, 0)`)
.call(d3.axisLeft(y).ticks(10))
Insert cell
yTitle = (g) => g
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", 0 - height / 2)
.attr("y", 0 - margin.left - 10)
.attr("class", "axis-label")
.style("text-anchor", "middle")
.style("font-size", 14)
.style("font-family", "sans-serif")
.text("Waste Generation per Capita (kg/person/day)")
Insert cell
// figure size
size = ({
height: height - margin.top - margin.bottom,
width: width - margin.left - margin.right
})
Insert cell
// figure margin
margin = ({ top: 20, right: 20, bottom: 20, left: 50 })
Insert cell
height = 500
Insert cell
d3Legend = require("d3-svg-legend")
Insert cell
d3 = require("d3@6")
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