trends = FileAttachment("trends.csv").csv({ typed: true })
optionsFor = chart => Array.from(
d3.group(trends.filter(d => d.chart === chart), d => d.variable),
([variable, rows]) => ({ variable, label: rows[0].label, group: rows[0].group })
).sort((a, b) => d3.ascending(a.group, b.group) || d3.ascending(a.label, b.label))
// Y axis fits the data: at least a 20-point window, padded and snapped to
// the gridline step, clamped to 0-100%. Lines do not need a zero baseline the way
// bars do, and the fixed minimum window keeps small changes from looking huge.
yAxis = (series) => {
const lo = d3.min(series, d => d.share), hi = d3.max(series, d => d.share);
const span = Math.max(hi - lo, 0.2), mid = (lo + hi) / 2, pad = span * 0.15;
let a = mid - span / 2 - pad, b = mid + span / 2 + pad;
if (a < 0) { b -= a; a = 0; }
if (b > 1) { a -= b - 1; b = 1; }
// Gridline step grows with the range so there are about 4-6 lines:
// 5 points for narrow ranges, up to 25 points for wide ones.
const step = [0.05, 0.1, 0.2, 0.25].find(k => (b - a) / k <= 6) ?? 0.25;
const domain = [Math.max(0, Math.floor(a / step) * step), Math.min(1, Math.ceil(b / step) * step)];
return { domain, ticks: d3.range(domain[0], domain[1] + 1e-9, step) };
}
trendPlot = (series) => Plot.plot({
className: "mh-chart",
title: series[0].label,
subtitle: `Share of U.S. mental health treatment facilities, ${series[0].year}–${series.at(-1).year}`,
caption: "Source: Dada J. Trends in the Services Offered by US Mental Health Treatment Facilities, 2015–2024. October 21, 2026. doi:10.5281/zenodo.23574161. Data from SAMHSA N-MHSS (2015–2020) and N-SUMHSS (2021–2024).",
width: Math.min(width, 860),
height: 420,
marginLeft: 56,
marginTop: 28,
style: { fontSize: "16px", fontFamily: "Figtree, system-ui, sans-serif", background: "transparent" },
x: { label: null, tickFormat: d3.format("d"), ticks: series.map(d => d.year) },
y: { label: null, ...yAxis(series), tickFormat: d3.format(".0%"), grid: true },
marks: [
Plot.lineY(series, { x: "year", y: "share", stroke: "#B51D5A", strokeWidth: 3 }),
Plot.dot(series, { x: "year", y: "share", fill: "#B51D5A", r: 5 }),
Plot.text(series.slice(0, 1),
{ x: "year", y: "share", text: d => d3.format(".0%")(d.share), dy: series[1].share > series[0].share ? 22 : -16, dx: 8, textAnchor: "start", fill: "#B51D5A", fontWeight: 600, fontSize: 17 }),
Plot.text(series.slice(-1),
{ x: "year", y: "share", text: d => d3.format(".0%")(d.share), dy: series.at(-2).share > series.at(-1).share ? 22 : -16, textAnchor: "end", fill: "#B51D5A", fontWeight: 600, fontSize: 17 }),
Plot.tip(series, Plot.pointerX({ x: "year", y: "share",
title: d => `${d.year}\n${d3.format(".1%")(d.share)} of ${d3.format(",")(d.n_facilities)} facilities that answered` }))
]
})