Public
Edited
Jan 17, 2023
Insert cell
Insert cell
Insert cell
// disclaimer: not all sections are finished
// but there should be already enough material for 3h
Insert cell
Insert cell
Insert cell
Insert cell
records = d3.csvParse(file_content, d3.autoType)
Insert cell
Insert cell
file_content = file.text()
Insert cell
Insert cell
file = FileAttachment("worldhappiness-2020-table-2.1.csv")
Insert cell
Insert cell
Insert cell
Insert cell
[1, 3, 4, 5, undefined].filter( value => value ) // astuce
Insert cell
({prop1: "contenu", prop2: [ {} , [] ] })
Insert cell
[0,1,2,3,4,5].filter(number => number % 2 == 0)
Insert cell
records_2019 = records
.filter(country => country.year==2019)
.map(function (obj){ return {
name: obj['Country name'],
gdp: obj['Log GDP per capita'],
corruption: obj['Perceptions of corruption'],
happiness: obj['Life Ladder']
}})
.filter( obj => obj.name && obj.gdp && obj.corruption && obj.happiness)
// your code here
// you may want to use a mix of filter() and map() methods
// or a for loop with the push() method
Insert cell
Insert cell
exemple = [
{name: "France", data: [
{year: 2015, happiness: 3, gdp:45, corruption: 1},
{year: 2016, happiness: 4, gdp: 65, corruption: 2}
]},
{name: "Portugal", data: [
{year: 2016, happiness: 3.3, gdp:12, corruption: 3}
]}
]
Insert cell
Insert cell
records
Insert cell
records_europe = {
// your code here
// (I provide a suggested structure, but you can destroy it if you don't like it !)
let recs = [] // used to store the reorganized records
let names = [] // used to store the names of already seen countries
let european_countries = ["Austria", "Belarus", "Belgium", "Croatia", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece"]
for(let record of records){
let name = record["Country name"]
// If it is a European country, do:
if( european_countries.includes(name) ) {
// > if it has never been seen, add a new entry {name: ..., data: []} to records and update names
if( ! names.includes( name ) ) {
names.push(name)
recs.push( { name: name, data: [] } )
}
// > add the new record to the data property of the corresponding object
let index = names.indexOf(name)
recs[index].data.push( { year: record["year"], corruption: record["Perceptions of corruption"]} )
// trouver name dans names -> à quelle place?
// recs[ # numero de la place ].data.push( donnees )
}
}
return recs
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// your code here
Insert cell
Insert cell
// your code here
Insert cell
Insert cell
Insert cell
// your code here
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
// your code here
// If you don't want to chose your own settings, I suggest you use circles:
// - of radius 5
// - of inner color "lightgrey" (attribute "fill")
// - of border color black (attribute "stroke")
// - of border width 0.4 (attribute "stroke-width")
const title = svg.append("text") // Give me text content and move me top left !
.attr("font-family", "tahoma")
const source = svg.append("text") // Give me text content and move me bottom right !
.attr("font-family", "tahoma")
.attr("font-size", 8)
.attr("text-anchor", "end")
.attr("fill", "grey")
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", 50)
.attr("width", width)
svg.append("circle").attr("cx", 20)
svg.append("circle").attr("cx", 50)
svg.append("circle").attr("cx", 80)
svg.append("circle").attr("cx", 110)
svg.append("circle").attr("cx", 140)
const welcome_text = "Select a couple of circles, or:"
const text = svg.append("text")
.text(welcome_text)
.attr("y", 30)
.attr("x", 160)
const selector = svg.append("text")
.text("CLICK HERE TO SELECT ALL")
.attr("y", 30)
.attr("x", 380)
.attr("cursor", "pointer")
const circles = svg.selectAll("circle")
.attr("cy", 25)
.attr("r", 10)
.attr("fill", "lightgrey")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.attr("cursor", "pointer")
function toggle(){
const circle = d3.select(this)
const current_color = circle.attr("fill")
circle.attr("fill", current_color == "grey" ? "lightgrey" : "grey")
svg.dispatch("update")
}
function select_all(){
circles.transition().duration(3000).attr("fill", "grey").transition().duration(3000).attr("fill", "lightgrey")
svg.dispatch("update")
}
function update_comment(){
const selected_circles = circles.filter( function() { return d3.select(this).attr("fill")=="grey" } )
const n = selected_circles.size()
const message = n > 1 ?
n + " circles selected" : n==1 ?
"1 single circle selected" : welcome_text
text.text(message)
}
circles.on("click", toggle)
svg.on("update", update_comment)
selector.on("click", select_all) // subscription
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// PLANNED
// introduction to inputs
Insert cell
Insert cell
// PLANNED
// use datum to store multiple stages
// combining hover and select events
Insert cell
Insert cell
// PLANNED
// coordinate two objects (when you click on one, something also happens on the others)
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", 30)
.attr("width", width)
const circle = svg.append("circle") // create a circle
.attr("cx", 15)
.attr("cy", 15)
.attr("r", 10)
let increment = 100
function move(){ // create a move function that adds "increment" to x
// and transitions between the previous and the new state
let cx = +circle.attr("cx") + increment
// change direction when at the end of the line
if( (increment > 0 && cx > width) || (increment < 0 && cx < 0)) increment = -increment
circle.transition().attr("cx", cx) // <------ transition() here
}
d3.interval(move, 1000) // repeat move every 1000 ms (= every 1 s)
return svg.node()
}
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", 110)
.attr("width", width)
svg.append("text").text("Click any of the circles!").attr("x", 10).attr("y", 105)
const circle1 = svg.append("circle").attr("cx", 50)
const circle2 = svg.append("circle").attr("cx", 150)
const circle3 = svg.append("circle").attr("cx", 250)
const circle4 = svg.append("circle").attr("cx", 350)
const circles = svg.selectAll("circle") // style all the circles
.attr("cy", 45)
.attr("r", 40)
.attr("fill", "white")
.attr("stroke", "black")
.style("cursor", "pointer")
circles.on("click", function(){ // every time one of circles is clicked, do:
let random_integer1 = d3.randomUniform(0, 360)()
let random_integer2 = d3.randomUniform(0, 1)()
let random_color = d3.cubehelix(random_integer1, 1, random_integer2)
let random_radius = d3.randomUniform(10, 45)()
circle1 .attr("fill", random_color).attr("r", random_radius) // no transition
circle2.transition().duration( 500).attr("fill", random_color).attr("r", random_radius) // short (0.5s)
circle3.transition().duration(1000).attr("fill", random_color).attr("r", random_radius) // medium (1s)
circle4.transition().duration(2000).attr("fill", random_color).attr("r", random_radius) // long (2s)
})
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("height", 70) // create the SVG object
// add three different text elenents, each with a datum
svg.append("text").attr("y", 20).datum({gdp: "Discover text: Click!", corruption: "Corruption content"})
svg.append("text").attr("y", 40).datum({gdp: "Click also here!", corruption: "More corruption content"})
svg.append("text").attr("y", 60).datum({gdp: "And here!", corruption: "Even more corruption content"})
svg.selectAll("text") // select all the texts
.on("click", update_text) // One can use regular attr(), style(), text() or on() methods
.style("cursor", "pointer") // ... as we used to (changes apply to all the texts).
.attr("fill", "red")
.text(d => d.gdp) // BUT one can ALSO use a function with only one attribute (the datum) :
// changes apply to all the text elements, but with their respective
// datum content, here the gdp element defined just before.
function update_text(){ // HERE is the real power:
d3.select(this) // d3.select(this) refers to the element that emitted a click event
.text(d => d.corruption) // but we can use the datum to update the text of this specific element.
}
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
// ---- proper graph construction ---------------
for(let record of correction_records_2019){
svg.append("circle")
// .attr("cx", correction_x(record.gdp)) // removal of .attr() method
.datum( record ) // datum addition
.attr("r", 5) // <----- MOVE ALL OF THESE METHODS
.attr("cy", correction_y(record.happiness))
.attr("fill", "lightgrey")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
}
svg.selectAll("circle")
.attr("cx", d => correction_x(d.gdp)) // .attr() method with an anonymous function
// ----- title and source --------------------
const title = svg.append("text") // Move me top left !
.text("The correlation between happiness and wealth is weak...")
.attr("x", width-margin.right)
.attr("text-anchor", "end")
.attr("y", margin.top/2)
.attr("font-family", "tahoma")
const source = svg.append("text") // Move me bottom right !
.text("Happiness Report (2020)")
.attr("x", width - margin.right)
.attr("y", height - 1)
.attr("font-family", "tahoma")
.attr("font-size", 8)
.attr("text-anchor", "end")
.attr("fill", "grey")
// ----- axes ---------------------------------
const x_axis_y_pos = height-2*margin.bottom/3
const x_axis = svg.append("g").attr("transform", "translate(0 "+x_axis_y_pos+")")
const make_x_axis = d3.axisBottom(correction_x)
make_x_axis(x_axis)
const x_axis_label = x_axis.append("text")
.text("Log GDP per capita")
.attr("fill", "grey")
.attr("y", -2)
.attr("x", (width-margin.right)/2)
.attr("text-anchor", "end")
const y_axis_x_pos = margin.left/2
const y_axis = svg.append("g").attr("transform", "translate("+y_axis_x_pos+" 0)")
const make_y_axis = d3.axisLeft(correction_y)
make_y_axis(y_axis)
const y_axis_label = y_axis.append("text")
.text("Average perceived happiness")
.attr("fill", "grey")
.attr("transform", "rotate(-90) translate(-50 10)")
.attr("text-anchor", "end")
// -------------------------------------------
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
x_scales = ({
gdp : d3.scaleLinear().domain( correction_domain_gdp).range(correction_range_x),
corruption : d3.scaleLinear().domain(correction_domain_corruption).range(correction_range_x)
})
Insert cell
Insert cell
Insert cell
Insert cell
{
const div = d3.create("div")
const select = div.append("select").style("position", "absolute").style("top", margin.top/2-10 + "px")
select.append("option").attr("value", "gdp").text("Wealth")
select.append("option").attr("value", "corruption").text("Corruption")
const svg = div.append("svg")
.attr("height", height)
.attr("width", width)
// ---- proper graph construction ---------------
for(let record of correction_records_2019){
svg.append("circle").datum(
{gdp: record.gdp, happiness: record.happiness, corruption: record.corruption}
)
}
svg.selectAll("circle")
.attr("r", 5)
.attr("fill", "lightgrey")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.attr("cx", d => x_scales.gdp(d.gdp))
.attr("cy", d => correction_y(d.happiness))
// ----- subscription and transitions ---------
// COMPLETE HERE
// 1. create a function that
// a. gets the value of the selector
// b. gets the corresponding scale
// c. computes the new x and y coordinates AND TRANSITIONS to them
// d. changes the x axis label ( x_axis_label.text() )
// e. changes the title ( title.text() )
// remember that we constructed three objects
// for instance: titles.gdp refers to the GDP title
// or that x_scales.corruption refers to the corruption x-scale
// 2. subscribe this function to the change event on the selector
// ----- title and source --------------------
const title = svg.append("text") // Move me top left !
.text(titles.gdp)
.attr("x", width-margin.right)
.attr("text-anchor", "end")
.attr("y", margin.top/2)
.attr("font-family", "tahoma")
const source = svg.append("text") // Move me bottom right !
.text("Happiness Report (2020)")
.attr("x", width - margin.right)
.attr("y", height - 1)
.attr("font-family", "tahoma")
.attr("font-size", 8)
.attr("text-anchor", "end")
.attr("fill", "grey")
// ----- axes ---------------------------------
const x_axis_y_pos = height-2*margin.bottom/3
const x_axis = svg.append("g").attr("transform", "translate(0 "+x_axis_y_pos+")")
const make_x_axis = d3.axisBottom(correction_x)
make_x_axis(x_axis)
const x_axis_label = x_axis.append("text")
.text(labels.gdp)
.attr("fill", "grey")
.attr("y", -2)
.attr("x", (width-margin.right)/2)
.attr("text-anchor", "end")
const y_axis_x_pos = margin.left/2
const y_axis = svg.append("g").attr("transform", "translate("+y_axis_x_pos+" 0)")
const make_y_axis = d3.axisLeft(correction_y)
make_y_axis(y_axis)
const y_axis_label = y_axis.append("text")
.text("Average perceived happiness")
.attr("fill", "grey")
.attr("transform", "rotate(-90) translate(-50 10)")
.attr("text-anchor", "end")
// -------------------------------------------
return div.node()
}
Insert cell
Insert cell
// PLANNED
// _d3.js_ has timers, i.e. objects that can trigger a callback function at regular intervals: ```js t = d3.interval(my_function, 300) // triggers my_function every 300 ms t.stop() // stops the timer ```
// **Q8.1** Implement a function `increment()` that increases the slider by one year, until it reaches the maximal year. _The slider's value is available through `<selection>.property("value")` and can be set through `<selection>.property("value", value)`._
// **Q8.2** Use a timer and the `increment()` function to implement the play functionality. _Hitting play when the cursor in on the maximal year should play the slider from the beginning._
// **Q8.3** Implement the stop functionality.
Insert cell
Insert cell
// PLANNED
// one can replace the select input by a range input
// and the data source from records_2019 to records_europe
// in order to produce an animation of hapiness vs. wealth over time
// implement play and stop buttons
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
world = FileAttachment("countries-50m.json").json()
Insert cell
Insert cell
country_collection = topojson.feature(world, "countries")
Insert cell
countries = country_collection.features
Insert cell
Insert cell
Insert cell
Insert cell
projection = d3.geoCylindricalEqualArea()
.fitHeight(height, country_collection) // this option scales the projection so that the whole collection
// of countries fit in the pre-defined height of the map
Insert cell
Insert cell
make_path = d3.geoPath(projection) // this is a function
Insert cell
countries[0].properties.name // get the name of the first country
Insert cell
make_path(countries[0]) // convert Zimbabwe's geometry to a path
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
for(let country of countries){
svg.append("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 0.3)
.attr("d", make_path(country))
}
return svg.node()

}
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
// step 1: from countries, keep only the European ones
const mediterranean = ["Albania", "Algeria", "Bosnia and Herz.", "Croatia", "N. Cyprus", "Cyprus", "Egypt", "France", "Greece", "Palestine", "Israel", "Italy", "Lebanon", "Libya", "Malta", "Macedonia", "Monaco", "Montenegro", "Morocco", "Portugal", "Serbia", "Slovenia", "Spain", "Syria", "Turkey", "Tunisia"]
const mediterranean_countries = countries.filter( country => ... )
// step 2: create a projection that fit into the dimensions height x width
const projection = d3.geoBonne()
.parallel(52)
.rotate([-15, 0])
.center([0, 52])
.translate([width / 2, 0])
.scale(width) // It can actually quit hard to tweak the parameters, so here you go!
// step 3: create your path factory
...
// step 4: populate your map with path elements
for(let country of mediterranean_countries){
...
}
return svg.node()
}
Insert cell
Insert cell
function toggle_country(){ ... }
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
for(let country of countries){
svg.append("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 0.3)
.attr("d", make_path(country))
}
return svg.node()

}
Insert cell
Insert cell
// PLANNED
// - a map and a chart side by side with some interaction between them
// - a couple of events to implement

