- Create AGENTS.md: comprehensive agent guide covering architecture, data flow, error bar convention (90% intervals), D3 usage patterns, null safety pitfalls, deployment checklist, and related repos - Update HANDOFF.md: mark CORS as resolved, document chart improvements (Chart 5 ridgeline rewrite with synthetic draws + smooth rendering) - Refresh README.md: accurate tech stack (D3 v7), updated file structure with new chart files, corrected API endpoint table
7.2 KiB
Agent Guide — CRDC Demo App
This document captures context, decisions, and guidance for agents working on this codebase. It is the primary source of truth for how to make changes safely.
Project Overview
A React + Vite static web app demonstrating the CRDC School Arrest Rate API. Visitors select a state, search for a school district, and see 6 charts comparing observed arrest data against Bayesian model estimates. Deployed via Gitea Actions to pages.civilytics.org/crdc-demo/.
Architecture Summary
- Frontend: React 19 + Vite (static site generation)
- Styling: Plain CSS custom properties matching Civilytics design tokens (
src/styles/tokens.css) - Charts: Mixed approach:
- Charts 1–3 use inline SVG with manual scales (no D3 dependency for these)
- Chart 4 (ModelDrawsComparison) uses D3.js v7 for data-driven rendering of quadrant comparisons
- Chart 5 (RateDensityRidgeline) uses D3.js v7 for density ridge visualizations
- API: Calls public read-only API directly from browser; no backend required
- Deployment: Static site deployed via Gitea Actions (
.gitea/workflows/pages.yml)
Key Components and Data Flow
App.jsx (router)
→ StateSelector (landing screen: state dropdown/grid)
→ DistrictSearch (search + "interesting" suggestions from /estimates?state=&year=)
→ LoadingAnimation (fetches all data in parallel, shows animated histogram grid)
→ ChartPanel (receives district object, fetches structured estimates for 6 charts)
├── ArrestsOverTime — SVG line chart by wave (3 years)
├── RateByGroupBar — SVG bar chart: observed vs modeled per group
├── DistrictVsNational — SVG comparison to national average
├── ModelDrawsComparison — D3 quadrant charts (4 model types × 1 year)
├── RateDensityRidgeline — D3 density ridges per race×sex group
└── ExceedanceProbability — P(district > national) per student group
Data Fetching Strategy (ChartPanel.jsx)
ChartPanel fetches all chart data on mount (after LoadingAnimation pre-fetched via batch calls):
-
Wave data for Charts 1–3: Fetches
unified_m3_modmodel estimates for years['21-22', '17-18', '15-16'].- Uses three-year models because they return observed arrest counts across all waves (one-year models only have data for the most recent wave).
-
Quad data for Charts 4–6: Fetches estimates from all four quadrant models (
unified_m1_mod,unified_m2_mod,unified_m3_mod,unified_m4_mod) for year21-22only (one year, as the most recent wave). -
National rates: Loaded once from a static JSON fixture or cached by
LoadingAnimation.
Error Bar Convention: 90% Intervals
The API returns 95% HPD intervals (count_lower, count_upper). However, all chart labels and calculations in this app use 90% intervals. When converting HPD bounds to standard deviations for synthetic draw generation (used in Chart 5's ridgelines), the divisor used is 3.29 (corresponding to z = 1.645 for a two-tailed 90% interval).
If you change this convention, update:
RateDensityRidgeline.jsx— SD calculation (intervalWidth / 3.29) and label textModelDrawsComparison.jsx— Legend labels mentioning "90%"- Any documentation referencing confidence/credible intervals
Common Pitfalls & Gotchas
1. Null Safety in Chart Components
Several API responses may return empty arrays or missing fields for districts with no arrests:
// Always use optional chaining and defaults:
const yearRow = (quadModels[q.key] || []).find(r => r.year === year)
const predMedian = yearRow?.count_median || 0
const enroll = row.stu_enroll || 1 // Prevent division by zero
2. D3 useEffect Dependency Arrays
When using useEffect for D3 rendering, always include all data dependencies to prevent stale renders:
// Correct — includes all props used inside the effect
}, [quadData, selectedModel, rateByGroup])
3. SVG Dimensions and Responsiveness
Charts use fixed dimensions with responsive containers (overflowX: 'auto' for wide content):
- Chart cards have
max-width: 70reminChartPanel.jsx(vs the default text width of ~60rem) - SVG elements should set both
width="100%"and a fixedviewBoxfor proper scaling
4. API Endpoint Availability
Not all endpoints are available to browser-based clients:
/api/v1/estimates/{leaid}— ✅ Returns estimates summary (median, lower, upper bounds)/api/v1/draws?...— ❌ Returns Parquet shard URL only; meant for bulk processing via DuckDB, not usable from the browser
If you need raw posterior draws in the browser app, a new API endpoint would be required. Currently, Chart 5 generates synthetic draws using normal approximation (d3.randomNormal) based on the interval bounds.
5. CORS Configuration
The CRDC API does not send CORS headers. When deployed to git-pages (static hosting), requests are blocked by same-origin policy unless a proxy is configured:
- The app auto-detects proxy availability via
VITE_PROXY_URLenvironment variable - If unset, the app attempts direct fetch — works when served from Docker/nginx or same-origin
Deployment Checklist
Before pushing to production:
- Build succeeds:
npm run build(check for new errors)2. No console errors in browser after hard refresh3. All 6 charts render with sample districts (test "Denver", "Mobile County") - Loading animation appears briefly, then transitions to ChartPanel
Commit Message Convention
Use descriptive commit messages that explain the why, not just the what:
# Good
git commit -m "Fix: use three-year model for wave data (returns observed counts across all years)"
git commit -m "Add D3 density ridges to Chart 5 with diamond markers for observed rates"
# Avoid vague messages
git commit -m "Fix charts" # Too generic
git commit -m "Update code" # No context
Testing Strategy
There are no automated tests in this project. Manual verification is required:1. Visual check: Load a district and verify all 6 charts render correctly2. Error console: Check browser DevTools for JavaScript errors3. Data accuracy: Compare observed values against API response (check Network tab) 4. Responsiveness: Resize window to ensure layout adapts
Style Guide References
- R code style: Follows tidyverse principles (
r-style-guideskill in agent knowledge base)- Chart aesthetic decisions should match patterns fromsocial_media_posts.mdandwhite_paper.qmd - Colors, typography, spacing are defined as CSS custom properties in
src/styles/tokens.css
Related Repositories
- crdc-arrests — The API server (Plumber/R) at
/home/jared/Nextcloud/Civilytics/Code/Civilytics/crdc-arrests/ - civilyticsR — R package with wordmark and visualization functions at
/home/jared/Nextcloud/Civilytics/Code/Civilytics/civilyticsR/
Git Conventions
- Remote:
https://gitea.civilytics.org/Civilytics/crdc-demo.git - Default branch:
main(notmaster) - Gitea Actions workflow auto-deploys on push to
mainvia.gitea/workflows/pages.yml - Always pull before making changes:
git pull origin main