Public
Edited
Nov 18, 2024
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legend = () => {
const wrapper = d3.create("div");

wrapper.append("div")
.style("font-family", franklinLight)
.style("font-size", "16px")
.style("font-weight", "bold")
.style("margin-bottom", "-4px")
.style("text-align", "center")
.text("Winter snowfall forecast");

wrapper.append("div")
.style("font-family", franklinLight)
.style("font-size", "16px")
.style("text-align", "center")
.style("margin-bottom", "8px")
.text("Compared to 1993-2016 average");
const tickSize = 0;
const margin = {left: 0, right: 0, top: 0, bottom: tickSize + 18};
const width = 250 - margin.left - margin.right;
const height = 12;
const svg = wrapper.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("display", "table")
.style("margin", "0 auto")
.style("overflow", "visible");

svg.call(gradient);

const values = extent;
const scale = d3.scaleLinear(extent, [0, width]);

const g = svg.append("g")
.attr("transform", `translate(${[margin.left, margin.top]})`);
g.append("rect")
.attr("fill", `url(#${gradient.id()})`)
.attr("height", height)
.attr("width", width);

const tick = g.selectAll(".tick")
.data(values)
.join("g")
.attr("class", "tick")
.attr("transform", d => `translate(${[scale(d), height]})`);

tick.append("line")
.attr("stroke", "black")
.attr("y2", tickSize);

tick.append("text")
.attr("dy", tickSize)
.attr("font-family", franklinLight)
.attr("font-size", 14)
.attr("text-anchor", (d, i) => i === 0 ? "start" : "end")
.attr("y", 14)
.text((d, i) => i === 0 ? "Less than normal" : "More");
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);

// States clip
context.save();
context.beginPath();
path(statesOuter);
context.clip();

for (let i = 0, l = contours.features.length; i < l; i++) {
const d = contours.features[i];
context.beginPath();
path(d);
const c = color(d.value);
context.fillStyle = c;
context.fill();
context.strokeStyle = c;
context.stroke();
yield wrapper.node();
}

context.restore();

// Alaska clip
context.save();
context.beginPath()
const cw = w * 0.25;
const ch = h * 0.25;
const cp = [
[0, 0],
[w, 0],
[w, h],
[cw, h],
[cw, h - ch],
[0, h - ch],
[0, 0]
]
context.moveTo(...cp[0]);
for (let i = 1, l = cp.length; i < l; i++) {
context.lineTo(...cp[i]);
}
context.closePath();
context.clip();
for (let i = 0, l = mask.features.length; i < l; i++) {
const d = mask.features[i];
context.beginPath();
path(d);
const c = "white";
context.fillStyle = c;
context.fill();
yield wrapper.node();
}

context.restore();
context.fillStyle = "none";
context.lineWidth = 1;
context.beginPath();
path(statesInner);
context.strokeStyle = "#494949";
context.stroke();
context.beginPath();
path(statesOuter);
context.strokeStyle = "#2a2a2a";
context.stroke();

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

const city = svg.selectAll(".city")
.data(cityLabels)
.join("g")
.attr("class", d => `city ${d.pos}`)
.attr("transform", d => `translate(${projection([d.lon, d.lat])})`);

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

city.append("circle")
.attr("class", "bg bg-1")
.attr("r", 3);
city.append("circle")
.attr("class", "fg")
.attr("r", 3);
city.append("text")
.text(d => d.name);

const state = svg.append("g")
.attr("class", "states")
.selectAll(".state")
.data(stateLabels)
.join("g")
.attr("class", d => `state`)
.attr("transform", d => `translate(${projection(d.coordinates(w))})`)
.style("display", d => d.hide && d.hide(w) ? "none" : "block")

