Files
crdc-demo/src/charts/RateByGroupBar.jsx
T
jared 52a0c77e31
Deploy to Git Pages / build-and-deploy (push) Failing after 19s
Initial commit: CRDC Arrests API demo app
React + Vite static site demonstrating the CRDC School Arrest Rate API.
Features 6 interactive charts comparing observed arrest data against Bayesian
model estimates across U.S. school districts, with Civilytics visual identity.

- State selector and district search with 'interesting' suggestions (top arrests)
- Animated histogram loading grid showing posterior draw progress
- Charts: time series, rate by group, district vs national, model comparison quadrants, density proxy, exceedance probability
- Static JSON fixture for national rates (no API changes needed)
- Dockerfile + docker-compose.yml for self-hosted deployment
- GitHub Actions workflow and _config.yml for Git Pages
2026-08-10 10:54:51 -04:00

113 lines
5.0 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Chart 2: Arrest rate per 1,000 students by student group (most recent year).
* Bar chart — observed rate vs. modeled median rate.
*/
const GROUP_COLORS = {
'White Female': '#4A74B0', // navy-400
'White Male': '#2E5590', // navy-500
'Black Female': '#AB00AA', // plum-like (from social media posts)
'Black Male': '#8B007F', // darker purple
'Hispanic Female': '#1FAA4A', // green-ish (from wp_group_palette)
'Hispanic Male': '#15903C',
'AI/AN Female': '#D6271E', // red for American Indian emphasis
'AI/AN Male': '#A6271D'
}
export default function RateByGroupBar({ data }) {
const width = 280
const height = 240
const margin = { top: 35, right: 20, bottom: 80, left: 45 }
const innerWidth = width - margin.left - margin.right
const barHeight = Math.min(16, (innerWidth * 0.7) / data.length)
// Y-axis scale — per 1k rate
const maxRate = Math.max(...data.map(d => d.observedRate), ...data.map(d => d.modeledMedian || 0), 0.5)
return (
<div className="cv-card" style={{ padding: 'var(--space-2)' }}>
{/* Chart title */}
<h3 style={{ fontSize: '0.85rem', marginBottom: 'var(--space-1)', color: 'var(--cv-ink-2)' }}>
Arrest rate by student group — 2021–22 (per 1,000)
</h3>
<svg width="100%" viewBox={`0 0 ${width} ${height}`} style={{ maxWidth: '100%' }}>
{/* Background */}
<rect x={margin.left} y={margin.top} width={innerWidth} height={height - margin.top - margin.bottom}
fill="var(--cv-paper-2)" rx={4} />
{/* Y-axis gridlines */}
{[0, 0.25, 0.5, 0.75, 1].map((frac) => {
const val = frac * maxRate
if (val > maxRate) return null
const y = margin.top + ((height - margin.top - margin.bottom) - (val / maxRate) * (height - margin.top - margin.bottom))
return (
<g key={`yg-${frac}`}>
<line x1={margin.left} y1={y} x2={margin.left + innerWidth} y2={y}
stroke="var(--cv-rule)" strokeWidth={1} />
<text x={margin.left - 8} y={y + 4} textAnchor="end" fontSize="0.65rem" fill="var(--cv-ink-3)">{val.toFixed(1)}</text>
</g>
)
})}
{/* Y-axis label */}
<text x={12} y={margin.top + (height - margin.top - margin.bottom) / 2} textAnchor="middle"
fontSize="0.7rem" fill="var(--cv-ink-3)" transform={`rotate(-90 12 ${margin.top + (height - margin.top - margin.bottom) / 2})`}>
Rate per 1,000 students
</text>
{/* Bars */}
{data.map((d, i) => {
const y = margin.top + i * barHeight + 4
const observedWidth = (d.observedRate / maxRate) * innerWidth
const modeledWidth = ((d.modeledMedian || d.observedRate) / maxRate) * innerWidth
return (
<g key={`${d.race}-${d.sex}`}>
{/* Observed bar */}
{d.observedArrests > 0 && (
<>
<rect x={margin.left} y={y + 2} width={observedWidth} height={barHeight - 4}
fill="var(--cv-navy-600)" rx={2} />
{/* Observed count label */}
{d.observedRate > maxRate * 0.1 && (
<text x={margin.left + observedWidth + 4} y={y + barHeight / 2 + 4}
fontSize="0.65rem" fill="var(--cv-ink)">{d.observedArrests}</text>
)}
</>
)}
{/* Modeled median (dashed outline) */}
{d.modeledMedian > 0 && d.modeledMedian !== d.observedRate * (d.stu_enroll || 1) / 1000 && (
<rect x={margin.left} y={y + 2} width={modeledWidth} height={barHeight - 4}
fill="none" stroke="var(--teal-600)" strokeWidth={1.5} rx={2} />
)}
{/* Group label */}
<text x={margin.left - 8} y={y + barHeight / 2 + 4} textAnchor="end"
fontSize="0.7rem" fill="var(--cv-ink)">{d.label}</text>
</g>
)
})}
{/* X-axis label */}
<text x={margin.left + innerWidth / 2} y={height - 10} textAnchor="middle"
fontSize="0.7rem" fill="var(--cv-ink-3)">Solid = observed · Dashed = modeled median</text>
{/* Legend */}
<g transform={`translate(${margin.left}, ${height - 40})`}>
<rect x={0} y={0} width={12} height={12} fill="var(--cv-navy-600)" rx={2} />
<text x={18} y={10} fontSize="0.65rem" fill="var(--cv-ink)">Observed arrests</text>
<rect x={140} y={0} width={12} height={12} fill="none" stroke="var(--teal-600)" strokeWidth={1.5} rx={2} />
<text x={158} y={10} fontSize="0.65rem" fill="var(--cv-ink)">Modeled median</text>
</g>
</svg>
{/* Note */}
<p style={{ fontSize: '0.72rem', color: 'var(--cv-ink-3)', marginTop: 'var(--space-1)' }}>
Bars show observed arrests per 1,000 students. Dashed outlines show the Bayesian model's median estimate (unified_m2_mod).
</p>
</div>
)
}