diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d8b83df --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1 @@ +package-lock.json diff --git a/frontend/app/api/populace/route.ts b/frontend/app/api/populace/route.ts index fe72978..10f3b82 100644 --- a/frontend/app/api/populace/route.ts +++ b/frontend/app/api/populace/route.ts @@ -39,6 +39,7 @@ export async function GET(request: Request) { { name: "build_manifest", path: `${prefix}/build_manifest.json`, url: hfResolveUrl(`${prefix}/build_manifest.json`, country) }, { name: "release_manifest", path: `${prefix}/release_manifest.json`, url: hfResolveUrl(`${prefix}/release_manifest.json`, country) }, { name: "calibration_diagnostics", path: `${prefix}/calibration_diagnostics.json`, url: hfResolveUrl(`${prefix}/calibration_diagnostics.json`, country) }, + { name: "demographics", path: `${prefix}/demographics.json`, url: hfResolveUrl(`${prefix}/demographics.json`, country) }, ], limitations: [ `Everything on this page is read live from the ${populaceRepo(country)} Hugging Face dataset; the current release is resolved through latest.json.`, diff --git a/frontend/components/populace/populace-overview-view.tsx b/frontend/components/populace/populace-overview-view.tsx index dfbc520..e5b0925 100644 --- a/frontend/components/populace/populace-overview-view.tsx +++ b/frontend/components/populace/populace-overview-view.tsx @@ -18,6 +18,7 @@ import { usePopulaceReleases, usePopulaceTargetTreemap, } from "@/lib/api/hooks/use-populace"; +import type { GeographyCoverageBlock } from "@/lib/api/hooks/use-populace"; function formatPublishedAt(value: string | null | undefined): string { if (!value) return "—"; @@ -176,6 +177,8 @@ export function PopulaceOverviewView() { + + ); } + +/** Household-record counts by geography — the release's sub-national + * resolution floor. Records, not weights: no calibration can rescue a + * geography with too few underlying records (48 districts under 50 in the + * 2026-07 national-only release blocked district features downstream). */ +function GeographyCoverageSection({ + coverage, +}: { + coverage: { + unit?: string; + states?: GeographyCoverageBlock | null; + congressional_districts?: GeographyCoverageBlock | null; + } | null; +}) { + const districts = coverage?.congressional_districts ?? null; + const states = coverage?.states ?? null; + if (!districts && !states) return null; + const under50 = districts?.n_under_50 ?? null; + const thinnest = districts?.counts + ? Object.entries(districts.counts) + .sort((a, b) => a[1] - b[1]) + .slice(0, 10) + : []; + return ( + +
+ + + + } + value={under50 == null ? "—" : fmt(under50, { digits: 0 })} + delta={under50 == null ? undefined : under50 === 0 ? "district-ready" : "blocks district analysis"} + tone={under50 == null ? undefined : under50 === 0 ? "positive" : "negative"} + /> + +
+ {thinnest.length > 0 && (under50 ?? 0) > 0 && ( +
+
+ Thinnest districts +
+
+ {thinnest.map(([district, count]) => ( + + {district}: {count} + + ))} +
+
+ )} +
+ ); +} diff --git a/frontend/lib/api/hooks/use-populace.ts b/frontend/lib/api/hooks/use-populace.ts index 3168c7d..44954af 100644 --- a/frontend/lib/api/hooks/use-populace.ts +++ b/frontend/lib/api/hooks/use-populace.ts @@ -149,6 +149,16 @@ export interface PopulaceFamilyFitRow { mean_abs_relative_error: number | null; } +export interface GeographyCoverageBlock { + n_geographies?: number | null; + household_records_min?: number | null; + household_records_median?: number | null; + household_records_max?: number | null; + n_under_50?: number | null; + n_under_100?: number | null; + counts?: Record; +} + export interface PopulaceCalibration { available: boolean; path?: string | null; @@ -159,6 +169,11 @@ export interface PopulaceCalibration { l0_lambda?: number | null; n_nonzero?: number | null; n_records?: number | null; + geography_coverage?: { + unit?: string; + states?: GeographyCoverageBlock | null; + congressional_districts?: GeographyCoverageBlock | null; + } | null; initial_loss?: number | null; final_loss?: number | null; loss_kind?: "normalized_target_loss" | "raw_optimizer_objective"; diff --git a/frontend/lib/populace/latest-artifact.ts b/frontend/lib/populace/latest-artifact.ts index 58086ae..02953f1 100644 --- a/frontend/lib/populace/latest-artifact.ts +++ b/frontend/lib/populace/latest-artifact.ts @@ -1463,6 +1463,10 @@ export interface Calibration { included_target_count: number; build_manifest: JsonObject; release_manifest: JsonObject; + // demographics.json geography_coverage: unweighted household-record counts + // by state / congressional district (the release's sub-national resolution + // floor). Null for releases published before the section existed. + geography_coverage: JsonObject | null; rows: TargetRow[]; } @@ -1499,6 +1503,7 @@ export function buildCalibration( updatedAt: string | null = null, buildManifest: JsonObject = {}, releaseManifest: JsonObject = {}, + demographics: JsonObject = {}, ): Calibration { const targets = Array.isArray(diag.targets) ? (diag.targets as TargetRow[]) : []; const skipped = Array.isArray(diag.skipped) ? (diag.skipped as JsonObject[]) : []; @@ -1536,6 +1541,9 @@ export function buildCalibration( included_target_count: includedTargetCount, build_manifest: buildManifest, release_manifest: releaseManifest, + geography_coverage: Object.keys(asObject(demographics.geography_coverage)).length + ? asObject(demographics.geography_coverage) + : null, rows, }; } @@ -1674,12 +1682,13 @@ async function loadReleaseUncached( updatedAt = ptr.updated_at; } const prefix = `releases/${id}`; - const [diag, buildManifest, releaseManifest] = await Promise.all([ + const [diag, buildManifest, releaseManifest, demographics] = await Promise.all([ hfJson(hfResolveUrl(`${prefix}/calibration_diagnostics.json`, country), revalidate), hfJson(hfResolveUrl(`${prefix}/build_manifest.json`, country), revalidate).catch(() => ({})), hfJson(hfResolveUrl(`${prefix}/release_manifest.json`, country), revalidate).catch(() => ({})), + hfJson(hfResolveUrl(`${prefix}/demographics.json`, country), revalidate).catch(() => ({})), ]); - return buildCalibration(diag, id, updatedAt, buildManifest, releaseManifest); + return buildCalibration(diag, id, updatedAt, buildManifest, releaseManifest, demographics); } function targetDiagnosticsMetadata(rows: TargetRow[]): TargetDiagnosticsMetadata { @@ -1938,6 +1947,7 @@ export function latestPopulaceCalibrationSummary(cal: Calibration) { total_targets: cal.rows.length, within_tolerance_count: withinToleranceCount(cal.rows), family_fit: familyFitSummary(cal.rows), + geography_coverage: cal.geography_coverage, }; }