Add EIA and utility rate map layers
This commit is contained in:
@@ -13,13 +13,14 @@
|
||||
"- Loads point and cluster summary CSVs from `output/`.\n",
|
||||
"- Recreates the cluster-colored Folium map.\n",
|
||||
"- Enriches point popups with HUC8 watershed, RUCA, tract demographics, and state energy context where available.\n",
|
||||
"- Adds separate layers for clustered points, isolated/noise points, cluster centroids, HUC8 watersheds, and state IM3 projected demand.\n",
|
||||
"- Adds separate layers for clustered points, isolated/noise points, cluster centroids, HUC8 watersheds, state IM3 projected demand, EIA generator capacity, and utility-rate context.\n",
|
||||
"- Saves a standalone HTML map to `output/enhanced_master_data_center_spatial_clusters_map.html`.\n",
|
||||
"\n",
|
||||
"Notes from `output/data_center_demographic_ruca_energy_summary.md`:\n",
|
||||
"- HUC8 watershed join is a recommended next step for water-context analysis.\n",
|
||||
"- `im3_state_projected_moderate_50` is populated and used for state projected demand context.\n",
|
||||
"- `seds_state_msn_year` is checked through the state context export, but it currently has no rows, so SEDS fields are blank until that table is populated.\n"
|
||||
"- `seds_state_msn_year` is checked through the state context export, but it currently has no rows, so SEDS fields are blank until that table is populated.\n",
|
||||
"- EIA generator capacity uses the latest available period in `public.energy_eia_operating_generator_capacity_flat`; utility-rate context uses `public.utility_rate_tracker_2025_2028`.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -97,6 +98,10 @@
|
||||
"SHOW_ELECTION_2020_LAYER = True\n",
|
||||
"SHOW_ELECTION_2024_LAYER = False\n",
|
||||
"SHOW_NRI_LAYER = True\n",
|
||||
"SHOW_EIA_GENERATOR_CAPACITY_LAYER = True\n",
|
||||
"EIA_GENERATOR_PERIOD = None # None uses the latest available EIA period.\n",
|
||||
"MAX_EIA_GENERATOR_PLANTS = 2000\n",
|
||||
"SHOW_UTILITY_RATE_TRACKER_LAYER = True\n",
|
||||
"\n",
|
||||
"OUTPUT_DIR.mkdir(exist_ok=True)\n",
|
||||
"print('points:', POINTS_CSV)\n",
|
||||
@@ -193,6 +198,9 @@
|
||||
"broadband_context = pd.DataFrame()\n",
|
||||
"election_context = pd.DataFrame()\n",
|
||||
"nri_context = pd.DataFrame()\n",
|
||||
"generator_capacity_plants = pd.DataFrame()\n",
|
||||
"utility_rate_tracker = pd.DataFrame()\n",
|
||||
"utility_rate_state_context = pd.DataFrame()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def load_zsh_secrets() -> None:\n",
|
||||
@@ -230,6 +238,7 @@
|
||||
"def load_optional_db_layers() -> None:\n",
|
||||
" global internet_cables_geojson, opposition_cases, drought_context, smoke_context\n",
|
||||
" global climate_context, broadband_context, election_context, nri_context, points\n",
|
||||
" global generator_capacity_plants, utility_rate_tracker, utility_rate_state_context\n",
|
||||
"\n",
|
||||
" if not ENABLE_DB_LAYER_LOAD:\n",
|
||||
" print('DB layer load disabled')\n",
|
||||
@@ -279,6 +288,176 @@
|
||||
" opposition_cases = pd.read_sql(opposition_sql, conn)\n",
|
||||
" print(f'opposition_cases rows: {len(opposition_cases):,}')\n",
|
||||
"\n",
|
||||
" if SHOW_EIA_GENERATOR_CAPACITY_LAYER:\n",
|
||||
" generator_sql = \"\"\"\n",
|
||||
" with selected_period as (\n",
|
||||
" select coalesce(%(period)s::text, max(period)) as period\n",
|
||||
" from public.energy_eia_operating_generator_capacity_flat\n",
|
||||
" ),\n",
|
||||
" latest_generators as (\n",
|
||||
" select g.*\n",
|
||||
" from public.energy_eia_operating_generator_capacity_flat g\n",
|
||||
" join selected_period sp on g.period = sp.period\n",
|
||||
" where g.geom is not null\n",
|
||||
" and g.latitude is not null\n",
|
||||
" and g.longitude is not null\n",
|
||||
" ),\n",
|
||||
" source_capacity as (\n",
|
||||
" select\n",
|
||||
" plant_id,\n",
|
||||
" energy_source_code,\n",
|
||||
" max(energy_source_desc) as energy_source_desc,\n",
|
||||
" sum(coalesce(nameplate_capacity_mw, 0)) as nameplate_capacity_mw,\n",
|
||||
" count(*) as generator_count\n",
|
||||
" from latest_generators\n",
|
||||
" group by plant_id, energy_source_code\n",
|
||||
" ),\n",
|
||||
" source_rank as (\n",
|
||||
" select\n",
|
||||
" *,\n",
|
||||
" row_number() over (\n",
|
||||
" partition by plant_id\n",
|
||||
" order by nameplate_capacity_mw desc nulls last, energy_source_code nulls last\n",
|
||||
" ) as rn\n",
|
||||
" from source_capacity\n",
|
||||
" ),\n",
|
||||
" source_mix as (\n",
|
||||
" select\n",
|
||||
" plant_id,\n",
|
||||
" string_agg(\n",
|
||||
" coalesce(energy_source_code, 'UNK') || ': ' ||\n",
|
||||
" round(nameplate_capacity_mw::numeric, 1)::text || ' MW',\n",
|
||||
" ', ' order by nameplate_capacity_mw desc nulls last\n",
|
||||
" ) as energy_source_mix\n",
|
||||
" from source_capacity\n",
|
||||
" group by plant_id\n",
|
||||
" ),\n",
|
||||
" plant_capacity as (\n",
|
||||
" select\n",
|
||||
" lg.period,\n",
|
||||
" lg.plant_id,\n",
|
||||
" max(lg.plant_name) as plant_name,\n",
|
||||
" max(lg.state_id) as state_id,\n",
|
||||
" max(lg.state_name) as state_name,\n",
|
||||
" string_agg(distinct nullif(lg.entity_name, ''), '; ') as entity_names,\n",
|
||||
" max(lg.balancing_authority_code) as balancing_authority_code,\n",
|
||||
" max(lg.balancing_authority_name) as balancing_authority_name,\n",
|
||||
" avg(lg.latitude) as latitude,\n",
|
||||
" avg(lg.longitude) as longitude,\n",
|
||||
" sum(coalesce(lg.nameplate_capacity_mw, 0)) as nameplate_capacity_mw,\n",
|
||||
" sum(coalesce(lg.net_summer_capacity_mw, 0)) as net_summer_capacity_mw,\n",
|
||||
" sum(coalesce(lg.net_winter_capacity_mw, 0)) as net_winter_capacity_mw,\n",
|
||||
" count(*) as generator_count\n",
|
||||
" from latest_generators lg\n",
|
||||
" group by lg.period, lg.plant_id\n",
|
||||
" )\n",
|
||||
" select\n",
|
||||
" pc.*,\n",
|
||||
" sr.energy_source_code as primary_energy_source_code,\n",
|
||||
" sr.energy_source_desc as primary_energy_source_desc,\n",
|
||||
" sm.energy_source_mix\n",
|
||||
" from plant_capacity pc\n",
|
||||
" left join source_rank sr on sr.plant_id = pc.plant_id and sr.rn = 1\n",
|
||||
" left join source_mix sm on sm.plant_id = pc.plant_id\n",
|
||||
" where pc.nameplate_capacity_mw > 0\n",
|
||||
" order by pc.nameplate_capacity_mw desc nulls last\n",
|
||||
" limit %(limit)s\n",
|
||||
" \"\"\"\n",
|
||||
" generator_capacity_plants = pd.read_sql(\n",
|
||||
" generator_sql,\n",
|
||||
" conn,\n",
|
||||
" params={'period': EIA_GENERATOR_PERIOD, 'limit': MAX_EIA_GENERATOR_PLANTS},\n",
|
||||
" )\n",
|
||||
" period_label = (\n",
|
||||
" generator_capacity_plants['period'].iloc[0]\n",
|
||||
" if not generator_capacity_plants.empty and 'period' in generator_capacity_plants\n",
|
||||
" else EIA_GENERATOR_PERIOD\n",
|
||||
" )\n",
|
||||
" print(\n",
|
||||
" f'eia generator capacity plants: {len(generator_capacity_plants):,} '\n",
|
||||
" f'(period {period_label}; top {MAX_EIA_GENERATOR_PLANTS:,} by nameplate MW)'\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" if SHOW_UTILITY_RATE_TRACKER_LAYER:\n",
|
||||
" utility_rate_sql = \"\"\"\n",
|
||||
" select\n",
|
||||
" utility_provider, state_name, state_code as utility_state_code, state_id,\n",
|
||||
" service_type, customer_count, total_revenue_increase_2025_2028,\n",
|
||||
" time_period, monthly_increase_amount, monthly_pct_increase_ratio,\n",
|
||||
" effective_date, effective_date_raw, status, source_file\n",
|
||||
" from public.utility_rate_tracker_2025_2028\n",
|
||||
" order by state_code, utility_provider, service_type, effective_date\n",
|
||||
" \"\"\"\n",
|
||||
" utility_rate_state_sql = \"\"\"\n",
|
||||
" with state_rollup as (\n",
|
||||
" select\n",
|
||||
" state_code as utility_state_code,\n",
|
||||
" max(state_name) as utility_state_name,\n",
|
||||
" count(*) as utility_rate_case_count,\n",
|
||||
" count(distinct utility_provider) as utility_rate_provider_count,\n",
|
||||
" count(*) filter (where lower(coalesce(service_type, '')) like 'electric%%') as utility_rate_electric_case_count,\n",
|
||||
" count(*) filter (where lower(coalesce(service_type, '')) like '%%gas%%') as utility_rate_gas_case_count,\n",
|
||||
" sum(coalesce(customer_count, 0)) as utility_rate_customer_count,\n",
|
||||
" sum(coalesce(total_revenue_increase_2025_2028, 0)) as utility_rate_total_revenue_increase_2025_2028,\n",
|
||||
" avg(monthly_increase_amount) as utility_rate_avg_monthly_increase_amount,\n",
|
||||
" avg(monthly_pct_increase_ratio) as utility_rate_avg_monthly_pct_increase_ratio,\n",
|
||||
" min(effective_date) as utility_rate_first_effective_date,\n",
|
||||
" max(effective_date) as utility_rate_last_effective_date\n",
|
||||
" from public.utility_rate_tracker_2025_2028\n",
|
||||
" group by state_code\n",
|
||||
" ),\n",
|
||||
" ranked_utilities as (\n",
|
||||
" select\n",
|
||||
" state_code as utility_state_code,\n",
|
||||
" utility_provider,\n",
|
||||
" service_type,\n",
|
||||
" total_revenue_increase_2025_2028,\n",
|
||||
" row_number() over (\n",
|
||||
" partition by state_code\n",
|
||||
" order by total_revenue_increase_2025_2028 desc nulls last, utility_provider\n",
|
||||
" ) as rn\n",
|
||||
" from public.utility_rate_tracker_2025_2028\n",
|
||||
" ),\n",
|
||||
" top_utilities as (\n",
|
||||
" select\n",
|
||||
" utility_state_code,\n",
|
||||
" string_agg(\n",
|
||||
" coalesce(utility_provider, 'Unknown') || ' (' || coalesce(service_type, 'service') || ')',\n",
|
||||
" '; ' order by rn\n",
|
||||
" ) as utility_rate_top_utilities\n",
|
||||
" from ranked_utilities\n",
|
||||
" where rn <= 3\n",
|
||||
" group by utility_state_code\n",
|
||||
" )\n",
|
||||
" select sr.*, tu.utility_rate_top_utilities\n",
|
||||
" from state_rollup sr\n",
|
||||
" left join top_utilities tu using (utility_state_code)\n",
|
||||
" order by utility_state_code\n",
|
||||
" \"\"\"\n",
|
||||
" utility_rate_tracker = pd.read_sql(utility_rate_sql, conn)\n",
|
||||
" utility_rate_state_context = pd.read_sql(utility_rate_state_sql, conn)\n",
|
||||
" print(f'utility_rate_tracker rows: {len(utility_rate_tracker):,}')\n",
|
||||
" print(f'utility_rate_state_context rows: {len(utility_rate_state_context):,}')\n",
|
||||
"\n",
|
||||
" if not utility_rate_state_context.empty:\n",
|
||||
" if not state_energy.empty and {'state_code', 'map_latitude', 'map_longitude'}.issubset(state_energy.columns):\n",
|
||||
" state_coords = state_energy[['state_code', 'map_latitude', 'map_longitude']].copy()\n",
|
||||
" state_coords = state_coords.rename(columns={'state_code': 'utility_state_code'})\n",
|
||||
" utility_rate_state_context = utility_rate_state_context.merge(\n",
|
||||
" state_coords, on='utility_state_code', how='left'\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" cols = [\n",
|
||||
" c for c in utility_rate_state_context.columns\n",
|
||||
" if c not in {'utility_state_code', 'map_latitude', 'map_longitude'}\n",
|
||||
" ]\n",
|
||||
" points = points.merge(\n",
|
||||
" utility_rate_state_context[['utility_state_code'] + cols],\n",
|
||||
" left_on='state',\n",
|
||||
" right_on='utility_state_code',\n",
|
||||
" how='left',\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" if SHOW_DROUGHT_AND_SMOKE_CONTEXT:\n",
|
||||
" drought_sql = \"\"\"\n",
|
||||
" select\n",
|
||||
@@ -443,6 +622,8 @@
|
||||
"- `public.data_center_broadband_connection` (broadband capacity layer + popup)\n",
|
||||
"- `public.data_center_rdh_precinct_vote_matches` (election context layer + popup)\n",
|
||||
"- `public.data_center_nri_exposure` (FEMA NRI multi-hazard risk layer + popup)\n",
|
||||
"- `public.energy_eia_operating_generator_capacity_flat` (latest-period generator capacity plant layer)\n",
|
||||
"- `public.utility_rate_tracker_2025_2028` (state utility-rate tracker layer + point popup enrichment)\n",
|
||||
"\n",
|
||||
"If DB credentials are unavailable, map generation still works with CSV/GeoJSON sources."
|
||||
]
|
||||
@@ -471,6 +652,8 @@
|
||||
"STATE_ENERGY_COLOR = '#f59e0b'\n",
|
||||
"INTERNET_CABLE_COLOR = '#7c3aed'\n",
|
||||
"OPPOSITION_CASE_COLOR = '#b91c1c'\n",
|
||||
"GENERATOR_CAPACITY_COLOR = '#15803d'\n",
|
||||
"UTILITY_RATE_COLOR = '#0f766e'\n",
|
||||
"\n",
|
||||
"# NRI hazard prefix -> human-readable label, used in the per-DC popup.\n",
|
||||
"NRI_HAZARDS = [\n",
|
||||
@@ -504,6 +687,16 @@
|
||||
" return f\"{prefix}{value:,.{decimals}f}{suffix}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def fmt_pct_ratio(value, decimals=1):\n",
|
||||
" if pd.isna(value):\n",
|
||||
" return ''\n",
|
||||
" try:\n",
|
||||
" value = float(value) * 100.0\n",
|
||||
" except (TypeError, ValueError):\n",
|
||||
" return clean_value(value)\n",
|
||||
" return fmt_number(value, decimals, suffix='%')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def cluster_color(cluster_id):\n",
|
||||
" if cluster_id == -1:\n",
|
||||
" return NOISE_COLOR\n",
|
||||
@@ -581,6 +774,33 @@
|
||||
" return '#0284c7'\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def generator_capacity_color(source_code):\n",
|
||||
" code = clean_value(source_code).upper()\n",
|
||||
" source_colors = {\n",
|
||||
" 'NG': '#f97316', # natural gas\n",
|
||||
" 'SUN': '#facc15', # solar\n",
|
||||
" 'WND': '#16a34a', # wind\n",
|
||||
" 'WAT': '#0284c7', # hydro\n",
|
||||
" 'NUC': '#7c3aed', # nuclear\n",
|
||||
" 'BIT': '#111827', 'SUB': '#374151', 'LIG': '#4b5563',\n",
|
||||
" 'DFO': '#b45309', 'RFO': '#92400e',\n",
|
||||
" }\n",
|
||||
" return source_colors.get(code, GENERATOR_CAPACITY_COLOR)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def utility_rate_color(avg_pct_ratio):\n",
|
||||
" if pd.isna(avg_pct_ratio):\n",
|
||||
" return '#94a3b8'\n",
|
||||
" pct = float(avg_pct_ratio) * 100.0\n",
|
||||
" if pct >= 15:\n",
|
||||
" return '#7f1d1d'\n",
|
||||
" if pct >= 10:\n",
|
||||
" return '#dc2626'\n",
|
||||
" if pct >= 5:\n",
|
||||
" return '#f59e0b'\n",
|
||||
" return '#0284c7'\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def top_nri_hazards(row, n=3):\n",
|
||||
" \"\"\"Return the top-N hazards by risk score for this DC, as 'Label: score' strings.\"\"\"\n",
|
||||
" pairs = []\n",
|
||||
@@ -639,6 +859,22 @@
|
||||
" {seds_note}\n",
|
||||
" '''\n",
|
||||
"\n",
|
||||
" utility_rate_lines = ''\n",
|
||||
" if hasattr(row, 'utility_rate_case_count') and pd.notna(row.utility_rate_case_count):\n",
|
||||
" utility_rate_lines = f'''\n",
|
||||
" <hr style=\"margin: 6px 0;\">\n",
|
||||
" <strong>Utility rate tracker (2025-2028)</strong><br>\n",
|
||||
" State: {clean_value(row.utility_state_name)}<br>\n",
|
||||
" Tracker cases: {fmt_number(row.utility_rate_case_count)}<br>\n",
|
||||
" Utilities: {fmt_number(row.utility_rate_provider_count)}<br>\n",
|
||||
" Electric / gas cases: {fmt_number(row.utility_rate_electric_case_count)} / {fmt_number(row.utility_rate_gas_case_count)}<br>\n",
|
||||
" Total revenue increase: {fmt_number(row.utility_rate_total_revenue_increase_2025_2028, 0, prefix='$')}<br>\n",
|
||||
" Avg monthly increase: {fmt_number(row.utility_rate_avg_monthly_increase_amount, 2, prefix='$')}<br>\n",
|
||||
" Avg monthly % increase: {fmt_pct_ratio(row.utility_rate_avg_monthly_pct_increase_ratio, 1)}<br>\n",
|
||||
" Effective dates: {clean_value(row.utility_rate_first_effective_date)} to {clean_value(row.utility_rate_last_effective_date)}<br>\n",
|
||||
" Top utilities: {clean_value(row.utility_rate_top_utilities)}<br>\n",
|
||||
" '''\n",
|
||||
"\n",
|
||||
" drought_lines = ''\n",
|
||||
" if hasattr(row, 'usdm_status') and pd.notna(row.usdm_status):\n",
|
||||
" drought_lines = f'''\n",
|
||||
@@ -752,6 +988,7 @@
|
||||
" {huc8_lines}\n",
|
||||
" {ruca_lines}\n",
|
||||
" {energy_lines}\n",
|
||||
" {utility_rate_lines}\n",
|
||||
" {drought_lines}\n",
|
||||
" {smoke_lines}\n",
|
||||
" {climate_lines}\n",
|
||||
@@ -830,6 +1067,46 @@
|
||||
" ''', max_width=380)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def generator_capacity_popup(row):\n",
|
||||
" primary = clean_value(row.primary_energy_source_desc) or clean_value(row.primary_energy_source_code)\n",
|
||||
" return folium.Popup(f'''\n",
|
||||
" <div style=\"font-family: system-ui, sans-serif; min-width: 300px; max-width: 430px;\">\n",
|
||||
" <strong>{clean_value(row.plant_name) or 'EIA generating plant'}</strong><br>\n",
|
||||
" Plant ID: {clean_value(row.plant_id)}<br>\n",
|
||||
" State: {clean_value(row.state_id)} ({clean_value(row.state_name)})<br>\n",
|
||||
" EIA period: {clean_value(row.period)}<br>\n",
|
||||
" <hr style=\"margin: 6px 0;\">\n",
|
||||
" Nameplate capacity: {fmt_number(row.nameplate_capacity_mw, 1, suffix=' MW')}<br>\n",
|
||||
" Net summer capacity: {fmt_number(row.net_summer_capacity_mw, 1, suffix=' MW')}<br>\n",
|
||||
" Net winter capacity: {fmt_number(row.net_winter_capacity_mw, 1, suffix=' MW')}<br>\n",
|
||||
" Generators: {fmt_number(row.generator_count)}<br>\n",
|
||||
" Primary source: {primary}<br>\n",
|
||||
" Source mix: {clean_value(row.energy_source_mix)}<br>\n",
|
||||
" Balancing authority: {clean_value(row.balancing_authority_code)} {clean_value(row.balancing_authority_name)}<br>\n",
|
||||
" Entity: {clean_value(row.entity_names)}\n",
|
||||
" </div>\n",
|
||||
" ''', max_width=460)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def utility_rate_popup(row):\n",
|
||||
" return folium.Popup(f'''\n",
|
||||
" <div style=\"font-family: system-ui, sans-serif; min-width: 300px; max-width: 430px;\">\n",
|
||||
" <strong>{clean_value(row.utility_state_name)} utility rate tracker</strong><br>\n",
|
||||
" State code: {clean_value(row.utility_state_code)}<br>\n",
|
||||
" <hr style=\"margin: 6px 0;\">\n",
|
||||
" Tracker cases: {fmt_number(row.utility_rate_case_count)}<br>\n",
|
||||
" Utility providers: {fmt_number(row.utility_rate_provider_count)}<br>\n",
|
||||
" Electric / gas cases: {fmt_number(row.utility_rate_electric_case_count)} / {fmt_number(row.utility_rate_gas_case_count)}<br>\n",
|
||||
" Customers represented: {fmt_number(row.utility_rate_customer_count)}<br>\n",
|
||||
" Total revenue increase: {fmt_number(row.utility_rate_total_revenue_increase_2025_2028, 0, prefix='$')}<br>\n",
|
||||
" Avg monthly increase: {fmt_number(row.utility_rate_avg_monthly_increase_amount, 2, prefix='$')}<br>\n",
|
||||
" Avg monthly % increase: {fmt_pct_ratio(row.utility_rate_avg_monthly_pct_increase_ratio, 1)}<br>\n",
|
||||
" Effective dates: {clean_value(row.utility_rate_first_effective_date)} to {clean_value(row.utility_rate_last_effective_date)}<br>\n",
|
||||
" Top utilities: {clean_value(row.utility_rate_top_utilities)}\n",
|
||||
" </div>\n",
|
||||
" ''', max_width=460)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def cable_style(_feature):\n",
|
||||
" return {'color': INTERNET_CABLE_COLOR, 'weight': 1.6, 'opacity': 0.45}\n",
|
||||
"\n",
|
||||
@@ -890,7 +1167,9 @@
|
||||
" font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;\n",
|
||||
" font-size: 12px;\n",
|
||||
" line-height: 1.35;\n",
|
||||
" min-width: 260px;\n",
|
||||
" min-width: 280px;\n",
|
||||
" max-height: 76vh;\n",
|
||||
" overflow-y: auto;\n",
|
||||
" \">\n",
|
||||
" <div style=\"font-weight: 700; margin-bottom: 6px;\">Overlay Legend</div>\n",
|
||||
"\n",
|
||||
@@ -921,9 +1200,25 @@
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#ea580c;margin-right:6px;\"></span>40-59 (rel. moderate)</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#dc2626;margin-right:6px;\"></span>60-79 (rel. high)</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#7f1d1d;margin-right:6px;\"></span>>= 80 (very high)</div>\n",
|
||||
"\n",
|
||||
" <div style=\"font-weight: 600; margin-top: 6px;\">EIA generator capacity</div>\n",
|
||||
" <div>Circle size = latest-period plant nameplate MW</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#f97316;margin-right:6px;\"></span>Natural gas</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#facc15;margin-right:6px;\"></span>Solar</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#16a34a;margin-right:6px;\"></span>Wind</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#0284c7;margin-right:6px;\"></span>Hydro</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#7c3aed;margin-right:6px;\"></span>Nuclear</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#374151;margin-right:6px;\"></span>Coal / other</div>\n",
|
||||
"\n",
|
||||
" <div style=\"font-weight: 600; margin-top: 6px;\">Utility rate tracker</div>\n",
|
||||
" <div>Circle size = total 2025-2028 revenue increase</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#0284c7;margin-right:6px;\"></span>< 5% avg monthly increase</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#f59e0b;margin-right:6px;\"></span>5-9.9%</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#dc2626;margin-right:6px;\"></span>10-14.9%</div>\n",
|
||||
" <div><span style=\"display:inline-block;width:10px;height:10px;background:#7f1d1d;margin-right:6px;\"></span>>= 15%</div>\n",
|
||||
" </div>\n",
|
||||
" \"\"\"\n",
|
||||
" map_obj.get_root().html.add_child(folium.Element(legend_html))"
|
||||
" map_obj.get_root().html.add_child(folium.Element(legend_html))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -951,6 +1246,8 @@
|
||||
" state_energy_layer = folium.FeatureGroup(name='State energy demand context (IM3 / SEDS)', show=False)\n",
|
||||
" cables_layer = folium.FeatureGroup(name='Internet cable network', show=False)\n",
|
||||
" opposition_layer = folium.FeatureGroup(name='Opposition cases', show=False)\n",
|
||||
" generator_capacity_layer = folium.FeatureGroup(name='EIA operating generator capacity (latest)', show=False)\n",
|
||||
" utility_rate_layer = folium.FeatureGroup(name='Utility rate tracker (2025-2028)', show=False)\n",
|
||||
" climate_layer = folium.FeatureGroup(name='Climate stress context', show=False)\n",
|
||||
" broadband_layer = folium.FeatureGroup(name='Broadband capacity context', show=False)\n",
|
||||
" election_2020_layer = folium.FeatureGroup(name='Election context (2020 precinct match)', show=False)\n",
|
||||
@@ -1020,6 +1317,57 @@
|
||||
" tooltip=f\"Opposition case: {row.state} ({clean_value(row.status)})\",\n",
|
||||
" ).add_to(opposition_layer)\n",
|
||||
"\n",
|
||||
" if SHOW_EIA_GENERATOR_CAPACITY_LAYER and not generator_capacity_plants.empty:\n",
|
||||
" gen_rows = generator_capacity_plants.dropna(subset=['latitude', 'longitude'])\n",
|
||||
" for row in gen_rows.itertuples(index=False):\n",
|
||||
" capacity = float(row.nameplate_capacity_mw) if pd.notna(row.nameplate_capacity_mw) else 0.0\n",
|
||||
" radius = max(4, min(18, 3 + capacity ** 0.5 / 6.0))\n",
|
||||
" color = generator_capacity_color(row.primary_energy_source_code)\n",
|
||||
" folium.CircleMarker(\n",
|
||||
" location=[row.latitude, row.longitude],\n",
|
||||
" radius=radius,\n",
|
||||
" color=color,\n",
|
||||
" fill=True,\n",
|
||||
" fill_color=color,\n",
|
||||
" fill_opacity=0.45,\n",
|
||||
" weight=1,\n",
|
||||
" popup=generator_capacity_popup(row),\n",
|
||||
" tooltip=(\n",
|
||||
" f\"EIA generator: {clean_value(row.plant_name)}; \"\n",
|
||||
" f\"{fmt_number(row.nameplate_capacity_mw, 0, suffix=' MW')}\"\n",
|
||||
" ),\n",
|
||||
" ).add_to(generator_capacity_layer)\n",
|
||||
"\n",
|
||||
" if (\n",
|
||||
" SHOW_UTILITY_RATE_TRACKER_LAYER\n",
|
||||
" and not utility_rate_state_context.empty\n",
|
||||
" and {'map_latitude', 'map_longitude'}.issubset(utility_rate_state_context.columns)\n",
|
||||
" ):\n",
|
||||
" rate_rows = utility_rate_state_context.dropna(subset=['map_latitude', 'map_longitude'])\n",
|
||||
" for row in rate_rows.itertuples(index=False):\n",
|
||||
" revenue_b = (\n",
|
||||
" float(row.utility_rate_total_revenue_increase_2025_2028) / 1_000_000_000.0\n",
|
||||
" if pd.notna(row.utility_rate_total_revenue_increase_2025_2028)\n",
|
||||
" else 0.0\n",
|
||||
" )\n",
|
||||
" radius = max(6, min(26, 5 + revenue_b ** 0.5 * 4.0))\n",
|
||||
" color = utility_rate_color(row.utility_rate_avg_monthly_pct_increase_ratio)\n",
|
||||
" folium.CircleMarker(\n",
|
||||
" location=[row.map_latitude, row.map_longitude],\n",
|
||||
" radius=radius,\n",
|
||||
" color=color,\n",
|
||||
" fill=True,\n",
|
||||
" fill_color=color,\n",
|
||||
" fill_opacity=0.48,\n",
|
||||
" weight=1.2,\n",
|
||||
" popup=utility_rate_popup(row),\n",
|
||||
" tooltip=(\n",
|
||||
" f\"Utility rates {row.utility_state_code}: \"\n",
|
||||
" f\"{fmt_number(row.utility_rate_total_revenue_increase_2025_2028, 0, prefix='$')} total increase; \"\n",
|
||||
" f\"avg monthly {fmt_pct_ratio(row.utility_rate_avg_monthly_pct_increase_ratio, 1)}\"\n",
|
||||
" ),\n",
|
||||
" ).add_to(utility_rate_layer)\n",
|
||||
"\n",
|
||||
" if SHOW_CLIMATE_LAYER:\n",
|
||||
" climate_rows = points_df.dropna(subset=['mean_summer_temperature_c']) if 'mean_summer_temperature_c' in points_df.columns else pd.DataFrame()\n",
|
||||
" for row in climate_rows.itertuples(index=False):\n",
|
||||
@@ -1148,6 +1496,8 @@
|
||||
" state_energy_layer.add_to(m)\n",
|
||||
" cables_layer.add_to(m)\n",
|
||||
" opposition_layer.add_to(m)\n",
|
||||
" generator_capacity_layer.add_to(m)\n",
|
||||
" utility_rate_layer.add_to(m)\n",
|
||||
" climate_layer.add_to(m)\n",
|
||||
" broadband_layer.add_to(m)\n",
|
||||
" election_2020_layer.add_to(m)\n",
|
||||
@@ -1195,8 +1545,9 @@
|
||||
"- filters by source/operator/state/cluster size\n",
|
||||
"- toggle layers for top-N clusters\n",
|
||||
"- water-stress overlays on top of the HUC8 layer\n",
|
||||
"- generator capacity / fuel mix overlays around each DC\n",
|
||||
"- nearest generator capacity / fuel mix summaries around each DC\n",
|
||||
"- opposition cases overlay\n",
|
||||
"- utility-rate filters by state or service type\n",
|
||||
"- cluster labels or summary panels\n",
|
||||
"- downloadable GeoJSON exports\n"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user