Public
Edited
Feb 4, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
lifeExpectancyByCountry = d3.groups(dataset, d => d.country /* specifies what key to use to group the entries */)
.map(([country, values]) => { /* this step is not necessary, but makes the data more readable */
return {country: country, values: values}
});
Insert cell
Insert cell
chartDimensions = {
let dimensions = {
svgWidth: 600,
svgHeight: 400,
margin: {top: 30, left: 40, bottom: 40, right: 100}
};
dimensions.width = dimensions.svgWidth - dimensions.margin.left - dimensions.margin.right;
dimensions.height = dimensions.svgHeight - dimensions.margin.top - dimensions.margin.bottom;
return dimensions;
}
Insert cell
Insert cell
xScale = d3.scaleLinear()
.domain(d3.extent(lifeExpectancyByCountry[0].values, d => d.year)) /* d3.extent returns [min, max] of the input array */
.range([0, chartDimensions.width]);
Insert cell
Insert cell
xScale(1955)
Insert cell
Insert cell
yScale = d3.scaleLinear()
.domain([0, d3.max(lifeExpectancyByCountry, v => d3.max(v.values, d => d.life_expect))])
.range([chartDimensions.height, 0]);
Insert cell
yScale(80)
Insert cell
Insert cell
{
let lineChart = d3.create("svg")
.attr("width", chartDimensions.svgWidth)
.attr("height", chartDimensions.svgHeight);

// create a line generator function that'll compute the path for an array of values
let lineGenerator = d3.line()
.x(d => xScale(d.year))
.y(d => yScale(d.life_expect));

lineChart.datum(lifeExpectancyByCountry[0]) // use .datum for only one entry
.append("path")
.attr("d", d => lineGenerator(d.values)) // call the lineGenerator with the values array passed in
.attr("fill", "none") // specify no fill for the space enclosed by the path
.attr("stroke", "black"); // set the color of the path
return lineChart.node();
}
Insert cell
Insert cell
lifeExpectancyByCountry8 = lifeExpectancyByCountry.slice(0, 8)
Insert cell
Insert cell
d3.schemeDark2
Insert cell
// create the scale function for mapping country name to color
colorScale = d3.scaleOrdinal()
.domain(lifeExpectancyByCountry8.map(d => d.country))
.range(d3.schemeDark2);
Insert cell
Insert cell
{
let lineChart = d3.create("svg")
.attr("id", "line-chart-8") // set the id of the svg so we can add to it later
.attr("width", chartDimensions.svgWidth)
.attr("height", chartDimensions.svgHeight);

let lineGroup = lineChart.append("g")
.attr("transform", `translate(${chartDimensions.margin.left}, ${chartDimensions.margin.top})`); // shift all bars by the margin offset

// create a line generator function that'll compute the path for an array of values
let lineGenerator = d3.line()
.x(d => xScale(d.year))
.y(d => yScale(d.life_expect));

lineGroup.selectAll("path")
.data(lifeExpectancyByCountry8)
.join("path")
.attr("d", d => lineGenerator(d.values))
.attr("fill", "none") // specify no fill for the space enclosed by the path
.attr("stroke", d => colorScale(d.country)) // set the color of the path
.attr("stroke-width", 2) // set the width of the lines
.attr("stroke-opacity", 0.7); // set the opacity of the lines
return lineChart.node();
}
Insert cell
Insert cell
{
const lineChart = d3.select("#line-chart-8");
// you can ignore the following, it's only done to allow redrawing elements in the Observable notebook
if (!lineChart.selectAll(".axisElements").empty()) {
lineChart.selectAll(".axisElements").remove()
}
// add x-axis
lineChart.append("g")
.attr("class", "axisElements")
.attr('transform', `translate(${chartDimensions.margin.left}, ${chartDimensions.height + chartDimensions.margin.top})`)
.call(d3.axisBottom(xScale).tickFormat(d3.format("d"))); // don't add commas to the year labels
// add y-axis
lineChart.append("g")
.attr("class", "axisElements")
.attr('transform', `translate(${chartDimensions.margin.left}, ${chartDimensions.margin.top})`)
.call(d3.axisLeft(yScale));
// add x-axis title
lineChart.append("text")
.attr("class", "axisElements")
.attr("x", chartDimensions.margin.left + chartDimensions.width / 2)
.attr("y", chartDimensions.svgHeight - 5)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Year");
// add y-axis title
lineChart.append("text")
.attr("class", "axisElements")
.attr("x", -(chartDimensions.margin.top + chartDimensions.height / 2))
.attr("y", 10)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Life Expectancy");
}
Insert cell
Insert cell
<svg width=100 height=30 style="border: 1px black solid">
<g style="transform: translate(5px, 10px)">
<line x1=0 y1=5 x2=10 y2=5 stroke="blue" stroke-width="2px"/>
<text x=15 y=8 fill="black" style="font-size: 12px">Country</text>
</g>
</svg>
Insert cell
Insert cell
{
const lineChart = d3.select("#line-chart-8");
// you can ignore the following, it's only done to allow redrawing elements in the Observable notebook
if (!lineChart.select("#legend-group").empty()) {
lineChart.select("#legend-group").remove()
}
const legendGroup = lineChart.append("g") // add a legend group
.attr("id", "legend-group")
.attr("transform",
`translate(${chartDimensions.margin.left + chartDimensions.width + 15}, ${chartDimensions.margin.top})`
);
const legendItems = legendGroup.selectAll("g")
.data(lifeExpectancyByCountry8.map(d => d.country))
.join("g")
.attr("transform", (d, index) => `translate(0, ${index * 12})`); // space each legend item by 12px vertically

legendItems.append("line") // we can use the <line> element because we only need straight lines
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 10)
.attr("y2", 5)
.attr("stroke", d => colorScale(d))
.attr("stroke-opacity", 0.7)
.attr("stroke-width", 2);

