Public
Edited
Mar 4, 2024
1 fork
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
legend = () => {
const wrapper = d3.create("div");

wrapper.append("div")
.style("font-family", franklinLight)
.style("line-height", "22px")
.style("margin-bottom", "4px")
.style("text-align", "center")
.html("Winter temperature change<br />per decade, 1980-2024");
const margin = { top: 0, bottom: 18 };
const width = 250;
const height = 16;
const scale = d3.scaleBand()
.domain(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 tickValues = [...d3.range(-1, 0.5, 0.5), coldValues[0], ...d3.range(0, 2, 0.5)]
const tick = g.selectAll(".tick")
.data(legendValues)
.join("g")
.attr("class", "tick")
.attr("transform", d => `translate(${scale(d)})`);

const epsilon = 1e-6;
tick.append("rect")
.attr("fill", d => d > 0 ? colorScale(d - epsilon) : colorScale(d + epsilon))
.attr("height", height)
.attr("x", -0.5)
.attr("width", (_, i, e) => scale.bandwidth() + (i < e.length - 1 ? 1 : 0)); // add some overlap
tick.append("line")
.filter(d => tickValues.includes(d))
.attr("stroke", "black")
.attr("transform", d => `translate(${d >= coldValues[0] ? scale.bandwidth() : 0})`)
.attr("y1", height)
.attr("y2", height + 4);

tick.append("text")
.filter(d => tickValues.includes(d))
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("transform", d => `translate(${d >= coldValues[0] ? scale.bandwidth() : 0})`)
.attr("y", height + 17)
.text((d, i, e) => d === coldValues[0] ? "0" : `${d < 0 ? "-" : d > 0 ? "+" : ""}${Math.abs(d)}${i === e.length - 1 ? "°F" : ""}`);
return wrapper.node();
}
Insert cell
map = () => {
const wrapper = d3.create("div")
.style("width", `${mapwidth}px`)
.style("height", `${mapheight}px`)
.style("position", "relative");

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

const canvas = wrapper.append("canvas")
.style("position", "absolute");
canvas.node().width = mapwidth;
canvas.node().height = mapheight;
const context = canvas.node().getContext("2d");
const path = d3.geoPath(projection, context);

if (clip) {
context.beginPath();
path(statesOuter);
context.clip();
}

data.forEach(d => {
context.beginPath();
path(square(d));
const c = colorScale(d[variable]);
context.fillStyle = c;
context.fill();
context.strokeStyle = c;
context.stroke();
});

path.context(null);
const svg = wrapper.append("svg")
.style("position", "absolute")
.attr("width", mapwidth)
.attr("height", mapheight);

svg.append("path")
.datum(statesOuter)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#222")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 0.5);

svg.append("path")
.datum(statesInner)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#888")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 0.5);
const citiesG = svg.selectAll(".city")
.data(cities)
.join("g")
.attr("class", d => `city ${d.pos} ${toSlugCase(d.name)}`)
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);

citiesG.append("circle")
.attr("class", "circle-bg")
.attr("r", 3);
citiesG.append("circle")
.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);
return wrapper.node();
}
Insert cell
Insert cell
css = `
.city circle {
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.boston {
display: ${mapwidth <= 480 ? "none" : "block"};
}
.city.boston text {
text-anchor: ${mapwidth <= 900 ? "end" : "start"};
transform: translate(${mapwidth <= 900 ? "-4px" : "4px"}, -4px);
}

.city.chicago text {
text-anchor: ${mapwidth <= 620 ? "end" : "start"};
transform: translate(${mapwidth <= 620 ? "-4px" : "4px"}, -4px);
}

.city.minneapolis {
display: ${mapwidth <= 480 ? "none" : "block"};
}

.city.new-york text {
text-anchor: ${mapwidth <= 660 ? "end" : "start"};
transform: translate(${mapwidth <= 600 ? "-4px" : "4px"}, -4px);
}

.city.philadelphia {
display: ${mapwidth <= 480 ? "none" : "block"};
}
.city.philadelphia text {
text-anchor: ${mapwidth <= 768 ? "end" : "start"};
transform: translate(${mapwidth <= 768 ? "-6px" : "6px"}, 4.5px);
}

.city.seattle text {
transform: translate(4px, ${mapwidth <= 400 ? "13px" : "-4px"});
}

.city.st-louis {
display: ${mapwidth <= 480 ? "none" : "block"};
}

.city.washington-dc text {
text-anchor: ${mapwidth <= 400 ? "end" : mapwidth <= 768 ? "middle" : "start"};
transform: ${mapwidth <= 400 ? "translate(-4px, 13px)" : mapwidth <= 768 ? "translate(0px, 16px)" : "translate(4px, 13px)"};
}

.city text {
font-size: ${mapwidth <= 480 ? "13px" : "14px"};
font-family: ${franklinLight};
}

.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
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 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 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", // 0.5
"#fd9c63", "#f69259", "#ee8850", "#e77d47", "#df733e", "#d76935", // 1
"#cc0000", "#bf0018", "#b20124", "#a5042b", "#980730", "#8b0a34" // 1.5
]
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", // -0.5
"#9fc4d7", "#95bed3", "#8cb7ce", "#82b1ca", "#78aac6", "#6ea4c1" // -1
]
Insert cell
extent = d3.extent(data, d => d[variable])
Insert cell
Insert cell
projection = d3.geoAlbersUsa().fitSize([mapwidth, mapheight], statesOuter)
Insert cell
statesTopo = FileAttachment("statesTopo.json").json()
Insert cell
statesOuter = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a === b)
Insert cell
statesInner = topojson.mesh(statesTopo, statesTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a !== b)
Insert cell
cities = FileAttachment("cities.json").json()
Insert cell
square = geoSquare()
.center(d => [d.longitude, d.latitude])
.area(cellsize ** 2);
Insert cell
Insert cell
cellsize = 4; // 4km
Insert cell
mapwidth = width
Insert cell
mapheight = mapwidth * 0.631
Insert cell
Insert cell
Insert cell
coordinates = FileAttachment("coordinates.csv").csv({ typed: true })
Insert cell
regression_19802024 = FileAttachment("regression_1980-2024@7.csv").csv({ typed: true })
Insert cell
data = regression_19802024.map(d => {
Object.assign(d, coordinates[d.index]);

d[variable] = d.slope * 10;
return d;
})
Insert cell
Insert cell
function toSlugCase(x) {
return x.toString().toLowerCase()
.replace(/\s+/g, "-") // Replace spaces with -
.replace(/[^\w\-]+/g, "") // Remove all non-word chars
.replace(/\-\-+/g, "-") // Replace multiple - with single -
.replace(/^-+/, "") // Trim - from start of text
.replace(/-+$/, ""); // Trim - from end of text
}
Insert cell
Insert cell
import { geoSquare } from "@climatelab/geosquare@267"
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import {toc} from "@climatelab/toc@44"
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