Public
Edited
Jul 29, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data = {
const data = [];
// https://data.giss.nasa.gov/gistemp/tabledata_v3/GLB.Ts+dSST.csv
await d3.csvParse(await FileAttachment("temperatures.csv").text(), (d, i, columns) => {
for (let i = 1; i < 13; ++i) { // pivot longer
data.push({date: new Date(Date.UTC(d.Year, i - 1, 1)), value: +d[columns[i]]});
}
});
return data;
}
Insert cell
Insert cell
Insert cell
width
Insert cell
height = 600
Insert cell
chartWidth = width
Insert cell
chartHeight = height / 2
Insert cell
Insert cell
margins = ({top: 40, right: 10, bottom: 50, left: 40})
Insert cell
Insert cell
Insert cell
allCharts = {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "scatterplot");
svg.append("g")
.attr("transform", `translate(0, ${chartHeight})`)
.attr("class", "linechart");
return svg.node();
}
Insert cell
{
const chartsContainer = d3.select(allCharts);
chartsContainer.select("g.scatterplot")
.call(subPlot1);
chartsContainer.select("g.linechart")
.call(subPlot2);
}
Insert cell
Insert cell
subPlot1 = svg => {

const xSubPlot1 = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margins.left, chartWidth - margins.right])

const ySubPlot1 = d3.scaleLinear()
.domain(d3.extent(data, d => d.value)).nice()
.range([chartHeight - margins.bottom, margins.top])

const color = d3.scaleSequential()
.domain([d3.max(data, d => d.value), -d3.max(data, d => d.value)])
.interpolator(d3.interpolateRdBu)

// x axis
const xaxis = svg.selectAll("g.xAxis")
.data([1])
.join("g")
.attr("class", "xAxis")
.attr("transform", `translate(0,${chartHeight - margins.bottom})`)
.call(d3.axisBottom(xSubPlot1).ticks(chartWidth / 80))
.call(g => g.select(".domain").remove());

