feat: update historical lake sensor data and improve wind chart component rendering

This commit is contained in:
David Fencl
2026-06-08 20:49:01 +02:00
parent 7a7abdd3e5
commit 48b44cd642
57 changed files with 2892 additions and 323 deletions
+270 -46
View File
@@ -36,7 +36,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver }:
const dict = t[language as Language].chart;
if (isWeather) {
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)' }}>
<div className="chart-tooltip" 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>
{payload.map((entry: any, index: number) => {
const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature';
@@ -50,7 +50,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver }:
);
}
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)' }}>
<div className="chart-tooltip" 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>
{[...payload].sort((a: any, b: any) => {
const order = ['level', 'inflow', 'outflow', 'temperature', 'precipitation'];
@@ -98,11 +98,11 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver }:
})}
{payload[0]?.payload?.qn ? (
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid var(--border-color)', fontSize: '0.8rem', color: '#f59e0b', display: 'flex', alignItems: 'center', gap: '4px' }}>
{language === 'cs' ? `Neověřené měření (QN: ${payload[0].payload.qn})` : `Unverified measurement (QN: ${payload[0].payload.qn})`}
{language === 'cs' ? 'Neověřené měření' : 'Unverified measurement'}
</div>
) : (
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid var(--border-color)', fontSize: '0.8rem', color: 'var(--color-green)', display: 'flex', alignItems: 'center', gap: '4px' }}>
{language === 'cs' ? 'Měření ověřeno dispečinkem' : 'Measurement verified'}
{language === 'cs' ? 'Měření ověřeno' : 'Measurement verified'}
</div>
)}
</div>
@@ -117,6 +117,31 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
const [lakeInfo, setLakeInfo] = useState<any>(null);
const [isSmoothed, setIsSmoothed] = useState(true);
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d' | '1y' | 'all'>('7d');
const [visibleSeries, setVisibleSeries] = useState({
level: true,
outflow: true,
inflow: true
});
const [visibleWeatherSeries, setVisibleWeatherSeries] = useState({
temp: true,
precip: true
});
const [isMobile, setIsMobile] = useState(false);
const [leftCustomDomain, setLeftCustomDomain] = useState<[number, number] | null>(null);
const [rightCustomDomain, setRightCustomDomain] = useState<[number, number] | null>(null);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
setLeftCustomDomain(null);
setRightCustomDomain(null);
}, [timeRange, lakeId]);
const dict = t[language].chart;
const topbarDict = t[language].topbar;
@@ -303,6 +328,124 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
avgOutflow24h
};
const getDefaultLeftDomain = (): [number, number] => {
const levels = chartData.map(d => d.level).filter(v => v !== null && v !== undefined && !isNaN(v));
if (levels.length === 0) return [0, 100];
const dataMin = Math.min(...levels);
const dataMax = Math.max(...levels);
if (isRiver) {
return [Math.max(0, Math.floor(dataMin - 10)), Math.ceil(dataMax + 10)];
} else {
let min = dataMin;
if (limits) limits.forEach(l => { if (l.level < min) min = l.level; });
let max = dataMax;
if (staticConfig?.maxLevel && staticConfig.maxLevel > max) max = staticConfig.maxLevel;
if (staticConfig?.storageLevel && staticConfig.storageLevel > max) max = staticConfig.storageLevel;
return [min - 0.5, max + 0.5];
}
};
const getDefaultRightDomain = (): [number, number] => {
const flows = chartData.flatMap(d => [d.outflow, d.inflow]).filter(v => v !== null && v !== undefined && !isNaN(v));
if (flows.length === 0) return [0, 10];
const dataMax = Math.max(...flows);
return [0, Math.max(dataMax, 1)];
};
const handleAxisDragStart = (
e: React.MouseEvent | React.TouchEvent,
axis: 'left' | 'right'
) => {
e.preventDefault();
const isTouchEvent = 'touches' in e;
if (isTouchEvent && e.touches.length === 2) {
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const dist = Math.abs(touch1.clientY - touch2.clientY);
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const onTouchMove = (moveEvent: TouchEvent) => {
if (moveEvent.touches.length === 2) {
moveEvent.preventDefault(); // Stop native page zooming
const mTouch1 = moveEvent.touches[0];
const mTouch2 = moveEvent.touches[1];
const currentDist = Math.abs(mTouch1.clientY - mTouch2.clientY);
if (currentDist > 5) {
const factor = dist / currentDist;
const center = (currentDomain[0] + currentDomain[1]) / 2;
const range = currentDomain[1] - currentDomain[0];
const newMin = center - (range * factor) / 2;
const newMax = center + (range * factor) / 2;
if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]);
} else {
setRightCustomDomain([Math.max(0, newMin), newMax]);
}
}
}
};
const onTouchEnd = () => {
window.removeEventListener('touchmove', onTouchMove);
window.removeEventListener('touchend', onTouchEnd);
};
window.addEventListener('touchmove', onTouchMove, { passive: false });
window.addEventListener('touchend', onTouchEnd);
return;
}
const startY = isTouchEvent ? e.touches[0].clientY : e.clientY;
const currentDomain = axis === 'left'
? (leftCustomDomain || getDefaultLeftDomain())
: (rightCustomDomain || getDefaultRightDomain());
const initialRange = currentDomain[1] - currentDomain[0];
const center = (currentDomain[1] + currentDomain[0]) / 2;
const onMove = (moveEvent: MouseEvent | TouchEvent) => {
if ('touches' in moveEvent) {
moveEvent.preventDefault(); // Stop native page scrolling
}
const clientY = 'touches' in moveEvent ? moveEvent.touches[0].clientY : moveEvent.clientY;
const deltaY = clientY - startY;
const factor = Math.pow(2.5, deltaY / 150);
const newMin = center - (initialRange * factor) / 2;
const newMax = center + (initialRange * factor) / 2;
if (axis === 'left') {
setLeftCustomDomain([newMin, newMax]);
} else {
setRightCustomDomain([Math.max(0, newMin), newMax]);
}
};
const onEnd = () => {
if (isTouchEvent) {
window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onEnd);
} else {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onEnd);
}
};
if (isTouchEvent) {
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('touchend', onEnd);
} else {
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onEnd);
}
};
const leftYAxisDomain = isRiver
? [
(dataMin: number) => Math.max(0, Math.floor(dataMin - 10)),
@@ -361,36 +504,45 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
</>
)}
<div style={{ color: 'var(--text-muted)', fontSize: '0.9rem', marginTop: '-0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<div style={{
color: 'var(--text-muted)',
fontSize: isMobile ? '0.75rem' : '0.9rem',
marginTop: isMobile ? '-1.1rem' : '-0.5rem',
marginBottom: isMobile ? '-0.5rem' : '0',
display: 'flex',
alignItems: 'center',
gap: isMobile ? '0.4rem' : '0.75rem',
flexWrap: 'wrap'
}}>
<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>
{latestData.qn ? (
<span style={{
fontSize: '0.75rem',
padding: '0.15rem 0.4rem',
fontSize: isMobile ? '0.65rem' : '0.75rem',
padding: isMobile ? '0.1rem 0.3rem' : '0.15rem 0.4rem',
borderRadius: '4px',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
color: '#f59e0b',
border: '1px solid rgba(245, 158, 11, 0.2)',
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem'
gap: '0.2rem'
}}>
{language === 'cs' ? `Neověřená data (QN: ${latestData.qn})` : `Unverified data (QN: ${latestData.qn})`}
{language === 'cs' ? 'Neověřená data' : 'Unverified data'}
</span>
) : (
<span style={{
fontSize: '0.75rem',
padding: '0.15rem 0.4rem',
fontSize: isMobile ? '0.65rem' : '0.75rem',
padding: isMobile ? '0.1rem 0.3rem' : '0.15rem 0.4rem',
borderRadius: '4px',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
color: 'var(--color-green)',
border: '1px solid rgba(34, 197, 94, 0.2)',
display: 'inline-flex',
alignItems: 'center',
gap: '0.25rem'
gap: '0.2rem'
}}>
{language === 'cs' ? 'Ověřená data dispečinkem' : 'Data verified by dispatch'}
{language === 'cs' ? 'Měření ověřeno' : 'Data verified'}
</span>
)}
</div>
@@ -426,38 +578,56 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
{/* CHART SECTION */}
<div className="chart-card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.25rem', flexWrap: 'wrap', gap: '1rem', padding: isMobile ? '0 0.75rem' : '0' }}>
<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 }}>
<div className="top-time-controls" style={{ margin: 0, display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '0.5rem' }}>
<button className={timeRange === '24h' ? 'active' : ''} onClick={() => setTimeRange('24h')}>24h</button>
<button className={timeRange === '7d' ? 'active' : ''} onClick={() => setTimeRange('7d')}>7d</button>
<button className={timeRange === '30d' ? 'active' : ''} onClick={() => setTimeRange('30d')}>30d</button>
<button className={timeRange === '1y' ? 'active' : ''} onClick={() => setTimeRange('1y')}>{dict.year}</button>
<button className={timeRange === 'all' ? 'active' : ''} onClick={() => setTimeRange('all')}>{dict.all}</button>
{(leftCustomDomain !== null || rightCustomDomain !== null) && (
<button
onClick={() => { setLeftCustomDomain(null); setRightCustomDomain(null); }}
style={{
backgroundColor: 'rgba(239, 68, 68, 0.15)',
color: '#ef4444',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '0.375rem',
padding: '0.4rem 0.75rem',
fontSize: '0.85rem',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s'
}}
>
Reset
</button>
)}
</div>
</div>
<div style={{ flex: 1, minHeight: '300px', width: '100%', marginTop: '1rem' }}>
<div style={{ flex: 1, minHeight: '300px', width: '100%', marginTop: '0', position: 'relative' }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 20, right: 0, left: 10, bottom: 0 }}>
<ComposedChart data={chartData} margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, 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={leftYAxisDomain as any} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(isRiver ? 0 : 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}} />
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: isMobile ? 10 : 12}} minTickGap={50} />
<YAxis yAxisId="left" domain={leftCustomDomain || (leftYAxisDomain as any)} stroke={visibleSeries.level ? "var(--text-muted)" : "transparent"} tick={{fill: visibleSeries.level ? 'var(--text-muted)' : 'transparent', fontSize: isMobile ? 10 : 12}} tickLine={visibleSeries.level ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} axisLine={visibleSeries.level ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} width={isMobile ? 42 : 60} tickFormatter={(v) => v.toFixed(isRiver ? 0 : 2)} />
<YAxis yAxisId="right" orientation="right" domain={rightCustomDomain || [0, (dataMax: number) => Math.max(dataMax, 1)]} stroke={(visibleSeries.outflow || visibleSeries.inflow) ? "var(--text-muted)" : "transparent"} tick={{fill: (visibleSeries.outflow || visibleSeries.inflow) ? 'var(--text-muted)' : 'transparent', fontSize: isMobile ? 10 : 12}} tickLine={(visibleSeries.outflow || visibleSeries.inflow) ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} axisLine={(visibleSeries.outflow || visibleSeries.inflow) ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} width={isMobile ? 35 : 60} tickFormatter={(v) => v.toFixed(1)} />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<Tooltip content={<CustomTooltip language={language} isRiver={isRiver} />} />
{/* 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: 11 }} />
{visibleSeries.level && 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: 11, className: 'chart-ref-label' }} />
))}
{!isRiver && staticConfig?.maxLevel && staticConfig?.storageLevel && Math.abs(staticConfig.maxLevel - staticConfig.storageLevel) < 0.05 ? (
{visibleSeries.level && !isRiver && staticConfig?.maxLevel && staticConfig?.storageLevel && Math.abs(staticConfig.maxLevel - staticConfig.storageLevel) < 0.05 ? (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
@@ -469,12 +639,13 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
? `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
fontSize: 11,
className: 'chart-ref-label'
}}
/>
) : (
<>
{!isRiver && staticConfig?.maxLevel && (
{visibleSeries.level && !isRiver && staticConfig?.maxLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.maxLevel}
@@ -486,11 +657,12 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
? `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
fontSize: 11,
className: 'chart-ref-label'
}}
/>
)}
{!isRiver && staticConfig?.storageLevel && (
{visibleSeries.level && !isRiver && staticConfig?.storageLevel && (
<ReferenceLine
yAxisId="left"
y={staticConfig.storageLevel}
@@ -502,31 +674,71 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
? `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
fontSize: 11,
className: 'chart-ref-label'
}}
/>
)}
</>
)}
<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} />
{!isRiver && <Line yAxisId="right" type={curveType} dataKey="inflow" stroke="var(--color-green)" strokeWidth={2} dot={false} isAnimationActive={animate} />}
<Area yAxisId="left" type={curveType} dataKey="level" stroke="var(--color-cyan)" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" isAnimationActive={animate} hide={!visibleSeries.level} />
<Line yAxisId="right" type={curveType} dataKey="outflow" stroke="var(--color-red)" strokeWidth={2} dot={false} isAnimationActive={animate} hide={!visibleSeries.outflow} />
{!isRiver && <Line yAxisId="right" type={curveType} dataKey="inflow" stroke="var(--color-green)" strokeWidth={2} dot={false} isAnimationActive={animate} hide={!visibleSeries.inflow} />}
</ComposedChart>
</ResponsiveContainer>
<div
onMouseDown={(e) => handleAxisDragStart(e, 'left')}
onTouchStart={(e) => handleAxisDragStart(e, 'left')}
style={{
position: 'absolute',
left: 0,
top: 0,
bottom: isMobile ? 25 : 35,
width: isMobile ? 42 : 60,
cursor: 'ns-resize',
zIndex: 10,
background: 'transparent'
}}
title={language === 'cs' ? 'Táhněte pro změnu měřítka osy hladiny' : 'Drag to scale level axis'}
/>
<div
onMouseDown={(e) => handleAxisDragStart(e, 'right')}
onTouchStart={(e) => handleAxisDragStart(e, 'right')}
style={{
position: 'absolute',
right: 0,
top: 0,
bottom: isMobile ? 25 : 35,
width: isMobile ? 35 : 60,
cursor: 'ns-resize',
zIndex: 10,
background: 'transparent'
}}
title={language === 'cs' ? 'Táhněte pro změnu měřítka osy průtoku' : 'Drag to scale flow axis'}
/>
</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 className="chart-legend-container" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '1.5rem', marginTop: '1rem', fontSize: '0.85rem', color: 'var(--text-main)', padding: isMobile ? '0 0.75rem' : '0' }}>
<span
onClick={() => setVisibleSeries(prev => ({ ...prev, level: !prev.level }))}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: visibleSeries.level ? 1 : 0.4, transition: 'opacity 0.2s', userSelect: 'none' }}
>
<div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-cyan)' }}></div>
{isRiver ? (language === 'cs' ? 'Vodní stav' : 'Water level') : dict.level}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span
onClick={() => setVisibleSeries(prev => ({ ...prev, outflow: !prev.outflow }))}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: visibleSeries.outflow ? 1 : 0.4, transition: 'opacity 0.2s', userSelect: 'none' }}
>
<div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-red)' }}></div>
{isRiver ? (language === 'cs' ? 'Průtok' : 'Flow') : dict.outflow}
</span>
{!isRiver && (
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span
onClick={() => setVisibleSeries(prev => ({ ...prev, inflow: !prev.inflow }))}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: visibleSeries.inflow ? 1 : 0.4, transition: 'opacity 0.2s', userSelect: 'none' }}
>
<div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-green)' }}></div>
{dict.inflow}
</span>
@@ -534,29 +746,41 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
</div>
{/* WEATHER CHART SECTION */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', marginTop: '2rem', flexWrap: 'wrap', gap: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.25rem', marginTop: '1.5rem', flexWrap: 'wrap', gap: '1rem', padding: isMobile ? '0 0.75rem' : '0' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)' }}>{language === 'cs' ? 'Počasí (Teplota a Srážky)' : 'Weather (Temperature & Precipitation)'}</h3>
</div>
<div style={{ flex: 1, minHeight: '200px', width: '100%', marginTop: '0.5rem' }}>
<div style={{ flex: 1, minHeight: '200px', width: '100%', marginTop: '0' }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 10, right: 0, left: 10, bottom: 0 }}>
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} minTickGap={50} />
<YAxis yAxisId="temp" domain={['auto', 'auto']} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} tickFormatter={(v) => v.toFixed(1)} />
<YAxis yAxisId="precip" orientation="right" domain={[0, 'auto']} stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: 12}} />
<ComposedChart data={chartData} margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, right: 0, left: 10, bottom: 0 }}>
<XAxis dataKey="date" stroke="var(--text-muted)" tick={{fill: 'var(--text-muted)', fontSize: isMobile ? 10 : 12}} minTickGap={50} />
<YAxis yAxisId="temp" domain={['auto', 'auto']} stroke={visibleWeatherSeries.temp ? "var(--text-muted)" : "transparent"} tick={{fill: visibleWeatherSeries.temp ? 'var(--text-muted)' : 'transparent', fontSize: isMobile ? 10 : 12}} tickLine={visibleWeatherSeries.temp ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} axisLine={visibleWeatherSeries.temp ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} width={isMobile ? 42 : 60} tickFormatter={(v) => v.toFixed(1)} />
<YAxis yAxisId="precip" orientation="right" domain={[0, 'auto']} stroke={visibleWeatherSeries.precip ? "var(--text-muted)" : "transparent"} tick={{fill: visibleWeatherSeries.precip ? 'var(--text-muted)' : 'transparent', fontSize: isMobile ? 10 : 12}} tickLine={visibleWeatherSeries.precip ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} axisLine={visibleWeatherSeries.precip ? { stroke: 'var(--text-muted)' } : { stroke: 'transparent' }} width={isMobile ? 35 : 60} />
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<Tooltip content={<CustomTooltip language={language} isWeather={true} />} />
<Bar yAxisId="precip" dataKey="precipitation" fill="var(--color-cyan)" fillOpacity={0.6} isAnimationActive={animate} />
<Line yAxisId="temp" type={curveType} dataKey="temperature" stroke="var(--color-red)" strokeWidth={2} dot={true} isAnimationActive={animate} />
<Bar yAxisId="precip" dataKey="precipitation" fill="var(--color-cyan)" fillOpacity={0.6} isAnimationActive={animate} hide={!visibleWeatherSeries.precip} />
<Line yAxisId="temp" type={curveType} dataKey="temperature" stroke="var(--color-red)" strokeWidth={2} dot={true} isAnimationActive={animate} hide={!visibleWeatherSeries.temp} />
</ComposedChart>
</ResponsiveContainer>
</div>
<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-red)' }}></div> {language === 'cs' ? 'Teplota vzduchu' : 'Temperature'} [°C]</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}><div style={{ width: '12px', height: '12px', backgroundColor: 'var(--color-cyan)', opacity: 0.6 }}></div> {language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]</span>
<div className="chart-legend-container" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '1rem', marginTop: '1rem', fontSize: '0.85rem', color: 'var(--text-main)', padding: isMobile ? '0 0.75rem' : '0' }}>
<span
onClick={() => setVisibleWeatherSeries(prev => ({ ...prev, temp: !prev.temp }))}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: visibleWeatherSeries.temp ? 1 : 0.4, transition: 'opacity 0.2s', userSelect: 'none' }}
>
<div style={{ width: '12px', height: '4px', backgroundColor: 'var(--color-red)' }}></div>
{language === 'cs' ? 'Teplota vzduchu' : 'Temperature'} [°C]
</span>
<span
onClick={() => setVisibleWeatherSeries(prev => ({ ...prev, precip: !prev.precip }))}
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', opacity: visibleWeatherSeries.precip ? 1 : 0.4, transition: 'opacity 0.2s', userSelect: 'none' }}
>
<div style={{ width: '12px', height: '12px', backgroundColor: 'var(--color-cyan)', opacity: 0.6 }}></div>
{language === 'cs' ? 'Srážky' : 'Precipitation'} [mm]
</span>
</div>
{/* Wind Chart placed inside the main card below the weather graph */}
+26 -10
View File
@@ -81,6 +81,14 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
const [loading, setLoading] = useState(true);
const [currentSpeed, setCurrentSpeed] = useState(0);
const [maxGust, setMaxGust] = useState(0);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768);
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const fetchWind = async () => {
@@ -182,15 +190,21 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
if (data.length === 0) return null;
return (
<div style={{ marginTop: '3rem', paddingTop: '1rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem' }}>
<div style={{ marginTop: '1.5rem', paddingTop: '0.5rem', display: 'flex', flexDirection: 'column', gap: '0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem', padding: isMobile ? '0 0.75rem' : '0', marginBottom: '0.25rem' }}>
<h3 style={{ margin: 0, fontSize: '1.1rem', color: 'var(--text-main)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<FiWind style={{ color: 'var(--color-cyan)' }} />
{language === 'cs' ? `Aktivita větru (${timeRange === '1y' || timeRange === 'all' ? 'denní maxima' : timeRange})` : `Wind Activity (${timeRange === '1y' || timeRange === 'all' ? 'daily max' : timeRange})`}
</h3>
<div style={{ display: 'flex', gap: '1.5rem' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{
display: 'flex',
gap: isMobile ? '0' : '1.5rem',
justifyContent: isMobile ? 'space-around' : 'flex-end',
width: isMobile ? '100%' : 'auto',
marginTop: isMobile ? '0.5rem' : '0'
}}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Aktuální rychlost' : 'Current Speed'}</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{currentSpeed.toFixed(1)}</span>
@@ -198,7 +212,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{language === 'cs' ? 'Max. nárazy' : 'Peak Gusts'}</span>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.25rem' }}>
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{maxGust.toFixed(1)}</span>
@@ -208,9 +222,9 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
</div>
</div>
<div style={{ flex: 1, minHeight: '280px', width: '100%', marginTop: '0.5rem' }}>
<div style={{ flex: 1, minHeight: '280px', width: '100%', marginTop: '0' }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data} margin={{ top: 20, right: 0, left: -20, bottom: 0 }}>
<ComposedChart data={data} margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="colorWind" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-cyan)" stopOpacity={0.4}/>
@@ -220,7 +234,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
<XAxis
dataKey="time"
stroke="var(--text-muted)"
tick={{fill: 'var(--text-muted)', fontSize: 11}}
tick={{fill: 'var(--text-muted)', fontSize: isMobile ? 10 : 11}}
minTickGap={60}
tickFormatter={(v) => {
const d = new Date(v);
@@ -229,7 +243,9 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
/>
<YAxis
stroke="var(--text-muted)"
tick={{fill: 'var(--text-muted)', fontSize: 11}}
tick={{fill: 'var(--text-muted)', fontSize: isMobile ? 10 : 11}}
width={isMobile ? 35 : 60}
tickFormatter={(v) => v.toFixed(1)}
/>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" vertical={false} />
<Tooltip content={<CustomWindTooltip language={language} windUnit={windUnit} />} />
@@ -258,7 +274,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
</ResponsiveContainer>
</div>
<div style={{ display: 'flex', justifyContent: 'center', gap: '1.5rem', fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'center', gap: '1.5rem', fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.5rem', padding: isMobile ? '0 0.75rem' : '0' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ display: 'inline-block', width: '12px', height: '3px', backgroundColor: 'var(--color-cyan)' }}></span>
<span>{language === 'cs' ? 'Rychlost větru' : 'Wind Speed'}</span>