Public
Edited
May 16
Insert cell
Insert cell
Plot.plot({
width: 1200,
height: 1000,
marginTop: 50,
marginBottom: 40,
marginLeft: 250,
padding: 0,
label: null,

// date‐based top axis
x: {
axis: "top",
tickFormat: d => d.getUTCFullYear().toString(),
tickRotate: 0
},

y: {
domain: [...new Set(cbsa_data.map(d => d.metro))]
},

color: {
type: "linear",
domain: [minValue, maxValue],
range: ["#f7fbff", "#49006a"],
legend: true,
label: "PM2.5 Air Quality Index"
},

marks: [
// heatmap
Plot.rect(coloredData, {
x: "date",
y: "metro",
fill: d => d.color,
stroke: "white",
strokeWidth: 0, // or 0.5 for a thinner line
title: d => `${d.metro} (${d.date.getUTCFullYear()}): ${
(d.value === 0 || isNaN(d.value)) ? "No data" : d.value.toFixed(1)
}`
}),

// vertical lines at each introduction date
Plot.ruleX(annotations, {
x: "date",
stroke: "black",
strokeWidth: 3,
opacity: 0.5
}),

// year labels above each line
Plot.text(annotations, {
x: "date",
text: d => d.label,
dy: 2,
lineAnchor: "top",
frameAnchor: "bottom",
textAnchor: "middle",
fill: "black",
fontWeight: "bold"
})
]
})
Insert cell
maxValue = d3.max(cbsa_data, d => d.value || -Infinity)
Insert cell
minValue = d3.min(cbsa_data, d => d.value || Infinity)
Insert cell
coloredData = cbsa_data.map(d => ({
...d,
color: (d.value === 0 || d.value == null || isNaN(d.value))
? "#f5f5f5" // light gray for zeros/missing
: lerpColor("#f7fbff", "#49006a", (d.value - minValue)/(maxValue - minValue))
}))
Insert cell
function lerpColor(a, b, t) {
const hexToRgb = hex => hex.match(/\w\w/g).map(x => parseInt(x, 16));
const rgbToHex = rgb => "#" + rgb.map(x => x.toString(16).padStart(2, "0")).join("");
const [r1, g1, b1] = hexToRgb(a), [r2, g2, b2] = hexToRgb(b);
return rgbToHex([
Math.round(r1 + (r2 - r1) * t),
Math.round(g1 + (g2 - g1) * t),
Math.round(b1 + (b2 - b1) * t)
]);
}
Insert cell
// Map value to t in [0,1]
function colorScale(value) {
if (value == null || value === 0 || isNaN(value)) return "#f5f5f5";
const t = (value - minValue) / (maxValue - minValue);
// Interpolate from light purple to dark purple:
return lerpColor("#f7fbff", "#49006a", t);
}
Insert cell
cbsa_data = {
const data = await FileAttachment("air_quality@4.json").json();
return data.map(d => ({
metro: d["CBSA Name"],
date: new Date(d.date),
value: +d.value,
}));
}
Insert cell
introductions = [1997, 2006, 2012, 2024].map(y => new Date(Date.UTC(y, 0, 1)))
Insert cell
annotations = introductions.map(date => {
const year = date.getUTCFullYear();
return {
date,
label: year === 2012
? `2012\n\nPM2.5 Regulations Strengthened`
: `${year}`
};
});
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