state
.filter(d => d.leader)
.append("polyline")
.attr("points", d => d.leader(w))
state.append("text")
.style("text-anchor", d => d.leader && d.leader(w) ? d.leaderAnchor(w) : "middle")
.attr("dx", d => d.leader && d.leader(w) ? d.leaderDx(w) : 0)
.attr("dy", d => d.leader && d.leader(w) ? d.leaderDy(w) : 4)
.attr("transform", d => {
if (!d.leader) return;
const points = d.leader(w);
if (points) {
return `translate(${points[points.length - 1]})`
}
else {
return;
}
})
.text(d => w <= 480 ? d.state_postal : d.state_post);
yield wrapper.node();
}
Insert cell
Insert cell
css = `
.city {
circle {
&.bg {
fill: none;
stroke: white;
}

&.bg-0 {
stroke-width: 6px;
stroke-opacity: 0.2;
}

&.bg-1 {
stroke-width: 3px;
stroke-opacity: 0.5;
}

&.fg {
fill: none;
stroke: black;
}
}

text {
fill: #2a2a2a;
font-family: ${franklinLight};
font-size: 14px;
paint-order: stroke fill;
stroke: #fff;
stroke-linejoin: round;
stroke-opacity: 0.5;
stroke-width: 2px;
@media only screen and (max-width: 480px) {
font-size: 12px;
}
}

&.e text {
transform: translate(6px, 4.5px);
@media only screen and (max-width: 480px) {
transform: translate(5px, 3.5px);
}
}

&.ne text {
transform: translate(4px, -4px);
@media only screen and (max-width: 480px) {
transform: translate(3px, -3px);
}
}

&.se text {
transform: translate(4px, 13px);
@media only screen and (max-width: 480px) {
transform: translate(3px, 11px);
}
}

&.w text {
text-anchor: end;
transform: translate(-6px, 4.5px);
@media only screen and (max-width: 480px) {
transform: translate(-5px, 3.5px);
}
}

&.nw text {
text-anchor: end;
transform: translate(-4px, -4px);
@media only screen and (max-width: 480px) {
transform: translate(-3px, -3px);
}
}

&.sw text {
text-anchor: end;
transform: translate(-4px, 13px);
@media only screen and (max-width: 480px) {
transform: translate(-3px, 11px);
}
}
}

.state {
polyline {
fill: none;
stroke: #2a2a2a;
shape-rendering: crispEdges;
}

text {
fill: #2a2a2a;
font-family: ${franklinLight};
font-size: 12px;
paint-order: stroke fill;
stroke: #fff;
stroke-linejoin: round;
stroke-opacity: 0.3;
stroke-width: 2px;
text-anchor: middle;

@media only screen and (max-width: 480px) {
font-size: 11px;
}
}
}
`
Insert cell
Insert cell
extent = [-0.1, 0.1]
Insert cell
interpolator = interpolatePalette(["#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3", "#f5f5f5", "#d3e7fa", "#84a4d7", "#5784c5", "#1366b3", "#0a3258"])
Insert cell
color = d3.scaleDiverging([extent[0], 0, extent[1]], interpolator)
Insert cell
Insert cell
interval = 10
Insert cell
offsets = d3.range(0, 100 + (100 / interval), 100 / interval)
Insert cell
colors = offsets.map(d3.scaleLinear(d3.extent(offsets), extent)).map(color)
Insert cell
gradient = gradientLinear()
.id("legend-gradient")
.offsets(offsets)
.colors(colors)
Insert cell
Insert cell
projection = d3.geoAlbersUsa()
.fitSize([w, h], statesOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
statesTopo = FileAttachment("conus-alaska.topo.json").json()
Insert cell
statesOuter = topojson.mesh(statesTopo, statesTopo.objects.states, (a, b) => a === b)
Insert cell
statesInner = topojson.mesh(statesTopo, statesTopo.objects.states, (a, b) => a !== b)
Insert cell
cityLabels = [
{
"name": "Anchorage",
"lon": -149.898654,
"lat": 61.215276,
"pos": "nw"
},
{
"name": "Atlanta",
"lon": -84.422,
"lat": 33.7628,
"pos": "ne"
},
{
"name": "Chicago",
"lon": -87.6866,
"lat": 41.8375,
"pos": "sw"
},
{
"name": "Dallas",
"lon": -96.7667,
"lat": 32.7935,
"pos": "ne"
},
{
"name": "Denver",
"lon": -104.8758,
"lat": 39.762,
"pos": "ne"
},
{
"name": "Houston",
"lon": -95.3885,
"lat": 29.786,
"pos": "ne"
},
{
"name": "Los Angeles",
"lon": -118.4068,
"lat": 34.1141,
"pos": "ne"
},
{
"name": "Miami",
"lon": -80.2101,
"lat": 25.784,
"pos": "nw"
},
{
"name": "Minneapolis",
"lon": -93.2678,
"lat": 44.9635,
"pos": "ne"
},
{
"name": "New York",
"lon": -73.9249,
"lat": 40.6943,
"pos": "nw"
},
{
"name": "Phoenix",
"lon": -112.0892,
"lat": 33.5722,
"pos": "se"
},
{
"name": "San Francisco",
"lon": -122.4449,
"lat": 37.7558,
"pos": "ne"
},
{
"name": "Seattle",
"lon": -122.3244,
"lat": 47.6211,
"pos": "ne"
},
{
"name": "St. Louis",
"lon": -90.2451,
"lat": 38.6359,
"pos": "ne"
},
{
"name": "D.C.",
"lon": -77.0163,
"lat": 38.9047,
"pos": "sw"
}
]
Insert cell
stateLabels = [
{"state_post":"Ala.","state_postal":"AL","coordinates": ww => [-86.828,32.79]},
{"state_post":"Ark.","state_postal":"AR","coordinates": ww => [-92.444,34.903]},
{"state_post":"Ariz.","state_postal":"AZ","coordinates": ww => [-111.661,34.292]},
{"state_post":"Calif.","state_postal":"CA","coordinates": ww => [-120.561302, 37.223069]},
{"state_post":"Colo.","state_postal":"CO","coordinates": ww => [-105.543,39]},
{"state_post":"Fla.","state_postal":"FL","coordinates": ww => [-81.381,27.693]},
{"state_post":"Ga.","state_postal":"GA","coordinates": ww => [-83.456,32.656]},
{"state_post":"Idaho","state_postal":"ID","coordinates": ww => [-114.5,43.5]},
{"state_post":"Ill.","state_postal":"IL","coordinates": ww => [-89.204,40.068]},
{"state_post":"Ind.","state_postal":"IN","coordinates": ww => [-86.279,39.907]},
{"state_post":"Iowa","state_postal":"IA","coordinates": ww => [-93.498,42.078]},
{"state_post":"Kan.","state_postal":"KS","coordinates": ww => [-98.331,38.5]},
{"state_post":"Ky.","state_postal":"KY","coordinates": ww => [-85.288,37.53]},
{"state_post":"La.","state_postal":"LA","coordinates": ww => [-92.511,31.482]},
{"state_post":"Maine","state_postal":"ME","coordinates": ww => [-69.1,45.2]},
{"state_post":"Mass.","state_postal":"MA","coordinates": ww => [-71.835,42.3]},
{"state_post":"Minn.","state_postal":"MN","coordinates": ww => [-94.661,46.802]},
{"state_post":"Miss.","state_postal":"MS","coordinates": ww => [-89.671,32.762]},
{"state_post":"Mo.","state_postal":"MO","coordinates": ww => [-92.446,38.303]},
{"state_post":"Mont.","state_postal":"MT","coordinates": ww => [-109.638,47.032]},
{"state_post":"Mich.","state_postal":"MI","coordinates": ww => [-84.8,43.1]},
{"state_post":"Neb.","state_postal":"NE","coordinates": ww => [-99.685,41.5]},
{"state_post":"Nev.","state_postal":"NV","coordinates": ww => [-116.652,40]},
{"state_post":"N.H.","state_postal":"NH","coordinates": ww => [-71.578,43.2]},
{"state_post":"N.M.","state_postal":"NM","coordinates": ww => [-106.11,34.422]},
{"state_post":"N.Y.","state_postal":"NY","coordinates": ww => [-75.031,42.9]},
{"state_post":"N.C.","state_postal":"NC","coordinates": ww => [-79.409,35.548]},
{"state_post":"N.D.","state_postal":"ND","coordinates": ww => [-100.302,47.467]},
{"state_post":"Ohio","state_postal":"OH","coordinates": ww => [-83.034,40.195]},
{"state_post":"Okla.","state_postal":"OK","coordinates": ww => [-97.511,35.588]},
{"state_post":"Ore.","state_postal":"OR","coordinates": ww => [-120.547,43.94]},
{"state_post":"Pa.","state_postal":"PA","coordinates": ww => [-77.801,40.875]},
{"state_post":"S.C.","state_postal":"SC","coordinates": ww => [-80.9,33.918]},
{"state_post":"S.D.","state_postal":"SD","coordinates": ww => [-100.234,44.439]},
{"state_post":"Tenn.","state_postal":"TN","coordinates": ww => [-86.342,35.847]},
{"state_post":"Tex.","state_postal":"TX","coordinates": ww => [-99.359,31.506]},
{"state_post":"Utah","state_postal":"UT","coordinates": ww => [-110.92,38.874]},
{"state_post":"Vt.","state_postal":"VT","coordinates": ww => [-72.665,44.5]},
{"state_post":"Va.","state_postal":"VA","coordinates": ww => [-78.891,37.517]},
{"state_post":"Wash.","state_postal":"WA","coordinates": ww => [-120.421,47.375]},
{"state_post":"W.Va.","state_postal":"WV","coordinates": ww => [-80.611,38.647]},
{"state_post":"Wis.","state_postal":"WI","coordinates": ww => [-90.027,44.638]},
{"state_post":"Wyo.","state_postal":"WY","coordinates": ww => [-107.552,43]},
{
"state_post":"R.I.","state_postal":"RI","coordinates": ww => [-71.595,41.695],
"leader": ww => {
const x = ww <= 550 ? 15 : ww <= 768 ? 20 : 10;
const y = ww <= 360 ? 7 : ww <= 400 ? 10 : ww <= 450 ? 13 : ww <= 550 ? 18 : ww <= 768 ? 25 : ww <= 1e3 ? 13 : 18;
return [[0,0],[0,y],[x,y]];
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Conn.","state_postal":"CT","coordinates": ww => [-72.6,41.65],
"leader": ww => {
if (ww <= 550) {
const y = ww <= 360 ? 15 : ww <= 450 ? 20 : ww <= 500 ? 25 : 30;
return [[0,0],[0,y],[10,y]]
}
if (ww <= 768) {
return undefined;
}
const y = ww <= 800 ? 22.5 : ww <= 1e3 ? 25 : 30;
return [[0,0],[0,y],[5,y]]
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"N.J.","state_postal":"NJ","coordinates": ww => [-74.733,40],
"leader": ww => [[0,0],[ww <= 400 ? 15 : ww <= 550 ? 20 : ww <= 768 ? 30 : 20,0]],
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Md.","state_postal":"MD","coordinates": ww => [-77.05,39.4],
"leader": ww => {
return undefined
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Del.","state_postal":"DE","coordinates": ww => [-75.416,38.826],
"leader": ww => {
return [[0,0],[ww <= 550 ? 15 : ww <= 768 ? 25 : 15,0]]
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Alaska","state_postal":"AK","coordinates": ww => [-151.272085, 64.992153],
}
]
Insert cell
Insert cell
maskRaw = FileAttachment("sfara-masked@1.csv").csv({ typed: true })
Insert cell
maskPoints = JSON.parse(JSON.stringify(maskRaw))
.map(d => [d.longitude, d.latitude, d.sfara === -9999 ? 1 : 0])
Insert cell
maskContour = d3.geoContour().thresholds(1)
Insert cell
mask = ({
type: "FeatureCollection",
features: maskContour(maskPoints)
})
Insert cell
raw = FileAttachment("sfara-jet-sst@1.csv").csv({ typed: true })
Insert cell
points = raw
.filter(f => f.sfara !== -9999)
.map(d => [d.longitude, d.latitude, d.sfara])
Insert cell
contour = d3.geoContour().thresholds(40)
Insert cell
contours = ({
type: "FeatureCollection",
features: contour(points)
})
Insert cell
Insert cell
w = width
Insert cell
h = w * 0.605
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { gradientLinear } from "@climatelab/gradient@60"
Insert cell
import { interpolatePalette } from "@climatelab/roll-your-own-color-palette-interpolator@346";
Insert cell
import { toc } from "@climatelab/toc@45"
Insert cell
d3 = require("d3@7", "d3-geo-voronoi@2")
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