Public
Edited
Feb 6, 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([/* add code here */, /* add code here */]);
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 => /* add code here */) // 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", /* add code here */)
.attr("y1", /* add code here */)
.attr("x2", /* add code here */)
.attr("y2", /* add code here */)
.attr("stroke", d => /* add code here */)
.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 => /* add code here */)
.attr('cx', d => /* add code here */)
.attr('cy', d => /* add code here */)
.attr('r', d => /* add code here */);

// 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 => /* add code here */), 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 */
d3.select("#scatter-filter-year")
.select("#yearLabel")
.text(/* add code here */);
}
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') /* color entering items with green */,
update => update /* color updated (i.e. existing) items with blue */,
exit => exit.remove() /* 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"),
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.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('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

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