formatDate = d3.utcFormat("%b %d, %Y")
// FSA county geometry, fetched from the boundary archives at run time so it tracks
// them. The vintage follows the program year: eight FSA counties changed footprint
// between dd17 and dd22 — Shoshone ID split out of Benewah and Kootenai, Sioux NE
// consolidated into 31165, King WA into 53033, Richmond City VA out of Henrico — so
// drawing an early year on dd22 leaves since-split territory blank.
//
// 2015 is the switchover: 16079 (Shoshone) first appears in the data that year, and
// from then on 16009, 16055 and 16079 all report, which only holds under the dd22
// arrangement. The island territories are in neither archive and are not drawn.
us = await d3.json(
year <= 2014
? "https://data.sustainable-fsa.com/fsa-counties-dd17/fsa-counties-dd17.topojson"
: "https://data.sustainable-fsa.com/fsa-counties-dd22/fsa-counties-dd22.topojson"
)
counties = topojson.feature(us, us.objects.counties)
states = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
data = FileAttachment("assets/fsa-normal-grazing-period-simple.csv").csv({typed: true})
romaO = FileAttachment("assets/colors.json").json()
colorCyclic = d3.scaleSequential()
.domain([1, 366])
.interpolator(t => romaO[Math.floor(t * (romaO.length - 1))])
batlowK = FileAttachment("assets/colors-duration.json").json()
colorDuration = d3.scaleSequential()
.domain([0, 52])
.clamp(true)
.interpolator(t => batlowK[Math.floor(t * (batlowK.length - 1))])filtered = data.filter(d => d.year === year && d.type === type)
color = variable === "duration_weeks" ? colorDuration : colorCyclic
countiesFiltered = {
const rowById = Object.fromEntries(
filtered.map(d => [String(d.id).padStart(5, "0"), d])
);
return {
type: "FeatureCollection",
features: counties.features.map(f => {
const row = rowById[String(f.id)];
return {
...f,
properties: {
id: f.id,
...f.properties,
...(row ?? {}),
value: row ? row[variable] : null
}
};
})
};
}// 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();
})viewof year = Inputs.range(
d3.extent(data, d => d.year),
{
step: 1,
label: "Year",
value: 2026
}
)
viewof type = Inputs.select(
[...new Set(data.map(d => d.type))].sort(),
{
label: "Forage Type",
value: "Native Pasture"
}
)
viewof variable = Inputs.select(
new Map([
["Season Start", "start_yday"],
["Season End", "end_yday"],
["Grazing Period Duration", "duration_weeks"]
]),
{
label: "Color by",
value: "duration_weeks"
}
)Plot.plot({
width: mapSize.width,
height: Math.max(300, mapSize.height),
projection: {
type: "albers",
domain: counties
},
grid: true,
color: {legend: false},
marks: [
Plot.geo(countiesFiltered, {
fill: d => d.properties.value != null ? color(d.properties.value) : "#ccc"
}),
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}, ${d.state}
FSA county code: ${String(d.id).padStart(5, "0")}
Crop Type: ${type}
Start: ${d.start_date ? formatDate(d.start_date) : "None"}
End: ${d.end_date ? formatDate(d.end_date) : "None"}
Duration: ${d.duration_weeks != null ? d.duration_weeks + " weeks" : "None"}`
}
),
Plot.geo(states, {stroke: "white"}),
Plot.image([{}], { x: -76, y: 30,
width: variable === "duration_weeks" ? 60 : 100,
height: variable === "duration_weeks" ? 125 : 100,
src: () => variable === "duration_weeks" ? "assets/legend-duration.png" : "assets/legend.png"}),
]
})