Public
Edited
Jun 18
Insert cell
Insert cell
Insert cell
viewof hoveredYear = Inputs.range([1961, 2023], {step: 1, value: 1990})
Insert cell
viewof selectedCountry = Inputs.select(
["None", ...new Set(climate.map(d => d.Country).filter(d => d !== "World"))].sort(),
{
value: "None",
label: "Add country to compare"
}
)
Insert cell
import {vl} from '@vega/vega-lite-api-v5'
Insert cell
d3 = require("d3@7")
Insert cell
topojson = require("topojson-client@3")
Insert cell
world = await d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
Insert cell
land = topojson.feature(world, world.objects.countries)
Insert cell
numericToISO3 = new Map([
["004", "AFG"], ["008", "ALB"], ["012", "DZA"], ["016", "ASM"], ["020", "AND"],
["024", "AGO"], ["028", "ATG"], ["031", "AZE"], ["032", "ARG"], ["036", "AUS"],
["040", "AUT"], ["044", "BHS"], ["048", "BHR"], ["050", "BGD"], ["051", "ARM"],
["052", "BRB"], ["056", "BEL"], ["060", "BMU"], ["064", "BTN"], ["068", "BOL"],
["070", "BIH"], ["072", "BWA"], ["076", "BRA"], ["084", "BLZ"], ["090", "SLB"],
["092", "VGB"], ["096", "BRN"], ["100", "BGR"], ["104", "MMR"], ["108", "BDI"],
["112", "BLR"], ["116", "KHM"], ["120", "CMR"], ["124", "CAN"], ["132", "CPV"],
["136", "CYM"], ["140", "CAF"], ["144", "LKA"], ["148", "TCD"], ["152", "CHL"],
["156", "CHN"], ["170", "COL"], ["174", "COM"], ["178", "COG"], ["180", "COD"],
["184", "COK"], ["188", "CRI"], ["191", "HRV"], ["192", "CUB"], ["196", "CYP"],
["203", "CZE"], ["204", "BEN"], ["208", "DNK"], ["212", "DMA"], ["214", "DOM"],
["218", "ECU"], ["222", "SLV"], ["226", "GNQ"], ["231", "ETH"], ["232", "ERI"],
["233", "EST"], ["242", "FJI"], ["246", "FIN"], ["250", "FRA"], ["262", "DJI"],
["266", "GAB"], ["270", "GMB"], ["275", "PSE"], ["276", "DEU"], ["288", "GHA"],
["292", "GIB"], ["296", "KIR"], ["300", "GRC"], ["304", "GRL"], ["308", "GRD"],
["316", "GUM"], ["320", "GTM"], ["324", "GIN"], ["328", "GUY"], ["332", "HTI"],
["340", "HND"], ["344", "HKG"], ["348", "HUN"], ["352", "ISL"], ["356", "IND"],
["360", "IDN"], ["364", "IRN"], ["368", "IRQ"], ["372", "IRL"], ["376", "ISR"],
["380", "ITA"], ["384", "CIV"], ["388", "JAM"], ["392", "JPN"], ["398", "KAZ"],
["400", "JOR"], ["404", "KEN"], ["408", "PRK"], ["410", "KOR"], ["414", "KWT"],
["417", "KGZ"], ["418", "LAO"], ["422", "LBN"], ["426", "LSO"], ["428", "LVA"],
["430", "LBR"], ["434", "LBY"], ["438", "LIE"], ["440", "LTU"], ["442", "LUX"],
["446", "MAC"], ["450", "MDG"], ["454", "MWI"], ["458", "MYS"], ["462", "MDV"],
["466", "MLI"], ["470", "MLT"], ["478", "MRT"], ["480", "MUS"], ["484", "MEX"],
["496", "MNG"], ["498", "MDA"], ["499", "MNE"], ["504", "MAR"], ["508", "MOZ"],
["512", "OMN"], ["516", "NAM"], ["520", "NRU"], ["524", "NPL"], ["528", "NLD"],
["531", "CUW"], ["533", "ABW"], ["534", "SXM"], ["540", "NCL"], ["548", "VUT"],
["554", "NZL"], ["558", "NIC"], ["562", "NER"], ["566", "NGA"], ["570", "NIU"],
["574", "NFK"], ["578", "NOR"], ["580", "MNP"], ["583", "FSM"], ["584", "MHL"],
["585", "PLW"], ["586", "PAK"], ["591", "PAN"], ["598", "PNG"], ["600", "PRY"],
["604", "PER"], ["608", "PHL"], ["612", "PCN"], ["616", "POL"], ["620", "PRT"],
["624", "GNB"], ["626", "TLS"], ["634", "QAT"], ["642", "ROU"], ["643", "RUS"],
["646", "RWA"], ["652", "BLM"], ["654", "SHN"], ["659", "KNA"], ["660", "AIA"],
["662", "LCA"], ["663", "MAF"], ["666", "SPM"], ["670", "VCT"], ["674", "SMR"],
["678", "STP"], ["682", "SAU"], ["686", "SEN"], ["688", "SRB"], ["690", "SYC"],
["694", "SLE"], ["702", "SGP"], ["703", "SVK"], ["704", "VNM"], ["705", "SVN"],
["706", "SOM"], ["710", "ZAF"], ["716", "ZWE"], ["724", "ESP"], ["728", "SSD"],
["729", "SDN"], ["740", "SUR"], ["748", "SWZ"], ["752", "SWE"], ["756", "CHE"],
["760", "SYR"], ["762", "TJK"], ["764", "THA"], ["768", "TGO"], ["772", "TKL"],
["776", "TON"], ["780", "TTO"], ["784", "ARE"], ["788", "TUN"], ["792", "TUR"],
["795", "TKM"], ["800", "UGA"], ["804", "UKR"], ["807", "MKD"], ["818", "EGY"],
["826", "GBR"], ["834", "TZA"], ["840", "USA"], ["858", "URY"], ["860", "UZB"],
["862", "VEN"], ["876", "WLF"], ["882", "WSM"], ["887", "YEM"], ["894", "ZMB"]
])

