Public
Edited
Jul 18, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map = () => {
const svg = d3.create("svg")
.attr("width", mapwidth)
.attr("height", mapheight);

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

svg.selectAll(".contour")
.data(egsGeo.features.filter(d => d.properties.CLASS < 6))
.join("path")
.attr("class", "contour")
.attr("d", path)
.attr("fill", d => color[d.properties.CLASS - 1])
.attr("stroke", d => color[d.properties.CLASS - 1]);

svg.append("path")
.datum(conusStatesInner)
.attr("class", "inner")
.attr("d", path);
svg.append("path")
.datum(conusStatesOuter)
.attr("class", "outer")
.attr("d", path);

const label = svg.selectAll(".label")
.data(stateLabels)
.join("g")
.attr("class", "label")
.attr("transform", d => `translate(${projection(d.coordinates(mapwidth))})`)
.style("display", d => d.hide && d.hide(mapwidth) ? "none" : "block")
label
.filter(d => d.leader)
.append("polyline")
.attr("points", d => d.leader(mapwidth))
label.append("text")
.style("text-anchor", d => d.leader && d.leader(mapwidth) ? d.leaderAnchor(mapwidth) : "middle")
.attr("dx", d => d.leader && d.leader(mapwidth) ? d.leaderDx(mapwidth) : 0)
.attr("dy", d => d.leader && d.leader(mapwidth) ? d.leaderDy(mapwidth) : 4)
.attr("transform", d => {
if (!d.leader) return;
const points = d.leader(mapwidth);
if (points) {
return `translate(${points[points.length - 1]})`
}
else {
return;
}
})
.text(d => mapwidth <= 768 ? d.state_postal : d.state_post);
svg.selectAll(".plant")
.data(plantsData)
.join("circle")
.attr("class", d => `plant${d.flag ? " plant-fervo" : ""}`)
.attr("fill", "none")
.attr("r", r)
.attr("stroke", "#2a2a2a")
.attr("transform", d => `translate(${projection([d.longitude, d.latitude])})`);

const anno = svg.selectAll(".anno")
.data(annoData)
.join("g")
.attr("class", d => `anno anno-${d.className}`)
.attr("transform", d => `translate(${projection([d.longitude(mapwidth), d.latitude(mapwidth)])})`)

anno.append("polyline");
anno.append("text")
.attr("text-anchor", d => d.textAnchor(mapwidth))
.attr("x", d => d.textX(mapwidth))
.attr("y", d => d.textY(mapwidth))
.html(d => d.textHtml(mapwidth));

anno
.each((d, i, e) => {
const angle = d.leaderAngle(mapwidth);
const a = geometric.pointTranslate([0, 0], angle, r);
const b = geometric.pointTranslate(a, angle, d.leaderLength(mapwidth));
const sel = d3.select(e[i]);
sel.select("polyline").attr("points", [a, b]);
sel.select("text").attr("transform", `translate(${b})`)
});

return svg.node()
}
Insert cell
Insert cell
color = ['#be2c25', '#d2623b', '#e48e57', '#f3b97e', '#fde2c4']
Insert cell
css = `
.inner, .outer {
fill: none;
stroke-linejoin: round;
}

.inner {
stroke: #2a2a2a;
stroke-width: 0.25px;
}

.outer {
stroke: #2a2a2a;
stroke-width: 0.5px;
}

.plant {
fill: none;
stroke: #2a2a2a;
}
.plant.plant-fervo {
fill: #2a2a2a;
stroke: #2a2a2a;
}

.anno polyline {
stroke: #2a2a2a;
}
.anno text {
fill: #2a2a2a;
font-family: ${franklinLight};
font-size: ${mapwidth <= 600 ? 14 : 16}px;
font-weight: bold;
paint-order: stroke fill;
stroke-opacity: 0.5;
stroke-width: 2px;
stroke-linejoin: round;
}

.label polyline {
fill: none;
stroke: #808080;
shape-rendering: crispEdges;
}
.label text {
fill: #494949;
font-family: ${franklinLight};
font-size: ${mapwidth <= 480 ? 9 : mapwidth <= 768 ? 12 : 14}px;
text-anchor: middle;
}
`
Insert cell
Insert cell
mapwidth = Math.min(960, width)
Insert cell
mapheight = mapwidth * 0.631
Insert cell
r = mapwidth <= 480 ? 2 : 3
Insert cell
Insert cell
projection = d3.geoAlbersUsa()
.fitSize([mapwidth, mapheight], conusStatesOuter)
Insert cell
path = d3.geoPath(projection)
Insert cell
conusStatesInner = topojson.mesh(conusStatesTopo, conusStatesTopo.objects.ne_10m_conus_states, (a, b) => a !== b)
Insert cell
conusStatesOuter = topojson.mesh(conusStatesTopo, conusStatesTopo.objects.ne_10m_conus_states, (a, b) => a === b)
Insert cell
conusStatesTopo = FileAttachment("ne_10m_conus_states.json").json()
Insert cell
Insert cell
egsGeo = topojson.feature(egsTopo, egsTopo.objects.GeothermalLCOE_NoExclusionsforAtlas)
Insert cell
egsTopo = FileAttachment("GeothermalLCOE_NoExclusionsforAtlas.json").json()
Insert cell
// Provided by Fervo Energy. Coordinates are approximate.
fervoDatum = ({ longitude: -112.931248, latitude: 38.507148, flag: true })
Insert cell
plants = d3
.groups(dataGeoOperatingConus, d => d.plant_name)
.map(([plant_name, entries]) => {
return {
plant_name,
// nameplate_capacity_mw: d3.sum(entries, d => d.nameplate_capacity_mw),
longitude: entries[0].longitude,
latitude: entries[0].latitude,
// entries
}
})
Insert cell
// No need to have so many overlapping circles
plantsData = {
const minDistance = 1e-3;
const plantsData = [];
plants.forEach(p0 => {
if (!plantsData.length) {
plantsData.push(p0);
}
else {
let remove = false;

for (let i = 0, l = plantsData.length; i < l; i++) {
const p1 = plantsData[i];
if (d3.geoDistance([p0.longitude, p0.latitude], [p1.longitude, p1.latitude]) < minDistance) {
remove = true;
break;
}
}

if (!remove) plantsData.push(p0);
}

});
plantsData.push(fervoDatum)
return plantsData
}
Insert cell
dataGeoOperatingConus = dataGenerators
.filter(d => d.technology === "Geothermal")
.filter(d => d.status === "(OP) Operating")
.filter(d => !["AK", "HI"].includes(d.plant_state))
Insert cell
dataGenerators = FileAttachment("may_generator2024.csv").csv({ typed: true })
Insert cell
top = plantsData[d3.maxIndex(plantsData, d => d.latitude)]
Insert cell
bot = plantsData[d3.minIndex(plantsData, d => d.latitude)]
Insert cell
annoData = [
{
longitude: ww => ww <= 600 ? bot.longitude : top.longitude,
latitude: ww => ww <= 600 ? bot.latitude : top.latitude,
leaderAngle: ww => ww <= 600 ? 90 : -90,
leaderLength: ww => ww <= 768 ? 15 : 20,
textAnchor: ww => ww <= 600 ? "end" : "middle",
textHtml: ww => ww <= 420 ? "<tspan>Conventional</tspan><tspan x=10 dy=15>geothermal</tspan><tspan x=10 dy=15>plants</tspan>" : `<tspan dy=${ww <= 600 ? 0 : -18}>Conventional</tspan><tspan x=${ww <= 600 ? 10 : 0} dy=${ww <= 600 ? 16 : 18}>geothermal plants</tspan>`,
textX: ww => ww <= 600 ? 10 : 0,
textY: ww => ww <= 600 ? 14 : -5,
className: "conventional"
},
{
longitude: ww => fervoDatum.longitude,
latitude: ww => fervoDatum.latitude,
leaderAngle: ww => 90,
leaderLength: ww => ww <= 420 ? 10 : ww <= 768? 15 : 20,
textAnchor: ww => "middle",
textHtml: ww => "Fervo’s plant",
textX: ww => 0,
textY: ww => 14,
className: "fervo"
}
]
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 => [-115.289,43.926]},
{"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],"hide": ww => ww <= 400},
{"state_post":"Mass.","state_postal":"MA","coordinates": ww => [-71.835,42.3],"hide": ww => ww <= 550},
{"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,39.354]},
{"state_post":"N.H.","state_postal":"NH","coordinates": ww => [-71.578,43.2],"hide": ww => ww <= 550},
{"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],"hide": ww => ww <= 550},
{"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],"hide": ww => ww <= 400},
{"state_post":"Wis.","state_postal":"WI","coordinates": ww => [-90.027,44.638]},
{"state_post":"Wyo.","state_postal":"WY","coordinates": ww => [-107.552,43]},
// Small northeastern states
{
"state_post":"R.I.","state_postal":"RI","coordinates": ww => [-71.595,41.695],
"hide": ww => ww <= 600,
"leader": ww => {
const y = ww <= 768 ? 10 : 18;
return [[0,0],[0,y],[3,y]]
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Conn.","state_postal":"CT","coordinates": ww => [-72.728,41.624],
"hide": ww => ww <= 600,
"leader": ww => {
const y = ww <= 650 ? 17 : ww <= 700 ? 18 : ww <= 768 ? 20 : ww <= 900 ? 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],
"hide": ww => ww <= 600,
"leader": ww => [[0,0],[15,0]],
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Md.","state_postal":"MD","coordinates": ww => [-77.05,39.4],
"hide": ww => ww <= 600,
"leader": ww => {
if (ww <= 768){
return [[0,0],[35,0]]
}
else {
return undefined;
}
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"Del.","state_postal":"DE","coordinates": ww => [-75.416,38.826],
"hide": ww => ww <= 600,
"leader": ww => {
if (ww <= 768) {
return [[0,0],[0,10],[10,10]]
}
else {
return [[0,0],[15,0]]
}
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
{
"state_post":"D.C.","state_postal":"DC","coordinates": ww => [-77.014,38.91],
"hide": ww => ww <= 600,
"leader": ww => {
const x = ww <= 768 ? 25 : 35;
const y = 20;
return [[0,0],[0,y],[x,y]]
},
"leaderAnchor": ww => "start",
"leaderDx": ww => 2,
"leaderDy": ww => 4
},
]
Insert cell
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
Insert cell
import { toc } from "@climatelab/toc@45"
Insert cell
geometric = require("geometric@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