Flood Risk Intelligence Dashboard
A Design Engineering Case Study
Executive Summary
This is a single-file, interactive flood-risk dashboard covering ZIP 32257 (Mandarin, Jacksonville FL) and four surrounding Duval County ZIPs. It's a production-quality, fully responsive tool built as one HTML file (no build step, no framework, no backend), yet it pulls live FEMA hazard data, coordinates three charts, and compares a five-ZIP portfolio.
It has four views: Overview (live data with coordinated charts), Portfolio (five-ZIP comparison), Flood Lookup (live ArcGIS REST queries), and Methodology (FEMA zone classification). The whole thing was built and iterated through an AI-native, bootstrap-file-driven workflow with Claude Code. I built it to demonstrate design-engineering depth, that workflow, and a direct line to the climate-risk intelligence space. It runs at the live demo.
Problem Statement
Flood-risk data is public, but it isn't accessible. FEMA's National Flood Hazard Layer lives behind ArcGIS REST APIs that assume GIS expertise. You need to know which layer to query, how to resolve a place to a polygon, and how to read the zone codes that come back.
The people who make decisions (homebuyers, agents, portfolio managers, lenders) have no clear visual language for differentiating risk. A AE, X-500, and X zone carry meaningfully different profiles: insurance requirements, premium costs, and lender mandates all change between them. FEMA's Map Service Center is accurate and functional, but it's built for lookups, not for weighing options.
The national property-data position in this market is strong; the gap is the intelligence layer, making risk legible at the property and portfolio level. This dashboard is a prototype of that layer.
Technical Architecture
Data layer
Hazard geometry comes from a live ArcGIS REST endpoint (FEMA's National Flood Hazard Layer, Layer 28), with the U.S. Census Geocoder resolving an address into the FIPS code that scopes a live lookup. A seeded dataset of 20 properties in ZIP 32257 (risk scores, zone classifications, pricing, square footage) backs the property views, and the tool falls back to seeded NFHL geography when the live ArcGIS fetch is unavailable, so it never dead-ends. The portfolio spans five ZIPs: 32257 Mandarin, 32256 Deerwood, 32258 Bartram Park, 32065 Orange Park, and 32073 Fleming Island.
Chart layer (Chart.js)
The Overview coordinates three charts: c1, a zone-distribution bar (AE / X-500 / X
counts under the active filter); c2, a median-price bar by zone; and c3,
a price-vs-risk scatter color-coded by zone. All transitions tween from the current rendered value
to the new target: bars and dots never reset to zero. The scatter runs
hover:{animationDuration:0}, intersect:true, and
hitRadius:6 so hovering doesn't trigger a full point scan on every mouse move.
Metric cards
A custom animateCountUps() reads data-from / data-target
(and data-decimals="1" for the average risk score) to tween the card numbers. Before a
DOM rebuild it snapshots the current displayed value and passes it as data-from, so the
number tweens up or down from wherever it already sits.
Portfolio view
Four synchronized controls (tile cards, the pfC1 bar, the pfC2
scatter, and the comparison-table rows) all call pfTileClick(zip) to set the shared
activeZip and rebuild. pfC1 is a stacked AE/X-500/X bar per ZIP, the
active one at full opacity and the rest at 20% (hex alpha EE / 33);
pfC2 plots risk against median price, the active dot blue (#1B3F6E) and
the rest grey, color tweened by a 400ms rAF RGB interpolator, since Chart.js snaps
backgroundColor and a manual loop is the only way to animate it.
Flood lookup, theme & responsive
The lookup queries NFHL Layer 28 live for polygon count and zone distribution (FIPS via the
Census Geocoder), rendering animated progress fills. Color is a CSS custom-property token system:
--surface, --bg, --text, --border,
--mg (purple), and the zone colors, driving a dark-default theme persisted to
localStorage, with syncThemeIcon() on DOMContentLoaded. The
layout reflows at 480px: a Tabler-icon bottom nav replaces the tab bar, the hero meta stacks, the
FEMA table drops two columns, and the lookup stats become a 2×2 grid. Getting the breakpoints right
meant replacing every hardcoded color with a token.
Design Decisions
The dashboard never resets a value to zero and counts up. Every transition (bars, counters,
scatter dots) captures the current rendered value and tweens to the new target, so the UI feels
like it knows where it was. The two-step Chart.js technique makes it reliable:
update('none') sets the baseline, then update() animates to it.
Any of four surfaces (tile cards, the bar chart, the scatter, the comparison table) can
change the active ZIP, and all route through one pfTileClick(). This wasn't the
original plan; it grew iteratively because each surface is a natural place to make a selection.
The result reads as coherent rather than hierarchical.
Portfolio emphasis model
All five ZIPs stay visible in the portfolio charts; none collapse. Selection is communicated with opacity (100% active, 20% inactive) and color (blue vs. grey dots). This preserves comparative context while directing attention, a deliberate choice over the common "filter to one" pattern.
Animation lock
The staggered first-build reveal is gated behind a _pfLocked flag that disables the
controls and dims the UI (.pf-animating) until it clears at 2200ms. Rapid clicking
mid-tween would corrupt the animation state; treating animation as a state rather than decoration,
and blocking input during it, is the correct systems decision.
Single-file constraint as feature
One HTML file has no hidden dependencies, no build-pipeline assumptions, and deploys anywhere instantly. It's also the right artifact for sharing: drop it in any browser and it works. The constraint enforces architectural discipline.
Engineering Challenges Solved
- Chart.js
backgroundColorarrays don't tween, so a manual rAF RGB interpolator (400ms easeOutQuad) transitions each dot's color. - Grid children default to
min-width:auto, breaking the 2×2 mobile layout, fixed withmin-width:0on both.lookup-statsand.lookup-stat. - Duplicate
@media(max-width:480px)blocks caused specificity conflicts, resolved with consolidation and!importantguards. - The mobile tab bar used
var(--earth)(a near-black brand color), so the sticky chrome stayed dark in day mode, so it was re-tokenized tovar(--surface)/var(--border). intersect:falseon the scatter hit-tested every point on every mousemove, switched tointersect:truewithhitRadius:6.- The theme icon fell out of sync on load, so
syncThemeIcon()now runs onDOMContentLoadedafter thelocalStoragerestore.
What This Demonstrates
The tool is the artifact of an AI-native workflow, built and iterated through bootstrap-file development with Claude Code, where the limiting factor becomes judgment, not typing. It spans the range the work requires: a data pipeline against live government endpoints, API integration with fallbacks, animation systems with hand-written interpolators, responsive layout, design tokens, and accessibility. Every notable choice (the delta animations, the portfolio emphasis model, the four-control synchronization) is deliberate, with a rationale. And it sits squarely on the climate-risk mission: making property risk legible at the interface layer. Production-quality and built in days.
Live Demo
Open it locally or drop it on any static host; there's nothing to compile.