flwo - denni sankey graf
All checks were successful
deploy / deploy (push) Successful in 1m19s
test / smoke-test (push) Successful in 3s

This commit is contained in:
Dusan Vojacek
2026-04-10 22:49:43 +02:00
parent 74ffa5c3e7
commit a65d134682
2 changed files with 56 additions and 5 deletions

View File

@@ -28,7 +28,7 @@ Základní 6 Wh veličin (import/export, PV, baterie, load) zůstává ve Fázi
## UI ## UI
- Sankey (`@nivo/sankey`) součet toků za zvolený měsíc. Síť je ve vizualizaci rozdělena na **Import ze sítě** a **Export do sítě** (jinak by vznikl cyklus síť↔baterie a knihovna hlásí „circular link“). - Sankey (`@nivo/sankey`) součet toků za **celý měsíc** nebo za **jeden vybraný den** (rozbalovací pole „Graf a karty“; klik na název dne v tabulce také přepne den). Síť je ve vizualizaci rozdělena na **Import ze sítě** a **Export do sítě** (jinak by vznikl cyklus síť↔baterie a knihovna hlásí „circular link“).
- Tři perspektivní karty (FVE / síť / baterie). - Tři perspektivní karty (FVE / síť / baterie).
- Tabulka dnů s rozbalením na 15min intervaly. - Tabulka dnů s rozbalením na 15min intervaly.

View File

@@ -1,5 +1,5 @@
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react' import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react'
import { Fragment, useMemo, useState } from 'react' import { Fragment, useEffect, useMemo, useState } from 'react'
import { EnergyFlowSankey, type FlowTotals } from '../components/EnergyFlowSankey' import { EnergyFlowSankey, type FlowTotals } from '../components/EnergyFlowSankey'
import { useEnergyFlowsDaily, useEnergyFlowsIntervals } from '../hooks/useEnergyFlows' import { useEnergyFlowsDaily, useEnergyFlowsIntervals } from '../hooks/useEnergyFlows'
@@ -132,10 +132,23 @@ export default function EnergyFlows() {
const [month, setMonth] = useState(currentMonth) const [month, setMonth] = useState(currentMonth)
const [expandedDay, setExpandedDay] = useState<string | null>(null) const [expandedDay, setExpandedDay] = useState<string | null>(null)
/** null = součet za celý měsíc; jinak ISO den pro Sankey + perspektivní karty */
const [scopeDay, setScopeDay] = useState<string | null>(null)
const { days, loading, error, reload } = useEnergyFlowsDaily(siteId, month) const { days, loading, error, reload } = useEnergyFlowsDaily(siteId, month)
const totals = useMemo(() => (days.length > 0 ? aggregateFlows(days) : null), [days]) useEffect(() => {
setScopeDay(null)
}, [month])
const totals = useMemo(() => {
if (days.length === 0) return null
if (scopeDay) {
const row = days.find((d) => d.day === scopeDay)
if (row) return aggregateFlows([row])
}
return aggregateFlows(days)
}, [days, scopeDay])
const flowOnly: FlowTotals | null = totals const flowOnly: FlowTotals | null = totals
? { ? {
@@ -201,6 +214,30 @@ export default function EnergyFlows() {
</div> </div>
) : ( ) : (
<> <>
{days.length > 0 && (
<div className="flex flex-wrap items-center gap-3 rounded-lg border border-slate-800 bg-slate-900/60 px-3 py-2">
<label htmlFor="energy-flow-scope" className="text-sm text-slate-400">
Graf a karty:
</label>
<select
id="energy-flow-scope"
className="max-w-[min(100%,20rem)] rounded-lg border border-slate-700 bg-slate-900 px-3 py-1.5 text-sm text-slate-200"
value={scopeDay ?? ''}
onChange={(e) => {
const v = e.target.value
setScopeDay(v === '' ? null : v)
}}
>
<option value="">Celý měsíc (součet)</option>
{days.map((d) => (
<option key={d.day} value={d.day}>
{fmtDay(d.day)}
</option>
))}
</select>
</div>
)}
{totals && ( {totals && (
<div className="grid gap-3 sm:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4"> <div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
@@ -243,7 +280,9 @@ export default function EnergyFlows() {
)} )}
<div className="rounded-xl border border-slate-800 bg-slate-900 p-4"> <div className="rounded-xl border border-slate-800 bg-slate-900 p-4">
<h2 className="mb-2 text-sm font-medium text-slate-300">Sankey součet za měsíc</h2> <h2 className="mb-2 text-sm font-medium text-slate-300">
Sankey {scopeDay ? fmtDay(scopeDay) : 'celý měsíc (součet)'}
</h2>
<EnergyFlowSankey totals={flowOnly} /> <EnergyFlowSankey totals={flowOnly} />
</div> </div>
@@ -281,7 +320,19 @@ export default function EnergyFlows() {
<span className="mr-1 inline-block w-4"> <span className="mr-1 inline-block w-4">
{expandedDay === row.day ? <ChevronUp size={14} /> : <ChevronDown size={14} />} {expandedDay === row.day ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</span> </span>
<button
type="button"
className={`rounded px-1 text-left underline-offset-2 hover:underline ${
scopeDay === row.day ? 'text-amber-300' : ''
}`}
title="Zobrazit tento den v Sankey a v kartách"
onClick={(e) => {
e.stopPropagation()
setScopeDay(row.day)
}}
>
{fmtDay(row.day)} {fmtDay(row.day)}
</button>
</td> </td>
<td className="px-3 py-2 text-right text-sm">{row.pv_production_kwh.toFixed(1)}</td> <td className="px-3 py-2 text-right text-sm">{row.pv_production_kwh.toFixed(1)}</td>
<td className="px-3 py-2 text-right text-sm">{row.grid_import_kwh.toFixed(2)}</td> <td className="px-3 py-2 text-right text-sm">{row.grid_import_kwh.toFixed(2)}</td>