import { MODEL_QUADRANTS } from '../hooks/useApi.js' import ChartLegend from '../components/ChartLegend.jsx' import { OBSERVED_MARK_COLOR, MODELED_AGGREGATE_COLOR } from '../utils/colors.js' /** * Chart 4: Modeled vs. observed arrests, faceted by the four model-quadrant * specifications (one-year/three-year × baseline/+referral-rate covariate). * Plain SVG (no D3) — the quadrant label already conveys 1yr/3yr, so all * four panels share one ink-diamond/navy-point-range convention rather than * doubling up with a redundant per-quadrant hue. */ const WAVE_LABELS = { '15-16': '2015–16', '17-18': '2017–18', '21-22': '2021–22' } const ALL_WAVES = ['15-16', '17-18', '21-22'] export default function ModelDrawsComparison({ data, waves = ALL_WAVES, districtName }) { const waveData = waves.map((year) => { const rows = (data.waves && data.waves[year]) || [] return { year, arrests: rows.reduce((sum, r) => sum + (r.observed_arrests || 0), 0) } }) const quadModels = (data && data.quadData) || {} const allValues = waveData.map((d) => d.arrests) Object.values(quadModels).forEach((rows) => { ;(rows || []).forEach((r) => { allValues.push(r.count_median || 0, r.count_upper || 0) }) }) const maxArrests = Math.min(Math.max(...allValues, 1), 50) // cap for readability return (

Model predictions vs. observed — by wave & model specification

{MODEL_QUADRANTS.map((q) => ( ))}

Three-year models span all three CRDC waves; one-year models show 2021–22 only.{' '} {districtName ? `District: ${districtName}.` : ''}

) } function QuadrantPanel({ q, waveData, quadModels, maxArrests }) { const isThreeYear = q.model.includes('m3') || q.model.includes('m4') const showYears = isThreeYear ? ALL_WAVES : ['21-22'] const width = 260 const height = 190 const margin = { top: 26, right: 12, bottom: 26, left: 34 } const innerWidth = width - margin.left - margin.right const innerHeight = height - margin.top - margin.bottom const xScale = (i) => margin.left + (showYears.length > 1 ? (i / (showYears.length - 1)) * innerWidth : innerWidth / 2) const yScale = (val) => margin.top + innerHeight - (Math.min(val, maxArrests) / maxArrests) * innerHeight return (
{q.label} {[0, 0.25, 0.5, 0.75, 1].map((frac) => { const val = frac * maxArrests const y = yScale(val) return ( {Math.round(val)} ) })} {showYears.map((year, i) => { const x = xScale(i) const obs = waveData.find((d) => d.year === year)?.arrests ?? 0 const yearRow = (quadModels[q.model] || []).find((r) => r.year === year) const obsY = yScale(obs) return ( {WAVE_LABELS[year]} {yearRow && ( <> )} ) })}
) }