Public
Edited
Mar 1, 2024
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
graphic = () => {
const wrapper = d3.create("div");

const chart = wrapper.selectAll(".chart")
.data(dataMultiples)
.join("div")
.attr("class", "chart");

chart.append("div")
.attr("class", "region-label")
.style("margin-bottom", (_, i) => `-${20 + (i ? 0 : marginTopExtraFirstChart)}px`)
.text(d => d[0]);

chart.each((data, i, e) => {
d3.select(e[i]).call(sel => chartGenerator(sel, data, i))
})

return wrapper.node();
}
Insert cell
marginTopExtraFirstChart = 45
Insert cell
chartGenerator = (chart, data, index) => {
const margin = ({left: 10, right: 10, top: 5 + (index ? 0 : marginTopExtraFirstChart), bottom: 24})
const regionsOrdered = data[1];
const cities = regionsOrdered.map(d => d.entries).flat();
const chartheight = (24 + maxR) * regionsOrdered.length;
const y = d3.scaleBand(regionsOrdered.map(d => d.subregion), [0, chartheight]);
const svg = chart.append("svg")
.attr("width", chartwidth + margin.left + margin.right)
.attr("height", chartheight + margin.top + margin.bottom)

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

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

g.append("g")
.call((g) => axisGenerator(g, chartheight, y, index));

g.selectAll(".subregion-label")
.data(regionsOrdered)
.join("text")
.attr("class", "subregion-label")
.attr("y", d => y(d.subregion) + y.bandwidth() / 2)
.text(d => d.subregion);

const statesProp = chartwidth <= 520 ? "states_abbr" : "states";
const statesY = chartwidth <= 350 ? 16 : chartwidth <= 520 ? 18 : 20;
const statesDy = chartwidth <= 350 ? 13 : chartwidth <= 520 ? 14 : 16;
g.selectAll(".states-label")
.data(regionsOrdered)
.join("text")
.attr("class", "states-label")
.attr("y", d => y(d.subregion) + y.bandwidth() / 2 + statesY)
.html(d => {
if (d.states.length < 5) {
return d[statesProp].join(", ");
}
else {
const grouper = Math.ceil(d.states.length / 2);
const groups = d3.groups(d[statesProp], (_, i) => i < grouper);
return groups.map((group, i) => `<tspan x=0 dy=${i * 16}>${group[1].join(", ")}</tspan>`)
}
})
g.selectAll(".baseline")
.data(regionsOrdered)
.join("line")
.attr("stroke", "#ccc")
.attr("shape-rendering", "crispEdges")
.attr("transform", d => `translate(0, ${y(d.subregion) + y.bandwidth()})`)
.attr("x2", chartwidth);

g.append("g")
.attr("class", "warts")
.selectAll(".wart")
.data(cities)
.join("path")
.attr("class", "wart")
.attr("d", d => semicircle(x(d.decadal_change), y(d.subregion), r(d.population)))
.attr("fill", d => colorScale(d.decadal_change))
.attr("fill-opacity", 0.8)
.attr("stroke", d => d3.color(colorScale(d.decadal_change)).darker(0.2))
.attr("transform", `translate(0, ${y.bandwidth()})`);

const cityLabel = g.selectAll(".city-label")
.data(dataCityLabels.filter(d => data[0] === d.region))
.join("g")
.attr("class", "city-label")
.attr("transform", d => {
const cx = x(d.decadal_change);
const cy = y(d.subregion) + y.bandwidth();
const radius = r(d.population);
return `translate(${geometric.pointTranslate([cx, cy], d.angle(chartwidth), radius)})`;
})

cityLabel.append("polyline")
.attr("points", d => {
return [[0, 0], geometric.pointTranslate([0, 0], d.angle(chartwidth), 5)]
})
.attr("stroke", d => d3.color(colorScale(d.decadal_change)).darker(0.2))
.style("display", d => d.display ? d.display(chartwidth) : "block")
cityLabel.append("text")
.attr("text-anchor", d => d.textAnchor(chartwidth))
.attr("transform", d => {
return `translate(${geometric.pointTranslate([0, 0], d.angle(chartwidth), 7)})`
})
.text(d => d.city);
return chart;
}
Insert cell
axisGenerator = (g, chartheight, y, i) => {
const tickValues = d3.range(-0.5, 1.5, 0.5);
const top = d3.axisTop(x)
.tickFormat(d => "")
.tickSize(chartheight - y.bandwidth())
.tickValues([0]);

const bottom = d3.axisBottom(x)
.tickFormat(d => `${d > 0 ? "+" : ""}${d}°F`)
.tickSize(10)
.tickValues(tickValues);

if (i === 0){
const axisTitle = g.append("text")
.attr("class", "axis-title")
.attr("transform", `translate(${x(0)}, -${marginTopExtraFirstChart - 12})`)
.html("<tspan>Winter temperature change</tspan><tspan x=0 dy=20>per decade, 1980-2024</tspan>")
const axisLabel = g.selectAll(".axis-label")
.data(["Colder", "Warmer"])
.join("text")
.attr("class", "axis-label")
.attr("fill", (_, i) => i === 0 ? "#6ea4c1" : "#d76935")
.attr("text-anchor", (_, i) => i ? "start" : "end")
.attr("transform", `translate(${x(0)}, -6)`)
.attr("x", (_, i) => (i * 2 - 1) * 10)
.attr("y", 12)
.text(d => d);
}
const axisTop = g.append("g")
.attr("class", "axis axis-top")
.attr("transform", `translate(0, ${chartheight})`)
.call(top)
.selectAll(".tick line")
.attr("y2", d => -top.tickSize() - (i === 0 ? 4 : 0) - (d ? 0 : y.bandwidth()))

const axisBottom = g.append("g")
.attr("class", "axis axis-bottom")
.attr("transform", `translate(0, ${chartheight})`)
.call(bottom);
return g;
}
Insert cell
Insert cell
heatValues = [0.083, 0.167, 0.25, 0.333, 0.417, 0.5, 0.583, 0.667, 0.75, 0.833, 0.917, 1, 1.083, 1.167, 1.25, 1.333, 1.417, 1.5]
Insert cell
heatColors = [
"#f8f4e2", "#f9e9ce", "#fadfbb", "#fad5a7", "#f9ca94", "#f7c081", // 0.5
"#fd9c63", "#f69259", "#ee8850", "#e77d47", "#df733e", "#d76935", // 1
"#cc0000", "#bf0018", "#b20124", "#a5042b", "#980730", "#8b0a34", // 1.5
]
Insert cell
coldValues = [-0.083, -0.167, -0.25, -0.333, -0.417, -0.5, -0.583, -0.667, -0.75, -0.833, -0.917, -1]
Insert cell
coldColors = [
"#eff4e9", "#e0ede6", "#d2e5e1", "#c4deda", "#b6d7d2", "#a9d0ca", // -0.5
"#9fc4d7", "#95bed3", "#8cb7ce", "#82b1ca", "#78aac6", "#6ea4c1", // -1
]
Insert cell
colorScale = value => {
if (value < 0) {
let index = -1;

for (let i = 0, l = coldValues.length; i < l; i++) {
const v = coldValues[i];
if (value > v) {
index = i;
break;
}
}
return coldColors[index];
}
else {
let index = -1;
for (let i = 0, l = heatValues.length; i < l; i++){
const v = heatValues[i];
if (value < v) {
index = i;
break;
}
}
return heatColors[index]
}
}
Insert cell
Insert cell
css = `
.chart {
margin-bottom: 20px;
}
.axis .domain {
display: none;
}
.axis .tick line {
stroke: #ccc;
}
.axis .tick text {
font-family: ${franklinLight};
font-size: 14px;
}
.axis-title {
font-family: ${franklinLight};
font-size: 16px;
text-anchor: middle;
}
.axis-label {
font-family: ${franklinLight};
font-size: 16px;
font-weight: bold;
}

.region-label {
font-family: ${franklinLight};
font-size: ${chartwidth <= 520 ? "16px" : "18px"};
font-weight: bold;
}
.subregion-label {
font-family: ${franklinLight};
font-size: ${chartwidth <= 520 ? "14px" : "16px"};
}
.states-label {
font-family: ${franklinLight};
font-size: ${chartwidth <= 350 ? "11px" : chartwidth <= 520 ? "12px" : "14px"};
}
.city-label text {
font-family: ${franklinLight};
font-size: 14px;
paint-order: stroke fill;
stroke: white;
stroke-linejoin: round;
stroke-opacity: 0.8;
stroke-width: 8px;
}
`
Insert cell
Insert cell
x = d3.scaleLinear(d3.extent(dataFlat, d => d.decadal_change), [ 0, chartwidth ])
Insert cell
r = d3.scaleSqrt([0, d3.max(dataFlat, d => d.population)], [0, maxR])
Insert cell
maxR = 60
Insert cell
Insert cell
margin = ({
left: r(dataFlat[d3.minIndex(dataFlat, d => d.decadal_change)].population),
right: r(dataFlat[d3.maxIndex(dataFlat, d => d.decadal_change)].population)
})
Insert cell
chartwidth = Math.min(640, width) - margin.left - margin.right
Insert cell
Insert cell
dataCityLabels = [
{"city":"Atlanta","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Boston","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Chicago","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Cleveland","angle": ww => -90,"textAnchor": ww => "middle","display": ww => ww <= 600 ? "none" : "block"},
{"city":"Dallas","state_post":"Tex.","angle": ww => -115,"textAnchor": ww => "end"},
{"city":"Detroit","angle": ww => -90,"textAnchor": ww => "middle","display": ww => ww <= 600 ? "none" : "block"},
{"city":"Houston","angle": ww => -65,"textAnchor": ww => "start"},
{"city":"Las Vegas","angle": ww => -90,"textAnchor": ww => "middle","display": ww => ww <= 480 ? "none" : "block"},
{"city":"Los Angeles","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Miami","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Minneapolis","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Nashville","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"New York","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"Philadelphia","angle": ww => -135,"textAnchor": ww => "end","display": ww => ww <= 600 ? "none" : "block"},
{"city":"Phoenix","angle": ww => -90,"textAnchor": ww => "middle"},
{"city":"San Diego","angle": ww => -90,"textAnchor": ww => "middle","display": ww => ww <= 680 ? "none" : "block"},
{"city":"St. Louis","angle": ww => -90,"textAnchor": ww => "middle","display": ww => ww <= 480 ? "none" : "block"},
].map(d => Object.assign(d, dataFlat.find(f => f.city === d.city)))
Insert cell
dataFlat = dataMultiples.map(d => d[1]).flat().map(d => d.entries).flat()
Insert cell
dataMultiples = d3.groups(ordered, d => d.region)
Insert cell
ordered = d3
.groups(citiesOutput, d => d.region, d => d.subregion)
.map(([region, subregions]) => {
const regionEntries = subregions.map(d => d[1]).flat();
return {
region,
subregions,
median: d3.median(regionEntries, d => d.decadal_change)
}
})
.sort((a, b) => d3.descending(a.median, b.median))
.map(({region, subregions}) => {
return subregions
.map(([subregion, entries]) => {
const { states, states_abbr } = regions.find(f => f.name === `${region} - ${subregion}`);
return {
region,
subregion,
entries,
states,
states_abbr,
median: d3.median(entries, d => d.decadal_change)
}
})
.sort((a, b) => d3.descending(a.median, b.median))
})
.flat()
Insert cell
citiesOutput = (await FileAttachment("cities-output@3.csv").csv())
.map(d => {
d.population = +d.population;
d.decadal_change = +d.slope * 10;
const [region, subregion] = regions.find(f => f.states.includes(d.state_post)).name.split(" - ");
d.region = region;
d.subregion = subregion;
return d;
})
.filter(d => d.population >= 25e3)
.sort((a, b) => d3.descending(a.population, b.population))
Insert cell
// Same as here: https://www.washingtonpost.com/climate-environment/interactive/2022/extreme-heat-risk-map-us/
// But I put Del., Md., D.C. in Northeast - Middle Atlantic
regions = [
{"name":"West - Mountain","states":["Ariz.","Colo.","Idaho","Mont.","Nev.","N.M.","Utah","Wyo."],"states_abbr":["AZ","CO","ID","MT","NV","NM","UT","WY"]},
{"name":"West - Pacific","states":["Calif.","Ore.","Wash."],"states_abbr":["CA","OR","WA"]},
{"name":"South - West South Central","states":["Ark.","La.","Okla.","Tex."],"states_abbr":["AK","LA","OK","TX"]},
{"name":"South - South Atlantic","states":["Fla.","Ga.","N.C.","S.C.","Va.","W.Va."],"states_abbr":["FL","GA","NC","SC","VA","WV"]},
{"name":"South - East South Central","states":["Ala.","Ky.","Miss.","Tenn."],"states_abbr":["AL","KY","MS","TN"]},
{"name":"Midwest - West North Central","states":["Iowa","Kan.","Minn.","Mo.","N.D.","Neb.","S.D."],"states_abbr":["IA","KS","MN","MO","ND","NE","SD"]},
{"name":"Midwest - East North Central","states":["Ill.","Ind.","Mich.","Ohio","Wis."],"states_abbr":["IL","IN","MI","OH","WI"]},
{"name":"Northeast - Middle Atlantic","states":["D.C.","Del.","Md.","N.J.","N.Y.","Pa."],"states_abbr":["DC","DE","MD","NJ","NY","PA"]},
{"name":"Northeast - New England","states":["Conn.","Mass.","Maine","N.H.","R.I.","Vt."],"states_abbr":["CT","MA","ME","NH","RI","VT"]}
]
Insert cell
Insert cell
function semicircle(cx, cy, r){
return "M" + (cx + r) + "," + cy + " a" + r + "," + r + " 1 1,0 -" + (r * 2) + ",0";
}
Insert cell
Insert cell
import { toc } from "@climatelab/toc@44"
Insert cell
import { franklinLight } from "@climatelab/fonts@46"
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