Commit c2c6ca0
authored
feat: EIA-860 Power Plants — 15K plants with map, list, and cross-entity integration (#14)
* feat: add EIA-860 power plants dataset, list/detail pages, map layers, and cross-entity integration
Phase 1: Data + Sync Script
- scripts/sync-power-plants.ts: downloads EIA-860 ZIP, parses plant + generator XLSX, outputs data/power-plants.json (15,082 plants from real EIA data)
- types/entities.ts: PowerPlant interface, FuelCategory type, color palette
- lib/data.ts: getAllPowerPlants, getPowerPlantBySlug, getPowerPlantsByUtility, getPowerPlantsByBalancingAuthority
- lib/formatting.ts: fuel type label/badge/color helpers, formatCapacity
- scripts/sync-all.ts: added power-plants step
Phase 2: List Page + Detail Page
- app/(shell)/power-plants/page.tsx: searchable, filterable DataTable with 15K+ rows (auto-virtualized), fuel/state/status/capacity filters
- app/(shell)/power-plants/[slug]/page.tsx: detail with map, technologies, grid relationships
- Navigation: added Power Plants to TopBar
- Homepage: added power plant count to stats grid + browse card
Phase 3: Map Layer + Tiles
- scripts/generate-plant-tiles.mjs: generates MVT tiles for power plant points (zoom 0-12)
- app/api/tiles/power-plants/[z]/[x]/[y]/route.ts: dynamic tile API for zoom 13+
- ExplorerMap: added zoom-gated power plant layers (>500MW at zoom 6-7, all at zoom 8+), fuel-type color coded, hover tooltips, click navigation
- next.config.mjs: power-plants tile rewrite rule
Phase 4: Cross-Entity + Automation
- UtilityDetailPanel: Power Plants section showing plants owned by utility
- BADetailPanel: Power Plants section showing plants in BA territory
- .github/workflows/sync-annual.yml: Oct 1 cron for EIA-860 annual data
- .github/workflows/sync-monthly.yml: daily cron for EIA-860M updates
- scripts/sync-power-plants-monthly.ts: monthly update scaffold
Data: xlsx, adm-zip added as devDependencies for XLSX parsing
* fix: load power plants via fetch to avoid ISR page size limit
The 8.7 MB power-plants.json was being statically imported into
lib/data.ts, causing the pre-rendered power-plants page to be 47.5 MB
— exceeding Vercel's 19 MB ISR limit.
Fix:
- Remove static import of power-plants.json from lib/data.ts
- Add lib/power-plants.ts with usePowerPlants() hook that fetches
/data/power-plants.json client-side (cached in memory after first load)
- Copy power-plants.json to public/data/ at build time via new
scripts/copy-power-plants.mjs
- Update list page, detail page, and explorer panels to use the hook
- Homepage uses hardcoded count instead of importing the full dataset
Result: power-plants page is 4.38 kB (down from 47.5 MB).
* fix: include power plant build steps in vercel.json
vercel.json had a custom buildCommand that bypassed npm's prebuild
script, so copy-power-plants.mjs and generate-plant-tiles.mjs never
ran on Vercel. This caused /data/power-plants.json to 404 and the
list page to show 'No power plants found'.
Changed buildCommand to use 'npm run prebuild' so it stays in sync
with package.json automatically.
* fix: add bg-background-default for dark mode on power plants page
The custom className on PageLayout overrode the default dark mode
background, causing the content area to stay white in dark mode
while the header/nav were properly dark.
* fix: apply body background token for dark mode support
The body element had no background-color, defaulting to white
regardless of theme. Now uses var(--color-background-body) which
the edges theme sets to #151a30 in dark mode.
* fix: zoom-gate territory layers and fix utility/BA links
- Split territory layer into overview (zoom 4-5, fill-only) and
detail (zoom 6+, fill+borders) mirroring the power plants pattern
- Add minZoom: 4 to grid-operator boundary layer
- Fix utility links: /utilities/slug → /explore?view=utility&slug=...
- Fix BA links: /balancing-authorities/slug → /explore?view=ba&slug=...
- Bump territory tile tolerance from 3→5 for simpler geometry
- Skip generating zoom 0-3 tiles (no longer rendered)
* feat: add standalone utility and BA detail pages
- /utilities/[slug] — full detail page with overview, operations,
territory map, grid relationships, served utilities, subsidiaries,
and linked power plants
- /balancing-authorities/[slug] — full detail page with overview,
territory map, member utilities, and linked power plants
- Power plant detail page links now use proper /utilities/ and
/balancing-authorities/ routes instead of explore query params
- Both pages use PageLayout with breadcrumbs and section navigation
* fix: aggressive territory zoom-gating and utility link fallback
Map performance:
- Zoom 5-6: Only major utilities (>100k customers) shown, fill-only
- Zoom 7-8: All territories, fill-only (no borders)
- Zoom 9+: All territories with borders
- No territories at zoom 0-4 (was causing freezing/timeouts)
- Added customerCount to territory tile properties for filtering
- Increased tile simplification tolerance to 8
- Grid operator boundaries gated at zoom 5+
Utility links:
- Unmatched utilities (82% of plants) now link to explore search
instead of showing dead text — e.g. 'U.S. Bureau of Reclamation'
links to /explore?view=utilities&q=U.S.+Bureau+of+Reclamation
- Section now shows for any plant with a utility name or BA, not just
those with matched IDs
- Label changed to 'Utility / Operator' since many are IPPs
* perf: pre-simplify territory geometry + aggressive zoom gating
Root cause: 2,905 territory polygons with ~409K vertices total was
overwhelming the GPU. An M4 Max laptop couldn't zoom without 570ms+
INP violations and freezing.
Geometry simplification:
- Added @turf/simplify to pre-process territories (Douglas-Peucker)
- 409K → 125K vertices (69.5% reduction) at 0.05° tolerance (~5.5km)
- Detail panels still load full-resolution GeoJSON for clicked entities
- geojson-vt tolerance back to 3 (pre-simplification handles it)
Zoom gating (much more aggressive):
- Zoom 0-6: No territories at all (clean, fast initial view)
- Zoom 7-8: Large utilities only (>50k customers, ~355 of 2,905)
- Zoom 9+: All territories, fill-only (no borders — they double draw calls)
- Power plants pushed later: major at zoom 8-9, all at zoom 10+
- Grid operator boundaries at zoom 7+, no borders
UX:
- Added 'Zoom in to explore' hint banner at low zoom levels
- Banner auto-hides when zoom >= 7 or entity is highlighted
- Tracks zoom level via mapbox zoomend event
* feat: standalone Utilities list page with Fuse.js search
- New /utilities page with 3,132 utilities in a searchable DataTable
- Fuse.js indexes: name, shortName, eiaName, slug, jurisdiction, eiaId
with weighted fuzzy matching (threshold 0.3, location-independent)
- Filters: segment (co-op, IOU, muni, etc.), status, jurisdiction
- Sort: name or customer count (search results are relevance-ordered)
- Avatar + name link, segment badge, customer count, jurisdiction, status
- Added 'Utilities' tab to top navigation between Explore and Power Plants
- Also wired Fuse.js into Power Plants page (replaces naive .includes())
- Shared useFuseSearch<T> hook in lib/search.ts for reuse
* fix: add prominent search bar to utilities and power plants pages
The DataControls search prop wasn't rendering a visible search input.
Added a standalone SearchInput component with:
- Full-width text input with magnifying glass icon
- Clear button when text is entered
- Live result count indicator
- Proper focus ring and dark mode support
- Placed prominently above the filter controls on both pages
* refactor: rename /utilities to /grid-operators + update About page
Victor's feedback: 'utilities' is too narrow — the dataset includes
distribution co-ops, G&T co-ops, IOUs, municipals, CCAs, federal,
and transmission operators. 'Grid Operators' is more accurate.
Changes:
- Moved app/(shell)/utilities/ → app/(shell)/grid-operators/
- Updated all internal links: /utilities/slug → /grid-operators/slug
- Nav tab: 'Utilities' → 'Grid Operators'
- Power plant fallback utility link: /explore?view=utilities&q=...
→ /grid-operators?q=... (pre-fills search via query param)
- Homepage 'Utilities' card → 'Grid Operators' linking to /grid-operators
- BA detail page utility links → /grid-operators/slug
- About page: added power plants (15,082) to data highlights, added
EIA-860 to data sources, added 'Browse Power Plants' and 'Explore Map'
links, fixed broken /grid-operators link
* fix: wrap useSearchParams in Suspense boundary for SSG
Next.js requires useSearchParams() to be inside a Suspense boundary
when pre-rendering static pages.1 parent fca2d55 commit c2c6ca0
File tree
34 files changed
+3788
-34
lines changed- .github/workflows
- app
- (shell)
- about
- balancing-authorities/[slug]
- grid-operators
- [slug]
- power-plants
- [slug]
- api/tiles/power-plants/[z]/[x]/[y]
- components
- explorer
- panels
- data
- lib
- scripts
- types
34 files changed
+3788
-34
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | | - | |
| 16 | + | |
| 17 | + | |
16 | 18 | | |
17 | | - | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
61 | 62 | | |
62 | 63 | | |
63 | 64 | | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | 65 | | |
68 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
69 | 73 | | |
70 | 74 | | |
71 | 75 | | |
| |||
0 commit comments