import { useState, useEffect } from 'react' import ChartPanel from './ChartPanel.jsx' import * as api from '../hooks/useApi.js' import { STUDENT_GROUPS, MODEL_QUADRANTS } from '../hooks/useApi.js' import { shortGroupLabel } from '../utils/colors.js' // CRDC waves to fetch (for Chart 1 — time series) const WAVES = ['21-22', '17-18', '15-16'] /** * Loading screen with animated histogram grid. * Fetches all required data in parallel, shows progress as bars fill. * When complete → renders ChartPanel directly (no transition needed). */ export default function LoadingAnimation({ district, state }) { const [loadedCount, setLoadedCount] = useState(0) const [totalCalls, setTotalCalls] = useState(0) const [error, setError] = useState(null) // Build the full list of API calls needed for all 3 charts useEffect(() => { let cancelled = false async function loadData() { try { // ——— Chart 1: Arrests over time (3 waves × default model) ——— const wavePromises = WAVES.map((year) => fetchDistrictEstimatesBatch(district.leaid, year) .then(() => !cancelled && setLoadedCount(c => c + 1)) .catch(() => { /* individual failure doesn't block */ }) ) // ——— Chart 2: Rate by group (8 groups × default model, most recent wave) ——— const groupPromises = STUDENT_GROUPS.map((sg) => fetchDistrictEstimatesBatch(district.leaid, '21-22', sg.race, sg.sex) .then(() => !cancelled && setLoadedCount(c => c + 1)) .catch(() => {}) ) // ——— Chart 3: posterior density, all 4 quadrant models so the dropdown can switch (8 groups × 4 = 32 calls) ——— const modelPromises = MODEL_QUADRANTS.flatMap((quad) => STUDENT_GROUPS.map((sg) => fetchDistrictEstimatesBatch(district.leaid, '21-22', sg.race, sg.sex, quad.model) .then(() => !cancelled && setLoadedCount(c => c + 1)) .catch(() => {}) ) ) // Set total before starting (for progress bar) const allPromises = [...wavePromises, ...groupPromises, ...modelPromises] if (!cancelled) setTotalCalls(allPromises.length) await Promise.all(allPromises) if (!cancelled) { // All data loaded — ChartPanel renders in place of this component // We use a render prop pattern: return when done setLoadedCount(allPromises.length) } } catch (err) { if (!cancelled) setError(err.message || 'Failed to load data') } } loadData() return () => { cancelled = true } }, [district.leaid]) // ——— Render the histogram grid animation or ChartPanel when done ——— const isComplete = loadedCount >= totalCalls && totalCalls > 0 if (isComplete) { return } if (error) { return renderError(error, district) } // Build the grid of "bars" — one per API call needed. // Total = WAVES (3) + STUDENT_GROUPS (8) + MODEL_QUADRANTS×STUDENT_GROUPS (4×8=32) const TOTAL_BARS = WAVES.length + STUDENT_GROUPS.length + (MODEL_QUADRANTS.length * STUDENT_GROUPS.length) // Build bar metadata for rendering const allBars = Array.from({ length: TOTAL_BARS }, (_, i) => { if (i < WAVES.length) return { subLabel: 'Time series', group: 'waves' } if (i < WAVES.length + STUDENT_GROUPS.length) { const sg = STUDENT_GROUPS[i - WAVES.length] return { subLabel: shortGroupLabel(sg.race, sg.sex), group: 'groups' } } const quadIdx = Math.floor((i - WAVES.length - STUDENT_GROUPS.length) / STUDENT_GROUPS.length) const sgIdx = (i - WAVES.length - STUDENT_GROUPS.length) % STUDENT_GROUPS.length const sg = STUDENT_GROUPS[sgIdx] return { subLabel: shortGroupLabel(sg.race, sg.sex), group: 'models', modelIdx: quadIdx } }) return (
{/* Header */} Fetching data from the CRDC Arrest Rate API

Building estimates for {district.lea_name} in {state === 'DC' ? "District of Columbia" : getStateName(state)}

{/* Progress counter */}
{loadedCount} of {totalCalls || allBars.length} datasets loaded {Math.round((loadedCount / (totalCalls || allBars.length)) * 100)}% complete
{/* Animated histogram grid — mirrors the Bayesian posterior draw theme */}
{/* Column headers */} {['Time series (Chart 1)', 'By student group (Chart 2)', 'Posterior density (Chart 3)'].map((h, i) => (
{h}
))} {/* Grid of animated bars — each represents one API call */} {allBars.map((bar, i) => { const filled = i < loadedCount // Random height for "data viz" aesthetic (deterministic via seed = index) const heightSeed = ((i * 37) % 100) + 20 // 20–120px range return (
{/* The animated bar */}
{filled && (
)}
{/* Label below */} {bar.subLabel}
) })}
{/* Context message */}

Fetching {totalCalls || allBars.length} datasets across 3 CRDC waves, 8 student groups, and 4 Bayesian model specifications...

{/* Model legend */}
{MODEL_QUADRANTS.map((q, i) => ( {q.label} ))}
) } // ——— API helper using the centralized client from useApi.js ——— // This ensures consistent error handling, envelope unwrapping, and CORS proxy support. async function fetchDistrictEstimatesBatch(leaid, year, race = null, sex = null, model = 'unified_m2_mod') { return api.fetchDistrictEstimates(leaid, { year, model, race, sex }) } function getStateName(code) { const names = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',DC:'District of Columbia',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming' } return names[code] || code } function renderError(error, district) { return (
ERROR

Failed to load data for {district.lea_name}

{error}. This may be a temporary API issue or the district may not have available estimates.

) }