Public
Edited
Mar 13, 2024
1 fork
11 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legendValues = d3.range(-28, 15).filter(d => d);
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("Change in expected arrival<br />of leaves, 1981-2023");
const margin = { top: 1, bottom: 32, right: 0 };
const width = 250;
const height = 16;
const scale = d3.scaleBand()
.domain(legendValues)
.range([0, width]);

const svg = wrapper.append("svg")
.attr("width", width + margin.right)
.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 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 => {
const v = d > 0 ? d - epsilon : d + epsilon;
return colorScale(v)
})
.attr("height", height)
.attr("x", -0.5)
.attr("width", (_, i, e) => scale.bandwidth() + (i < e.length - 1 ? 1 : 0)); // add some overlap

const tickValues = [...d3.range(-28, 0, 7), -1, ...d3.range(0, 21, 7)]
tick.append("line")
.filter(d => tickValues.includes(d))
.attr("stroke", "black")
.attr("transform", d => `translate(${-0.5 + (d >= valuesEarlier[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(${-0.5 + (d >= valuesEarlier[0] ? scale.bandwidth() : 0)})`)
.attr("y", height + 17)
.html((d, i, e) => d === valuesEarlier[0] ? "0" : `${Math.abs(d)}`);

const adverb = g.selectAll(".adverb")
.data(["days earlier", "days later"])
.join("text")
.attr("class", "adverb")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("x", (_, i) => i * width)
.attr("y", height + 30)
.text(d => d)

const noTrend = svg.append("g")
.attr("transform", `translate(${[width + 24, margin.top]})`);

noTrend.append("rect")
.attr("height", height)
.attr("width", scale.bandwidth())
.attr("fill", "none")
.attr("shape-rendering", "crispEdges")
.attr("stroke", "#000");

noTrend.append("text")
.attr("font-size", 14)
.attr("x", scale.bandwidth() + 4)
.attr("y", height / 2 + 4)
.text("No trend")
return wrapper.node();
}
Insert cell
map = () => {
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);

// Clipping will take a long time
if (clip) {
context.save();
context.beginPath();
path(usGeoOuter);
context.clip();
}
data
.forEach((d, i) => {
context.beginPath();
path(square(d));
const c = colorScale(d.trend);
context.fillStyle = c;
context.fill();
context.strokeStyle = c;
context.stroke();
});

if (clip) {
context.restore();
}
context.fillStyle = "none";
context.beginPath();
path(usGeoInner);
context.strokeStyle = "#e9e9e9"
context.stroke();
context.beginPath();
path(usGeoOuter);
context.strokeStyle = "#d5d5d5"
context.stroke();

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

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("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
robotext = () => {
const f = d3.format(",")
const csvL = csv.length;
const l = data.length;
const l0 = data.filter(d => d.slope < 0).length;
const l1 = data.filter(d => d.slope > 0).length;
return html`<div style="max-width: 640px">Of ${f(l)} cells with a trend, ${f(l0)} (${(l0 / l * 100).toFixed(2)}%) are trending earlier. Of all ${f(csvL)} cells, ${(l0 / csvL * 100).toFixed(2)}% are trending earlier, ${(l1 / csvL * 100).toFixed(2)}% are trending later, and ${((csvL - (l0 + l1)) / csvL * 100).toFixed(2)}% show no trend at all.</div>`
}
Insert cell
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: ${w <= 480 ? "none" : "block"};
}
.city.boston text {
text-anchor: ${w <= 900 ? "end" : "start"};
transform: translate(${w <= 900 ? "-4px" : "4px"}, -4px);
}

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

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

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

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

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

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

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

.city text {
font-size: ${w <= 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
colorsLater = [
// l95 - l75
"#f6eff6", "#f1e3f1", "#ecd7ec", "#e7cbe7", "#e2c0e1", "#dcb4dc", "#d7a8d7",

// l71.67 - l51.67
"#a2a1ce", "#9796c8", "#8d8ac2", "#827fbd", "#7774b7", "#6b6ab1", "#605fab"
]
Insert cell
colorsEarlier = [
// l95 - l75
"#ffffe5", "#fcfcd7", "#faf9ca", "#f6f5bc", "#f3f2ae", "#f0efa1", "#ecec93",

// l71.67 - l51.67
"#c5e085", "#beda7c", "#b7d473", "#b0ce6a", "#a9c860", "#a2c257", "#9bbc4e",

// l48.33 - l28.33
"#68a550", "#61994a", "#598d44", "#52823f", "#4b7639", "#446b33", "#3d602e",

// l25 - l5
"#375629", "#2f4a24", "#273f1f", "#1f341a", "#172a15", "#101f10", "#031607"
]
Insert cell
valuesEarlier = d3.range(1, 29).map(d => d * -1)
Insert cell
valuesLater = d3.range(1, 15)
Insert cell
colorScale = value => {
const fl = Math.floor(Math.abs(value));
if (value < 0) {
return colorsEarlier[fl] || colorsEarlier[colorsEarlier.length - 1];
}
else {
return colorsLater[fl] || colorsLater[colorsLater.length - 1];
}
}
Insert cell
Insert cell
projection = d3.geoAlbers()
.fitSize([w, h], usGeoOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
Insert cell
w = width
Insert cell
h = w * 0.631
Insert cell
Insert cell
startYear = 1981
Insert cell
endYear = 2023
Insert cell
csv = FileAttachment("regression_1981-2023@2.csv").csv({ typed: true })
Insert cell
data = csv
.filter(d => d.p_value < 0.5) // filter out cells that have no trend whatsoever
.map(d => {
d.trend = +d.slope * (endYear - startYear);
return d;
})
Insert cell
usTopo = FileAttachment("ne_50m_admin_1_states_provinces_lakes@1.json").json()
Insert cell
usGeoInner = topojson.mesh(usTopo, usTopo.objects.ne_50m_admin_1_states_provinces_lakes, (a, b) => a !== b)
Insert cell
usGeoOuter = topojson.mesh(usTopo, usTopo.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
cellsize = 4; // PRISM is 4km resolution (https://prism.oregonstate.edu/explorer/)
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 { toc } from "@climatelab/toc@44"
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { geoSquare } from "@climatelab/geosquare@267"
Insert cell
netcdf = import("https://cdn.skypack.dev/netcdfjs@2.0.2?min").then(d => d.NetCDFReader)
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