feat: update water level datasets and improve Tooltip component responsiveness
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { type Language, t } from '../translations';
|
||||
import { TbSwimming, TbSailboat } from 'react-icons/tb';
|
||||
|
||||
@@ -8,14 +8,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const DisclaimerModal = ({ language, setLanguage }: Props) => {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isAccepted = localStorage.getItem('hladinator_disclaimer_accepted');
|
||||
if (!isAccepted) {
|
||||
setShow(true);
|
||||
}
|
||||
}, []);
|
||||
const [show, setShow] = useState(() => {
|
||||
return !localStorage.getItem('hladinator_disclaimer_accepted');
|
||||
});
|
||||
|
||||
const handleAccept = () => {
|
||||
localStorage.setItem('hladinator_disclaimer_accepted', 'true');
|
||||
|
||||
@@ -25,13 +25,38 @@ interface LipnoData {
|
||||
qn?: string;
|
||||
}
|
||||
|
||||
interface LakeInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
river: string;
|
||||
navigationForbidden?: boolean;
|
||||
}
|
||||
|
||||
interface TooltipPayloadItem {
|
||||
value: number;
|
||||
dataKey: string;
|
||||
name: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
language: Language;
|
||||
lakeId: string | null;
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver, coordinate, viewBox }: any) => {
|
||||
interface CustomTooltipProps {
|
||||
active?: boolean;
|
||||
payload?: TooltipPayloadItem[];
|
||||
label?: string;
|
||||
language: Language;
|
||||
isWeather?: boolean;
|
||||
isRiver?: boolean;
|
||||
coordinate?: { x: number; y: number };
|
||||
viewBox?: { width: number; height: number };
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver, coordinate, viewBox }: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const dict = t[language as Language].chart;
|
||||
const isLeft = coordinate && viewBox && coordinate.x > viewBox.width / 2;
|
||||
@@ -40,7 +65,7 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver, c
|
||||
return (
|
||||
<div className={tooltipClass} style={{ backgroundColor: 'var(--bg-card)', padding: '0.4rem 0.6rem', border: '1px solid var(--border-color)', borderRadius: '0.5rem', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', fontSize: '0.8rem' }}>
|
||||
<p style={{ margin: '0 0 0.25rem 0', fontWeight: 'bold', color: 'var(--text-main)', fontSize: '0.85rem' }}>{label}</p>
|
||||
{payload.map((entry: any, index: number) => {
|
||||
{payload.map((entry: TooltipPayloadItem, index: number) => {
|
||||
const isTemp = entry.name === 'temperature' || entry.dataKey === 'temperature';
|
||||
return (
|
||||
<p key={index} style={{ margin: 0, color: 'var(--text-main)' }}>
|
||||
@@ -54,12 +79,12 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver, c
|
||||
return (
|
||||
<div className={tooltipClass} style={{ backgroundColor: 'var(--bg-card)', padding: '0.4rem 0.6rem', border: '1px solid var(--border-color)', borderRadius: '0.5rem', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', fontSize: '0.8rem' }}>
|
||||
<p style={{ margin: '0 0 0.25rem 0', fontWeight: 'bold', color: 'var(--text-main)', fontSize: '0.85rem' }}>{label}</p>
|
||||
{[...payload].sort((a: any, b: any) => {
|
||||
{[...payload].sort((a: TooltipPayloadItem, b: TooltipPayloadItem) => {
|
||||
const order = ['level', 'inflow', 'outflow', 'temperature', 'precipitation'];
|
||||
const indexA = order.indexOf(a.dataKey);
|
||||
const indexB = order.indexOf(b.dataKey);
|
||||
return (indexA === -1 ? 99 : indexA) - (indexB === -1 ? 99 : indexB);
|
||||
}).map((entry: any, index: number) => {
|
||||
}).map((entry: TooltipPayloadItem, index: number) => {
|
||||
let labelStr = '';
|
||||
let unit = '';
|
||||
let color = '';
|
||||
@@ -116,9 +141,9 @@ const CustomTooltip = ({ active, payload, label, language, isWeather, isRiver, c
|
||||
const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
const [data, setData] = useState<LipnoData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lakeInfo, setLakeInfo] = useState<any>(null);
|
||||
const [lakeInfo, setLakeInfo] = useState<LakeInfo | null>(null);
|
||||
const [isSmoothed, setIsSmoothed] = useState(true);
|
||||
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d' | '1y' | 'all'>('7d');
|
||||
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d' | '1y' | 'all'>('24h');
|
||||
const [visibleSeries, setVisibleSeries] = useState({
|
||||
level: true,
|
||||
outflow: true,
|
||||
@@ -133,6 +158,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
const [rightCustomDomain, setRightCustomDomain] = useState<[number, number] | null>(null);
|
||||
const [tooltipY, setTooltipY] = useState<number | undefined>(undefined);
|
||||
const [weatherTooltipY, setWeatherTooltipY] = useState<number | undefined>(undefined);
|
||||
const { isFavorite, toggleFavorite } = useFavorites();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setIsMobile(window.innerWidth <= 768);
|
||||
@@ -141,10 +167,12 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const [prevDeps, setPrevDeps] = useState({ timeRange, lakeId });
|
||||
if (prevDeps.timeRange !== timeRange || prevDeps.lakeId !== lakeId) {
|
||||
setPrevDeps({ timeRange, lakeId });
|
||||
setLeftCustomDomain(null);
|
||||
setRightCustomDomain(null);
|
||||
}, [timeRange, lakeId]);
|
||||
}
|
||||
|
||||
const dict = t[language].chart;
|
||||
const topbarDict = t[language].topbar;
|
||||
@@ -154,7 +182,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
fetch(`/data/lakes_index.json?t=${Date.now()}`)
|
||||
.then(res => res.json())
|
||||
.then(indexData => {
|
||||
const found = indexData.find((l: any) => l.id === lakeId);
|
||||
const found = indexData.find((l: LakeInfo) => l.id === lakeId);
|
||||
setLakeInfo(found || { name: 'Lipno 1', river: 'Vltava' });
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
@@ -166,7 +194,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
let lastValidLevel: number | null = null;
|
||||
const formattedData = json.map((item: any) => {
|
||||
const formattedData = json.map((item: { timestamp: string, level?: number, flow?: number, inflow?: number, volume?: number, temperature?: number, precipitation?: number, qn?: string }) => {
|
||||
const outflow = item.flow === null || isNaN(item.flow) ? 0 : item.flow;
|
||||
let level = item.level === null || isNaN(item.level) ? 0 : item.level;
|
||||
|
||||
@@ -263,7 +291,6 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
const targetMs7d = nowMs - 7 * 24 * 60 * 60 * 1000;
|
||||
const targetMs30d = nowMs - 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const { isFavorite, toggleFavorite } = useFavorites();
|
||||
const isFav = lakeId ? isFavorite(lakeId) : false;
|
||||
|
||||
let level24hAgo = latestData.level;
|
||||
@@ -493,11 +520,11 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginLeft: 'auto' }}>
|
||||
<IconTooltip content={(lakeInfo as any).navigationForbidden ? (language === 'cs' ? 'Koupání zakázáno' : 'Swimming forbidden') : (language === 'cs' ? 'Koupání (bez omezení)' : 'Swimming allowed')}>
|
||||
<TbSwimming size={24} color={(lakeInfo as any).navigationForbidden ? 'var(--color-red)' : 'var(--color-green)'} style={{ opacity: (lakeInfo as any).navigationForbidden ? 0.5 : 0.8 }} />
|
||||
<IconTooltip content={lakeInfo.navigationForbidden ? (language === 'cs' ? 'Koupání zakázáno' : 'Swimming forbidden') : (language === 'cs' ? 'Koupání (bez omezení)' : 'Swimming allowed')}>
|
||||
<TbSwimming size={24} color={lakeInfo.navigationForbidden ? 'var(--color-red)' : 'var(--color-green)'} style={{ opacity: lakeInfo.navigationForbidden ? 0.5 : 0.8 }} />
|
||||
</IconTooltip>
|
||||
<IconTooltip content={(lakeInfo as any).navigationForbidden ? (language === 'cs' ? 'Plavba zakázána' : 'Navigation forbidden') : (language === 'cs' ? 'Plavba (i bezmotorová) povolena' : 'Navigation allowed')}>
|
||||
<TbSailboat size={24} color={(lakeInfo as any).navigationForbidden ? 'var(--color-red)' : 'var(--color-green)'} style={{ opacity: (lakeInfo as any).navigationForbidden ? 0.5 : 0.8 }} />
|
||||
<IconTooltip content={lakeInfo.navigationForbidden ? (language === 'cs' ? 'Plavba zakázána' : 'Navigation forbidden') : (language === 'cs' ? 'Plavba (i bezmotorová) povolena' : 'Navigation allowed')}>
|
||||
<TbSailboat size={24} color={lakeInfo.navigationForbidden ? 'var(--color-red)' : 'var(--color-green)'} style={{ opacity: lakeInfo.navigationForbidden ? 0.5 : 0.8 }} />
|
||||
</IconTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,7 +643,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, right: 0, left: 10, bottom: 0 }}
|
||||
onMouseMove={(state: any) => {
|
||||
onMouseMove={(state: { chartY?: number } | null | undefined) => {
|
||||
if (state && state.chartY !== undefined) {
|
||||
const isBottomHalf = state.chartY > 150;
|
||||
const targetY = isBottomHalf ? 5 : 180;
|
||||
@@ -632,7 +659,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<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="left" domain={leftCustomDomain || (leftYAxisDomain as [number, number] | ['auto', 'auto'])} 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} />
|
||||
@@ -773,7 +800,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, right: 0, left: 10, bottom: 0 }}
|
||||
onMouseMove={(state: any) => {
|
||||
onMouseMove={(state: { chartY?: number } | null | undefined) => {
|
||||
if (state && state.chartY !== undefined) {
|
||||
const isBottomHalf = state.chartY > 100;
|
||||
const targetY = isBottomHalf ? 5 : 110;
|
||||
@@ -793,7 +820,7 @@ const LakeDetail = ({ language, lakeId, windUnit = 'kmh' }: Props) => {
|
||||
/>
|
||||
|
||||
<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} />
|
||||
<Line yAxisId="temp" type="basis" dataKey="temperature" stroke="var(--color-red)" strokeWidth={3} dot={false} isAnimationActive={animate} hide={!visibleWeatherSeries.temp} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { MapContainer, TileLayer, Marker, Popup, Tooltip } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
@@ -71,6 +71,27 @@ const LakeMap = ({ language }: Props) => {
|
||||
.catch(err => console.error('Error fetching map lakes:', err));
|
||||
}, []);
|
||||
|
||||
const randomStats = useMemo(() => {
|
||||
const stats: Record<string, { area: string; depth: string }> = {};
|
||||
const simpleHash = (str: string) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash << 5) - hash + str.charCodeAt(i);
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash);
|
||||
};
|
||||
|
||||
lakes.forEach(l => {
|
||||
const hash = simpleHash(l.id);
|
||||
stats[l.id] = {
|
||||
area: (((hash % 500) / 10) + 10).toFixed(1),
|
||||
depth: (((hash % 300) / 10) + 5).toFixed(1)
|
||||
};
|
||||
});
|
||||
return stats;
|
||||
}, [lakes]);
|
||||
|
||||
const filteredLakes = lakes.filter(lake =>
|
||||
lake.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
@@ -148,11 +169,11 @@ const LakeMap = ({ language }: Props) => {
|
||||
<div className="map-lake-stats">
|
||||
<div>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Rozloha' : 'Area'}</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 50 + 10).toFixed(1)} km²</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{randomStats[lake.id]?.area || '0.0'} km²</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ color: 'var(--text-muted)', display: 'block' }}>{language === 'cs' ? 'Hloubka' : 'Depth'}</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{(Math.random() * 30 + 5).toFixed(1)}m</span>
|
||||
<span style={{ color: 'var(--text-main)' }}>{randomStats[lake.id]?.depth || '0.0'}m</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@ export const RiversOverview = ({ language }: Props) => {
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// Filter only rivers
|
||||
const filtered = data.filter((item: any) => item.type === 'river');
|
||||
const filtered = data.filter((item: Partial<River>) => item.type === 'river');
|
||||
setRivers(filtered);
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
|
||||
@@ -26,10 +26,6 @@ export const Tooltip = ({ content, children }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!show) {
|
||||
setPositionStyle({
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,10 +63,16 @@ export const Tooltip = ({ content, children }: Props) => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ position: 'relative', display: 'inline-flex' }}
|
||||
onMouseEnter={() => setShow(true)}
|
||||
onMouseEnter={() => {
|
||||
setPositionStyle({ left: '50%', transform: 'translateX(-50%)' });
|
||||
setShow(true);
|
||||
}}
|
||||
onMouseLeave={() => setShow(false)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!show) {
|
||||
setPositionStyle({ left: '50%', transform: 'translateX(-50%)' });
|
||||
}
|
||||
setShow(!show);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh'
|
||||
// Refresh weather every 15 minutes
|
||||
const interval = setInterval(fetchWeather, 15 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [lat, lng]);
|
||||
}, [lat, lng, windUnit]);
|
||||
|
||||
const dict = {
|
||||
cs: { title: 'Počasí a Vítr (Aktuálně)', error: 'Data nedostupná', wind: 'Vítr', gusts: 'Nárazy', temp: 'Teplota' },
|
||||
@@ -113,8 +113,18 @@ export const WeatherWidget = ({ lat, lng, language, sensorTemp, windUnit = 'kmh'
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', lineHeight: 1.1, color: 'var(--text-main)', whiteSpace: 'nowrap' }}>
|
||||
{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>
|
||||
<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>
|
||||
|
||||
@@ -27,7 +27,17 @@ const getCompassDirection = (degrees: number, language: 'cs' | 'en') => {
|
||||
return directions[index];
|
||||
};
|
||||
|
||||
const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh', coordinate, viewBox }: any) => {
|
||||
interface CustomWindTooltipProps {
|
||||
active?: boolean;
|
||||
payload?: { payload: WindDataPoint, value: number, name: string }[];
|
||||
label?: string;
|
||||
language: 'cs' | 'en';
|
||||
windUnit?: 'kmh' | 'ms';
|
||||
coordinate?: { x: number; y: number };
|
||||
viewBox?: { width: number; height: number };
|
||||
}
|
||||
|
||||
const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh', coordinate, viewBox }: CustomWindTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const isLeft = coordinate && viewBox && coordinate.x > viewBox.width / 2;
|
||||
const tooltipClass = `chart-tooltip ${isLeft ? 'tooltip-left' : 'tooltip-right'}`;
|
||||
@@ -52,7 +62,16 @@ const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh',
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '2px', color: 'var(--text-muted)' }}>
|
||||
<FiWind />
|
||||
<span>{language === 'cs' ? 'Směr' : 'Direction'}: <strong>{data.dirStr} ({data.dir}°)</strong></span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
{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)` }}
|
||||
>
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,7 +80,7 @@ const CustomWindTooltip = ({ active, payload, label, language, windUnit = 'kmh',
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomWindDot = (props: any) => {
|
||||
const CustomWindDot = (props: { cx?: number; cy?: number; payload?: WindDataPoint }) => {
|
||||
const { cx, cy, payload } = props;
|
||||
|
||||
if (!cx || !cy || payload.dir === undefined) return null;
|
||||
@@ -78,10 +97,11 @@ const CustomWindDot = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'kmh' }: WindChartProps) => {
|
||||
export const WindChart = ({ lat, lng, language, timeRange = '24h', windUnit = 'kmh' }: WindChartProps) => {
|
||||
const [data, setData] = useState<WindDataPoint[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentSpeed, setCurrentSpeed] = useState(0);
|
||||
const [currentDir, setCurrentDir] = useState(0);
|
||||
const [maxGust, setMaxGust] = useState(0);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [tooltipY, setTooltipY] = useState<number | undefined>(undefined);
|
||||
@@ -169,6 +189,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
|
||||
setData(downsampled);
|
||||
setMaxGust(maxG);
|
||||
setCurrentSpeed(speeds[closestIdx] || speeds[speeds.length - 1] || 0);
|
||||
setCurrentDir(dirs[closestIdx] || dirs[dirs.length - 1] || 0);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -180,7 +201,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
|
||||
if (lat && lng) {
|
||||
fetchWind();
|
||||
}
|
||||
}, [lat, lng, language, timeRange]);
|
||||
}, [lat, lng, language, timeRange, windUnit]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -209,9 +230,16 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
|
||||
}}>
|
||||
<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' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||
<span style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'var(--text-main)' }}>{currentSpeed.toFixed(1)}</span>
|
||||
<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' }}
|
||||
>
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,7 +258,7 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
|
||||
<ComposedChart
|
||||
data={data}
|
||||
margin={isMobile ? { top: 5, right: 5, left: 5, bottom: 0 } : { top: 5, right: 0, left: -20, bottom: 0 }}
|
||||
onMouseMove={(state: any) => {
|
||||
onMouseMove={(state: { chartY?: number } | null | undefined) => {
|
||||
if (state && state.chartY !== undefined) {
|
||||
const isBottomHalf = state.chartY > 140;
|
||||
const targetY = isBottomHalf ? 5 : 160;
|
||||
@@ -275,7 +303,11 @@ export const WindChart = ({ lat, lng, language, timeRange = '7d', windUnit = 'km
|
||||
fillOpacity={1}
|
||||
fill="url(#colorWind)"
|
||||
isAnimationActive={true}
|
||||
dot={<CustomWindDot />}
|
||||
dot={(props: any) => {
|
||||
const step = Math.max(1, Math.floor(data.length / (isMobile ? 15 : 30)));
|
||||
if (props.index % step !== 0 && props.index !== data.length - 1) return null;
|
||||
return <CustomWindDot key={props.index} {...props} />;
|
||||
}}
|
||||
activeDot={{ r: 6, fill: 'var(--color-cyan)', stroke: '#1e293b', strokeWidth: 2 }}
|
||||
/>
|
||||
<Line
|
||||
|
||||
Reference in New Issue
Block a user