fix: rank district suggestions over the full state, not the first 500 rows
DistrictSearch built its "most arrests" list from /estimates?state=XX&year=21-22&limit=500. That endpoint returns rows ORDER BY LEAID, RACE, SEX at eight rows per district, so a 500-row cap is not a sample of the state — it is the ~62 lowest-LEAID districts in it. Measured against California (11,488 rows, 1,715 districts): the old read covered 68 districts, and 6 of the true top 8 were invisible to it. It suggested districts with 1 and 2 arrests as the state's most notable, while San Diego Unified (178), Fresno Unified (77) and Kern High (69) never appeared. Replaced with a committed fixture, public/data/top_districts.json, generated by scripts/build-top-districts.mjs. The script pages each state to completion using meta.total from the response envelope and fails loudly on a short read, since a silent truncation there would reintroduce exactly this bug. 51 states, 135 requests, ~104KB, following the national_rates.json precedent. Re-run it only when a new CRDC wave lands. The search screen also loses a multi-second fetch on every visit, and the hardcoded "Try Derby (KS), Paterson (NJ)" hint goes with it — the real list supersedes it. Degrades to search-only if the fixture is missing. fetchStateDistricts() is kept for scripts and ad-hoc use, with its JSDoc now warning that any short read ranks by LEAID. pages.yml gains public/** in its paths filter: the fixture ships with the build, so regenerating it has to be able to trigger a deploy on its own.
This commit is contained in:
@@ -1,10 +1,42 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { searchDistricts, fetchStateDistricts } from '../hooks/useApi.js'
|
||||
import { searchDistricts } from '../hooks/useApi.js'
|
||||
|
||||
const TOP_DISTRICTS_URL = `${import.meta.env.BASE_URL}data/top_districts.json`
|
||||
const SUGGESTION_COUNT = 8
|
||||
|
||||
// Module-level cache: the fixture covers every state, so it is fetched at most
|
||||
// once per page load no matter how many states the visitor browses through.
|
||||
let topDistrictsPromise = null
|
||||
|
||||
function loadTopDistricts() {
|
||||
if (!topDistrictsPromise) {
|
||||
topDistrictsPromise = fetch(TOP_DISTRICTS_URL)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} loading ${TOP_DISTRICTS_URL}`)
|
||||
return res.json()
|
||||
})
|
||||
.catch((err) => {
|
||||
// Don't poison the cache with a rejected promise — let a later visit retry.
|
||||
topDistrictsPromise = null
|
||||
throw err
|
||||
})
|
||||
}
|
||||
return topDistrictsPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* District search screen with:
|
||||
* 1. "Interesting" suggestions — districts with the most arrests in the selected state (fetched once)
|
||||
* 1. "Interesting" suggestions — the districts with the most arrests in the
|
||||
* selected state, read from the committed `public/data/top_districts.json`
|
||||
* fixture (built by `scripts/build-top-districts.mjs`).
|
||||
* 2. Live-search as you type → /api/v1/districts?q=...&state=XX
|
||||
*
|
||||
* The suggestions used to be computed at runtime from
|
||||
* `/estimates?state=XX&year=21-22&limit=500`. That endpoint returns rows
|
||||
* `ORDER BY LEAID, RACE, SEX` at eight rows per district, so the cap selected
|
||||
* the ~62 lowest-LEAID districts in the state rather than the busiest ones —
|
||||
* California has 11,488 rows. The fixture is ranked over the complete result
|
||||
* set, and it also removes a multi-second fetch from this screen.
|
||||
*/
|
||||
export default function DistrictSearch({ state, onSelect, onBack }) {
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -13,38 +45,32 @@ export default function DistrictSearch({ state, onSelect, onBack }) {
|
||||
const [loadingSugg, setLoadingSugg] = useState(true)
|
||||
const [loadingSearch, setLoadingSearch] = useState(false)
|
||||
|
||||
// ——— Fetch interesting suggestions (top arrests in this state) once on mount ———
|
||||
// ——— Read the suggestion fixture for this state ———
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function loadSuggestions() {
|
||||
try {
|
||||
// /estimates?state=XX&year=21-22 returns all districts; sort by observed_arrests desc client-side
|
||||
const data = await fetchStateDistricts(state, '21-22', 500)
|
||||
setLoadingSugg(true)
|
||||
|
||||
// Aggregate arrests per district (sum across race×sex groups), then sort
|
||||
const byLeaid = {}
|
||||
for (const row of data) {
|
||||
if (!byLeaid[row.leaid]) {
|
||||
byLeaid[row.leaid] = { leaid: row.leaid, lea_name: row.lea_name, state: row.state, observed_arrests: 0 }
|
||||
}
|
||||
byLeaid[row.leaid].observed_arrests += (row.observed_arrests || 0)
|
||||
}
|
||||
|
||||
const sorted = Object.values(byLeaid).sort((a, b) => b.observed_arrests - a.observed_arrests)
|
||||
|
||||
if (!cancelled) {
|
||||
// Take top 8 for suggestions; filter to those with >0 arrests
|
||||
setSuggestions(sorted.filter(d => d.observed_arrests > 0).slice(0, 8))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load interesting districts:', err)
|
||||
loadTopDistricts()
|
||||
.then((fixture) => {
|
||||
if (cancelled) return
|
||||
const forState = fixture?.states?.[state] || []
|
||||
setSuggestions(
|
||||
forState.slice(0, SUGGESTION_COUNT).map((d) => ({
|
||||
leaid: d.leaid,
|
||||
lea_name: d.name,
|
||||
state,
|
||||
observed_arrests: d.arrests,
|
||||
})),
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to load suggested districts:', err)
|
||||
if (!cancelled) setSuggestions([]) // degrade gracefully — just show search box
|
||||
} finally {
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoadingSugg(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
loadSuggestions()
|
||||
return () => { cancelled = true }
|
||||
}, [state])
|
||||
|
||||
@@ -158,11 +184,6 @@ export default function DistrictSearch({ state, onSelect, onBack }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hint */}
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--cv-ink-3)', marginTop: 'var(--space-4)' }}>
|
||||
Tip: Try districts like Derby (KS), Paterson (NJ), or Mobile County (AL) — they have notable arrest rates.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user