Insert cell
climate = await FileAttachment("climate.csv").csv({typed: true})
Insert cell
world_temp = climate.find(d => d.Country === "World")
Insert cell
globalAverage = {
let years = d3.range(1961, 2025);
return years.map(year => ({
year,
value: +world_temp[String(year)]
}));
}
Insert cell
lineChart = {
const container = d3.create("div")
.style("width", "600px");

// Create the plot
const plot = Plot.plot({
width: 600,
height: 400,
marginBottom: 80, // Extra space for slider
x: {
label: "Year →",
tickFormat: d => d.toString()
},
y: {
label: "↑ Global Temp Anomaly (°C)"
},
marks: [
Plot.line(globalAverage, {
x: "year",
y: "value",
stroke: "steelblue",
strokeWidth: 2
}),
Plot.ruleY([0]),
Plot.dot(globalAverage, {
x: "year",
y: "value",
r: d => d.year === hoveredYear ? 6 : 2,
fill: "steelblue",
stroke: "white",
strokeWidth: 1
}),
Plot.ruleX([hoveredYear])
]
});

container.node().appendChild(plot);

// Add slider below the chart
const sliderContainer = container.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("background", "#f8f9fa")
.style("border-top", "1px solid #dee2e6");

const sliderLabel = sliderContainer.append("label")
.style("display", "block")
.style("margin-bottom", "10px")
.style("font-weight", "bold")
.text("Selected Year");

const slider = sliderContainer.append("input")
.attr("type", "range")
.attr("min", 1961)
.attr("max", 2023)
.attr("step", 1)
.attr("value", hoveredYear)
.style("width", "400px")
.style("margin-bottom", "10px");

const yearDisplay = sliderContainer.append("div")
.style("font-size", "18px")
.style("color", "#007acc")
.text(hoveredYear);

// Update on slider change
slider.on("input", function() {
const newYear = +this.value;
hoveredYear.value = newYear;
hoveredYear.dispatchEvent(new CustomEvent("input"));
yearDisplay.text(newYear);
});

return container.node();
}
Insert cell
climateMapData = climate
.filter(d => d.Country !== "World")
.map(d => ({
country: d.Country,
value: +d[hoveredYear]
}))
Insert cell
dataByISO = d3.index(climate.filter(d => d.year === hoveredYear), d => d.iso3)
Insert cell
viewof year = Inputs.range([1961, 2024], {step: 1, label: "Year",value: 1961})
Insert cell
mapChart = {
let data = climate.filter(d => d.Year === hoveredYear)
return Plot.plot({
width: 900,
height: 600,
title: "Change in Surface Temperature from Baseline Climatology",
projection: "equal-earth",
color: {
type: "diverging",
scheme: "RdBu",
reverse: true,
legend: true,
label: "°C Temperature Change",
domain: [-1, 5]
},
marks: [
Plot.geo(land, {
fill: d => {
const iso3 = numericToISO3.get(String(d.id).padStart(3, "0"));
const row = climate.find(r => r.ISO3 === iso3);
return row?.[String(year)] ?? null;
},
stroke: "#ccc"
}),
Plot.graticule(),
Plot.sphere()
]
})
}

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