Files
ems/frontend/src/components/EnergyFlowSankey.tsx
Dusan Vojacek 74ffa5c3e7
All checks were successful
deploy / deploy (push) Successful in 1m17s
test / smoke-test (push) Successful in 3s
fix sankey
2026-04-10 22:42:10 +02:00

108 lines
3.0 KiB
TypeScript

import { ResponsiveSankey } from '@nivo/sankey'
export type FlowTotals = {
pv_to_load_kwh: number
pv_to_batt_kwh: number
pv_to_grid_kwh: number
batt_to_load_kwh: number
batt_to_grid_kwh: number
grid_to_load_kwh: number
grid_to_batt_kwh: number
}
/**
* d3-sankey vyžaduje acyklický graf. Jeden uzel „Síť“ vede na cyklus
* Síť→Baterie (nabíjení) a Baterie→Síť (export) → „circular link“.
* Rozdělení na Import (zdroj) a Export (stok) cyklus odstraní.
*/
const NODES = [
{ id: 'FVE' },
{ id: 'Import ze sítě' },
{ id: 'Baterie' },
{ id: 'Spotřeba' },
{ id: 'Export do sítě' },
] as const
function buildLinks(t: FlowTotals): { source: string; target: string; value: number }[] {
const out: { source: string; target: string; value: number }[] = []
const add = (source: string, target: string, v: number) => {
if (v > 0.0005) out.push({ source, target, value: v })
}
add('FVE', 'Spotřeba', t.pv_to_load_kwh)
add('FVE', 'Baterie', t.pv_to_batt_kwh)
add('FVE', 'Export do sítě', t.pv_to_grid_kwh)
add('Baterie', 'Spotřeba', t.batt_to_load_kwh)
add('Baterie', 'Export do sítě', t.batt_to_grid_kwh)
add('Import ze sítě', 'Spotřeba', t.grid_to_load_kwh)
add('Import ze sítě', 'Baterie', t.grid_to_batt_kwh)
return out
}
type Props = {
totals: FlowTotals | null
}
export function EnergyFlowSankey({ totals }: Props) {
if (!totals) {
return (
<div className="flex h-[440px] items-center justify-center text-sm text-slate-500">
Žádná data
</div>
)
}
const links = buildLinks(totals)
if (links.length === 0) {
return (
<div className="flex h-[440px] items-center justify-center text-sm text-slate-500">
V tomto měsíci nejsou žádné modelované toky (chybí audit / telemetrie).
</div>
)
}
return (
<div className="h-[440px] w-full min-h-[320px]">
<ResponsiveSankey
data={{ nodes: [...NODES], links }}
margin={{ top: 24, right: 180, bottom: 24, left: 24 }}
align="justify"
sort="input"
colors={{ scheme: 'set2' }}
nodeOpacity={1}
nodeHoverOpacity={1}
nodeThickness={20}
nodeSpacing={28}
nodeBorderWidth={0}
linkOpacity={0.45}
linkHoverOpacity={0.75}
linkContract={2}
enableLinkGradient
labelPosition="outside"
labelOrientation="horizontal"
labelPadding={12}
labelTextColor={{ from: 'color', modifiers: [['darker', 1.2]] }}
theme={{
background: 'transparent',
labels: {
text: {
fill: '#e2e8f0',
fontSize: 12,
fontWeight: 500,
},
},
tooltip: {
container: {
background: '#1e293b',
color: '#f8fafc',
fontSize: 12,
borderRadius: 8,
border: '1px solid #334155',
},
},
}}
valueFormat={(v) => `${Number(v).toFixed(2)} kWh`}
/>
</div>
)
}