// views/RegionalIndiaView.jsx - Visual 1: India Demand & Supply twin hex maps // Two panels side-by-side using a stylised hex-grid of states. // Panel A - demand side: state fill = supply reliability (poor/med/good); // circle marker = peak demand CAGR. // Panel B - supply side: state fill = RE generation in GWh (4 buckets, pale→dark green), // toggleable to RE as % of total generation; circle marker = BESS pipeline GWh. // Click any hex → state detail drawer slides in from the right. const { useState: useStateR, useMemo: useMemoR } = React; // ─── Hex geometry helpers ─── function hexPointsPointy(cx, cy, r) { const pts = []; for (let i = 0; i < 6; i++) { const a = (Math.PI / 180) * (60 * i - 30); pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); } return pts.map(p => p.join(',')).join(' '); } function hexCenter(col, row, r) { const w = Math.sqrt(3) * r; const h = 1.5 * r; const x = col * w + (row % 2 ? w / 2 : 0); const y = row * h; return { x, y }; } // ─── Encoding scales ─── const RELIABILITY_FILL = { poor: '#bf4f59', med: '#e6a23c', good: '#5a8c66', }; const RELIABILITY_LABEL = { poor: 'Poor (<18 h/day)', med: 'Medium (18-22 h/day)', good: 'Good (>22 h/day)', }; // 4 RE generation buckets (GWh) const RE_GWH_BUCKETS = [ { max: 1000, fill: '#dbe9d6', label: '< 1 GWh-thou (≤ 1,000 GWh)' }, { max: 10000, fill: '#a6cca2', label: '1,000-10,000 GWh' }, { max: 30000, fill: '#5a9c69', label: '10,000-30,000 GWh' }, { max: Infinity, fill: '#0e5e3a', label: '> 30,000 GWh' }, ]; // 4 RE % buckets const RE_PCT_BUCKETS = [ { max: 10, fill: '#dbe9d6', label: '< 10%' }, { max: 25, fill: '#a6cca2', label: '10-25%' }, { max: 50, fill: '#5a9c69', label: '25-50%' }, { max: Infinity, fill: '#0e5e3a', label: '> 50%' }, ]; function bucketFor(value, scale) { for (const b of scale) if (value <= b.max) return b; return scale[scale.length - 1]; } // ─── State areas (sq km) and size scaling ─── // Used to scale individual hex radii so big states (RJ, MP, MH) read large // and small states/UTs (DL, GA, SK) read small. const AREAS = { RJ:342239, MP:308245, MH:307713, UP:240928, GJ:196024, KA:191791, AP:162968, OD:155707, CG:135191, TN:130058, TG:112077, BR:94163, WB:88752, AR:83743, JH:79714, AS:78438, LA:59146, HP:55673, UK:53483, PB:50362, HR:44212, JK:42241, KL:38852, ML:22429, MN:22327, MZ:21081, NL:16579, TR:10491, SK:7096, GA:3702, DL:1484, }; const MAX_AREA = 342239; // Rajasthan function rForState(code, baseR) { const a = AREAS[code]; if (!a) return baseR * 0.7; // sqrt-scaled, clamped 0.42..0.95 of base radius (avoids overlap) const f = 0.42 + 0.55 * Math.sqrt(a / MAX_AREA); return baseR * Math.min(0.95, Math.max(0.42, f)); } // ─── State centroids (lat/lon → outer SVG coords) ─── // Approximate state centroids in decimal degrees, projected via equirectangular // into the same outer-SVG coord system used to render the India outline overlay. // Calibration: the rendered outline occupies x∈[47, 432], y∈[-34, 373] in outer // SVG coords, mapping to lon∈[68.1°, 97.4°E] and lat∈[37.1°, 6.7°N]. const LATLON = { JK:[33.8,76.0], LA:[34.5,77.7], HP:[31.8,77.2], UK:[30.0,79.0], PB:[31.0,75.5], HR:[29.0,76.1], DL:[28.7,77.1], RJ:[27.0,74.0], UP:[26.9,80.9], GJ:[22.7,71.6], MP:[23.6,78.7], BR:[25.7,85.3], JH:[23.6,85.3], WB:[22.6,87.9], OD:[20.2,85.4], CG:[21.3,81.9], MH:[19.7,75.7], TG:[17.9,79.3], AP:[15.9,79.7], KA:[14.9,76.0], GA:[15.3,74.1], KL:[10.3,76.4], TN:[11.1,78.7], SK:[27.6,88.5], AR:[28.2,94.7], NL:[26.2,94.5], AS:[26.2,92.9], ML:[25.5,91.4], MN:[24.7,93.9], TR:[23.8,91.7], MZ:[23.4,92.9], }; const LON_PER_X = (432 - 47) / (97.4 - 68.1); // ≈ 13.14 units/° const LAT_PER_Y = (373 - (-34)) / (37.1 - 6.7); // ≈ 13.39 units/° function geoCenter(code) { const ll = LATLON[code]; if (!ll) return { x: 0, y: 0 }; const [lat, lon] = ll; return { x: 47 + (lon - 68.1) * LON_PER_X, y: -34 + (37.1 - lat) * LAT_PER_Y, }; } // Circle marker radius - log-ish scaling to keep tiny states visible without // the big ones blowing out the map. function circleR_CAGR(cagr) { if (!cagr || cagr <= 0) return 0; // 3.5% → 6px, 6% → 11px, 8.5% → 16px (cap 18) return Math.min(18, 4 + (cagr - 3) * 2.6); } function circleR_BESS(gwh) { if (!gwh || gwh <= 0) return 0; // 0.3 → 4, 1 → 6, 3 → 10, 8 → 16, cap 20 return Math.min(20, 3 + Math.sqrt(gwh) * 5.5); } // ─── One hex map panel ─── function HexPanel({ panel, states, grid, mode, hovered, onHover, onPick }) { const r = grid.hexR; // Compute bounding box across all states const centers = states.map(s => geoCenter(s.code)); const pad = r + 16; const minX = Math.min(...centers.map(c => c.x)) - pad; const maxX = Math.max(...centers.map(c => c.x)) + pad; const minY = Math.min(...centers.map(c => c.y)) - pad; const maxY = Math.max(...centers.map(c => c.y)) + pad; const vbW = maxX - minX; const vbH = maxY - minY; function fillFor(s) { if (panel === 'A') { return RELIABILITY_FILL[s.demand.reliability] || '#888'; } // panel B if (mode === 'pct') return bucketFor(s.supply.rePercent, RE_PCT_BUCKETS).fill; return bucketFor(s.supply.reGenerationGWh, RE_GWH_BUCKETS).fill; } function circleR(s) { if (panel === 'A') return circleR_CAGR(s.demand.peakDemandCAGR); return circleR_BESS(s.supply.bessPipelineGWh); } function circleTitle(s) { if (panel === 'A') return `Peak demand CAGR ${s.demand.peakDemandCAGR}%`; return `BESS pipeline ${s.supply.bessPipelineGWh} GWh`; } return ( ); } // ─── Detail drawer ─── function StateDrawer({ state, panel, mode, national, onClose }) { if (!state) return null; const d = state.demand, s = state.supply; const fuels = s.reByFuel; const total = (fuels.solar + fuels.wind + fuels.hydro + fuels.other) || 1; const fuelRows = [ { k:'Solar', v:fuels.solar, c:'#fdce07' }, { k:'Wind', v:fuels.wind, c:'#8bcdd2' }, { k:'Hydro', v:fuels.hydro, c:'#2a6a86' }, { k:'Other', v:fuels.other, c:'#aa6227' }, ]; return (
Fill = hours of supply per day. Marker = peak demand CAGR.
Fill = {bMode==='pct' ? 'RE as % of total generation' : 'RE generation in GWh'}. Marker = BESS pipeline (GWh).