/* ============================================================ Charts — hand-rolled SVG (no recharts dep) ============================================================ */ /* global React */ const { useMemo: useMemoChart } = React; function LineChart20({ years, withSolar, withoutSolar, breakEvenYear, height = 160 }) { // withSolar: array of cumulative net savings per year (capex offset) // withoutSolar: cumulative cost paid to discom const allVals = [...withSolar, ...withoutSolar]; const minY = Math.min(...allVals); const maxY = Math.max(...allVals); const W = 380, H = height; const PAD = { l: 44, r: 12, t: 14, b: 22 }; const innerW = W - PAD.l - PAD.r, innerH = H - PAD.t - PAD.b; const x = i => PAD.l + (i / (years - 1)) * innerW; const y = v => PAD.t + innerH - ((v - minY) / (maxY - minY || 1)) * innerH; const pathSolar = withSolar.map((v, i) => `${i === 0 ? "M" : "L"}${x(i)} ${y(v)}`).join(" "); const pathNo = withoutSolar.map((v, i) => `${i === 0 ? "M" : "L"}${x(i)} ${y(v)}`).join(" "); const yTicks = [minY, 0, maxY * 0.5, maxY]; const xTicks = [0, 5, 10, 15, 19]; return ( {yTicks.map((t, i) => ( {t === 0 ? "₹0" : Math.abs(t) >= 100000 ? `₹${(t/100000).toFixed(1)}L` : `₹${Math.round(t/1000)}k`} ))} {xTicks.map(i => ( y{i+1} ))} {/* zero line emphasized */} {/* withoutSolar (negative grows) */} {/* withSolar */} {/* break-even marker */} {breakEvenYear != null && breakEvenYear > 0 && breakEvenYear < years && ( break-even · y{breakEvenYear} )} {/* legend */} cumulative net with solar grid-only spend ); } function BarChart({ data, height = 96, color = "#0d0d0c", highlightIdx = null }) { // data: [{label, value}] const W = 380, H = height; const PAD = { l: 6, r: 6, t: 6, b: 18 }; const innerW = W - PAD.l - PAD.r, innerH = H - PAD.t - PAD.b; const max = Math.max(...data.map(d => d.value)); const bw = innerW / data.length * 0.72; const gap = innerW / data.length * 0.28; return ( {data.map((d, i) => { const h = (d.value / max) * innerH; const x = PAD.l + i * (bw + gap) + gap/2; const yPos = PAD.t + innerH - h; const fill = highlightIdx === i ? "#c2410c" : color; return ( {d.label} ); })} ); } function Histogram({ buckets, height = 120, color = "#0d0d0c" }) { // buckets: [{label, value}] const W = 380, H = height; const PAD = { l: 22, r: 6, t: 8, b: 22 }; const innerW = W - PAD.l - PAD.r, innerH = H - PAD.t - PAD.b; const max = Math.max(...buckets.map(b => b.value)); const bw = innerW / buckets.length; return ( {[0, 0.5, 1].map(t => { const v = max * t; const y = PAD.t + innerH - t * innerH; return ( {Math.round(v)} ); })} {buckets.map((b, i) => { const h = (b.value / max) * innerH; const x = PAD.l + i * bw + 1; const y = PAD.t + innerH - h; return ( {b.label} ); })} ); } function AzimuthRose({ buildings, height = 140 }) { // 16 wedges representing azimuth bins const W = 380, H = height; const cx = W / 2, cy = H / 2 + 6; const r = Math.min(W, H) / 2 - 18; const bins = new Array(16).fill(0); buildings.forEach(b => { if (b.tilt === 0) return; // skip flat const idx = Math.round(b.azimuth / 22.5) % 16; bins[idx] += b.kWp; }); const max = Math.max(...bins, 1); const wedges = bins.map((v, i) => { const a1 = (i - 0.5) * 22.5 * Math.PI / 180 - Math.PI / 2; const a2 = (i + 0.5) * 22.5 * Math.PI / 180 - Math.PI / 2; const len = (v / max) * r; const x1 = cx + Math.cos(a1) * len, y1 = cy + Math.sin(a1) * len; const x2 = cx + Math.cos(a2) * len, y2 = cy + Math.sin(a2) * len; const fillT = v / max; const fluxStops = [[26,17,71],[238,109,61],[247,167,34],[253,231,37]]; const c = (() => { const n = fluxStops.length - 1; const t = fillT * n; const i0 = Math.floor(t); const f = t - i0; const a = fluxStops[i0], bb = fluxStops[Math.min(n, i0 + 1)]; return `rgb(${Math.round(a[0]+(bb[0]-a[0])*f)}, ${Math.round(a[1]+(bb[1]-a[1])*f)}, ${Math.round(a[2]+(bb[2]-a[2])*f)})`; })(); return ; }); const labels = [ { l: "N", a: 0 }, { l: "E", a: 90 }, { l: "S", a: 180 }, { l: "W", a: 270 }, ]; return ( {/* compass rings */} {[0.33, 0.66, 1].map(t => ( ))} {wedges} {labels.map(({ l, a }) => { const rad = a * Math.PI / 180 - Math.PI / 2; const x = cx + Math.cos(rad) * (r + 10), y = cy + Math.sin(rad) * (r + 10); return {l}; })} flat-roof ); } function Sparkline({ data, height = 22, color = "#57574f" }) { if (!data || data.length === 0) return null; const W = 100, H = height; const min = Math.min(...data), max = Math.max(...data); const x = i => (i / (data.length - 1)) * W; const y = v => H - ((v - min) / (max - min || 1)) * H; const path = data.map((v, i) => `${i === 0 ? "M" : "L"}${x(i)} ${y(v)}`).join(" "); return ( ); } Object.assign(window, { LineChart20, BarChart, Histogram, AzimuthRose, Sparkline });