Public
Edited
Jun 7, 2024
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
legend = () => {
const wrapper = d3.create("div")
.attr("class", "legend");

wrapper.append("div")
.attr("class", "legend-title")
.text("Days with heat waves that stress transformers");
const margin = {left: 1, right: 42, top: 1, bottom: 19};
const width = 280 - margin.left - margin.right;
const height = 12;
const rect0Width = 16
const svg = wrapper.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
svg.call(gradient);

const scale = d3.scaleLinear()
.domain([0, 120])
.range([0, width - rect0Width]);

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

const rectGradient = g.append("rect")
.attr("fill", `url(#${gradient.id()})`)
.attr("x", rect0Width)
.attr("width", width - rect0Width)
.attr("height", height);

const rect0 = g.append("rect")
.attr("class", "rect-0")
.attr("width", rect0Width)
.attr("height", height);

const tick = g.selectAll(".tick")
.data(d3.range(0, 150, 30))
.join("g")
.attr("class", "tick")
.attr("transform", d => `translate(${rect0Width + scale(d)}, ${height})`);
tick.append("text")
.attr("x", (d, i, e) => i === e.length - 1 ? 10 : 0)
.attr("y", 14)
.text((d, i, e) => i === 0 ? 1 : `${i === e.length - 1 ? ">" : ""}${d}${i === e.length - 1 ? " days" : ""}`);
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 (clipToggle) {
context.save();
context.beginPath();
path(usGeoOuter);
context.clip();
}

context.lineWidth = 1.5
geo.features
.forEach((d, i) => {
context.beginPath();
path(d);
const c = colorScale(d.properties[dataVariable]);
context.fillStyle = c;
context.fill();
context.strokeStyle = c;
context.stroke();
});

if (clipToggle) {
context.restore();
}
context.fillStyle = "none";

context.lineWidth = 0.5;
context.beginPath();
path(usGeoInner);
context.strokeStyle = "#aaaaaa"
context.stroke();
context.beginPath();
path(usGeoOuter);
context.strokeStyle = "#808080"
context.stroke();

const svg = wrapper.append("svg")
.style("pointer-events", "none")
.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("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);

return wrapper.node();
}
Insert cell
Insert cell
css = `
.city circle.bg {
fill: none;
stroke: none;
}
.city circle.bg-0 {
stroke-width: 5px;
stroke-opacity: 0.1;
}
.city circle.bg-1 {
stroke-width: 2px;
stroke-opacity: 0.2;
}
.city circle.fg {
fill: white;
fill-opacity: 0.2;
stroke: #2a2a2a;
}

.city text {
fill: #2a2a2a;
font-family: ${franklinLight};
font-size: 14px;
}
.city text.bg {
fill: none;
stroke: white;
}
.city text.bg-0 {
stroke-width: 5px;
stroke-opacity: 0.1;
}
.city text.bg-1 {
stroke-width: 2px;
stroke-opacity: 0.2;
}

.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.n text {
text-anchor: middle;
transform: translate(0px, -8px);
}

.legend {
display: table;
margin: 0 auto;
}
.legend .legend-title {
font-family: ${franklinLight};
font-size: 16px;
text-align: center;
line-height: 18px;
width: 170px;
margin: 0 auto;
margin-bottom: 4px;
}
.legend .rect-0 {
fill: white;
shape-rendering: crispEdges;
stroke: #808080;
stroke-width: 0.5px;
}
.legend .tick text {
font-family: ${franklinLight};
font-size: 14px;
text-anchor: middle;
}
`
Insert cell
Insert cell
colors = ["#fff6e4", "#ffe9c2", "#ffd79a", "#fdc072", "#faab56", "#f79a3d", "#f4872f", "#e4712e", "#ca5c2c", "#a74b27", "#893e21", "#71341b", "#612219", "#301326"];
Insert cell
interpolator = interpolatePalette(colors)
Insert cell
colorScale = v => v < 1 ? d3.scaleSequential([0, 0.99, 1], ["#fff", "#fff", colors[0]])(v) : d3.scaleSequential([1, 120], interpolator)(v)
Insert cell
Insert cell
gradientColors = [1, 40, 80, 120].map(colorScale);
Insert cell
gradient = gradientLinear()
.id("legend-gradient")
.offsets([0, 100 / 3, 200 / 3, 100])
.colors(gradientColors);
Insert cell
Insert cell
projection = d3.geoAlbers()
.fitSize([w, h], usGeoOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
usTopo = FileAttachment("statesTopo.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
Insert cell
dataVariable = selectVariable === "Mid-century" ? "e_2050" : "e_baseline"
Insert cell
// Gridded ata from ICF Climate Center
data = FileAttachment("heatwave-days.csv").csv({ typed: true })
Insert cell
geo = ({
type: "FeatureCollection",
features: data.map(properties => {
const { lon, lat } = properties;
const [dlon, dlat] = kmToDegrees(cellsize / 2, lat);
const w = lon - dlon;
const e = lon + dlon;
const n = lat + dlat;
const s = lat - dlat;
return {
type: "Feature",
properties,
geometry: {
type: "Polygon",
// wn, en, es, ws, wn
coordinates: [
[[w, n], [e, n], [e, s], [w, s], [w, n]]
]
}
}
})
})
Insert cell
square = geoSquare()
.center(d => [d.properties.lon, d.properties.lat])
.area(cellsize ** 2);
Insert cell
cellsize = 6.1 // km
Insert cell
cities = [
{
"name": "Austin",
"lon": -97.80144533,
"lat": 30.31565466660589,
"pos": "nw"
},
{
"name": "Atlanta",
"lon": -84.422,
"lat": 33.7628,
"pos": "ne"
},
{
"name": "Boston",
"lon": -71.0852,
"lat": 42.3188,
"pos": "nw"
},
{
"name": "Chicago",
"lon": -87.6866,
"lat": 41.8375,
"pos": "sw"
},
{
"name": "Denver",
"lon": -104.8758,
"lat": 39.762,
"pos": "ne"
},
{
"name": "Las Vegas",
"lon": -115.122366,
"lat": 36.172082,
"pos": "ne"
},
{
"name": "Los Angeles",
"lon": -118.4068,
"lat": 34.1141,
"pos": "ne"
},
{
"name": "Memphis",
"lon": -89.9343,
"lat": 35.0337,
"pos": "ne"
},
{
"name": "Miami",
"lon": -80.2101,
"lat": 25.784,
"pos": "nw"
},
{
"name": "Minneapolis",
"lon": -93.2678,
"lat": 44.9635,
"pos": "nw"
},
{
"name": "New Orleans",
"lon": -90.118127,
"lat": 29.9617775,
"pos": "nw"
},
{
"name": "New York",
"lon": -73.9249,
"lat": 40.6943,
"pos": "nw"
},
{
"name": "Oklahoma City",
"lon": -97.50495,
"lat": 35.40335,
"pos": "ne"
},
{
"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": "D.C.",
"lon": -77.0163,
"lat": 38.9047,
"pos": "sw"
}
]
Insert cell
Insert cell
w = width
Insert cell
h = w * 0.631
Insert cell
Insert cell
function kmToDegrees(kilometers, latitude = 0) {
const KM_PER_DEGREE_LATITUDE = 111.32;

return [
// Convert kilometers to degrees longitude, adjusted for latitude
kilometers / (KM_PER_DEGREE_LATITUDE * Math.cos(latitude * Math.PI / 180)),
// Convert kilometers to degrees latitude
kilometers / KM_PER_DEGREE_LATITUDE
]
}
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 { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { geoSquare } from "@climatelab/geosquare@267"
Insert cell
import { gradientLinear } from "@climatelab/gradient@60"
Insert cell
import { interpolatePalette } from "@climatelab/roll-your-own-color-palette-interpolator@326";
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