open-navigator / web_app /src /utils /censusDataDictionary.ts
jcbowyer's picture
Clean HuggingFace deployment without binary files
e59d91d
Raw
History Blame Contribute Delete
15.2 kB
import type { CensusValueMode } from './censusMapValueMode'
/**
* Short ACS-style descriptions for census map UI tooltips (aligned with common subject-table wording).
* Keys are metric slugs from ``manifest.json``; fallback copy is provided for unknown slugs.
*/
/** Plain-English entry points for the census map metric picker (non-technical framing). */
export const CENSUS_METRIC_EXPLORE_QUESTIONS: Record<string, string> = {
median_household_income: 'How much do households earn?',
median_home_value: 'What are typical home values?',
median_gross_rent: 'What do renters pay?',
per_capita_income: 'How much income per person?',
total_population: 'Where do more people live?',
median_age: 'How old is the population?',
gini_income_inequality: 'How unequal is income?',
median_gross_rent_pct_hhincome: 'How heavy is rent compared to income?',
travel_time_to_work_minutes: 'How long are commutes?',
housing_units: 'Where are there more housing units?',
poverty_universe: 'What population is used for poverty stats?',
labor_force: 'How large is the labor force?',
sex_by_age_table_total: 'How large is the sex-by-age table total?',
race_table_total: 'How large is the race table total?',
hispanic_latino_by_race_total: 'How large is the Hispanic/Latino-by-race table total?',
population_income_below_poverty_level: 'How many people are below the poverty line?',
employed_civilian: 'How many people are employed (civilian 16+)?',
unemployed_civilian: 'How many people are unemployed (civilian 16+)?',
health_insurance_civilian_noninstitutional_total: 'How large is the health insurance coverage universe?',
health_insurance_under19_table_total: 'How large is the under-19 health insurance table total?',
population_25_and_over_education_universe: 'How many adults 25+ are in the education table?',
school_enrollment_total: 'How many people are enrolled in school?',
}
/**
* Table-total rows that duplicate population or universe counts — omitted from map metric pickers
* (still present in ``manifest.json`` and payloads for compatibility).
*/
export const CENSUS_EXPLORER_HIDDEN_METRIC_SLUGS: ReadonlySet<string> = new Set([
'sex_by_age_table_total',
'race_table_total',
'hispanic_latino_by_race_total',
])
export function censusMetricExploreQuestion(slug: string, label: string): string {
const q = CENSUS_METRIC_EXPLORE_QUESTIONS[slug]
if (q) return q
const trimmed = label.trim()
if (trimmed) return `How does ${trimmed} compare across places?`
return 'What do you want to explore on the map?'
}
export type CensusChoroLegendSemantics = {
/** Left side of the ramp (conceptual “low” end). */
lowEnd: string
/** Right side of the ramp (conceptual “high” end). */
highEnd: string
/** One short sentence tying color intensity to the metric. */
gradientHint: string
}
/** Human-readable legend copy so users map color intensity to meaning (depends on value mode). */
export function censusChoroLegendSemantics(
slug: string,
valueMode: CensusValueMode,
metricLabel: string,
): CensusChoroLegendSemantics {
const ml = metricLabel.trim() || slug.replace(/_/g, ' ')
if (valueMode === 'yoy') {
return {
lowEnd: 'Larger decrease',
highEnd: 'Larger increase',
gradientHint: 'Darker colors = larger increase vs the prior year in the year list (lighter = larger decrease).',
}
}
if (valueMode === 'vs_natl') {
return {
lowEnd: 'Further below U.S.',
highEnd: 'Further above U.S.',
gradientHint: 'Darker colors = further above the national benchmark for this metric (when available).',
}
}
if (slug === 'median_household_income') {
return {
lowEnd: 'Lower income',
highEnd: 'Higher income',
gradientHint: 'Darker colors = higher median household income on this scale.',
}
}
if (slug === 'median_home_value' || slug === 'median_gross_rent' || slug === 'per_capita_income') {
return {
lowEnd: `Lower ${ml.toLowerCase()}`,
highEnd: `Higher ${ml.toLowerCase()}`,
gradientHint: `Darker colors = higher ${ml.toLowerCase()} on this scale.`,
}
}
if (slug === 'median_gross_rent_pct_hhincome' && valueMode === 'raw') {
return {
lowEnd: 'Rent takes a smaller bite of income',
highEnd: 'Rent takes a larger bite of income',
gradientHint:
'Each area is shaded by the published median rent-to-income ratio for renter households paying cash rent (ACS table B25071). Those medians are almost always under 100%. Huge “%” values mean the map bundle was built with the wrong table column—re-export census static data.',
}
}
if (slug === 'gini_income_inequality') {
if (valueMode === 'raw') {
return {
lowEnd: 'More equal (lower Gini)',
highEnd: 'Less equal (higher Gini)',
gradientHint:
'Darker colors = higher inequality on this scale. The A–F row below ties the same 0–1 Gini index to those letters (lower is better).',
}
}
return {
lowEnd: `Lower ${ml.toLowerCase()}`,
highEnd: `Higher ${ml.toLowerCase()}`,
gradientHint: `Darker colors = higher ${ml.toLowerCase()} (not always “better”—read the metric notes).`,
}
}
if (slug === 'travel_time_to_work_minutes') {
return {
lowEnd: `Lower ${ml.toLowerCase()}`,
highEnd: `Higher ${ml.toLowerCase()}`,
gradientHint: `Darker colors = higher ${ml.toLowerCase()} (not always “better”—read the metric notes).`,
}
}
return {
lowEnd: 'Lower values',
highEnd: 'Higher values',
gradientHint: `Darker colors = higher ${ml.toLowerCase()} along the mapped range.`,
}
}
export const CENSUS_FIELD_HELP: Record<string, string> = {
median_household_income:
'Median household income in the past 12 months (inflation-adjusted dollars). Half of households earn more and half earn less; not comparable to per capita income.',
median_home_value:
'Median value of owner-occupied housing units. Based on respondents’ estimate of what the property would sell for, not tax assessment.',
median_gross_rent:
'Median gross rent including utilities (if paid) for renter-occupied units paying cash rent. Contract rent plus estimated average monthly cost of utilities and fuels.',
per_capita_income:
'Mean income computed for every man, woman, and child in the area. It is derived by dividing the total income of all people 15+ by the total population in that scope.',
total_population:
'Total population — count of all people living in the geography at the time of the ACS sample, including group quarters.',
median_age:
'Median age of all people in the geography. Half the population is older and half is younger.',
gini_income_inequality:
'Gini index of income inequality for households. 0 indicates perfect equality (everyone has the same income); 1 indicates maximum inequality.',
median_gross_rent_pct_hhincome:
'Median gross rent as a percent of household income for renter households paying cash rent (ACS table B25071). It is one median percentage for the area, not a sum across people. Typical state medians are often roughly 25–55%.',
travel_time_to_work_minutes:
'Mean travel time to work in minutes for workers 16+ who did not work from home (ACS subject table S0801). One-way usual commute.',
housing_units:
'Housing units — separate living quarters where people live or could live; includes occupied and vacant units.',
poverty_universe:
'Population for whom poverty status is determined — used as the denominator for poverty rate calculations in detailed tables.',
labor_force:
'Civilian labor force — people 16+ who are employed or unemployed and actively looked for work in the past four weeks.',
sex_by_age_table_total:
'Total row of ACS table B01001 (sex by age). Often close to total population; use B01003 for official population total.',
race_table_total: 'Total row of ACS table B02001 (race).',
hispanic_latino_by_race_total: 'Total row of ACS table B03002 (Hispanic or Latino origin by race).',
population_income_below_poverty_level:
'Count of people for whom poverty status is determined and whose income in the past 12 months was below the poverty threshold (B17001).',
employed_civilian: 'Civilian employed population age 16+ (B23025).',
unemployed_civilian: 'Civilian unemployed population age 16+ (B23025).',
health_insurance_civilian_noninstitutional_total:
'Civilian noninstitutionalized population — universe for detailed health insurance coverage (B27001).',
health_insurance_under19_table_total:
'Total row for ACS table B27010 (insurance coverage by age for children / under 19; exact universe follows Census labels for your vintage).',
population_25_and_over_education_universe:
'Population 25 years and over — universe for detailed educational attainment (B15003).',
school_enrollment_total: 'Population enrolled in school — total from school enrollment by age (B14001).',
}
export function censusMetricHelpText(slug: string, label: string): string {
return CENSUS_FIELD_HELP[slug] ?? `${label}: ACS 5-year estimate for this geography. See Census Data API / subject definitions for the underlying table.`
}
/** How “top” / winner rows rank this metric (drives default sort and bar order). */
export type CensusMetricRankDirection = 'higher' | 'lower' | 'neutral'
export const CENSUS_METRIC_RANK_DIRECTION: Record<string, CensusMetricRankDirection> = {
median_household_income: 'higher',
median_home_value: 'higher',
median_gross_rent: 'neutral',
per_capita_income: 'higher',
total_population: 'neutral',
median_age: 'neutral',
gini_income_inequality: 'lower',
median_gross_rent_pct_hhincome: 'lower',
travel_time_to_work_minutes: 'lower',
housing_units: 'neutral',
poverty_universe: 'neutral',
labor_force: 'higher',
sex_by_age_table_total: 'neutral',
race_table_total: 'neutral',
hispanic_latino_by_race_total: 'neutral',
population_income_below_poverty_level: 'lower',
employed_civilian: 'higher',
unemployed_civilian: 'lower',
health_insurance_civilian_noninstitutional_total: 'neutral',
health_insurance_under19_table_total: 'neutral',
population_25_and_over_education_universe: 'neutral',
school_enrollment_total: 'neutral',
}
export function censusMetricRankDirection(slug: string): CensusMetricRankDirection {
return CENSUS_METRIC_RANK_DIRECTION[slug] ?? 'neutral'
}
/**
* Detects obviously wrong **raw** bundles (e.g. wrong ACS column in older exports) so we can warn instead of
* letting users interpret garbage numbers.
*/
export function censusMetricStaleDataNote(
slug: string,
valueMode: CensusValueMode,
rawValues: (number | null | undefined)[],
): string | null {
if (valueMode !== 'raw') return null
const nums = rawValues.filter((x): x is number => typeof x === 'number' && Number.isFinite(x))
if (nums.length < 8) return null
if (slug === 'median_gross_rent_pct_hhincome') {
const hi = Math.max(...nums)
if (hi > 100) {
return 'Rent-to-income values above 100% usually mean this bundle was built from the wrong Census column (B25070 “total renters” instead of B25071 “median rent % of income”). From the repo root, re-download B25071 parquets and run .venv/bin/python scripts/datasources/census/export_census_map_static.py for your year(s), then hard-refresh the app.'
}
}
if (slug === 'travel_time_to_work_minutes') {
const hi = Math.max(...nums)
if (hi > 240) {
return 'Commute “times” in the hundreds of hours usually mean worker counts were mistaken for minutes (wrong S0801 column in an older export). Re-download S0801 and re-run export_census_map_static.py with the current script, then hard-refresh.'
}
}
return null
}
/** Sort comparator so “better” values sort first (top of bar list). */
export function compareRankedMetricValues(a: number, b: number, slug: string): number {
return censusMetricRankDirection(slug) === 'lower' ? a - b : b - a
}
/** One-line explanation under the winner callout (keep short — sits under “#1 for this metric”). */
export function censusMetricWinnerCaption(slug: string): string {
const d = censusMetricRankDirection(slug)
if (d === 'higher') return 'Higher values rank first.'
if (d === 'lower') return 'Lower values rank first.'
return 'Ordered by largest value first.'
}
/** Tooltip / aria text: data dictionary + table + how rankings interpret the metric. */
export function censusMetricFullHelp(
slug: string,
meta: { label: string; table?: string } | undefined,
): string {
const label = meta?.label ?? slug
const base = censusMetricHelpText(slug, label)
const tbl = meta?.table ? `\n\nACS summary table: ${meta.table}.` : ''
const d = censusMetricRankDirection(slug)
const rank =
d === 'higher'
? '\n\nRanking: higher values are treated as more favorable. The top bar list and winner use that order (largest first).'
: d === 'lower'
? '\n\nRanking: lower values are treated as more favorable. The top bar list and winner use that order (smallest first).'
: '\n\nRanking: the leaderboard orders by largest displayed value first. This metric has no built-in “higher or lower is always better” rule—use context when comparing places.'
return `${base}${tbl}${rank}`
}
/** Slugs where we omit the “Census label:” line under the map picker (universe / table totals; redundant with the dropdown). */
const CENSUS_MAP_SLUGS_HIDE_OFFICIAL_LABEL = new Set([
'population_25_and_over_education_universe',
'health_insurance_under19_table_total',
'health_insurance_civilian_noninstitutional_total',
'poverty_universe',
])
export function censusMapShowOfficialCensusLabel(slug: string): boolean {
return !CENSUS_MAP_SLUGS_HIDE_OFFICIAL_LABEL.has(slug)
}
export const CENSUS_MAP_UI_HELP = {
year: `The selected year is the end year of the ACS 5-year period. Each estimate pools five consecutive years of responses, so it is labeled by the latest year in that window (not a single calendar year snapshot).`,
metric:
'Pick the question you want to answer on the map. Each topic uses published Census ACS tables — open the (i) next to the picker for definitions.',
vizFilled: 'Choropleth colors each region by the mapped value using the selected color transform.',
vizBubble: 'Bubble map shows the same value as circle area at each region’s centroid; color also reflects magnitude.',
scale: 'Nonlinear scales spread or compress the numeric range before mapping to color or bubble size (useful for skewed metrics).',
mapValue:
'Raw uses the published estimate. % change vs prior year compares to the previous year in the year slider order. % vs national compares to a U.S. or population-weighted benchmark when exported.',
play: 'Plays once through each year (oldest to newest) using the same metric and view, then stops.',
allGeographiesTable:
'Sortable list for the selected year and map value mode; numbers match the map. Click a row where supported to drill down.',
} as const