Public
Edited
Nov 7, 2023
2 forks
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legend = () => {
const margin = { top: 20, bottom: 18 };
const width = 300;
const height = 16;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height + margin.top + margin.bottom)
.attr("font-family", franklinLight)
.style("margin", "0 auto")
.style("display", "table")
.call(gradient);
const g = svg.append("g")
.attr("transform", `translate(0, ${margin.top})`)
g.append("text")
.attr("font-size", 18)
.attr("dy", -8)
.attr("x", width / 2)
.attr("font-weight", "bold")
.attr("text-anchor", "middle")
.html("Arrival of spring leaves");
g.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", `url(#${gradient.id()})`);

const tick = g.selectAll(".tick")
.data([-20, 20])
.join("g")
.attr("class", "tick")
.attr("transform", (d, i) => `translate(${i ? width : 0})`);

tick.append("text")
.attr("font-size", 14)
.attr("text-anchor", (d, i) => i ? "end" : "start")
.attr("y", height + 14)
.text((d, i) => `${Math.abs(d)}${i ? " days late" : " days early"}`);
return svg.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();
}
S
.forEach((cell, i) => {
if (cell || cell === 0) {
context.fillStyle = scale(cell);
const x = X[i % X.length];
const y = Y[(i / X.length) | 0];
const p = projection([x, y]);
context.fillRect(p[0] | 0, p[1] | 0, 1, 1);
}
});

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
note = () => d3.create("div")
.style("color", "#666")
.style("font-family", franklinLight)
.style("font-size", "14px")
.text("Data as of March 14")
.node()
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
colors = ["#286b1e", "#5e8f22", "#92b52f", "#c8db4d", "#ffffab", "#eac2d2", "#d095b7", "#b06c99", "#7d516f"]
Insert cell
interpolator = interpolatePalette(colors)
Insert cell
scale = d3.scaleDiverging([-20, 0, 20], interpolator)
Insert cell
interval = 1 / (colors.length - 1)
Insert cell
offsets = d3.range(0, 100 + interval * 100, interval * 100)
Insert cell
gradient = gradientLinear()
.offsets(offsets)
.colors(colors)
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
anomaly = FileAttachment("leaf_anomaly_2023-03-16.nc")
Insert cell
nc = anomaly
.arrayBuffer()
.then((buffer) => new netcdf(buffer))
Insert cell
S0 = nc.getDataVariable("six_leaf_anomaly")
Insert cell
S = S0.map((d, i) => (d !== -9999 ? d : null))
Insert cell
X = nc.getDataVariable("lon")
Insert cell
Y = nc.getDataVariable("lat")
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
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 "@harrystevens/toc"
Insert cell
import { franklinLight } from "1dec0e3505bd3624"
Insert cell
import { gradientLinear } from "@washpostgraphics/gradient-generators"
Insert cell
import { interpolatePalette } from "@harrystevens/roll-your-own-color-palette-interpolator"
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