// y axis
const yaxis = svg.selectAll("g.yAxis")
.data([1])
.join("g")
.attr("class", "yAxis")
.attr("transform", `translate(${margins.left},0)`)
.call(d3.axisLeft(ySubPlot1).ticks(null, "+"))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line")
.clone()
.attr("x2", chartWidth - margins.right - margins.left)
.attr("stroke-opacity", d => d === 0 ? 1 : 0.1))
.call(g => g.append("text")
.attr("fill", "#000")
.attr("x", 5)
.attr("y", margins.top * 2 / 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Anomaly (°C)"));

const circles = svg
.selectAll()
.data(data)
.join("circle")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.attr("cx", d => xSubPlot1(d.date))
.attr("cy", d => ySubPlot1(d.value))
.attr("fill", d => color(d.value))
.attr("r", 2)

// Add a title
const title = svg.selectAll("text.chartTitle")
.data([1])
.join("text")
.attr("class", "chartTitle")
.attr("x", chartWidth / 2)
.attr("y", (margins.top / 2))
.attr("text-anchor", "middle")
.attr("front-weight", "bold")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-size", "15px")
.text("Global temperature change, 1880 - 2020")
}
Insert cell
Insert cell
subPlot2 = svg => {
// Bin the data.
const bins = d3.bin()
.thresholds(40)
.value((d) => d.value)
(data)

// Declare the x (horizontal position) scale.
const x = d3.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range([margins.left, chartWidth - margins.right])

// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, d3.max(bins, (d) => d.length)])
.range([chartHeight - margins.bottom, margins.top])

const barColor = "lightblue"
// Add a rect for each bin.
const bars = svg
.selectAll()
.data(bins)
.join("rect")
.attr("x", (d) => x(d.x0) + 1)
.attr("width", (d) => x(d.x1) - x(d.x0) - 1)
.attr("y", (d) => y(d.length))
.attr("height", (d) => y(0) - y(d.length))
.attr("fill", barColor)
.attr("opacity", 0.8)
.attr("class", "bars")

const xaxis = svg.selectAll("g.xAxis")
.data([1])
.join("g")
.attr("class", "xAxis")
.call(d3.axisBottom(x).tickFormat(d3.format(",.2f")))
.attr("transform", `translate(0, ${chartHeight - margins.bottom})`)
.selectAll("text.title")
.data([1])
.join("text")
.attr("class", "title")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("y", + 30)
.attr("fill", "currentColor")
.attr("x", chartWidth - 80)
.text("Temperature anomaly ⭢")
const yaxis = svg.selectAll("g.yAxis")
.data([1])
.join("g")
.attr("class", "yAxis")
.call(d3.axisLeft(y))
.attr("transform", `translate(${margins.left}, 0)`)
.selectAll("text.title")
.data([1])
.join("text")
.attr("class", "title")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("y", margins.top /2)
.attr("fill", "currentColor")
.attr("x", 5)
.attr("y", margins.top * 2 / 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("↑ Frequency")

// title
const title = svg.selectAll("text.chartTitle")
.data([1])
.join("text")
.attr("class", "chartTitle")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.attr("fill", "currentColor")
.attr("y", margins.top /2)
.attr("x", chartWidth/2)
.attr("text-anchor", "middle")
.text("Temperature anomaly of selected years");
}
Insert cell
Insert cell
Insert cell
Insert cell
mutable brushed = []
Insert cell
xSubPlot1 = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margins.left, chartWidth - margins.right])
Insert cell
ySubPlot1 = d3.scaleLinear()
.domain(d3.extent(data, d => d.value)).nice()
.range([chartHeight - margins.bottom, margins.top])
Insert cell
color = d3.scaleSequential()
.domain([d3.max(data, d => d.value), -d3.max(data, d => d.value)])
.interpolator(d3.interpolateRdBu)
Insert cell
brush = d3.brush()
.extent([
[d3.min(xSubPlot1.range()), d3.min(ySubPlot1.range())],
[d3.max(xSubPlot1.range()), d3.max(ySubPlot1.range())]
])
.on("brush end", (event) => {
if (event.selection === null) {
mutable brushed = [];
} else {
const [[x0, y0], [x1, y1]] = event.selection;
mutable brushed = data
.filter(d => {
return x0 <= xSubPlot1(d.date) &&
x1 >= xSubPlot1(d.date) &&
y0 <= ySubPlot1(d.value) &&
y1 >= ySubPlot1(d.value)
}).map(d => d.date)
}
})
Insert cell
Insert cell
updateSubPlot1 = svg => {

svg.selectAll(".brushContainer")
.data([1])
.join("g")
.attr("class", "brushContainer")
.call(brush);

// x axis
const xaxis = svg.selectAll("g.xAxis")
.data([1])
.join("g")
.attr("class", "xAxis")
.attr("transform", `translate(0,${chartHeight - margins.bottom})`)
.call(d3.axisBottom(xSubPlot1).ticks(chartWidth / 80))
.call(g => g.select(".domain").remove());

// y axis
const yaxis = svg.selectAll("g.yAxis")
.data([1])
.join("g")
.attr("class", "yAxis")
.attr("transform", `translate(${margins.left},0)`)
.call(d3.axisLeft(ySubPlot1).ticks(null, "+"))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll("text.title")
.data([1])
.join("text")
.attr("class", "title")
.attr("fill", "currentColor")
.attr("x", 5)
.attr("y", margins.top * 2 / 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Anomaly (°C)"));

const circles = svg
.selectAll("circle.dot")
.data(data)
.join("circle")
.attr("stroke", "#000")
.attr("class", "dot")
.attr("stroke-opacity", 0.2)
.attr("cx", d => xSubPlot1(d.date))
.attr("cy", d => ySubPlot1(d.value))
.attr("fill", d => {
if(brushed.includes(d.date)) {
return color(d.value);
} else {
return "none";
}
})
.attr("r", 2)

// Add a title
const title = svg.selectAll("text.chartTitle")
.data([1])
.join("text")
.attr("class", "chartTitle")
.attr("x", chartWidth / 2)
.attr("y", (margins.top / 2))
.attr("text-anchor", "middle")
.attr("front-weight", "bold")
.attr("font-family", "Helvetica Neue, Arial")
.attr("font-size", "15px")
.text("Global temperature change, 1880 - 2020")
}
Insert cell
Insert cell
updateSubPlot2 = svg => {
// Bin the data.
const bins = d3.bin()
.thresholds(40)
.value((d) => d.value)
(d3.filter(data, d => brushed.indexOf(d.date) >= 0))

// Declare the x (horizontal position) scale.
const x = d3.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range([margins.left, chartWidth - margins.right])

// Declare the y (vertical position) scale.
const y = d3.scaleLinear()
.domain([0, d3.max(bins, (d) => d.length)])
.range([chartHeight - margins.bottom, margins.top])

const barColor = "lightblue"

// Add a rect for each bin.
const bars = svg
.selectAll("rect.bars")
.data(bins)
.join("rect")
.attr("x", (d) => x(d.x0) + 1)
.attr("width", (d) => x(d.x1) - x(d.x0) - 1)
.attr("y", (d) => y(d.length))
.attr("height", (d) => y(0) - y(d.length))
.attr("fill", barColor)
.attr("opacity", 0.8)
.attr("class", "bars")

const xaxis = svg.selectAll("g.xAxis")
.data([1])
.join("g")
.attr("class", "xAxis")
.call(d3.axisBottom(x).tickFormat(d3.format(",.2f")))
.attr("transform", `translate(0,${chartHeight - margins.bottom})`)
.selectAll("text.title")
.data([1])
.join("text")
.attr("class", "title")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("y", + 30)
.attr("fill", "currentColor")
.attr("x", chartWidth - 80)
.text("Temperature anomaly ⭢")
const yaxis = svg.selectAll("g.yAxis")
.data([1])
.join("g")
.attr("class", "yAxis")
.call(d3.axisLeft(y))
.attr("transform", `translate(${margins.left}, 0)`)
.selectAll("text.title")
.data([1])
.join("text")
.attr("class", "title")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("y", 50)
.attr("fill", "currentColor")
.attr("x", 5)
.attr("y", margins.top * 2 / 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("↑ Frequency")

// title
const title = svg.selectAll("text.chartTitle")
.data([1])
.join("text")
.attr("class", "chartTitle")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.attr("fill", "currentColor")
.attr("y", margins.top /2)
.attr("x", chartWidth/2)
.attr("text-anchor", "middle")
.text("Temperature anomaly of selected years")
}
Insert cell
Insert cell
finalCanvas = {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const subPlot1 = svg.append("g")
.attr("class", "subPlot1");
const sublot2 = svg.append("g")
.attr("transform", `translate(0, ${chartHeight})`)
.attr("class", "subPlot2");
return svg.node();
}
Insert cell
updateFinalCanvas = {
const chartsContainer = d3.select(finalCanvas);
chartsContainer.select("g.subPlot1")
.call(updateSubPlot1)
chartsContainer.select("g.subPlot2")
.call(updateSubPlot2)
}
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require("d3@7")
Insert cell
import {toc} from "@jonfroehlich/collapsible-toc"
Insert cell
import {textcolor} from "@observablehq/text-color-annotations-in-markdown"
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