178 lines
8.6 KiB
TypeScript
178 lines
8.6 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, ComposedChart, ReferenceLine } from 'recharts';
|
|
import { type Language, t } from '../translations';
|
|
import KpiCards from './KpiCards';
|
|
|
|
interface LipnoData {
|
|
timestamp: string;
|
|
date: string;
|
|
level: number;
|
|
inflow: number;
|
|
outflow: number;
|
|
volume: number;
|
|
fullness: number;
|
|
}
|
|
|
|
interface Props {
|
|
language: Language;
|
|
lakeId: string | null;
|
|
}
|
|
|
|
const CustomTooltip = ({ active, payload, label, language }: any) => {
|
|
if (active && payload && payload.length) {
|
|
const dict = t[language].chart;
|
|
return (
|
|
<div style={{ backgroundColor: 'var(--bg-card)', padding: '1rem', border: '1px solid var(--border-color)', borderRadius: '0.5rem', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)' }}>
|
|
<p style={{ margin: '0 0 0.5rem 0', fontWeight: 'bold', color: 'var(--text-main)' }}>{label}</p>
|
|
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.level}: <span style={{ fontWeight: 'bold' }}>{payload[0].value.toFixed(2)} m n. m.</span></p>
|
|
<p style={{ margin: 0, color: 'var(--text-main)' }}>{dict.outflow}: <span style={{ fontWeight: 'bold' }}>{payload[1].value.toFixed(1)} m³/s</span></p>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const LakeDetail = ({ language, lakeId }: Props) => {
|
|
const [data, setData] = useState<LipnoData[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [lakeInfo, setLakeInfo] = useState<any>(null);
|
|
const [isSmoothed, setIsSmoothed] = useState(true);
|
|
const dict = t[language].chart;
|
|
const topbarDict = t[language].topbar;
|
|
|
|
useEffect(() => {
|
|
fetch('/data/lakes_index.json')
|
|
.then(res => res.json())
|
|
.then(indexData => {
|
|
const found = indexData.find((l: any) => l.id === lakeId);
|
|
setLakeInfo(found || { name: 'Lipno 1', river: 'Vltava' });
|
|
})
|
|
.catch(err => console.error(err));
|
|
|
|
const internalId = lakeId ? lakeId.split('|')[0] : 'VLL1';
|
|
|
|
fetch(`/data/${internalId}.json`)
|
|
.then(res => res.json())
|
|
.then(json => {
|
|
const formattedData = json.map((item: any) => {
|
|
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
|
|
|
|
return {
|
|
timestamp: item.timestamp,
|
|
date: new Date(item.timestamp).toLocaleString(language === 'cs' ? 'cs-CZ' : 'en-GB', {
|
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
|
hour: '2-digit', minute: '2-digit'
|
|
}),
|
|
level: item.level === null || isNaN(item.level) ? 0 : item.level,
|
|
outflow: outflow,
|
|
inflow: item.inflow || 0,
|
|
volume: item.volume || 0,
|
|
fullness: 0
|
|
};
|
|
});
|
|
setData(formattedData);
|
|
setLoading(false);
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load data', err);
|
|
setLoading(false);
|
|
});
|
|
}, [language, lakeId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: 'var(--bg-dark)', color: 'var(--text-main)' }}>
|
|
<div style={{ fontSize: '1.25rem' }}>Loading HLADINATOR...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const latestData = data[data.length - 1] || { level: 0, inflow: 0, outflow: 0, volume: 0, fullness: 0 };
|
|
const curveType = isSmoothed ? 'monotone' : 'linear';
|
|
|
|
// Find the last record that actually has flow data (often the very last record is incomplete on PVL)
|
|
const lastValidFlowData = [...data].reverse().find(d => d.outflow > 0) || latestData;
|
|
|
|
const kpiData = {
|
|
level: latestData.level,
|
|
inflow: lastValidFlowData.inflow,
|
|
outflow: lastValidFlowData.outflow,
|
|
volume: lakeInfo?.volume || 0,
|
|
fullness: lakeInfo?.capacity || 0
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }}>
|
|
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginTop: '-0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
<span>{topbarDict.updated} {new Date().toLocaleDateString(language === 'cs' ? 'cs-CZ' : 'en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}, {new Date().toLocaleTimeString(language === 'cs' ? 'cs-CZ' : 'en-GB', { hour: '2-digit', minute: '2-digit' })} UTC</span>
|
|
<div className="status-dot"></div>
|
|
</div>
|
|
|
|
<KpiCards data={kpiData} language={language} lakeName={lakeInfo ? lakeInfo.name : 'Lipno 1'} />
|
|
|
|
{/* CHART SECTION */}
|
|
<div className="chart-card">
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
|
|
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)' }}>{dict.title} {lakeInfo ? `${lakeInfo.name} ${lakeInfo.river ? `- ${lakeInfo.river}` : ''}` : 'Lipno 1 - Vltava'}</h3>
|
|
<div className="top-time-controls" style={{ margin: 0 }}>
|
|
<button className="active">24h</button>
|
|
<button>7d</button>
|
|
<button>30d</button>
|
|
<button>{dict.year}</button>
|
|
<button>{dict.all}</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ flex: 1, minHeight: '300px', width: '100%', marginTop: '1rem' }}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart data={data} margin={{ top: 20, right: 0, left: 10, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id="colorLevel" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.5}/>
|
|
<stop offset="95%" stopColor="var(--color-cyan)" stopOpacity={0}/>
|
|
</linearGradient>
|
|
</defs>
|
|
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} minTickGap={50} />
|
|
<YAxis yAxisId="left" domain={['dataMin - 0.5', 'dataMax + 0.5']} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(2)} />
|
|
<YAxis yAxisId="right" orientation="right" domain={[0, (dataMax: number) => Math.max(dataMax, 1)]} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} />
|
|
|
|
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
|
|
<Tooltip content={<CustomTooltip language={language} />} />
|
|
|
|
{/* Data Series */}
|
|
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" />
|
|
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-orange)" strokeWidth={2} dot={false} />
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Chart Legend */}
|
|
<div className="chart-legend-container" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '1rem', marginTop: '1rem', fontSize: '0.85rem', color: 'var(--text-main)' }}>
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-cyan)' }}></div> {dict.level}</span>
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-orange)' }}></div> {dict.outflow}</span>
|
|
</div>
|
|
|
|
{/* Smoothed Toggle Control */}
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '1rem', marginTop: '2rem', marginBottom: '1rem' }}>
|
|
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>{dict.view}</span>
|
|
<div style={{ display: 'flex', alignItems: 'center', fontSize: '0.9rem' }}>
|
|
<span style={{ color: !isSmoothed ? 'var(--text-main)' : 'var(--text-muted)', transition: '0.2s', marginRight: '0.5rem' }}>{dict.raw}</span>
|
|
<div
|
|
className={`toggle-switch ${isSmoothed ? 'on' : ''}`}
|
|
onClick={() => setIsSmoothed(!isSmoothed)}
|
|
></div>
|
|
<span style={{ color: isSmoothed ? 'var(--text-main)' : 'var(--text-muted)', transition: '0.2s', marginLeft: '0.5rem' }}>{dict.smoothed}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="dashboard-footer" style={{ marginTop: '0' }}>
|
|
<span>{dict.dataSources} <a href="https://www.chmi.cz/" target="_blank" rel="noreferrer">ČHMÚ</a>, <a href="https://www.pvl.cz/" target="_blank" rel="noreferrer">pvl.cz</a></span>
|
|
<span>{dict.createdIn}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LakeDetail;
|