<!-- You may have noticed that some countries are locked away from interaction by overlaying heavily countries that mask them. Use the `sort` method to reorder `countries_svg` with the biggest countries first, so that they are appended first and do not cover less populated contries. Observe that the data come along, by moving the sort at different place in the code, for instance in between the creation of `circle` elements and of `text` elements. _The `sort` method needs a function comparing two elements and returning a negative number if the first should come before the second, and negative otherwise. For instance `(a,b) => a.income-b.income` would sort the countries by increasing income. -->
Insert cell
Insert cell
Insert cell
Insert cell
records
Insert cell
correction_records_2019 = records
.filter(record => record.year == 2019) // filter 2019
.map(function(record){
return {
name: record["Country name"],
gdp: record["Log GDP per capita"],
corruption: record["Perceptions of corruption"],
happiness: record["Life Ladder"]
}
})
.filter(record => record.gdp && record.corruption && record.happiness ) // no missing
Insert cell
correction_domain_gdp = d3.extent(correction_records_2019, record => record.gdp)
Insert cell
correction_domain_happiness = d3.extent(correction_records_2019, record => record.happiness)
Insert cell
correction_domain_corruption = d3.extent(correction_records_2019, record => record.corruption).reverse()
Insert cell
correction_range_x = [margin.left, width-margin.right]
Insert cell
Insert cell
correction_x = d3.scaleLinear().domain(correction_domain_gdp).range(correction_range_x)
// d3.scaleLog for logarithmic
Insert cell
correction_y = d3.scaleLinear().domain(correction_domain_happiness).range(correction_range_y)
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
for(let record of correction_records_2019){
svg.append("circle")
.attr("r", 5)
.attr("cy", correction_y(record.happiness))
.attr("cx", correction_x(record.gdp))
.attr("fill", "lightgrey")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
}
const title = svg.append("text") // Move me top left !
.text("The correlation between happiness and wealth is weak")
.attr("x", margin.left)
.attr("y", margin.top/2)
.attr("font-family", "tahoma")
const source = svg.append("text") // Move me bottom right !
.text("Happiness Report (2020)")
.attr("x", width - margin.right)
.attr("y", height - 1)
.attr("font-family", "tahoma")
.attr("font-size", 8)
.attr("text-anchor", "end")
.attr("fill", "grey")
const x_axis_y_pos = height-2*margin.bottom/3
const x_axis = svg.append("g").attr("transform", "translate(0 "+x_axis_y_pos+")")
const make_x_axis = d3.axisBottom(correction_x)
make_x_axis(x_axis)
x_axis.append("text")
.text("Log GDP per capita")
.attr("fill", "grey")
.attr("y", -2)
.attr("x", (width-margin.right)/2)
.attr("text-anchor", "end")
const y_axis_x_pos = margin.left/2
const y_axis = svg.append("g").attr("transform", "translate("+y_axis_x_pos+" 0)")
const make_y_axis = d3.axisLeft(correction_y)
make_y_axis(y_axis)
y_axis.append("text")
.text("Average perceived happiness")
.attr("fill", "grey")
.attr("transform", "rotate(-90) translate(-50 10)")
.attr("text-anchor", "end")
return svg.node()
}
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", 50)
.attr("width", width)
svg.append("circle").attr("cx", 20)
svg.append("circle").attr("cx", 50)
svg.append("circle").attr("cx", 80)
svg.append("circle").attr("cx", 110)
svg.append("circle").attr("cx", 140)
const welcome_text = "Select a couple of circles, or:"
const text = svg.append("text")
.text(welcome_text)
.attr("y", 30)
.attr("x", 160)
const selector = svg.append("text")
.text("CLICK HERE TO SELECT ALL")
.attr("y", 30)
.attr("x", 380)
.attr("cursor", "pointer")
const circles = svg.selectAll("circle")
.attr("cy", 25)
.attr("r", 10)
.attr("fill", "lightgrey")
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.attr("cursor", "pointer")
function toggle(){
const circle = d3.select(this)
const current_color = circle.attr("fill")
circle.attr("fill", current_color == "grey" ? "lightgrey" : "grey")
svg.dispatch("update")
}
function update_comment(){
const selected_circles = circles.filter( function() { return d3.select(this).attr("fill")=="grey" } )
const n = selected_circles.size()
const message = n > 1 ?
n + " circles selected" : n==1 ?
"1 single circle selected" : welcome_text
text.text(message)
}
function select_all(){ // <---------- HERE
circles.attr("fill", "grey")
svg.dispatch("update")
}
selector.on("click", select_all) // <---------- HERE
circles.on("click", toggle)
svg.on("update", update_comment)
return svg.node()
}
Insert cell
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
for(let country of countries){
svg.append("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 0.3)
.attr("d", make_path(country))
.datum({selected: false}) // <--- for storing information about the state
.on("click", correction_toggle_country) // <--- event subscription
}
return svg.node()

}
Insert cell
correction_toggle_country = function(){

const path = d3.select(this) // <---- get the element that fired the click event
const selected = ! path.datum().selected // <---- if was selected, it should not be anymore
path.attr("fill", selected ? "red" : "white") // <---- change fill color accordingly
path.datum().selected = selected // <---- updagte the datum
}
Insert cell
{
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
// step 1: from countries, keep only the European ones
const mediterranean = ["Albania", "Algeria", "Bosnia and Herz.", "Croatia", "N. Cyprus", "Cyprus", "Egypt", "France", "Greece", "Palestine", "Israel", "Italy", "Lebanon", "Libya", "Malta", "Macedonia", "Monaco", "Montenegro", "Morocco", "Portugal", "Serbia", "Slovenia", "Spain", "Syria", "Turkey", "Tunisia"]
const mediterranean_countries = countries.filter( country => mediterranean.includes(country.properties.name) )
// step 2: create your projection that fit into the dimensions height x width
const projection = d3.geoBonne()
.parallel(52)
.rotate([-15, 0])
.center([0, 52])
.translate([width / 2, 0])
.scale(width)
// step 3: create your path factory
const make_path = d3.geoPath(projection)
// step 4: populate your map with paths
for(let country of mediterranean_countries){
svg.append("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 0.3)
.attr("d", make_path(country))
}
return svg.node()
}
Insert cell
Insert cell
d3 = require("d3@5", "d3-geo-projection@2", "d3-array@^2.10")
Insert cell
topojson = require("topojson-client@3")
Insert cell
import {Range} from "@observablehq/inputs"
Insert cell
import {viewof context, update} from "@d3/projection-transitions"
Insert cell
import {chart} from "@d3/easing-animations"
Insert cell
update
Insert cell
style = html`<style type="text/css">
code, pre{
font-size: 80%;
background-color: #eee;
border-radius: 0.3em;
padding: 3px 4px 1px;
}
</style>`
Insert cell
// possible improvements
// [ ] add d3.extent and alike to a table
// [ ] map clipping
// [ ] correction last exercice section 5
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart2 = {
// const height = margin.top+margin.bottom // overwrite general height
const svg = d3.create("svg")
.attr("height", height)
.attr("width", width)
const text = svg.append("text")
.attr("y", margin.top/2)
const line = d3.line()
.x( d => correction_x(d.gdp) )
.y( d => correction_y(d.happiness) )
const paths = svg.append("g")
const circles = svg.append("g")
for(let country of countries3){
let path = paths.append("path")
.attr("stroke", "grey")
.attr("fill", "none")
.attr("stroke-dasharray", "1 1")
// .property("name", country.name)
.property("data", country.data)
}
for(let country of countries3){
circles.append("circle")
.attr("r", 3)
.attr("fill", "grey")
.attr("fill-opacity", 0.7)
.attr("stroke", "black")
.attr("stroke-width", 0.4)
//.attr("cx", x(country.data[0].gdp))
//.attr("cy", y(country.data[0].happiness))
.property("name", country.name)
.property("data", country.data)
}
const x_axis_y_pos = height-margin.bottom/2
const x_axis = svg.append("g").attr("transform", "translate(0 "+x_axis_y_pos+")")
const make_x_axis = d3.axisBottom(correction_x)
make_x_axis(x_axis)
x_axis.append("text")
.text("Log GDP per capita")
.attr("fill", "grey")
.attr("y", -2)
.attr("x", correction_range_x[1])
.attr("text-anchor", "end")
const y_axis_x_pos = margin.left/2
const y_axis = svg.append("g").attr("transform", "translate("+y_axis_x_pos+" 0)")
const make_y_axis = d3.axisLeft(correction_y)
make_y_axis(y_axis)
return Object.assign(svg.node(), {
update(year) {
circles.selectAll("circle").transition()
.attr("cx", function(){
const data = d3.select(this).property("data")
return( correction_x( data[year-2015].gdp ))
})
.attr("cy", function(){
const data = d3.select(this).property("data")
return( correction_y( data[year-2015].happiness ))
})
}
})

return(svg.node())
}
Insert cell
chart2.update(year)
Insert cell
countries3 = {
let countries = {}, name, record, year, index
let authorized_years = [2015, 2016, 2017, 2018, 2019]
for(record of records){
name = record["Country name"]
year = record["year"]
if( ! authorized_years.includes(year)) continue
if( (! record["Life Ladder"]) || (! record["Log GDP per capita"]) ) continue
if( ! countries[name]) countries[name] = {name: name, count: 0, data: []}
index = year - authorized_years[0]
countries[name].count += 1
countries[name].data[index] = {}
countries[name].data[index].happiness = record["Life Ladder"]
countries[name].data[index].gdp = record["Log GDP per capita"]
}
return Object.values(countries).filter(c => c.count == 5)
}
Insert cell
records_europe2 = {
let rec, recs = [] // used to store the reorganized records
let name, names = [] // used to store the names of already seen countries
let european_countries = ["Austria", "Belarus", "Belgium", "Croatia", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece"]
for(let rec of records){
let name = rec["Country name"]
if( european_countries.includes(name) ) {
if( ! names.includes(name) ) {
recs.push({name: name, data:[]})
names.push(name)
}
let index = names.indexOf(name)
recs[index].data.push({happiness: rec["Life Ladder"], year: rec["year"], gdp: rec["Log GDP per capita"]})
}
}
return recs
}
Insert cell
// chart.update(year)
Insert cell
{
// const height = margin.top+margin.bottom // overwrite general height
let map_width = height
const svg = d3.create("svg")
.attr("height", 0.8*height)
.attr("width", 2*width)
const map = svg.append("g")
.attr("transform", "translate("+0.8*width+" 0) scale(0.8)")
for(let country of countries){
map.append("path")
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 0.3)
.attr("d", make_path(country))
.property("name", country.properties.name)
}
const paths = map.selectAll("path")
.property("selected", false)
const graph = svg.append("g")
.attr("transform", "scale(0.8)")
const text = graph.append("text")
.attr("y", margin.top/2)
for(let record of correction_records_2019){
graph.append("circle")
.attr("r", 5)
.attr("cy", correction_y(record.happiness)) // .attr("cy", margin.top)
.attr("cx", correction_x(record.gdp))
.attr("fill-opacity", 0.3)
.attr("stroke", "black")
.attr("stroke-width", 0.4)
.property("name", record["Country name"])
//.attr("stroke-opacity", 1)
}
const circles = graph.selectAll("circle")
.property("selected", false)
.property("hovered", false)
//.datum( function(){ return( {selected: false , hovered: false} ) } )
function toggle_circle( circle ){
circle.property("selected", ! circle.property("selected") )
circle.attr("stroke-width", circle.property("selected") ? 4 : 0.4)
circle.dispatch("update")
}
function toggle_path( path ){
path.property("selected", ! path.property("selected") )
path.attr("fill", path.property("selected") ? "red" : "white")
}
function highlight(){
const circle = d3.select(this)
circle.property("hovered", true)
circle.attr("fill", "pink")
circle.dispatch("update")
}
function unhighlight(){
const circle = d3.select(this)
circle.property("hovered", false)
circle.attr("fill", "black")
circle.dispatch("update")
}
let selected_countries = new Set()
function update_comment(){
const circle = d3.select(this)
const country = circle.property("name")
if( circle.property("selected") || circle.property("hovered")){
selected_countries.add(country)
} else {
selected_countries.delete(country)
}
let first_5_countries = Array.from(selected_countries.values())
if(first_5_countries.length > 5){
first_5_countries.length = 6
first_5_countries[5] = "..."
}
text.text( first_5_countries.join(", ") )
}
circles.on("click", function() {
const circle = d3.select(this)
const path = paths.filter(function(){
return( circle.property("name") == d3.select(this).property("name"))
})
toggle_path( path )
toggle_circle( circle )
} )
// circles.on("mouseenter", highlight)
// circles.on("mouseleave", unhighlight)
circles.on("update", update_comment)
paths.on("click", function() {
const path = d3.select(this)
const circle = circles.filter(function(){
return( path.property("name") == d3.select(this).property("name"))
})
toggle_path( path )
toggle_circle( circle )
} )
//paths.on("mouseenter", highlight2)
//paths.on("mouseleave", unhighlight2)
const x_axis_y_pos = height-margin.bottom/2
const x_axis = graph.append("g").attr("transform", "translate(0 "+x_axis_y_pos+")")
const make_x_axis = d3.axisBottom(correction_x)
make_x_axis(x_axis)
x_axis.append("text")
.text("Log GDP per capita")
.attr("fill", "grey")
.attr("y", -2)
.attr("x", correction_range_x[1])
.attr("text-anchor", "end")
const y_axis_x_pos = margin.left/2
const y_axis = graph.append("g").attr("transform", "translate("+y_axis_x_pos+" 0)")
const make_y_axis = d3.axisLeft(correction_y)
make_y_axis(y_axis)

return(svg.node())
}
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