feat: implement sensor glitch detection for water levels and update data cleaning logic

This commit is contained in:
David Fencl
2026-06-08 19:45:37 +02:00
parent 62c861e610
commit f8a7be7fa3
28 changed files with 222 additions and 64 deletions
+16 -7
View File
@@ -32,7 +32,7 @@ npm run data:update
```
Tento příkaz provede dvě věci:
1. `npm run scrape`: Otevře stránky povodí pro všech 12 přehrad, přečte tabulky s historickými měřeními a najde "Aktuální hodnoty", odkud vytáhne exaktní **přítok, objem, srážky a teplotu**. Tato data inteligentně sloučí s tvojí lokální databází (`public/data/*.json`). Pokud Povodí aktuálně počasí neposkytuje, skript zrecykluje tvou dřívější uloženou hodnotu, aby se graf "nerozbil".
1. `npm run scrape`: Otevře stránky povodí pro všech **53 nádrží a říčních stanic**, přečte tabulky s historickými měřeními a najde "Aktuální hodnoty", odkud vytáhne exaktní **přítok, odtok, objem, srážky a teplotu**. Tato data inteligentně sloučí s tvojí lokální databází (`public/data/*.json`) a automaticky doplňuje chybějící hodnoty přítoku/objemu z minula, aby graf neměl výpadky k nule.
2. `npm run build-index`: Zaktualizuje hlavní indexový soubor `lakes_index.json`, který aplikace využívá pro vykreslení rychlých náhledů (např. v levém menu nebo na mapě).
---
@@ -56,17 +56,26 @@ Pokud ti běží počítač (nebo domácí server/Raspberry Pi) nepřetržitě,
### Možnost B: Pomocí GitHub Actions (Pro Produkci)
Až projekt nahraješ na GitHub, můžeš si vytvořit workflow soubor (např. `.github/workflows/scrape.yml`), který bude skript spouštět na serverech GitHubu zdarma každou hodinu, a výsledné `.json` soubory automaticky commitne a publikuje na web.
### Možnost C: Jednoduchý integrovaný spouštěč (Nejlehčí)
Pokud nechceš řešit složitý systémový crontab, napsal jsem pro tebe přímo do Node.js malý spouštěč. Stačí si otevřít další okno terminálu a napsat:
### Možnost C: Jednoduchý integrovaný spouštěč (Doporučeno pro vývoj)
Pokud nechceš řešit složitý systémový crontab, je v projektu připraven inteligentní plánovač. Stačí si otevřít další okno terminálu a napsat:
```bash
npm run data:watch 10
npm run data:watch
```
Tento příkaz ihned provede první stažení a následně bude aplikaci automaticky aktualizovat **každých 10 minut** (číslo na konci si můžeš libovolně přepsat podle toho, jak často chceš stahovat). Skript poběží, dokud okno terminálu nezavřeš.
Tento příkaz provede okamžitou aktualizaci a poté automaticky spouští stahování vždy 7 minut po každém 10minutovém kroku (např. 18:07, 18:17, 18:27...). Tento posun zaručuje, že Povodí už stihlo na svůj web nahrát nová data a nestahuješ staré hodnoty.
---
## 🛠️ Oprava chyb v historii (Zuby / Nuly v grafu)
Pokud ti aplikace delší dobu neběžela (např. při vypnutém počítači) a následně došlo k doplnění dat z historie, mohly se v grafech přítoku a objemu objevit falešné propady k nule (zuby). Pro vyčištění celé historie a dopočítání těchto bodů z posledních známých hodnot spusť:
```bash
npm run data:fix
```
Tento skript projde všechny datové JSON soubory, detekuje anomálie/nuly a opraví je.
## 📁 Struktura klíčových datových složek
* `/scripts/lakesConfig.ts` - Tady najdeš definici všech 12 sledovaných přehrad (včetně jejich ID pro Povodí Vltavy, GPS souřadnic, maximálních objemů a stavebních kót). Sem můžeš přidávat nové přehrady.
* `/scripts/lakesConfig.ts` - Tady najdeš definici všech **53 sledovaných nádrží a řek** (včetně jejich ID pro Povodí Vltavy, GPS souřadnic, maximálních objemů a stavebních kót). Sem můžeš přidávat nové stanice.
* `/public/data/` - Zde se ukládají vygenerovaná JSON data. V produkci musí být tyto soubory přístupné jako statické assety.
* `/src/components/` - Obsahuje samotné vizuální karty, Leaflet mapu a detailní `LakeDetail.tsx` (kde se vykresluje hydrologický a meteorologický graf přes Recharts).
* `/src/components/` - Obsahuje samotné vizuální karty, Leaflet mapu a detailní `LakeDetail.tsx` (kde se vykresluje hydrologický a meteorologický graf přes Recharts s automatickým čištěním chyb a senzorických úletů).
+1
View File
@@ -13,6 +13,7 @@
"build-index": "tsx scripts/buildIndex.ts",
"data:update": "npm run scrape && npm run build-index",
"data:watch": "tsx scripts/watchData.ts",
"data:fix": "tsx scripts/fix_lake_inflows.ts",
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage"
+9
View File
@@ -2317,5 +2317,14 @@
"volume": 0,
"temperature": 26,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 96,
"flow": 8.667,
"inflow": 0,
"volume": 0,
"temperature": 26,
"precipitation": 0
}
]
+9
View File
@@ -2207,5 +2207,14 @@
"volume": 0,
"temperature": 25.5,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 90,
"flow": 4.745,
"inflow": 0,
"volume": 0,
"temperature": 25.5,
"precipitation": 0
}
]
+1 -1
View File
@@ -8220,7 +8220,7 @@
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 463.42,
"flow": 0,
"inflow": 6.048207396473533,
"inflow": 0,
"volume": 0,
"temperature": 23.7,
"precipitation": 0
+1 -1
View File
@@ -8886,7 +8886,7 @@
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 416.72,
"flow": 0,
"inflow": 7.8121741402451015,
"inflow": 0,
"volume": 0.09,
"temperature": 23.8,
"precipitation": 0
+1 -1
View File
@@ -8859,7 +8859,7 @@
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 448.81,
"flow": 0,
"inflow": 4.662341665814382,
"inflow": 0,
"volume": 0.67,
"temperature": 23.8,
"precipitation": 0
+1 -1
View File
@@ -8076,7 +8076,7 @@
"timestamp": "2026-06-08T17:00:00.000Z",
"level": 580.99,
"flow": 0,
"inflow": 9.58165824042722,
"inflow": 0,
"volume": 0.03,
"temperature": 23,
"precipitation": 0
+10 -1
View File
@@ -8939,7 +8939,7 @@
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 407.64,
"flow": 0,
"flow": 0.2,
"inflow": 0.4,
"volume": 0.14,
"temperature": 23,
@@ -8953,5 +8953,14 @@
"volume": 0.14,
"temperature": 22.3,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 407.64,
"flow": 0,
"inflow": 0.4,
"volume": 0.14,
"temperature": 22.3,
"precipitation": 0
}
]
+1 -1
View File
@@ -8202,7 +8202,7 @@
"timestamp": "2026-06-08T17:00:00.000Z",
"level": 632.76,
"flow": 0,
"inflow": 10.515147444523139,
"inflow": 0,
"volume": 0,
"temperature": 21.7,
"precipitation": 0
+1 -1
View File
@@ -8202,7 +8202,7 @@
"timestamp": "2026-06-08T17:00:00.000Z",
"level": 635.7,
"flow": 0,
"inflow": 5.418107522688602,
"inflow": 0,
"volume": 0,
"temperature": 22,
"precipitation": 0
+9
View File
@@ -2245,5 +2245,14 @@
"volume": 0,
"temperature": 24,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 82,
"flow": 2.666,
"inflow": 0,
"volume": 0,
"temperature": 24,
"precipitation": 0
}
]
+1 -1
View File
@@ -8751,7 +8751,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 563.66,
"flow": 0.02,
"inflow": 0.01,
"inflow": 0,
"volume": 0.46,
"temperature": 22.3,
"precipitation": 0
+9
View File
@@ -2252,5 +2252,14 @@
"volume": 0,
"temperature": 24.9,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 43,
"flow": 4.73,
"inflow": 0,
"volume": 0,
"temperature": 24.9,
"precipitation": 0
}
]
+9
View File
@@ -2317,5 +2317,14 @@
"volume": 0,
"temperature": 23,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 25,
"flow": 2.95,
"inflow": 0,
"volume": 0,
"temperature": 23,
"precipitation": 0
}
]
+1 -1
View File
@@ -8103,7 +8103,7 @@
"timestamp": "2026-06-08T16:20:00.000Z",
"level": 670.52,
"flow": 0,
"inflow": 8.227779864526358,
"inflow": 0,
"volume": 0,
"temperature": 21.8,
"precipitation": 0
+1 -1
View File
@@ -8922,7 +8922,7 @@
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 575.7,
"flow": 0.02,
"inflow": 11.662041143485649,
"inflow": 0,
"volume": 1.06,
"temperature": 21.4,
"precipitation": 0
+9
View File
@@ -2270,5 +2270,14 @@
"volume": 0,
"temperature": 24,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:10:00.000Z",
"level": 68,
"flow": 1.452,
"inflow": 0,
"volume": 0,
"temperature": 24,
"precipitation": 0
}
]
+9
View File
@@ -8926,5 +8926,14 @@
"volume": 0.1,
"temperature": 23.6,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 0,
"flow": 0.06,
"inflow": 0.07,
"volume": 0.1,
"temperature": 23.6,
"precipitation": 0
}
]
+1 -1
View File
@@ -8913,7 +8913,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 668.4,
"flow": 0,
"inflow": 10.98258553088203,
"inflow": 0,
"volume": 0.39,
"temperature": 23.8,
"precipitation": 0
+1 -1
View File
@@ -8634,7 +8634,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 385.01,
"flow": 0.02,
"inflow": 6.257864375579982,
"inflow": 0,
"volume": 0.84,
"temperature": 22.5,
"precipitation": 0
+1 -1
View File
@@ -8904,7 +8904,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 678.6,
"flow": 0,
"inflow": 8.204576504122992,
"inflow": 0,
"volume": 0.16,
"temperature": 20.6,
"precipitation": 0
+1 -1
View File
@@ -8958,7 +8958,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 588.39,
"flow": 0.08,
"inflow": 8.36186446216757,
"inflow": 0,
"volume": 0.32,
"temperature": 23,
"precipitation": 0
+1 -1
View File
@@ -8913,7 +8913,7 @@
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 0,
"flow": 0.05,
"inflow": 8.055084527934373,
"inflow": 0,
"volume": 0.41,
"temperature": 21.4,
"precipitation": 0
+9
View File
@@ -2243,5 +2243,14 @@
"volume": 0,
"temperature": 26.1,
"precipitation": 0
},
{
"timestamp": "2026-06-08T17:30:00.000Z",
"level": 46,
"flow": 53.88,
"inflow": 0,
"volume": 0,
"temperature": 26.1,
"precipitation": 0
}
]
+1 -1
View File
@@ -9219,7 +9219,7 @@
"timestamp": "2026-06-08T17:20:00.000Z",
"level": 369.61,
"flow": 15.18,
"inflow": 11.42,
"inflow": 0,
"volume": 19.77,
"temperature": 24.4,
"precipitation": 0
+34 -34
View File
@@ -69,7 +69,7 @@
"level": "369.61",
"capacity": 93.7,
"storageDiff": -0.49,
"inflow": "11.4",
"inflow": "0.0",
"outflow": "15.2",
"volume": 19.77,
"maxVolume": 21.1,
@@ -658,7 +658,7 @@
"level": "670.52",
"capacity": 80.2,
"storageDiff": -0.88,
"inflow": "8.2",
"inflow": "0.0",
"outflow": "0.0",
"volume": 1.3,
"maxVolume": 1.6,
@@ -751,7 +751,7 @@
"level": "588.39",
"capacity": 32,
"storageDiff": -0.21,
"inflow": "8.4",
"inflow": "0.0",
"outflow": "0.1",
"volume": 0.32,
"maxVolume": 1,
@@ -813,7 +813,7 @@
"level": "580.99",
"capacity": 2.1,
"storageDiff": -1.22,
"inflow": "9.6",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.03,
"maxVolume": 1.4,
@@ -841,9 +841,9 @@
"name": "Humenice",
"river": "",
"priority": false,
"level": "534.70",
"level": "0.00",
"capacity": 12.5,
"storageDiff": -1.3,
"storageDiff": 0,
"inflow": "0.1",
"outflow": "0.1",
"volume": 0.1,
@@ -852,7 +852,6 @@
"lat": 48.784,
"lng": 14.735,
"sparkline": [
534.69,
534.69,
0,
534.69,
@@ -863,7 +862,8 @@
0,
534.69,
534.69,
534.7
534.7,
0
],
"type": "lake"
},
@@ -875,7 +875,7 @@
"level": "0.00",
"capacity": 34.2,
"storageDiff": 0,
"inflow": "8.1",
"inflow": "0.0",
"outflow": "0.1",
"volume": 0.41,
"maxVolume": 1.2,
@@ -906,7 +906,7 @@
"level": "575.70",
"capacity": 70.7,
"storageDiff": -0.9,
"inflow": "11.7",
"inflow": "0.0",
"outflow": "0.0",
"volume": 1.06,
"maxVolume": 1.5,
@@ -968,7 +968,7 @@
"level": "448.81",
"capacity": 100,
"storageDiff": 0.02,
"inflow": "4.7",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.67,
"maxVolume": 0.5,
@@ -999,7 +999,7 @@
"level": "463.42",
"capacity": 4.6,
"storageDiff": -0.61,
"inflow": "6.0",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0,
"maxVolume": 0.3,
@@ -1030,7 +1030,7 @@
"level": "668.40",
"capacity": 100,
"storageDiff": 0,
"inflow": "11.0",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.39,
"maxVolume": 0.3,
@@ -1061,7 +1061,7 @@
"level": "385.01",
"capacity": 100,
"storageDiff": 0.01,
"inflow": "6.3",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.84,
"maxVolume": 0.4,
@@ -1092,7 +1092,7 @@
"level": "678.60",
"capacity": 80,
"storageDiff": 0,
"inflow": "8.2",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.16,
"maxVolume": 0.2,
@@ -1123,7 +1123,7 @@
"level": "632.76",
"capacity": 4.4,
"storageDiff": -0.13,
"inflow": "10.5",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0,
"maxVolume": 0.5,
@@ -1154,7 +1154,7 @@
"level": "635.70",
"capacity": 0,
"storageDiff": -0.66,
"inflow": "5.4",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0,
"maxVolume": 0.7,
@@ -1185,7 +1185,7 @@
"level": "416.72",
"capacity": 90,
"storageDiff": 0.04,
"inflow": "7.8",
"inflow": "0.0",
"outflow": "0.0",
"volume": 0.09,
"maxVolume": 0.1,
@@ -1213,18 +1213,17 @@
"name": "Praha - Malá Chuchle",
"river": "Vltava",
"priority": false,
"level": "49.00",
"level": "46.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "60.9",
"outflow": "53.9",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 50.0294,
"lng": 14.3986,
"sparkline": [
45,
46,
46,
47,
@@ -1235,7 +1234,8 @@
47,
47,
46,
49
49,
46
],
"type": "river"
},
@@ -1279,14 +1279,13 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "8.6",
"outflow": "8.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.9642,
"lng": 14.0792,
"sparkline": [
100,
100,
99,
99,
@@ -1297,6 +1296,7 @@
96,
96,
96,
96,
96
],
"type": "river"
@@ -1341,14 +1341,13 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "4.8",
"outflow": "4.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.3083,
"lng": 14.1436,
"sparkline": [
41,
44,
45,
45,
@@ -1359,6 +1358,7 @@
45,
45,
44,
43,
43
],
"type": "river"
@@ -1383,9 +1383,9 @@
25,
25,
25,
26,
26,
25,
26,
26,
25,
25,
25,
@@ -1399,18 +1399,17 @@
"name": "Bechyně",
"river": "Lužnice",
"priority": false,
"level": "81.00",
"level": "82.00",
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "2.6",
"outflow": "2.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
"lat": 49.2931,
"lng": 14.4758,
"sparkline": [
82,
80,
79,
82,
@@ -1421,7 +1420,8 @@
85,
84,
83,
81
81,
82
],
"type": "river"
},
@@ -1589,7 +1589,7 @@
"capacity": 0,
"storageDiff": 0,
"inflow": "0.0",
"outflow": "4.6",
"outflow": "4.7",
"volume": 0,
"maxVolume": 0,
"navigationForbidden": false,
@@ -1603,10 +1603,10 @@
87,
87,
87,
87,
88,
89,
89,
90,
90
],
"type": "river"
+74 -7
View File
@@ -121,12 +121,34 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
.catch(err => console.error(err));
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
const staticConfig = lakesConfig.find(l => l.id.split('|')[0] === internalId);
fetch(`/data/${internalId}.json?t=${Date.now()}`)
.then(res => res.json())
.then(json => {
let lastValidLevel: number | null = null;
const formattedData = json.map((item: any) => {
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
let level = item.level === null || isNaN(item.level) ? 0 : item.level;
// Outlier/sensor glitch detection
if (level > 0) {
if (staticConfig && staticConfig.minLevel && staticConfig.maxLevel) {
const minAllowed = staticConfig.minLevel - 5;
const maxAllowed = staticConfig.maxLevel + 5;
if (level < minAllowed || level > maxAllowed) {
// Glitch detected, fallback to last known valid level
level = lastValidLevel !== null ? lastValidLevel : (staticConfig.minLevel + staticConfig.maxLevel) / 2;
} else {
lastValidLevel = level;
}
} else {
lastValidLevel = level;
}
} else if (lastValidLevel !== null) {
// Level is 0 or less, fallback
level = lastValidLevel;
}
return {
timestamp: item.timestamp,
@@ -134,7 +156,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
level: item.level === null || isNaN(item.level) ? 0 : item.level,
level: level,
outflow: outflow,
inflow: item.inflow || 0,
volume: item.volume || 0,
@@ -393,13 +415,58 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{/* Data Series */}
{limits && limits.map((limit, idx) => (
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `${limit.labelCs} (${limit.level.toFixed(2)} m n. m.)` : `${limit.labelEn} (${limit.level.toFixed(2)} m a.s.l.)`, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 12 }} />
<ReferenceLine key={idx} yAxisId="left" y={limit.level} stroke={limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b'} strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `${limit.labelCs} (${limit.level.toFixed(2)} m n. m.)` : `${limit.labelEn} (${limit.level.toFixed(2)} m a.s.l.)`, fill: limit.type === 'danger' ? 'var(--color-red)' : '#f59e0b', fontSize: 11 }} />
))}
{!isRiver && staticConfig?.maxLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.maxLevel} stroke="var(--color-orange)" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)` : `Max retention level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`, fill: 'var(--color-orange)', fontSize: 12 }} />
)}
{!isRiver && staticConfig?.storageLevel && (
<ReferenceLine yAxisId="left" y={staticConfig.storageLevel} stroke="#a855f7" strokeDasharray="3 3" label={{ position: 'insideBottomLeft', value: language === 'cs' ? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)} m n. m.)` : `Storage space level (${staticConfig.storageLevel.toFixed(2)} m a.s.l.)`, fill: '#a855f7', fontSize: 12 }} />
{!isRiver && staticConfig?.maxLevel && staticConfig?.storageLevel && Math.abs(staticConfig.maxLevel - staticConfig.storageLevel) < 0.05 ? (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
value: language === 'cs'
? `Max. retenční / zásobní hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)`
: `Max retention / storage level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`,
fill: 'var(--color-orange)',
fontSize: 11
}}
/>
) : (
<>
{!isRiver && staticConfig?.maxLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
stroke="var(--color-orange)"
strokeDasharray="3 3"
label={{
position: 'insideBottomRight',
value: language === 'cs'
? `Maximální retenční hladina (${staticConfig.maxLevel.toFixed(2)} m n. m.)`
: `Max retention level (${staticConfig.maxLevel.toFixed(2)} m a.s.l.)`,
fill: 'var(--color-orange)',
fontSize: 11
}}
/>
)}
{!isRiver && staticConfig?.storageLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.storageLevel}
stroke="#a855f7"
strokeDasharray="3 3"
label={{
position: 'insideBottomLeft',
value: language === 'cs'
? `Hladina zásobního prostoru (${staticConfig.storageLevel.toFixed(2)} m n. m.)`
: `Storage space level (${staticConfig.storageLevel.toFixed(2)} m a.s.l.)`,
fill: '#a855f7',
fontSize: 11
}}
/>
)}
</>
)}
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} />
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-red)" strokeWidth={2} dot={false} isAnimationActive={animate} />