feat: update lake water data and optimize visual components for real-time monitoring

This commit is contained in:
David Fencl
2026-06-08 22:32:10 +02:00
parent 8fe39b7ab0
commit 4939d1c5dc
60 changed files with 3255 additions and 380 deletions
+9 -5
View File
@@ -4,9 +4,11 @@ interface Props {
value: number;
size?: number;
strokeWidth?: number;
hideText?: boolean;
color?: string;
}
export const CircularProgress: React.FC<Props> = ({ value, size = 60, strokeWidth = 6 }) => {
export const CircularProgress: React.FC<Props> = ({ value, size = 60, strokeWidth = 6, hideText = false, color = 'var(--color-cyan)' }) => {
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - (value / 100) * circumference;
@@ -23,7 +25,7 @@ export const CircularProgress: React.FC<Props> = ({ value, size = 60, strokeWidt
cy={size / 2}
/>
<circle
stroke="var(--color-cyan)"
stroke={color}
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
@@ -34,9 +36,11 @@ export const CircularProgress: React.FC<Props> = ({ value, size = 60, strokeWidt
transform={`rotate(-90 ${size / 2} ${size / 2})`}
/>
</svg>
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.25, fontWeight: 'bold' }}>
{value > 0 ? `${value.toFixed(1)}%` : 'N/A'}
</div>
{!hideText && (
<div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: size * 0.25, fontWeight: 'bold' }}>
{value > 0 ? `${value.toFixed(1)}%` : 'N/A'}
</div>
)}
</div>
);
};
+120 -40
View File
@@ -12,9 +12,12 @@ interface KpiData {
inflow: number;
outflow: number;
volume: number;
currentVolume?: number;
fullness: number;
storageDiff?: number;
minDiff?: number;
minDiffLabelCs?: string;
minDiffLabelEn?: string;
avgInflow24h?: number;
avgOutflow24h?: number;
}
@@ -28,9 +31,28 @@ interface Props {
const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Props) => {
const [showTooltip, setShowTooltip] = useState(false);
const [showMinTooltip, setShowMinTooltip] = useState(false);
const dict = t[language].kpi;
const flowDiff = data.inflow - data.outflow;
// Graf: pokud přibývá → přítok vs průměr přítoku; pokud ubývá → odtok vs průměr odtoku
let visualFlowValue = 0;
if (flowDiff >= 0) {
// Voda přibývá → jak velký je přítok vůči průměru
if (data.avgInflow24h && data.avgInflow24h > 0) {
visualFlowValue = Math.min(100, (data.inflow / data.avgInflow24h) * 100);
} else if (data.inflow > 0) {
visualFlowValue = 50;
}
} else {
// Voda ubývá → jak velký je odtok vůči průměru
if (data.avgOutflow24h && data.avgOutflow24h > 0) {
visualFlowValue = Math.min(100, (data.outflow / data.avgOutflow24h) * 100);
} else if (data.outflow > 0) {
visualFlowValue = 50;
}
}
useEffect(() => {
if (showTooltip) {
const timer = setTimeout(() => {
@@ -40,6 +62,15 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
}
}, [showTooltip]);
useEffect(() => {
if (showMinTooltip) {
const timer = setTimeout(() => {
setShowMinTooltip(false);
}, 3500);
return () => clearTimeout(timer);
}
}, [showMinTooltip]);
if (isRiver) {
return (
<>
@@ -90,8 +121,8 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
)}
</div>
<div style={{
width: '70px', height: '70px', borderRadius: '50%',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
width: '70px', height: '70px', borderRadius: '50%',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
border: '2px dashed var(--color-cyan)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--color-cyan)', flexShrink: 0
@@ -163,41 +194,38 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
</div>
)}
</div>
{/* Flow Circle */}
<div style={{
width: '70px',
height: '70px',
borderRadius: '50%',
border: `4px solid ${flowDiff >= 0 ? 'rgba(74, 222, 128, 0.2)' : 'rgba(248, 113, 113, 0.2)'}`,
borderTopColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
borderRightColor: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'rotate(-45deg)',
flexShrink: 0
}}>
<span style={{ transform: 'rotate(45deg)', color: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', textAlign: 'center', lineHeight: 1.2 }}>
<div style={{ fontSize: '0.8rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</div>
<div style={{ fontSize: '0.6rem', opacity: 0.8 }}>m³/s</div>
</span>
{/* Flow Gauge using CircularProgress */}
<div style={{ position: 'relative', width: '70px', height: '70px', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ position: 'absolute', top: 0, left: 0 }}>
<CircularProgress
value={visualFlowValue || 0.1}
size={70}
strokeWidth={6}
hideText={true}
color={flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)'}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', color: flowDiff >= 0 ? 'var(--color-green)' : 'var(--color-red)', fontWeight: 'bold', lineHeight: 1.2 }}>
<span style={{ fontSize: '0.85rem' }}>{flowDiff > 0 ? '+' : ''}{flowDiff.toFixed(1)}</span>
<span style={{ fontSize: '0.6rem', opacity: 0.8 }}>m³/s</span>
</div>
</div>
</div>
</div>
{/* CARD 3: CAPACITY */}
<div className="kpi-card">
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.4rem', position: 'relative' }}>
{dict.fullness}
<span
<span
onClick={() => setShowTooltip(!showTooltip)}
style={{ cursor: 'pointer', fontSize: '0.85rem', opacity: 0.6, padding: '0 4px' }}
>
</span>
{showTooltip && (
<div
<div
onClick={() => setShowTooltip(false)}
style={{
position: 'absolute',
@@ -221,25 +249,77 @@ const KpiCards = ({ data, language, lakeName = 'Lipno 1', isRiver = false }: Pro
</div>
)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', flex: 1, minWidth: 0, paddingRight: '0.5rem' }}>
<div style={{ fontSize: '1.7rem', fontWeight: 'bold', lineHeight: 1, marginBottom: '0.5rem', whiteSpace: 'nowrap', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)' }}>
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)} m` : `${data.storageDiff.toFixed(2)} m`) : (data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A')}
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
{dict.volume}: {data.volume.toFixed(1)} <span style={{ fontSize: '0.7rem' }}>mil. m³</span>
</div>
{data.minDiff !== undefined && (
<div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
{language === 'cs' ? 'K minimu:' : 'To min:'} <span style={{ color: data.minDiff < 0.5 ? 'var(--color-red)' : 'var(--color-green)' }}>{data.minDiff.toFixed(2)} m</span>
</div>
)}
<div style={{ position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center', height: '130px', marginTop: '-1rem' }}>
{/* Circular Progress Ring */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '130px', height: '130px', zIndex: 1 }}>
<CircularProgress value={data.fullness} size={130} strokeWidth={10} hideText={true} />
</div>
<div style={{ flexShrink: 0 }}>
<CircularProgress value={data.fullness} size={80} strokeWidth={8} />
{/* Percentage Text */}
<div style={{ position: 'absolute', top: '24px', left: '50%', transform: 'translateX(-50%)', zIndex: 10, fontSize: '0.95rem', fontWeight: 'bold', color: 'var(--text-main)', textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>
{data.fullness > 0 ? `${data.fullness.toFixed(1)}%` : 'N/A'}
</div>
{/* Center Data: Main Level Difference */}
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', marginTop: '-4px', zIndex: 10, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<span style={{ fontSize: '1.9rem', fontWeight: 'bold', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)', lineHeight: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>
{data.storageDiff !== undefined && data.storageDiff !== 0 ? (data.storageDiff > 0 ? `+${data.storageDiff.toFixed(2)}` : `${data.storageDiff.toFixed(2)}`) : ''}
</span>
<span style={{ position: 'absolute', left: '100%', bottom: '0.15rem', marginLeft: '0.2rem', fontSize: '0.8rem', color: data.storageDiff && data.storageDiff < 0 ? 'var(--color-red)' : 'var(--color-cyan)', whiteSpace: 'nowrap' }}>m</span>
</div>
{/* Bottom Inside Data: Min Diff */}
{data.minDiff !== undefined && (
<div
style={{ position: 'absolute', bottom: '26px', left: '50%', transform: 'translateX(-50%)', zIndex: 20, fontSize: '0.9rem', fontWeight: 'bold', color: data.minDiff < 0.5 ? 'var(--color-red)' : 'var(--color-green)', cursor: 'pointer', textShadow: '0 2px 10px rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', gap: '0.25rem' }}
onClick={() => setShowMinTooltip(!showMinTooltip)}
>
<span>{data.minDiff.toFixed(2)} m</span>
<span style={{ fontSize: '0.75rem', opacity: 0.7, fontWeight: 'normal' }}></span>
{showMinTooltip && (
<div
onClick={(e) => { e.stopPropagation(); setShowMinTooltip(false); }}
style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginTop: '8px',
backgroundColor: 'var(--bg-card)',
border: '1px solid var(--border-color)',
padding: '0.75rem',
borderRadius: '8px',
width: '220px',
zIndex: 100,
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
color: 'var(--text-main)',
fontSize: '0.85rem',
lineHeight: 1.4,
cursor: 'pointer',
whiteSpace: 'normal',
textAlign: 'center',
fontWeight: 'normal',
textShadow: 'none'
}}>
{language === 'cs' ? (data.minDiffLabelCs || 'K minimu') : (data.minDiffLabelEn || 'To min')}
</div>
)}
</div>
)}
</div>
{/* Bottom Elements */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', marginTop: '0.2rem' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.4rem', fontSize: '0.85rem', whiteSpace: 'nowrap' }}>
<span style={{ color: 'var(--text-muted)' }}>{dict.volume}:</span>
<span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>
{data.currentVolume !== undefined && data.volume > 0 ? `${data.currentVolume.toFixed(1)} / ` : ''}{data.volume.toFixed(1)} mil. m³
</span>
</div>
</div>
</div>
</>
);
+5 -2
View File
@@ -351,10 +351,13 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
levelDiff30d,
inflow: lastValidFlowData.inflow,
outflow: lastValidFlowData.outflow,
volume: lakeInfo?.volume || 0,
volume: lakeInfo?.maxVolume || lakeInfo?.volume || 0,
currentVolume: latestData.volume,
fullness: lakeInfo?.capacity || 0,
storageDiff: lakeInfo?.storageDiff,
minDiff: staticConfig?.minLevel ? latestData.level - staticConfig.minLevel : undefined,
minDiff: (limits && limits.length > 0) ? latestData.level - limits[0].level : (staticConfig?.minLevel ? latestData.level - staticConfig.minLevel : undefined),
minDiffLabelCs: limits && limits.length > 0 ? limits[0].labelCs : undefined,
minDiffLabelEn: limits && limits.length > 0 ? limits[0].labelEn : undefined,
avgInflow24h,
avgOutflow24h
};
+1 -1
View File
@@ -151,7 +151,7 @@ const LakesOverview = ({ language }: Props) => {
const loadData = () => {
fetch(`/data/lakes_index.json?t=${Date.now()}`)
.then(res => res.json())
.then(data => setLakes(data))
.then(data => setLakes(data.filter((l: Lake & { type?: string }) => l.type !== 'river')))
.catch(err => console.error(err));
};
+64 -43
View File
@@ -99,55 +99,76 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh'
<div className="kpi-card" style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>{dict.title}</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem', alignItems: 'center' }}>
<div style={{ position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center', height: '170px', marginTop: '-1.5rem' }}>
{/* Left Column: Wind */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}>
<div style={{
width: '40px', height: '40px', borderRadius: '50%', backgroundColor: 'rgba(0, 195, 255, 0.1)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--color-cyan)', fontSize: '1.2rem',
transform: `rotate(${data.windDir}deg)`
}} title={`Wind direction: ${data.windDir}°`}>
<FiWind style={{ transform: 'rotate(-90deg)' }} /> {/* Assume icon points UP by default, wind from south (180) should point UP. Arrow should point where wind is GOING. */}
</div>
{/* SVG Compass Ring */}
<svg width="180" height="180" viewBox="0 0 260 260" style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
<circle cx="130" cy="130" r="100" fill="transparent" stroke="rgba(255,255,255,0.03)" strokeWidth="30" />
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: '4px' }}>
{data.windSpeed.toFixed(1)}
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', fontWeight: 'normal' }}>
{windUnit === 'kmh' ? 'km/h' : 'm/s'} {getCompassDirection(data.windDir, language)}
</span>
<svg
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ color: 'var(--text-muted)', transform: `rotate(${data.windDir}deg)`, transition: 'transform 0.3s ease' }}
>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginTop: '4px', whiteSpace: 'nowrap' }}>
{dict.gusts}: <span style={{ color: data.windGusts > (windUnit === 'kmh' ? 50 : 13.8) ? 'var(--color-red)' : 'var(--text-main)' }}>{data.windGusts.toFixed(1)} {windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div>
</div>
{/* Generate Ticks */}
{Array.from({ length: 72 }).map((_, i) => {
const angle = i * 5;
const isMajor = angle % 90 === 0;
const isMedium = angle % 45 === 0;
const innerR = isMajor ? 90 : isMedium ? 100 : 105;
const outerR = 115;
const rad = (angle - 90) * (Math.PI / 180);
const x1 = 130 + innerR * Math.cos(rad);
const y1 = 130 + innerR * Math.sin(rad);
const x2 = 130 + outerR * Math.cos(rad);
const y2 = 130 + outerR * Math.sin(rad);
if (isMajor) return null; // Put text here instead
return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke="rgba(255,255,255,0.15)" strokeWidth={isMedium ? 2 : 1} />;
})}
<text x="130" y="25" fill="var(--text-muted)" fontSize="18" fontWeight="bold" textAnchor="middle" alignmentBaseline="middle">{language === 'cs' ? 'S' : 'N'}</text>
<text x="235" y="130" fill="var(--text-muted)" fontSize="18" fontWeight="bold" textAnchor="middle" alignmentBaseline="middle">{language === 'cs' ? 'V' : 'E'}</text>
<text x="130" y="235" fill="var(--text-muted)" fontSize="18" fontWeight="bold" textAnchor="middle" alignmentBaseline="middle">{language === 'cs' ? 'J' : 'S'}</text>
<text x="25" y="130" fill="var(--text-muted)" fontSize="18" fontWeight="bold" textAnchor="middle" alignmentBaseline="middle">{language === 'cs' ? 'Z' : 'W'}</text>
{/* Direction Indicator */}
{(() => {
const dirRad = (data.windDir + 180 - 90) * (Math.PI / 180);
const x = 130 + 94 * Math.cos(dirRad);
const y = 130 + 94 * Math.sin(dirRad);
return (
<g transform={`translate(${x}, ${y}) rotate(${data.windDir})`}>
<path d="M-8,-8 L0,8 L8,-8 L0,-4 Z" fill="var(--color-cyan)" />
</g>
);
})()}
</svg>
{/* Center Data */}
<FiWind size={26} color="var(--color-cyan)" style={{ position: 'absolute', top: '26px', left: '50%', transform: 'translateX(-50%)', zIndex: 10 }} />
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', marginTop: '-6px', zIndex: 10, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<span style={{ fontSize: '2.8rem', fontWeight: 'bold', color: 'var(--text-main)', lineHeight: 1, textShadow: '0 2px 10px rgba(0,0,0,0.5)' }}>{data.windSpeed.toFixed(1)}</span>
<span style={{ position: 'absolute', left: '100%', bottom: '0.3rem', marginLeft: '0.2rem', fontSize: '0.9rem', color: 'var(--text-main)', whiteSpace: 'nowrap' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
</div>
{/* Right Column: Other Info */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem', borderLeft: '1px solid var(--border-color)', paddingLeft: '1rem', whiteSpace: 'nowrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem' }} title={sensorTemp !== undefined ? (language === 'cs' ? 'Měřeno přímo senzorem na hrázi' : 'Measured by sensor at the dam') : 'OpenMeteo API'}>
<FiThermometer color="var(--color-orange)" />
<span style={{ fontWeight: 'bold' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
<FiSunrise color="#f59e0b" />
<span>{formatTime(data.sunrise)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.9rem', color: 'var(--text-muted)' }}>
<FiSunset color="#f59e0b" />
<span>{formatTime(data.sunset)}</span>
</div>
<div style={{ position: 'absolute', bottom: '50px', left: '50%', transform: 'translateX(-50%)', zIndex: 10, fontSize: '0.75rem', color: 'var(--color-purple)', whiteSpace: 'nowrap' }}>
{dict.gusts}: {data.windGusts.toFixed(1)} {windUnit === 'kmh' ? 'km/h' : 'm/s'}
</div>
{/* Corner Elements */}
<div style={{ position: 'absolute', bottom: '0px', left: '0px', display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '1rem' }} title={sensorTemp !== undefined ? (language === 'cs' ? 'Měřeno přímo senzorem na hrázi' : 'Measured by sensor at the dam') : 'OpenMeteo API'}>
<FiThermometer color="var(--color-orange)" size={18} />
<span style={{ fontWeight: 'bold', color: 'var(--text-main)' }}>{sensorTemp !== undefined ? sensorTemp.toFixed(1) : data.temp.toFixed(1)} °C</span>
</div>
<div style={{ position: 'absolute', bottom: '0px', right: '0px', display: 'flex', flexDirection: 'column', gap: '0.3rem', fontSize: '0.9rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', color: 'var(--text-main)' }}>
<FiSunrise color="var(--color-orange)" size={16} />
<span style={{ fontWeight: 'bold' }}>{formatTime(data.sunrise)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', color: 'var(--text-main)' }}>
<FiSunset color="var(--color-orange)" size={16} />
<span style={{ fontWeight: 'bold' }}>{formatTime(data.sunset)}</span>
</div>
</div>
</div>
</div>
);
+3 -3
View File
@@ -66,7 +66,7 @@ const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh',
{language === 'cs' ? 'Směr' : 'Direction'}: <strong>{data.dirStr} ({data.dir}°)</strong>
<svg
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ transform: `rotate(${data.dir}deg)` }}
style={{ transform: `rotate(${data.dir + 180}deg)` }}
>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
@@ -86,7 +86,7 @@ const CustomWindDot = (props: { cx?: number; cy?: number; payload?: WindDataPoin
if (!cx || !cy || payload.dir === undefined) return null;
return (
<g transform={`translate(${cx},${cy}) rotate(${payload.dir}) scale(1.5)`}>
<g transform={`translate(${cx},${cy}) rotate(${payload.dir + 180}) scale(1.5)`}>
<path
d="M0,-6 L-4,4 L0,2 L4,4 Z"
fill="var(--color-cyan)"
@@ -235,7 +235,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '24h', windUnit = 'k
<span style={{ fontSize: '0.9rem', color: 'var(--color-cyan)' }}>{windUnit === 'kmh' ? 'km/h' : 'm/s'}</span>
<svg
width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
style={{ color: 'var(--color-cyan)', marginLeft: '2px', transform: `rotate(${currentDir}deg)`, transition: 'transform 0.3s ease' }}
style={{ color: 'var(--color-cyan)', marginLeft: '2px', transform: `rotate(${currentDir + 180}deg)`, transition: 'transform 0.3s ease' }}
>
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>