Remodel/packages/web/components/TornadoChart.tsx
Mannu e6dc39aa33 [S1-T12/T13] P&L revenue breakdown + collapsible rows + UI polish
- Engine: Add ppa_revenue_cr, mcp_revenue_cr, tariff, units to PnLRow
- Engine: Split PPA vs MCP revenue in P&L computation
- Web: Collapsible rows for PPA/MCP Revenue and Opex
- Web: Highlighted rows (Total Revenue, EBITDA, EBIT, PBT, PAT)
- Web: Units above Tariff in breakdown, bg-blue-50 highlight
- Fix sticky column z-index for horizontal scroll
- CLAUDE.md: Add project documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 10:42:36 +05:30

98 lines
2.7 KiB
TypeScript

"use client";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ReferenceLine,
ResponsiveContainer,
Cell,
} from "recharts";
export interface TornadoEntry {
param_name: string;
low_value: number;
high_value: number;
base_kpi: number;
low_kpi: number;
high_kpi: number;
swing: number;
}
interface Props {
entries: TornadoEntry[];
kpiLabel?: string;
baseValue?: number;
}
interface ChartRow {
name: string;
low: number;
high: number;
base: number;
lowLabel: string;
highLabel: string;
}
export function TornadoChart({ entries, kpiLabel = "Equity IRR", baseValue }: Props) {
if (entries.length === 0) return null;
const base = baseValue ?? entries[0]?.base_kpi ?? 0;
const isPercent = kpiLabel.toLowerCase().includes("irr") || kpiLabel.toLowerCase().includes("cuf");
const fmtKpi = (v: number) =>
isPercent ? `${(v * 100).toFixed(1)}%` : v.toFixed(2);
const data: ChartRow[] = entries.map((e) => ({
name: e.param_name,
low: Math.min(e.low_kpi, e.high_kpi) - base,
high: Math.max(e.low_kpi, e.high_kpi) - base,
base,
lowLabel: fmtKpi(Math.min(e.low_kpi, e.high_kpi)),
highLabel: fmtKpi(Math.max(e.low_kpi, e.high_kpi)),
}));
const absMax = Math.max(...data.map((d) => Math.max(Math.abs(d.low), Math.abs(d.high))));
const domain = [-absMax * 1.1, absMax * 1.1];
return (
<div>
<h3 className="font-semibold mb-3 text-sm">
Sensitivity: {kpiLabel} (base = {fmtKpi(base)})
</h3>
<ResponsiveContainer width="100%" height={Math.max(200, entries.length * 45)}>
<BarChart
data={data}
layout="vertical"
margin={{ top: 0, right: 40, left: 120, bottom: 0 }}
>
<XAxis
type="number"
domain={domain}
tickFormatter={(v) => isPercent ? `${(v * 100).toFixed(1)}%` : v.toFixed(2)}
tick={{ fontSize: 10 }}
/>
<YAxis type="category" dataKey="name" tick={{ fontSize: 11 }} width={120} />
<Tooltip
formatter={(value) => {
const v = typeof value === "number" ? value : 0;
return [
`${fmtKpi(base + v)}${isPercent ? `${(v * 100).toFixed(1)}%` : v.toFixed(2)})`,
"",
];
}}
/>
<ReferenceLine x={0} stroke="#888" strokeWidth={1.5} />
<Bar dataKey="low" name="Low" stackId="a" fill="transparent" />
<Bar dataKey="high" name="High" stackId="a" radius={[0, 3, 3, 0]}>
{data.map((entry, index) => (
<Cell key={index} fill={entry.high >= 0 ? "#10b981" : "#f43f5e"} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
}