Public
Edited
Aug 13, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legendValues = [...coldValues.slice().reverse(), ...heatValues]
Insert cell
legendData = d3
.range(-1, 2, 0.5)
.map((value, i, e) => ({
type: "f",
position: value,
label: legendLabel(value, i, e, "°F")
}));
Insert cell
legendLabel = (value, i, e, unit) => {
return `${(value < 0 ? "-" : value > 0 ? "+" : "")}${Math.abs(value)}${[0, e.length - 1].includes(i) ? unit : ""}`
}
Insert cell
legend = () => {
const wrapper = d3.create("div").style("width", `${w}px`)

wrapper.append("div")
.style("font-family", franklinLight)
.style("line-height", "22px")
.style("margin-bottom", "4px")
.style("text-align", "center")
.html(`${season} temperature change<br />per decade, ${startYear}-${endYear}`);
const margin = { top: 0, bottom: 18 };
const width = 300;
const height = 16;
const scaleBand = d3.scaleBand()
.domain(legendValues)
.range([0, width]);
const scaleLinear = d3.scaleLinear()
.domain(d3.extent(legendValues))
.range([0, width]);

const svg = wrapper.append("svg")
.attr("width", width)
.attr("height", height + margin.top + margin.bottom)
.attr("font-family", franklinLight)
.style("margin", "0 auto")
.style("display", "table")
.style("overflow", "visible");
const g = svg.append("g")
.attr("transform", `translate(0, ${margin.top})`)

const epsilon = 1e-6;
const rect = g.selectAll(".rect")
.data(legendValues)
.join("rect")
.attr("class", "rect")
.attr("transform", d => `translate(${scaleBand(d)})`)
.attr("fill", d => d > 0 ? colorScale(d - epsilon) : colorScale(d + epsilon))
.attr("height", height)
.attr("x", -0.5)
.attr("width", (_, i, e) => scaleBand.bandwidth() + (i < e.length - 1 ? 1 : 0)); // add some overlap
const tick = g.selectAll(".tick")
.data(legendData)
.join("g")
.attr("transform", d => `translate(${scaleLinear(d.position)})`);

tick.append("line")
.attr("y1", d => 0)
.attr("y2", d => height + 4)
.attr("stroke", "#2a2a2a");

tick.append("text")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("y", d => height + 17)
.text(d => d.label);
return wrapper.node();
}
Insert cell
map = function*() {
const wrapper = d3.create("div")
.style("width", `${w}px`)
.style("height", `${h}px`)
.style("position", "relative");

wrapper.append("style").html(css);

const canvas = wrapper.append("canvas")
.style("position", "absolute");
canvas.node().width = w;
canvas.node().height = h;
const context = canvas.node().getContext("2d");
path.context(context);

for (let i = 0, l = X.length * Y.length; i < l; i++) {
const lon = X[i % X.length];
const lat = Y[i / X.length | 0];

let w = Math.max(-180, lon - cell_res);
let e = Math.min(180, lon + cell_res);
let n = Math.min(90, lat + cell_res);
let s = Math.max(-90, lat - cell_res);

if (n > 0 && s < 0) {
if (lon > 0) s = 0;
if (lon < 0) n = 0;
}

context.beginPath();
path(feature(n, e, w, s));

const c = colorScale(values[season.toLowerCase()][i] * 10);

context.fillStyle = c;
context.fill();
context.strokeStyle = c;
context.stroke();

if (i % 1e4 === 0) yield wrapper.node();
}

context.fillStyle = "none";
context.beginPath();
path(countriesGeoInner);
context.strokeStyle = "#494949";
context.stroke();
context.beginPath();
path(countriesGeoOuter);
context.strokeStyle = "#2a2a2a";
context.stroke();

const svg = wrapper.append("svg")
.style("overflow", "visible")
.style("position", "absolute")
.attr("width", w)
.attr("height", h);

// Mask hack
const center = [w / 2, h / 2];
svg.selectAll("polygon")
.data([0, 1])
.join("polygon")
.attr("fill", "white")
.attr("points", d => [
...swoopy.arc([w / 2, 0], center, 1 - 2 * d),
...swoopy.arc(center, [w / 2, h], 1 - 2 * d),
[d * w, h],
[d * w, 0]
]);

svg.selectAll(".sphere")
.data([0, 1])
.join("circle")
.attr("class", "sphere")
.attr("cx", w / 2)
.attr("cy", d => h / 4 + (h / 2) * d)
.attr("fill", "none")
.attr("r", w / 2)
.attr("stroke", "#494949");
const citiesG = svg.selectAll(".city")
.data(cities)
.join("g")
.attr("class", d => `city ${d.pos}`)
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);

