diff --git a/src/charts/RateByGroupBar.jsx b/src/charts/RateByGroupBar.jsx index c47d467..0c8a0fb 100644 --- a/src/charts/RateByGroupBar.jsx +++ b/src/charts/RateByGroupBar.jsx @@ -2,6 +2,7 @@ import ChartLegend from '../components/ChartLegend.jsx' import ApproxNote from '../components/ApproxNote.jsx' import { raceColor, OBSERVED_MARK_COLOR, SHORT_RACE_LABEL } from '../utils/colors.js' import { fitSkewedInterval } from '../utils/distributionApprox.js' +import { groupKey, hasDrawsForAll } from '../utils/drawGroups.js' import { quantile } from '../utils/kde.js' import { niceTicks } from '../utils/niceTicks.js' import { useDrawDistribution } from '../hooks/useDrawDistribution.js' @@ -34,7 +35,18 @@ function buildBox(d, draws) { export default function RateByGroupBar({ data, leaid, state }) { const groups = data.map((d) => ({ race: d.race, sex: d.sex, stuEnroll: d.enrollment })) - const { status, drawsByGroup } = useDrawDistribution({ leaid, state, model: MODEL, year: '21-22', groups }) + const { drawsByGroup } = useDrawDistribution({ leaid, state, model: MODEL, year: '21-22', groups }) + + // Only the RACE_ORDER × SEX_PANELS cells are actually drawn, so coverage is + // judged against those — not against everything the API returned. The hook's + // 'ready' status is an any-group signal and would over-claim here: individual + // boxes still fall back to fitSkewedInterval whenever their group is missing + // from the shard. A null map (loading, or the whole fetch failed) fails this + // check too, so the error path still shows the note. + const renderedGroups = data.filter( + (d) => RACE_ORDER.includes(d.race) && SEX_PANELS.some((p) => p.sex === d.sex), + ) + const allGroupsHaveDraws = hasDrawsForAll(drawsByGroup, renderedGroups) const maxRate = Math.max( ...data.map((d) => Math.max(d.observedRate, d.rateUpper ?? d.modeledMedian ?? 0)), @@ -47,7 +59,7 @@ export default function RateByGroupBar({ data, leaid, state }) {

Arrest rate by student group — 2021–22 (per 1,000)

- {status !== 'ready' && } + {!allGroupsHaveDraws && }
{SEX_PANELS.map(({ sex, label }) => ( @@ -70,7 +82,7 @@ export default function RateByGroupBar({ data, leaid, state }) { ]} />

- {status === 'ready' + {allGroupsHaveDraws ? "Box = 25th–75th percentile of 500 real posterior draws; whisker = the model's reported 90% interval; white tick = median." : "Box = modeled 25th–75th percentile (fitted approximation); whisker = the model's reported 90% interval; white tick = median."} {' '}The dark diamond is the observed rate. @@ -115,7 +127,7 @@ function SexPanel({ label, rows, drawsByGroup, ticks, niceMax }) { const boxTop = midY - ROW_HEIGHT * 0.26 const boxBottom = midY + ROW_HEIGHT * 0.26 const color = raceColor(d.race) - const draws = drawsByGroup?.[`${d.race}_${d.sex}`] + const draws = drawsByGroup?.[groupKey(d.race, d.sex)] const box = buildBox(d, draws) const observedX = xScale(d.observedRate) diff --git a/src/charts/RateDensityRidgeline.jsx b/src/charts/RateDensityRidgeline.jsx index a6abe7b..9e79910 100644 --- a/src/charts/RateDensityRidgeline.jsx +++ b/src/charts/RateDensityRidgeline.jsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { MODEL_QUADRANTS } from '../hooks/useApi.js' import { raceColor, OBSERVED_MARK_COLOR, RACE_LABELS, SHORT_RACE_LABEL } from '../utils/colors.js' import { fitSkewedInterval, densityCurve } from '../utils/distributionApprox.js' +import { groupKey, hasDrawsForAll } from '../utils/drawGroups.js' import { kdeCurve } from '../utils/kde.js' import { niceTicks } from '../utils/niceTicks.js' import { useDrawDistribution } from '../hooks/useDrawDistribution.js' @@ -39,7 +40,18 @@ export default function RateDensityRidgeline({ quadData, leaid, state }) { const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL) const rows = (quadData && quadData[selectedModel]) || [] const groups = rows.map((r) => ({ race: r.race, sex: r.sex, stuEnroll: r.stu_enroll || 0 })) - const { status, drawsByGroup } = useDrawDistribution({ leaid, state, model: selectedModel, year: '21-22', groups }) + const { drawsByGroup } = useDrawDistribution({ leaid, state, model: selectedModel, year: '21-22', groups }) + + // Only the RACE_ORDER × SEX_COLUMNS cells get a ridge, so coverage is judged + // against those. The hook's 'ready' status is an any-group signal: individual + // ridges still fall back to densityCurve when their group is missing from the + // shard, so gating the note on `status` alone would hide it while part of the + // chart is an approximation. A null map (loading or a failed fetch) also fails + // this check, so the error path still shows the note. + const renderedGroups = rows.filter( + (r) => RACE_ORDER.includes(r.race) && SEX_COLUMNS.some((c) => c.sex === r.sex), + ) + const allGroupsHaveDraws = hasDrawsForAll(drawsByGroup, renderedGroups) const modelSelect = (