From b135f480c006c89d79cb9ef4d86379ae807929a8 Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Mon, 10 Aug 2026 12:50:19 -0400 Subject: [PATCH] Fix chart sizing, model predictions, 90% intervals, and deep link district name - Increase SVG dimensions in all 6 charts to prevent label truncation - Fix ModelDrawsComparison: pass quadData so predictions render for three-year models - Change error bars from 95% to 90% (legend text + SD calculation) - Widen chart viewport: max-width 70rem vs default 60rem text width - Fix deep link: fetch district name from API instead of showing 'Loading...' --- src/App.jsx | 22 ++++++++++++++++++---- src/charts/ArrestsOverTime.jsx | 6 +++--- src/charts/DistrictVsNational.jsx | 6 +++--- src/charts/ExceedanceProbability.jsx | 12 ++++++------ src/charts/ModelDrawsComparison.jsx | 10 +++++----- src/charts/ObservedRateDensity.jsx | 12 ++++++------ src/charts/RateByGroupBar.jsx | 6 +++--- src/components/ChartPanel.jsx | 7 +++++-- 8 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 3fbf6e8..57ec3b5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import DistrictSearch from './components/DistrictSearch.jsx' import LoadingAnimation from './components/LoadingAnimation.jsx' import ChartPanel from './components/ChartPanel.jsx' import Footer from './components/Footer.jsx' +import { searchDistricts } from './hooks/useApi.js' /** * CRDC Arrests API Demo App — main router. @@ -20,11 +21,24 @@ export default function App() { const leaid = params.get('leaid') const stateParam = params.get('state') if (leaid && stateParam) { - // Deep link: skip straight to loading → results. - // We need to fetch the district name since we only have LEAID from URL. + // Deep link: fetch district name first, then go to loading. setSelectedState(stateParam) - setDistrict({ leaid, lea_name: 'Loading…', state: stateParam }) - setStep('loading') + + // Fetch district name from API + searchDistricts(leaid, stateParam).then(results => { + const district = results?.find(d => d.leaid === leaid) + if (district) { + setDistrict(district) + } else { + // Fallback if district not found + setDistrict({ leaid, lea_name: 'Unknown District', state: stateParam }) + } + setStep('loading') + }).catch(() => { + // Fallback on error + setDistrict({ leaid, lea_name: 'Unknown District', state: stateParam }) + setStep('loading') + }) } }, []) diff --git a/src/charts/ArrestsOverTime.jsx b/src/charts/ArrestsOverTime.jsx index 969b1bb..4d8fcab 100644 --- a/src/charts/ArrestsOverTime.jsx +++ b/src/charts/ArrestsOverTime.jsx @@ -15,9 +15,9 @@ export default function ArrestsOverTime({ data, districtName }) { ) // Chart dimensions (SVG-based, no D3 dependency) - const width = 260 - const height = 220 - const margin = { top: 40, right: 80, bottom: 50, left: 55 } + const width = 360 + const height = 280 + const margin = { top: 50, right: 90, bottom: 60, left: 65 } const innerWidth = width - margin.left - margin.right const innerHeight = height - margin.top - margin.bottom diff --git a/src/charts/DistrictVsNational.jsx b/src/charts/DistrictVsNational.jsx index f8a05d3..8210e76 100644 --- a/src/charts/DistrictVsNational.jsx +++ b/src/charts/DistrictVsNational.jsx @@ -4,9 +4,9 @@ */ export default function DistrictVsNational({ topGroup, nationalRate, rateByGroup }) { - const width = 280 - const height = 240 - const margin = { top: 35, right: 20, bottom: 60, left: 55 } + const width = 360 + const height = 300 + const margin = { top: 45, right: 30, bottom: 80, left: 70 } const innerWidth = width - margin.left - margin.right // Compute max rate for scaling (district + national) diff --git a/src/charts/ExceedanceProbability.jsx b/src/charts/ExceedanceProbability.jsx index f79ecfa..779645e 100644 --- a/src/charts/ExceedanceProbability.jsx +++ b/src/charts/ExceedanceProbability.jsx @@ -11,7 +11,7 @@ export default function ExceedanceProbability({ rateByGroup, nationalRates }) { // Compute exceedance probability for each group using normal approximation: // P(district_rate > national) ≈ Φ((modeled_median - national) / modeled_sd) - // where sd is derived from the interval width (upper - lower) / (2 * z_{0.975}) + // where sd is derived from the interval width (upper - lower) / (2 * z_{0.95}) const computed = rateByGroup.map(d => { const natKey = `${d.race}_${d.sex}` const nationalRate = nationalRates[natKey] || 0 @@ -25,8 +25,8 @@ export default function ExceedanceProbability({ rateByGroup, nationalRates }) { // Derive SD from interval width if available, else use observed as proxy const lowerPerK = ((d.rate_lower || 0) * 1000) || (modeledRatePerK * 0.7) const upperPerK = ((d.rate_upper || 0) * 1000) || (modeledRatePerK * 1.3) - // sd ≈ (upper - lower) / (2 * 1.96) for a 95% interval under normality - const modeledSd = Math.max((upperPerK - lowerPerK) / 3.92, 0.05) + // sd ≈ (upper - lower) / (2 * 1.645) for a 90% interval under normality + const modeledSd = Math.max((upperPerK - lowerPerK) / 3.29, 0.05) // P(district > national) using normal CDF approximation const z = (modeledRatePerK - nationalRate) / modeledSd @@ -42,9 +42,9 @@ export default function ExceedanceProbability({ rateByGroup, nationalRates }) { } }).sort((a, b) => b.probExceeds - a.probExceeds) - const width = 300 - const height = 260 - const margin = { top: 40, right: 80, bottom: 70, left: 55 } + const width = 400 + const height = 320 + const margin = { top: 50, right: 100, bottom: 90, left: 70 } const innerWidth = width - margin.left - margin.right return ( diff --git a/src/charts/ModelDrawsComparison.jsx b/src/charts/ModelDrawsComparison.jsx index 2ed715b..090f55a 100644 --- a/src/charts/ModelDrawsComparison.jsx +++ b/src/charts/ModelDrawsComparison.jsx @@ -58,7 +58,7 @@ export default function ModelDrawsComparison({ data, waves, districtName }) { {/* Note */}

- Red circles = observed arrests. Teal points + error bars (three-year models only) show Bayesian model median and 95% interval. One-year models can only compare against the most recent wave. + Red circles = observed arrests. Teal points + error bars (three-year models only) show Bayesian model median and 90% interval. One-year models can only compare against the most recent wave.

) @@ -66,9 +66,9 @@ export default function ModelDrawsComparison({ data, waves, districtName }) { /** Single quadrant chart: predicted vs observed for one model specification */ function QuadrantChart({ q, waveData, quadModels, maxArrests }) { - const width = 280 - const height = 240 - const margin = { top: 35, right: 20, bottom: 60, left: 45 } + const width = 360 + const height = 280 + const margin = { top: 45, right: 25, bottom: 70, left: 55 } // For three-year models show all 3 waves; for one-year only the most recent const showYears = (q.key.includes('m3') || q.key.includes('m4')) ? ['15-16', '17-18', '21-22'] : ['21-22'] @@ -165,7 +165,7 @@ function QuadrantChart({ q, waveData, quadModels, maxArrests }) { <> - Model (median + 95%) + Model (median + 90%) )} diff --git a/src/charts/ObservedRateDensity.jsx b/src/charts/ObservedRateDensity.jsx index 8bd41f8..3e4823f 100644 --- a/src/charts/ObservedRateDensity.jsx +++ b/src/charts/ObservedRateDensity.jsx @@ -51,9 +51,9 @@ export default function ObservedRateDensity({ quadData }) { // Max value for scaling the ridgeline chart const maxRate = Math.max(...modelResults.map(r => r.modeledUpper), ...modelResults.map(r => r.observedRate), 0.5) - const width = 280 - const height = 220 - const margin = { top: 30, right: 20, bottom: 60, left: 45 } + const width = 380 + const height = 280 + const margin = { top: 40, right: 30, bottom: 80, left: 60 } return (
@@ -89,7 +89,7 @@ export default function ObservedRateDensity({ quadData }) { {/* Observed rate bar (solid) */} - {/* Model interval (dashed outline showing 95% range) */} + {/* Model interval (dashed outline showing 90% range) */} {/* Model median marker */} @@ -125,13 +125,13 @@ export default function ObservedRateDensity({ quadData }) { Observed rate - Model (median + 95% interval) + Model (median + 90% interval) {/* Note */}

- Solid blue = observed rate per 1,000 students. Teal outline + median line shows each model's posterior distribution (95% interval). Dashed lines indicate the range of plausible values according to each Bayesian specification. + Solid blue = observed rate per 1,000 students. Teal outline + median line shows each model's posterior distribution (90% interval). Dashed lines indicate the range of plausible values according to each Bayesian specification.

) diff --git a/src/charts/RateByGroupBar.jsx b/src/charts/RateByGroupBar.jsx index a1e8b52..59ec0da 100644 --- a/src/charts/RateByGroupBar.jsx +++ b/src/charts/RateByGroupBar.jsx @@ -15,9 +15,9 @@ const GROUP_COLORS = { } export default function RateByGroupBar({ data }) { - const width = 280 - const height = 240 - const margin = { top: 35, right: 20, bottom: 80, left: 45 } + const width = 360 + const height = 300 + const margin = { top: 45, right: 30, bottom: 100, left: 60 } const innerWidth = width - margin.left - margin.right const barHeight = Math.min(16, (innerWidth * 0.7) / data.length) diff --git a/src/components/ChartPanel.jsx b/src/components/ChartPanel.jsx index f319dd4..e3be686 100644 --- a/src/components/ChartPanel.jsx +++ b/src/components/ChartPanel.jsx @@ -102,7 +102,10 @@ export default function ChartPanel({ district, state }) {
{/* Charts 1–3: Observed & descriptive */} @@ -112,7 +115,7 @@ export default function ChartPanel({ district, state }) { {/* Charts 4–6: Bayesian model distributions */} - +