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 |