Skip to content

Commit c2c6ca0

Browse files
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

34 files changed

+3788
-34
lines changed

.github/workflows/sync-annual.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Sync Annual Data
2+
3+
on:
4+
schedule:
5+
- cron: '0 12 1 10 *' # Oct 1 at noon UTC (after EIA-860 typically releases in Sept)
6+
workflow_dispatch: {} # Manual trigger
7+
8+
jobs:
9+
sync:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
cache: npm
17+
- run: npm ci
18+
- run: npx tsx scripts/sync-power-plants.ts
19+
- name: Commit & push if changed
20+
run: |
21+
git config user.name "github-actions[bot]"
22+
git config user.email "github-actions[bot]@users.noreply.github.com"
23+
git add data/power-plants.json
24+
git diff --staged --quiet || (git commit -m "data: update power plants (EIA-860 annual)" && git push)

.github/workflows/sync-monthly.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Sync Monthly Data
2+
3+
on:
4+
schedule:
5+
- cron: '0 14 * * *' # Daily at 2pm UTC — check for new EIA-860M
6+
workflow_dispatch: {} # Manual trigger
7+
8+
jobs:
9+
sync:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
cache: npm
17+
- run: npm ci
18+
- run: npx tsx scripts/sync-power-plants-monthly.ts
19+
- name: Commit & push if changed
20+
run: |
21+
git config user.name "github-actions[bot]"
22+
git config user.email "github-actions[bot]@users.noreply.github.com"
23+
git add data/power-plants.json
24+
git diff --staged --quiet || (git commit -m "data: update power plants (EIA-860M monthly)" && git push)

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@ node_modules
99
# Territory GeoJSON in public/ is copied from data/ at build time
1010
public/data/territories/
1111

12+
# Power plants JSON in public/ is copied from data/ at build time
13+
public/data/power-plants.json
14+
1215
# Pre-generated vector tiles (built by scripts/generate-tiles.mjs)
1316
public/tiles/

app/(shell)/about/page.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Badge, Card, PageLayout, Section } from "@texturehq/edges";
44
import Link from "next/link";
55

66
const dataSources = [
7+
{ name: "EIA-860", description: "Annual Electric Generator Report — 15,082 power plants, generator details, fuel types, and capacity data" },
78
{ name: "EIA-861", description: "Annual electric power industry report — utility ownership, customers, sales, and revenue data" },
89
{ name: "HIFLD", description: "Homeland Infrastructure Foundation-Level Data — electric service territory boundaries" },
910
{ name: "CEC", description: "California Energy Commission — CCA territory data and California-specific utility information" },
@@ -12,9 +13,9 @@ const dataSources = [
1213
];
1314

1415
const dataHighlights = [
15-
{ label: "Utility Territories", value: "3,132", icon: "⚡" },
16+
{ label: "Grid Operators", value: "3,132", icon: "⚡" },
17+
{ label: "Power Plants", value: "15,082", icon: "🏭" },
1618
{ label: "Grid Infrastructure", value: "ISOs, RTOs, BAs", icon: "🔌" },
17-
{ label: "Rate Structures", value: "Growing", icon: "💰" },
1819
{ label: "Territory Boundaries", value: "3,000+ GeoJSON", icon: "🗺️" },
1920
];
2021

@@ -61,11 +62,14 @@ export default function AboutPage() {
6162
</div>
6263

6364
<div className="mt-6 flex flex-wrap gap-3">
64-
<Link href="/utilities">
65-
<Badge size="lg" shape="pill" variant="info">Browse Utilities →</Badge>
66-
</Link>
6765
<Link href="/grid-operators">
68-
<Badge size="lg" shape="pill" variant="warning">Browse Grid Operators →</Badge>
66+
<Badge size="lg" shape="pill" variant="info">Browse Grid Operators →</Badge>
67+
</Link>
68+
<Link href="/power-plants">
69+
<Badge size="lg" shape="pill" variant="warning">Browse Power Plants →</Badge>
70+
</Link>
71+
<Link href="/explore">
72+
<Badge size="lg" shape="pill" variant="success">Explore Map →</Badge>
6973
</Link>
7074
<a href="https://github.com/TextureHQ/opengrid" target="_blank" rel="noopener noreferrer">
7175
<Badge size="lg" shape="pill" variant="default">View on GitHub →</Badge>

0 commit comments

Comments
 (0)