legendItems.append("text")
.attr("x", 15)
.attr("y", 8)
.attr("fill", "black")
.style("font-size", "12px")
.text(d => d);
}
Insert cell
Insert cell
dimensions = ({
width: width, // this time let's use the built-in width value provided by Observable
height: 500,
margin: { top: 10, right: 10, bottom: 20, left: 20 }
})
Insert cell
dataInitial = dataset.filter(d => d.year === 1955)
Insert cell
x = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.fertility)])
/* instead of always starting at 0 for range like we have been doing, we can simply put the margin as start value to apply the offset */
.range([dimensions.margin.left, dimensions.width - dimensions.margin.right])
.nice() // format the values to look "nice"
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.life_expect)])
.range([dimensions.height - dimensions.margin.bottom, dimensions.margin.top])
.nice()
Insert cell
color = d3.scaleOrdinal()
.domain(dataset.map(d => d.cluster))
.range(d3.schemeTableau10) // use a d3 built-in color scheme
Insert cell
Insert cell
size = d3.scaleSqrt()
.domain(d3.extent(dataset, 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', dimensions.width)
.attr('height', dimensions.height);

// position and populate the x-axis
svg.append('g')
.attr('transform', `translate(0, ${dimensions.height - dimensions.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', dimensions.width - dimensions.margin.right)
.attr('y', -10)
.text('Fertility');

// position and populate the y-axis
svg.append('g')
.attr('transform', `translate(${dimensions.margin.left}, 0)`)
.call(d3.axisLeft(y))
// Add y-axis title 'text' element.
.append('text')
.attr('transform', `translate(20, ${dimensions.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('id', 'yearLabel')
.attr('x', 40)
.attr('y', dimensions.height - dimensions.margin.bottom - 20)
.attr('fill', '#ccc')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 500)
.attr('font-size', 80)
.text("1955");
const legend = svg.append('g')
.attr('transform', `translate(${dimensions.width - 160}, ${dimensions.margin.top})`)
.call(colorLegend); // <-- legend helper is included in Utilities section

// 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.append("g")
.attr("id", "countryGroup")
.selectAll('circle')
.data(dataInitial, d => d.country)
.join('circle')
.attr("class", "countryCircles") // give these circles a class so we can select them by this class
.sort((a, b) => b.pop - a.pop) // <-- sort so smaller circles are drawn last and won't be blocked by larger circles
.attr('opacity', 0.75)
.attr('fill', d =>color(d.country))
.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
{
// create the container SVG element
const svg = d3.create('svg')
.attr('width', dimensions.width)
.attr('height', dimensions.height);

// position and populate the x-axis
svg.append('g')
.attr('transform', `translate(0, ${dimensions.height - dimensions.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', dimensions.width - dimensions.margin.right)
.attr('y', -10)
.text('Fertility');

// position and populate the y-axis
svg.append('g')
.attr('transform', `translate(${dimensions.margin.left}, 0)`)
.call(d3.axisLeft(y))
.append('text')
.attr('transform', `translate(20, ${dimensions.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('id', 'yearLabel')
.attr('x', 40)
.attr('y', dimensions.height - dimensions.margin.bottom - 20)
.attr('fill', '#ccc')
.attr('font-family', 'Helvetica Neue, Arial')
.attr('font-weight', 500)
.attr('font-size', 80)
.text("1955");
const legend = svg.append('g')
.attr('transform', `translate(${dimensions.width - 160}, ${dimensions.margin.top})`)
.call(colorLegend); // <-- legend helper is included in Utilities section

const countries = svg.append("g")
.attr("id", "countryGroup")
.selectAll('circle')
.data(dataInitial, d => d.country)
.join('circle')
.sort((a, b) => b.pop - a.pop) // sort so smaller circles are drawn last and won't be blocked by larger circles
.attr("class", "countryCircles") // give these circles a class so we can select them by this class
.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))
/****** show tooltip and border when hovered over ******/
.on("mouseover", function (event, d) { // <-- need to use the regular function definition to have access to "this"
svg.select("#tooltip-text")
.text(d.country);
let positionOffest = 8;
svg.select("#tooltip")
// move the tooltip to where the cursor is
.attr("transform", `translate(${x(d.fertility) + positionOffest}, ${y(d.life_expect) + positionOffest})`)
.style("display", "block"); // make tooltip visible
d3.select(this)
.attr("stroke", "#333333")
.attr("stroke-width", 2);
})
.on("mouseout", function (event, d) {
svg.select("#tooltip").style("display", "none"); // hide tooltip
d3.select(this).attr("stroke", "none"); // undo the stroke
});

/****** Tooltip Code ******/
const tooltipGroup = svg.append("g") // the tooltip needs to be added last so that it stays on top of all circles
.attr("id", "tooltip")
.style("display", "none") // hidden by default
.append("text")
.attr("id", "tooltip-text")
.attr("x", 5)
.attr("y", 15)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", "black");

return svg.node();
}
Insert cell
Insert cell
Insert cell
// the yearFilter stores the currently selected year
viewof yearFilter = Scrubber(
d3.range(1955, 2006 /* the max is exclusive*/, 5), // min to max years in 5 year increments
{ autoplay: false, delay: 1500, loop: false } // experiment with these settings!
)
Insert cell
Insert cell
Insert cell
updateYear = function (newYear) {
d3.select("#scatter-filter-year")
.selectAll(".countryCircles")
.data(dataset.filter(d => d.year == newYear), 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!
/* add code here */
.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))
d3.select("#scatter-filter-year")
.select("#yearLabel")
.text(newYear);
}
Insert cell
//uncomment the function call below when your updateYear function is ready
updateYear(yearFilter)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const ol = d3.select(html`<ol id="enter-update-exit"></ol>`);
ol.selectAll("li") // select all list elements (orange circle above, empty selection at this point)
.data(topTenList) // |- bind all our data values (blue circle above)
.join("li") // |- to an "li" element
.text(d => `${d.country}: ${d.pop}`) // set the text content of the "li" element
return ol.node();
}
Insert cell
Insert cell
{
const ol = d3.select('ol#enter-update-exit'); // this ensures that the code in this cell
// updates the html ol created in the cell above
// use a new dataset, manipulable with the variables defined below
const newData = dataset
.filter(d => d.year === year) // keep only data for this specific year
.sort((a, b) => b.pop - a.pop) // sort the entries by population in descending order
.slice(0, topN); // takes only the topN entries
ol.selectAll("li") // select all list elements (orange circle above)
.data(newData, d => d.country) // bind all our data values (blue circle above), using country name as key
.join(
enter => enter.append('li').attr('fill', 'green') /* color entering items with green */,
update => update.attr('fill', 'blue') /* color updated (i.e. existing) items with blue */,
exit => exit.remove().attr('fill', 'red') /* color exiting items with red instead of removing */
).text(d => `${d.country}: ${d.pop}`);
}
Insert cell
Insert cell
year = 2005 // note that the dataset only contains entries for 1955-2005 in increments of 5 years
Insert cell
topN = 10
Insert cell
Insert cell
Insert cell
Insert cell
viewof yearDynamic = Scrubber(
d3.range(1955, 2006, 5),
{ autoplay: false, delay: 1500, loop: false }
)
Insert cell
Insert cell
updateYearDynamic = function (newYear) {
const countries = d3.select("#scatter-dynamic")
.select("#countryGroup")
.selectAll(".countryCircles")
.data(dataDynamic.filter(d => d.year === newYear), 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", "countryCircles")
.attr('opacity', 0.0)
.attr('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('fill', d => color(d.cluster))
.attr('r', d => 0),
update => update,
// Add code to customize how countries exit the scene.
// Idea: use transition to fade out to transparent and shrink to zero size before removal
exit => exit
.transition()
.duration(1000)
.attr('opacity', 0.0)
.attr('r', d=>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)
.ease(d3.easeCubic)
.attr('cx', d => x(d.fertility))
.attr('cy', d => y(d.life_expect))
.attr('opacity', 0.75)
.attr('fill', d => color(d.cluster))
.attr('r', d => size(d.pop))
d3.select("#scatter-dynamic")
.select("#yearLabel")
.text(newYear);
}
Insert cell
updateYearDynamic(yearDynamic)
Insert cell
Insert cell
Insert cell
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