scaleSequential = (await import("https://cdn.jsdelivr.net/npm/d3@7/+esm")).scaleSequential
// a format for dates
formatDate = d3.utcFormat("%b %d, %Y") // e.g., "Apr 18, 2025"
// this archive is drought-only
disaster = "Drought"
// counties
us = await d3.json("https://data.sustainable-fsa.com/fsa-counties-dd17/fsa-counties-dd17.topojson");
counties = topojson.feature(us, us.objects.counties)
// states, but just the boundaries between them
states = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
// weekly LFP eligibility data, simplified
data = FileAttachment("assets/fsa-lfp-eligibility-web-simple.csv").csv({typed: true})
// Load color palettes
color =
new Map([
["1 Month", "#FFFF54"],
["2 Month", "#F3AE3D"],
["3 Month", "#6D4E16"],
["4 Month", "#EA3323"],
["5 Month", "#7316A2"],
["Eligible", "#EA3323"]
])
variable = "payment_type"// Size the map to its card. A ResizeObserver notifies as soon as the card
// receives its real dimensions, so the FIRST render is correctly sized —
// the stdlib `width` is only re-evaluated on window resize, which left the
// initial paint at the wrong scale until the viewport changed.
mapSize = Generators.observe((notify) => {
const body = document.querySelector(".card .card-body");
const ro = new ResizeObserver((entries) => {
const r = entries[0].contentRect;
if (r.width > 0 && r.height > 0) notify({width: r.width, height: r.height});
});
ro.observe(body);
return () => ro.disconnect();
})filtered = data.filter(d => d.year === year && d.type === type && d.disaster === disaster)
// Create a lookup table keyed by `id`
valueById = Object.fromEntries(filtered.map(d => [String(d.id).padStart(5, "0"), d[variable]]))
// Merge values into counties.features
countiesFiltered = {
// Build lookup from filtered CSV: id → full row
const rowById = Object.fromEntries(
filtered.map(d => [String(d.id).padStart(5, "0"), d])
);
// Rebuild counties with joined properties
return {
type: "FeatureCollection",
features: counties.features.map(f => {
const id = String(f.id);
const row = rowById[id];
return {
...f,
value: row ? row[variable] : null,
properties: {
id: f.id,
...f.properties,
...(row ?? {}),
value: row ? row[variable] : null // explicitly add value for coloring
}
};
})
};
}Plot.plot({
width: mapSize.width,
height: Math.max(300, mapSize.height - 70), // leave room for the legend
projection: {
type: "albers",
domain: counties
},
grid: true,
color: {
domain: ["1 Month", "2 Month", "3 Month", "4 Month", "5 Month"],
range: ["#FFFF54", "#F3AE3D", "#6D4E16", "#EA3323", "#7316A2"],
unknown: "#ccc",
legend: true
},
marks: [
Plot.geo(countiesFiltered, {
fill: "value"
}),
Plot.dot(
countiesFiltered.features.map(f => {
const [x, y] = d3.geoCentroid(f);
return {
...f.properties,
x,
y
};
}),
{
x: "x",
y: "y",
r: 3,
fill: "transparent",
stroke: "none",
tip: true,
title: d => `${d.county} County, ${d.state}
FIPS: ${String(d.id).padStart(5, "0")}
Program Year: ${year}
Crop Type: ${type}
Disaster: ${d.disaster ? d.disaster : "None"}
Start Date: ${d.disaster_start_date ? formatDate(d.disaster_start_date) : "None"}
`
}
),
Plot.geo(states, {stroke: "white"}),
]
})