// IndiaNetworkView.jsx - Regional Collaboration Network · India (dummy) // Mirrors the global NetworkView layout, but operates on the India dataset // and generates synthetic edges so the structure can be reviewed before the // real collaboration data is collected. const { useMemo: useMemoIN, useState: useStateIN, useEffect: useEffectIN } = React; // India archetype palette - covers the organisation types present in INDIA_DATA // (Regional overview, v22_06). Distinct, readable hues since colour = identity // in the node-link view. const TYPE_COLORS_IN = { 'Industry player': '#0a0a0a', 'MDB/IFI/DFI': '#3b6ea8', 'Private investor': '#7a5da8', 'Financial intermediary (domestic)': '#0e2c66', 'Philanthropy': '#e07b4a', 'NGO': '#d9b441', 'Coalition or multi-stakeholder platform': '#5a8c66', 'Intergovernmental organization (IGO)': '#8c5983', 'Bilateral development agency': '#c87a4e', 'Policymakers and regulators': '#1a4d6b', 'Regulated infrastructure operator': '#2a9d8f', }; function IndiaNetworkView({ orgs, allOrgs, edges, onPickOrg, highlightOrgId, dataset }) { const facets = (dataset || window.INITIATIVES_DATA).facets; const [hover, setHover] = useStateIN(null); const [positions, setPositions] = useStateIN(null); // Filter sets - drive node dimming (mirrors the global Collaboration Network). const [filterTypes, setFilterTypes] = useStateIN(() => new Set()); const [filterBarriers, setFilterBarriers] = useStateIN(() => new Set()); const W = 1600, H = 1000; const COMMUNITIES_R = 110; const COMMUNITIES_EXCL = 195; // Real collaboration edges from the global dataset, filtered to those // between visible India-relevant orgs. const visibleIds = useMemoIN(() => new Set(orgs.map(o => o.id)), [orgs]); const liveEdges = useMemoIN( () => (edges || []).filter(e => visibleIds.has(e.a) && visibleIds.has(e.b)), [edges, visibleIds] ); const degree = useMemoIN(() => { const m = new Map(); for (const o of orgs) m.set(o.id, 0); for (const e of liveEdges) { m.set(e.a, (m.get(e.a) || 0) + 1); m.set(e.b, (m.get(e.b) || 0) + 1); } return m; }, [orgs, liveEdges]); useEffectIN(() => { const cx = W / 2, cy = H / 2; // Hard clamp - projects a position outside the Communities exclusion zone // if it has drifted inside. Always called after integration so the rule // is satisfied at every step (not just on average). const clampOutside = (p) => { const ex = p.x - cx, ey = p.y - cy; const dist = Math.sqrt(ex * ex + ey * ey); if (dist < COMMUNITIES_EXCL) { const target = COMMUNITIES_EXCL + 8; if (dist < 0.5) { // Effectively at the centre - shove out in a stable direction. p.x = cx + target; p.y = cy; } else { const scale = target / dist; p.x = cx + ex * scale; p.y = cy + ey * scale; } // Kill inward velocity so the spring doesn't immediately pull back in. p.vx = 0; p.vy = 0; } }; const sorted = [...orgs].sort((a, b) => degree.get(b.id) - degree.get(a.id)); const n = sorted.length; const pos = new Map(); sorted.forEach((o, i) => { const deg = degree.get(o.id); // Start everything well outside the exclusion zone, on three loose rings. // Horizontal stretch matches the 1.6:1 canvas aspect ratio so the seed // positions fill an ellipse, not a tiny central circle. const r = deg === 0 ? 440 : (deg > 8 ? 260 : 360); const a = (i / n) * Math.PI * 2 + (deg === 0 ? Math.PI / 5 : 0); pos.set(o.id, { x: cx + Math.cos(a) * r * 1.5 + (Math.random() - 0.5) * 60, y: cy + Math.sin(a) * r + (Math.random() - 0.5) * 60, vx: 0, vy: 0, }); }); const ITER = 460; const REPULSE = 22000; const SPRING = 0.04; const SPRING_LEN = 230; const CENTER_PULL = 0.004; const DAMP = 0.80; // Elliptical "soft canvas" - nodes are clamped to lie inside an ellipse // that fits the visible area. Removes the rectangular corners that the // previous bounds clamp turned into accumulation zones. const ELLIPSE_A = W / 2 - 70; // ~730 const ELLIPSE_B = H / 2 - 50; // ~450 const ids = orgs.map(o => o.id); for (let it = 0; it < ITER; it++) { for (let i = 0; i < ids.length; i++) { const a = pos.get(ids[i]); for (let j = i + 1; j < ids.length; j++) { const b = pos.get(ids[j]); let dx = a.x - b.x, dy = a.y - b.y; let d2 = dx * dx + dy * dy; if (d2 < 1) d2 = 1; const f = REPULSE / d2; const d = Math.sqrt(d2); const fx = (dx / d) * f, fy = (dy / d) * f; a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; } } for (const e of liveEdges) { const a = pos.get(e.a), b = pos.get(e.b); if (!a || !b) continue; const dx = b.x - a.x, dy = b.y - a.y; const d = Math.sqrt(dx * dx + dy * dy) || 1; const f = (d - SPRING_LEN) * SPRING; const fx = (dx / d) * f, fy = (dy / d) * f; a.vx += fx; a.vy += fy; b.vx -= fx; b.vy -= fy; } for (const id of ids) { const p = pos.get(id); p.vx += (cx - p.x) * CENTER_PULL; p.vy += (cy - p.y) * CENTER_PULL; const ex = p.x - cx, ey = p.y - cy; const dist = Math.sqrt(ex * ex + ey * ey) || 1; if (dist < COMMUNITIES_EXCL) { const push = (COMMUNITIES_EXCL - dist) * 1.2; p.vx += (ex / dist) * push; p.vy += (ey / dist) * push; } p.vx *= DAMP; p.vy *= DAMP; p.x += p.vx; p.y += p.vy; // Elliptical clamp - project nodes back onto the ellipse boundary // when they drift outside it. Removes the rectangular corners. const nx = (p.x - cx) / ELLIPSE_A; const ny = (p.y - cy) / ELLIPSE_B; const norm = Math.sqrt(nx * nx + ny * ny); if (norm > 1) { p.x = cx + (p.x - cx) / norm; p.y = cy + (p.y - cy) / norm; p.vx *= 0.3; p.vy *= 0.3; // shed inertia so they don't bounce } clampOutside(p); } } const arr = [...pos.entries()].map(([id, p]) => ({ id, p })); const LABEL_W = 180, LABEL_H = 26; const ellipseClamp = (p) => { const nx = (p.x - cx) / ELLIPSE_A; const ny = (p.y - cy) / ELLIPSE_B; const norm = Math.sqrt(nx * nx + ny * ny); if (norm > 1) { p.x = cx + (p.x - cx) / norm; p.y = cy + (p.y - cy) / norm; } }; for (let pass = 0; pass < 40; pass++) { let moved = false; for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { const a = arr[i].p, b = arr[j].p; const dx = Math.abs(a.x - b.x), dy = Math.abs(a.y - b.y); if (dx < LABEL_W && dy < LABEL_H) { const push = (LABEL_H - dy) / 2 + 1; if (a.y < b.y) { a.y -= push; b.y += push; } else { a.y += push; b.y -= push; } ellipseClamp(a); ellipseClamp(b); moved = true; } } } if (!moved) break; } // Final guarantee: no node sits inside the Communities exclusion zone, // even if the label de-collision pass nudged something inward. for (const id of ids) clampOutside(pos.get(id)); setPositions(pos); }, [orgs, liveEdges]); if (!positions) return
Organisations active in India’s energy ecosystem and their publicly recorded partnerships. Hover a node to highlight its partners. Click for full details. Edges are symmetric - each pair appears once.