Public
Edited
Nov 22, 2023
Paused
9 forks
Importers
51 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
timestamp = d3.min([
`${date.toISOString().slice(0, 11)}${d3.format("02d")(hour)}`,
new Date(+new Date() - 3600 * 1000).toISOString().slice(0, 13)
])
Insert cell
// Note: initially I had a unique URL (a fixed datetime), but for a live map it's better to check
// the API against the _current_ time. Once you do that, it’s a simple change to add a date picker
// and a range slider for the hour. The line below ensures that we don’t look “in the future”. And
// indeed we don’t want the current time precisely, because the API can be a few minutes late—so
// we’ll check one hour before the current time.
url = `https://www.airnowapi.org/aq/data/?startDate=${timestamp}&endDate=${timestamp}&parameters=PM25&BBOX=-130,25,-65,55&dataType=A&format=text/csv&API_KEY=${key}`
Insert cell
function getData(url) {
return d3
.text(url)
.then((d) =>
d3.csvParse("lat,lon,date,indicator,PM2.5,??\n" + d, d3.autoType)
);
}
Insert cell
air = cachedData(url)
Insert cell
// This function wraps the logic that fetches & parses the data with a cache, to animate the map
// a bit faster when we play with the date/time controls.
cachedData = {
const M = new Map();
return async function (url) {
if (M.has(url)) return Promise.resolve(M.get(url));
const c = await getData(url);
M.set(url, c);
return c;
};
}
Insert cell
// Here’s a copy, in case the API stops working
// air = FileAttachment("air.csv").csv({typed: true})
Insert cell
Insert cell
color = ({
label: "PM2.5",
type: "threshold",
domain: [50, 100, 150, 200, 300],
range: [
"rgb(156, 216, 78)", // Good
"rgb(250, 207, 57)", // Moderate
"rgb(249, 144, 73)", // Unhealthy for Sensitive Groups
"rgb(246, 94, 95)", // Unhealthy
"rgb(160, 112, 182)", // Very Unhealthy
"rgb(160, 106, 123)" // Hazardous
]
})
Insert cell
Insert cell
Insert cell
Plot.plot({
color,
marks: [
Plot.dot(air, {
x: "lon",
y: "lat",
fill: "PM2.5",
r: 4,
stroke: "white"
})
]
})
Insert cell
Insert cell
// Plot.dot(air, { x: "lon", y: "lat", fill: "PM2.5", fx: "date" }).plot()
Insert cell
// Group by date, and return the group with the largest key.
recent = d3
.greatest(
d3.group(air, (d) => d.date),
(d) => d[0]
)[1]
.filter((d) => d["PM2.5"] > 0) // remove the points with value -999
Insert cell
Insert cell
visibility().then(() => map(width))
Insert cell
// this is a function because we want to show the map at the top of the page.
map = (width) =>
Plot.plot({
width,
projection: "albers",
color: { ...color, legend: true },
marks: [
// this creates a path to clip the contour mark.
Plot.geo(nation, {
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c).children[0];
return svg`<clipPath id=nation>${g}`;
}
}),

Plot.contour(recent, {
x: "lon",
y: "lat",
fill: "PM2.5",
interpolate: "random-walk",
blur: 3,
thresholds: [0, ...color.domain],
stroke: "currentColor",
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c);
g.setAttribute("clip-path", "url(#nation)"); // add clip
return g;
}
}),

Plot.geo(nation, { stroke: "currentColor" }),

Plot.dot(recent, {
x: "lon",
y: "lat",
fill: "PM2.5",
stroke: "currentColor",
strokeOpacity: 0.8,
strokeWidth: 0.75,
tip: true
}),

Plot.text([timestamp], {
frameAnchor: "top-right",
fontSize: 16,
fontVariant: "tabular-nums"
})
]
})
Insert cell
us = FileAttachment("us-counties-10m.json").json()
Insert cell
nation = topojson.feature(us, us.objects.nation)
Insert cell
key = "62F6D106-6781-4DC5-8A63-005044B666E2" // API key
Insert cell
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