An R Shiny application for exploring Affirmatively Furthering Fair Housing (AFFH) data with interactive maps and statistics.
For a walkthrough of the dashboard's features and controls, see the User Guide.
The dashboard provides interactive exploration of AFFH demographic indicators and opportunity metrics at State, County, and Census Tract levels.
Maps
- Multi-geography choropleth maps with automatic zoom and basemap label overlay (city/road names rendered above polygons)
- R/ECAP tract overlay highlighting with red dashed borders (Tract level only)
- Address search via US Census Bureau geocoder (no API key required); red circle marker persists after search bar collapses
- PNG map export via html2canvas with non-blocking toast notification on capture failure
- Ghosted previous-state overlay during geography transitions (30% opacity, still pan/zoomable)
- Fullscreen mode with
invalidateSizeon exit to recalculate Leaflet dimensions
Selection and Navigation
- Cascading dropdown selectors (Geography β Focus Area β Category β Variable) with step progress indicator
- Interactive tooltips with context-aware formatting (area name, variable label, value)
- Map click populates all downstream outputs (value boxes, charts, tables)
Charts
- Time-series bar charts with parent-geography comparison overlays (up to 3 levels at Tract: tract, county, state)
- Category comparison horizontal bar charts for race/ethnicity-disaggregated variables
Tables
- Accordion-style indicator tables organized by category, with year-range column headers (e.g., "2009-2012")
- Table filter toggle to isolate the selected variable's category and row
- Automatic dropping of all-NA year columns
- CSV download combining all categories for the clicked area
Data Notes
- Sources and methodology notes accordion with pulse animation on variable change
- Value boxes with parent-geography benchmark comparisons
# Load package code and data
devtools::load_all()
# Run the application
shiny::runApp()
# Run tests
devtools::test()
# Regenerate internal data objects (if needed)
source("data-raw/data_prep.R")R/
βββ affh_shinyApp.R # Main app entry point (Data Dashboard + User Guide)
βββ server.R # Server logic (reactives, observers, outputs)
βββ config.R # Data loading and configuration
βββ sidebar.R # Geography/variable selection UI
βββ summary_stats_panel.R # Indicator tables UI
βββ map_helpers.R # Leaflet proxy update functions
βββ map_output.R # Initial leaflet render
βββ data_helpers.R # Pure data transformation functions
βββ comparison_data.R # Comparison benchmark data
βββ geography_names.R # FIPS-to-name conversion
βββ affh_transform.R # AFFH data pivoting/splitting
βββ load_affh_data.R # CSV/Parquet data loading with error handling
βββ get_fips_geometry.R # Geometry extraction by FIPS
βββ table.R # Reactable rendering (year-range labels, all-NA column dropping)
βββ step_progress.R # Step progress indicator builder
βββ barplot_output.R # Plotly bar charts (time series + category comparison)
βββ observe_selectize.R # Cascading dropdown helper
βββ text.R # UI text constants
βββ user_guide.R # User Guide page content builder
βββ notes_accordion.R # Sources accordion UI
βββ style_constants.R # Color and styling constants
βββ constants.R # App constants (TABLE_CONFIG, geographies)
data/
βββ final/ # AFFH metrics (CSV for State/County, Parquet for Tract)
βββ lookup/ # Variable and metric lookup tables
β βββ input_geo_var_lookup.csv # Maps variables β geographies and categories
β βββ metric_table_lookup.csv # Metric display configuration
β βββ tooltip_var_lookup.csv # Tooltip variable definitions
β βββ race_ethnicity_vars_lookup.csv # Race/ethnicity comparison groups
β βββ data_years_lookup.csv # Year β display label mapping for table headers
βββ counties_sf.parquet # County geometries
βββ tracts_sf.parquet # Tract geometries
βββ states_sf.rda # State geometries (shifted for national view)
βββ state_county_xwalk.rda # FIPS crosswalk table
tests/testthat/ # Unit tests (42 tests across 8 files)
www/
βββ map-loading.js # Client-side JS handlers (overlays, map capture, selectize)
βββ styles.css # Custom CSS (step progress, animations, map overlays)
docs/
βββ user-guide/ # End-user dashboard walkthrough
βββ testing/ # QA checklist and technical debt tracker
βββ audit/ # Codebase architecture audit (historical)
βββ prompts/ # Claude Code development prompts
βββ presentation/ # Project presentation (Quarto + reveal.js)
βββ blog/ # Blog post draft
βββ exploratory/ # Early exploratory notebooks
deploy.R # Deployment script (manifest generation + rsconnect deploy)
flowchart TB
subgraph UI["UI Layer"]
sidebar["sidebar.R<br/>Geography & Variable Selection"]
map_panel["affh_shinyApp.R<br/>Map + Value Box"]
stats_panel["summary_stats_panel.R<br/>Indicator Tables"]
end
subgraph Server["Server Layer (server.R)"]
subgraph Reactives["Reactives"]
geo_select["Geography Selection<br/>focus_geo_config, selected_poly"]
var_select["Variable Selection<br/>variable_choices, selected_metric_data"]
click_state["Click State<br/>click_id, clicked_leaflet_geo"]
data_pipe["Data Pipelines<br/>map_data, barplot_data, clicked_geo_data"]
end
subgraph Observers["Observers"]
obs["UI Updates<br/>observe_selectize, update_map"]
end
subgraph Outputs["Outputs"]
out["Render Functions<br/>map, tables, barplot, text"]
end
end
subgraph Helpers["Helper Functions"]
data_helpers["data_helpers.R<br/>prepare_barplot_data<br/>prepare_title_text"]
comparison["comparison_data.R<br/>prepare_comparison_data<br/>prepare_category_comparison_data"]
geo_names["geography_names.R<br/>get_geography_display_name"]
step_prog["step_progress.R<br/>build_step_progress_ui"]
map_helpers["map_helpers.R<br/>update_map"]
geo_helpers["get_fips_geometry.R<br/>get_fips_geometry"]
transform["affh_transform.R<br/>affh_transform"]
end
subgraph Data["Data Layer"]
config["config.R<br/>counties_sf, tracts_sf, states_sf"]
load["load_affh_data.R<br/>AFFH CSVs + Parquet"]
lookup["Lookup Tables<br/>input_geo_var_lookup<br/>metric_table_lookup<br/>race_ethnicity_vars_lookup<br/>data_years_lookup"]
end
sidebar --> geo_select
sidebar --> var_select
geo_select --> data_pipe
var_select --> data_pipe
click_state --> data_pipe
data_pipe --> obs
obs --> out
data_pipe --> data_helpers
data_pipe --> comparison
data_pipe --> transform
geo_select --> geo_helpers
config --> geo_helpers
load --> data_pipe
lookup --> transform
out --> map_panel
out --> stats_panel
flowchart LR
A[User selects geography] --> B[selected_poly]
B --> C[get_fips_geometry]
C --> D[counties_sf / tracts_sf]
E[User selects variable] --> F[selected_metric_data]
F --> G[load_affh_data]
B --> H[map_data]
F --> H
H --> J[update_map]
J --> K[Leaflet Proxy]
L[User clicks map] --> M[click_id]
M --> N[clicked_geo_data]
N --> O[affh_transform]
O --> P[Reactable Tables]
M --> Q[barplot_data]
Q --> R[prepare_barplot_data]
R --> S[Time-Series Chart]
M --> T[category_comparison_data]
T --> U[prepare_category_comparison_data]
U --> V[Category Comparison Chart]
- Geography Selection: User picks State/County/Tract β triggers geometry load
- Variable Selection: Cascading dropdowns filter available variables
- Map Click: Clicking a polygon triggers data table and chart updates
- Proxy Updates: Map redraws via
leafletProxy()without full re-render
All FIPS codes are stored as character strings with leading zeros: - State: 2 digits ("01" for Alabama) - County: 5 digits ("01001" for Autauga County) - Tract: 11 digits ("01001020100")
- UI builders:
build_*()functions return bslib/htmltools objects - Pure helpers:
prepare_*()functions indata_helpers.Rare testable without Shiny - Internal helpers:
.function_name()convention for non-exported functions
| Package | Purpose |
|---|---|
| bslib | Bootstrap 5 UI components |
| leaflet | Interactive maps |
| plotly | Interactive charts |
| reactable | Interactive tables |
| data.table | Fast data filtering |
| sf | Spatial data handling |
| arrow | Parquet I/O |
| logger | Console logging |
| Service | Usage | API Key Required |
|---|---|---|
| US Census Bureau Geocoder | Address search via leaflet.extras |
No |
| CARTO Voyager raster tiles | Basemap tiles (labels + no-labels layers) | Yes (free, see Configuration) |
| html2canvas CDN | PNG map export in browser | No |
| Census TIGER/Line | Geometry regeneration (data-raw/ scripts only) |
No |
- Geographic boundaries: Census TIGER/Line via
tigrispackage - AFFH metrics: Pre-processed CSVs and Parquet files in
data/final/
| Variable | Purpose |
|---|---|
CARTO_API_KEY |
Basemap tile key. Since August 2026 CARTO watermarks keyless raster tile requests with "API KEY REQUIRED". Request a free key (5M tiles/month) at https://carto.com/basemaps/apikey. |
Copy .Renviron.example to .Renviron in the project root and fill in the key. The file is gitignored, is read explicitly by app.R at startup, and is bundled into the deployment by deploy.R. The key is visible in browser tile requests by design, so rely on CARTO's referrer restrictions and quota rather than secrecy. Tile URLs are built in .add_carto_tiles() (R/map_helpers.R) using the ?key= query parameter.
| File | What it controls |
|---|---|
R/config.R |
Data file paths, lookup table loading, lazy geometry caching |
R/style_constants.R |
Brand color palette, map styling, chart colors, table styling |
R/constants.R |
TABLE_CONFIG (table output IDs/titles), EXCLUDED_STATE_FIPS (territory filtering) |
R/text.R |
UI labels and display text |
The app is deployed to shinyapps.io (urban-main account, app nfha-fair-housing-tool) using rsconnect. The deploy.R script is the single entry point: it defines the explicit list of app files, refreshes the file list in manifest.json, and calls rsconnect::deployApp(manifestPath = "manifest.json").
# Prerequisite: .Renviron with CARTO_API_KEY exists locally (see Configuration)
source("deploy.R")When R source files, data files, or web assets are added or removed, update the app_files vector in deploy.R to match.
Package versions are pinned in the committed manifest.json (R 4.4.0 package set) rather than snapshotted from the local library on each deploy. A snapshot taken from R 4.6 pulled in terra 1.9-x, which fails to compile against the GDAL version on the shinyapps.io build image, and the older terra 1.8-86 in turn fails to compile against a current Homebrew GDAL locally. Deploying from the pinned manifest sidesteps both. Only set regenerate_manifest <- TRUE in deploy.R when a new package dependency is genuinely required, and expect to resolve server build failures if you do.
- Update CSV/Parquet files in
data/final/ - If lookup tables change:
source("data-raw/generate_lookup_tables.R") - If Census boundaries change:
source("data-raw/data_prep.R") - Clear memoization caches:
clear_affh_cache() - Run tests:
devtools::test() - Deploy:
source("deploy.R")
| Document | Path | Description |
|---|---|---|
| User Guide | docs/user-guide/ |
Dashboard walkthrough for end users |
| User Testing Guide | docs/testing/ |
68-test manual QA checklist |
| Technical Debt | docs/testing/ |
Flagged items for future improvement |
| Codebase Audit | docs/audit/ |
Architecture audit (historical, pre-cleanup) |
| Claude Code Prompts | docs/prompts/ |
Reusable prompts used during development |
- Loading overlay timeout: The MutationObserver intended to detect Leaflet render completion doesn't fire; the loading overlay always falls back to a 3-second timeout. See
www/map-loading.js. - Connecticut geometries: Uses 2021-vintage Census geometries because the 2022 vintage replaced CT's 8 historical counties with 9 planning regions that don't match the AFFH data's FIPS codes. See
data-raw/data_prep.R. - Data load errors:
load_affh_data()wraps file reads intryCatchand shows a red toast notification on failure instead of surfacing raw R errors.
| Symptom | Cause | Fix |
|---|---|---|
| White map after tab switch | Stale Leaflet container dimensions | Already fixed β invalidateSize() called before fitBounds() on tab return |
| Wrong county names for Connecticut | 2022+ Census vintage returns planning regions | data_prep.R fetches year = 2021 for CT; re-run if geometries were regenerated without this |
| Map zooms to entire US instead of selected state | Stale bounding box after data.table subset |
rebuild_sf_bbox() reconstructs the sfc column; already applied in geometry pipeline |
| Variable not appearing in dropdown | Missing from lookup table | Add entry to data/lookup/input_geo_var_lookup.csv |
| Data not updating after file change | Memoization cache serving stale results | Run clear_affh_cache() |
| "API KEY REQUIRED" watermark on basemap | CARTO_API_KEY missing at deploy time, or browser cached old tiles |
Ensure .Renviron exists locally with the key, redeploy, hard-refresh the browser |
Deploy fails building terra on the server |
manifest.json regenerated from a newer local R library |
Restore the committed manifest.json and keep regenerate_manifest <- FALSE in deploy.R |