citiesG.append("circle")
.attr("class", "bg bg-0")
.attr("r", 3);

citiesG.append("circle")
.attr("class", "bg bg-1")
.attr("r", 3);
citiesG.append("circle")
.attr("class", "fg")
.attr("r", 3);

citiesG.append("text")
.attr("class", "bg bg-0")
.text(d => d.name);

citiesG.append("text")
.attr("class", "bg bg-1")
.text(d => d.name);

citiesG.append("text")
.attr("class", "fg")
.text(d => d.name);

yield wrapper.node();
}
Insert cell
feature = (n, e, w, s) => {
return {
type: "Feature",
geometry: {
type: "Polygon",
// wn, en, es, ws, wn
coordinates: [
[[w, n], [e, n], [e, s], [w, s], [w, n]]
]
}
}
}
Insert cell
Insert cell
css = `
.city circle.bg {
fill: none;
stroke: white;
}
.city circle.bg-0 {
stroke-width: 6px;
stroke-opacity: 0.2;
}
.city circle.bg-1 {
stroke-width: 3px;
stroke-opacity: 0.5;
}
.city circle.fg {
fill: none;
stroke: black;
}

.city.ne text {
transform: translate(4px, -4px);
}
.city.se text {
transform: translate(4px, 13px);
}
.city.e text {
transform: translate(6px, 4.5px);
}
.city.nw text {
text-anchor: end;
transform: translate(-4px, -4px);
}
.city.sw text {
text-anchor: end;
transform: translate(-4px, 13px);
}
.city.w text {
text-anchor: end;
transform: translate(-6px, 4.5px);
}

.city text {
font-size: 14px;
font-family: ${franklinLight};
fill: black;
}

.city text.bg {
fill: white;
stroke: white;
}

.city text.bg-0 {
stroke-width: 4px;
stroke-opacity: 0.2;
}

.city text.bg-1 {
stroke-width: 2px;
stroke-opacity: 0.5;
}
`
Insert cell
Insert cell
colorScale = value => {
if (value < 0) {
let index = -1;

for (let i = 0, l = coldValues.length; i < l; i++) {
const v = coldValues[i];
if (value > v) {
index = i;
break;
}
}
return index === -1 ? coldColors[coldColors.length - 1] : coldColors[index];
}
else {
let index = -1;
for (let i = 0, l = heatValues.length; i < l; i++){
const v = heatValues[i];
if (value < v) {
index = i;
break;
}
}
return index === -1 ? heatColors[heatColors.length - 1] : heatColors[index]
}
}
Insert cell
heatValues = [0.083, 0.167, 0.25, 0.333, 0.417, 0.5, 0.583, 0.667, 0.75, 0.833, 0.917, 1, 1.083, 1.167, 1.25, 1.333, 1.417, 1.5]
Insert cell
heatColors = [
"#f8f4e2", "#f9e9ce", "#fadfbb", "#fad5a7", "#f9ca94", "#f7c081", // 1
"#fd9c63", "#f69259", "#ee8850", "#e77d47", "#df733e", "#d76935", // 2
"#cc0000", "#bf0018", "#b20124", "#a5042b", "#980730", "#8b0a34" // 3
]
Insert cell
coldValues = [-0.083, -0.167, -0.25, -0.333, -0.417, -0.5, -0.583, -0.667, -0.75, -0.833, -0.917, -1]
Insert cell
coldColors = [
"#eff4e9", "#e0ede6", "#d2e5e1", "#c4deda", "#b6d7d2", "#a9d0ca", // -1
"#9fc4d7", "#95bed3", "#8cb7ce", "#82b1ca", "#78aac6", "#6ea4c1" // -2
]
Insert cell
Insert cell
// See https://observablehq.com/@d3/azimuthal-equidistant-hemispheres
projection = d3.geoInterrupt(
d3.geoAzimuthalEquidistantRaw,
[[ // northern hemisphere
[[-180, 0], [ -90, 90], [ 0, 0]],
[[ 0, 0], [ 90, 90], [ 180, 0]]
], [ // southern hemisphere
[[-180, 0], [ -90, -90], [ 0, 0]],
[[ 0, 0], [ 90, -90], [ 180, 0]]
]]
)
.rotate([-20, 0, 90])
.angle(-90)
.fitSize([w, h], { type: "Sphere"})
Insert cell
path = d3.geoPath(projection)
Insert cell
topo = FileAttachment("topo_110m.topo.json").json()
Insert cell
countriesGeoInner = topojson.mesh(topo, topo.objects.countries, (a, b) => a !== b)
Insert cell
countriesGeoOuter = topojson.mesh(topo, topo.objects.countries, (a, b) => a === b)
Insert cell
cities = [
{
"name": "Cairo",
"lon": 31.236526,
"lat": 30.043171,
"pos": "sw"
},
{
"name": "Delhi",
"lon": 77.232684,
"lat": 28.648466,
"pos": "se"
},
{
"name": "Johannesburg",
"lon": 28.033843,
"lat": -26.210499,
"pos": "sw"
},
{
"name": "London",
"lon": -0.125125,
"lat": 51.506809,
"pos": "nw"
},
{
"name": "Los Angeles",
"lon": -118.4068,
"lat": 34.1141,
"pos": "ne"
},
{
"name": "Moscow",
"lon": 37.618492,
"lat": 55.751091,
"pos": "ne"
},
{
"name": "São Paulo",
"lon": -46.641614,
"lat": -23.559922,
"pos": "se"
},
{
"name": "Shanghai",
"lon": 121.475418,
"lat": 31.219231,
"pos": "ne"
},
{
"name": "Sydney",
"lon": 151.207691,
"lat": -33.865304,
"pos": "se"
},
{
"name": "Tokyo",
"lon": 139.761857,
"lat": 35.676985,
"pos": "ne"
},
{
"name": "D.C.",
"lon": -77.0163,
"lat": 38.9047,
"pos": "sw"
}
]
Insert cell
Insert cell
file = FileAttachment("seasonal_slopes_1944_2023_v3.nc")
Insert cell
startYear = 1944
Insert cell
endYear = 2023
Insert cell
nc = file
.arrayBuffer()
.then((buffer) => new netcdf(buffer))
Insert cell
winter = nc.getDataVariable("winter_slope")
Insert cell
summer = nc.getDataVariable("summer_slope")
Insert cell
values = ({ winter, summer })
Insert cell
X = nc.getDataVariable("longitude")
Insert cell
Y = nc.getDataVariable("latitude")
Insert cell
cell_res = d3.median(d3.pairs(X), ([a, b]) => Math.abs(a - b))
Insert cell
Insert cell
h = w * 2
Insert cell
w = 640
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { toc } from "@climatelab/toc@45"
Insert cell
d3 = require("d3@7", "d3-geo-projection@4")
Insert cell
netcdf = import("https://cdn.skypack.dev/netcdfjs@2.0.2?min").then(d => d.NetCDFReader)
Insert cell
swoopy = require("swoopy@0.0.17")
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