import { useMemo, useState, useCallback } from 'react' import Highcharts from 'highcharts' import { HighchartsReact } from 'highcharts-react-official' import { format } from 'date-fns' import { de } from 'date-fns/locale' import './WeatherDashboard.css' // Build-Informationen (werden beim Build eingef\u00fcgt) const buildDate = __BUILD_DATE__ const version = __VERSION__ // Deutsche Lokalisierung für Highcharts Highcharts.setOptions({ lang: { months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], shortMonths: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'], weekdays: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], resetZoom: 'Zoom zurücksetzen' }, time: { useUTC: false } }) const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange, showTable = false, onToggleTable }) => { // State für Anleitung const [showAnleitung, setShowAnleitung] = useState(false) // Schwellwert für Datenlücken (abhängig vom Zeitraum) const gapThresholdMs = useMemo(() => { if (timeRange === '24h') return 2 * 60 * 60 * 1000 // 2 Stunden return 1.5 * 24 * 3600 * 1000 // 1,5 Tage }, [timeRange]) // Fügt null-Einträge in Lücken ein, damit Highcharts die Linie unterbricht const withGaps = useCallback((pairs) => { const result = [] for (let i = 0; i < pairs.length; i++) { result.push(pairs[i]) if (i < pairs.length - 1 && pairs[i + 1][0] - pairs[i][0] > gapThresholdMs) { result.push([(pairs[i][0] + pairs[i + 1][0]) / 2, null]) } } return result }, [gapThresholdMs]) // State für benutzerdefinierten Zeitbereich const [showCustomRangeModal, setShowCustomRangeModal] = useState(false) const [customStartDate, setCustomStartDate] = useState('') const [customEndDate, setCustomEndDate] = useState('') const [customError, setCustomError] = useState('') // Handler für benutzerdefinierten Zeitbereich const handleOpenCustomRange = () => { // Versuche gespeicherten Zeitbereich zu laden try { const savedRange = localStorage.getItem('customTimeRange') if (savedRange) { const { start, end } = JSON.parse(savedRange) setCustomStartDate(start.split('T')[0]) setCustomEndDate(end.split('T')[0]) } else { // Setze Standardwerte (letzte 7 Tage) const end = new Date() const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000) setCustomStartDate(format(start, 'yyyy-MM-dd')) setCustomEndDate(format(end, 'yyyy-MM-dd')) } } catch (e) { // Bei Fehler: Standardwerte verwenden const end = new Date() const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000) setCustomStartDate(format(start, 'yyyy-MM-dd')) setCustomEndDate(format(end, 'yyyy-MM-dd')) } setCustomError('') setShowCustomRangeModal(true) } const handleApplyCustomRange = () => { if (!customStartDate || !customEndDate) { setCustomError('Bitte Start- und Enddatum auswählen') return } const diffDays = Math.floor((new Date(customEndDate) - new Date(customStartDate)) / (1000 * 60 * 60 * 24)) if (diffDays < 0) { setCustomError('Enddatum muss nach dem Startdatum liegen') return } if (diffDays > 365) { setCustomError('Maximaler Zeitraum ist 1 Jahr (365 Tage)') return } const startStr = customStartDate + 'T00:00' const endStr = customEndDate + 'T23:59' // Zeitbereich im localStorage speichern try { localStorage.setItem('customTimeRange', JSON.stringify({ start: customStartDate, end: customEndDate })) } catch (e) { // Fehler beim Speichern ignorieren console.warn('Konnte Zeitbereich nicht speichern:', e) } // Anwenden onTimeRangeChange('custom', { start: startStr, end: endStr }) setShowCustomRangeModal(false) } const handleCancelCustomRange = () => { setShowCustomRangeModal(false) setCustomError('') } // Daten vorbereiten und nach Zeit sortieren (älteste zuerst) const sortedData = useMemo(() => { return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime)) }, [data]) // Aktuelle Werte aus separaten currentData (immer 24h) const sortedCurrentData = useMemo(() => { return [...currentData].sort((a, b) => new Date(a.datetime) - new Date(b.datetime)) }, [currentData]) // Zeitraum-Label const timeRangeLabel = useMemo(() => { if (typeof timeRange === 'object' && timeRange.type === 'custom') { const start = new Date(timeRange.start) const end = new Date(timeRange.end) return `${format(start, 'dd.MM.yyyy HH:mm', { locale: de })} - ${format(end, 'dd.MM.yyyy HH:mm', { locale: de })}` } switch (timeRange) { case '24h': return 'Die letzten 24 Stunden' case '7d': return 'Die letzten 7 Tage' case '30d': return 'Die letzten 30 Tage' case '365d': return 'Die letzten 365 Tage' default: return 'Die letzten 24 Stunden' } }, [timeRange]) // Aggregations-Zusatz für Chart-Titel const aggregationSuffix = useMemo(() => { // Custom range: basierend auf days if (typeof timeRange === 'object' && timeRange.type === 'custom') { const days = timeRange.days || 1 if (days >= 7) { return ' (Tagesmittel)' } else { return ' (Stundenmittel)' } } // Vordefinierte Bereiche switch (timeRange) { case '7d': case '30d': case '365d': return ' (Tagesmittel)' default: return '' } }, [timeRange]) // Spezieller Suffix für Regen const rainSuffix = useMemo(() => { if (typeof timeRange === 'object' && timeRange.type === 'custom') { const days = timeRange.days || 1 return days >= 7 ? ' (pro Tag)' : '' } switch (timeRange) { case '7d': case '30d': case '365d': return ' (pro Tag)' default: return '' } }, [timeRange]) // Spezieller Suffix für Temperatur bei 30d/365d const temperatureSuffix = useMemo(() => { // Custom range: basierend auf days if (typeof timeRange === 'object' && timeRange.type === 'custom') { const days = timeRange.days || 1 if (days >= 7) { return ' (Tages-Min/Max)' } return '' } // Vordefinierte Bereiche switch (timeRange) { case '7d': case '30d': case '365d': return ' (Tages-Min/Max)' default: return '' } }, [timeRange]) // Spezieller Suffix für Windböen bei 30d/365d const windGustSuffix = useMemo(() => { // Custom range: basierend auf days if (typeof timeRange === 'object' && timeRange.type === 'custom') { const days = timeRange.days || 1 if (days >= 7) { return ' (TagesMax)' } return '' } // Vordefinierte Bereiche switch (timeRange) { case '7d': case '30d': case '365d': return ' (TagesMax)' default: return '' } }, [timeRange]) // Hilfsfunktion: Dynamischen Y-Bereich berechnen. // minHalfSpan: halbe Mindestspanne (z.B. 5 → Bereich mind. 10 Einheiten) const calcYRange = (values, minHalfSpan) => { if (values.length === 0) return { yMin: null, yMax: null } const min = Math.min(...values) const max = Math.max(...values) if (max - min < minHalfSpan * 2) { const center = (max + min) / 2 return { yMin: center - minHalfSpan, yMax: center + minHalfSpan } } return { yMin: min, yMax: max } } // Hilfsfunktion: BarTrend-Wert (Davis VantagePro) → Pfeil + Label // Werte laut Davis Serial Communication Reference Rev 2.6.1: // -60 = Falling Rapidly, -20 = Falling Slowly, 0 = Steady, // 20 = Rising Slowly, 60 = Rising Rapidly, 80/'P' = kein Trend const barTrendArrow = (trend) => { switch (trend) { case -60: return { arrow: '⬇⬇', label: 'Fällt schnell' } case -20: return { arrow: '⬇', label: 'Fällt langsam' } case 0: return { arrow: '→', label: 'Stabil' } case 20: return { arrow: '⬆', label: 'Steigt langsam' } case 60: return { arrow: '⬆⬆', label: 'Steigt schnell' } default: return null } } // Gemeinsame Chart-Optionen (angepasst an Zeitraum) const getCommonOptions = () => { // Prüfe, ob es ein custom range ist const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 // X-Achsen-Konfiguration basierend auf Zeitraum let xAxisConfig = { type: 'datetime', gridLineWidth: 1, gridLineColor: 'rgba(0, 0, 0, 0.1)' } // Zeitspanne für X-Achse berechnen (für festen Zeitrahmen) const now = new Date().getTime() let xAxisMin, xAxisMax let tooltipDateFormat = '%d.%m.%Y' if (isCustomRange) { // Custom range Konfiguration - Min/Max aus Daten nehmen if (customDays >= 7) { xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' } tooltipDateFormat = '%d.%m.%Y' } else { xAxisConfig.labels = { format: '{value:%d.%m %H:%M}', align: 'center' } tooltipDateFormat = '%d.%m.%Y %H:%M' } // X-Achsen-Bereich aus den tatsächlichen Daten bestimmen if (sortedData.length > 0) { xAxisMin = new Date(sortedData[0].datetime).getTime() xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime() } else { xAxisMin = null xAxisMax = null } } else { // Vordefinierte Bereiche switch (timeRange) { case '24h': xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' } xAxisMin = now - 24 * 3600 * 1000 xAxisMax = now tooltipDateFormat = '%d.%m.%Y %H:%M' break case '7d': xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' } xAxisMin = now - 7 * 24 * 3600 * 1000 xAxisMax = now tooltipDateFormat = '%d.%m.%Y' break case '30d': xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' } xAxisMin = now - 30 * 24 * 3600 * 1000 xAxisMax = now tooltipDateFormat = '%d.%m.%Y' break case '365d': xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' } tooltipDateFormat = '%b %Y' // Bei 365d: Min/Max aus vorhandenen Daten berechnen if (sortedData.length > 0) { xAxisMin = new Date(sortedData[0].datetime).getTime() xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime() } else { xAxisMin = null xAxisMax = null } break default: xAxisConfig.tickInterval = 4 * 3600 * 1000 xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' } xAxisMin = now - 24 * 3600 * 1000 xAxisMax = now tooltipDateFormat = '%d.%m.%Y %H:%M' } } // Min/Max für X-Achse setzen xAxisConfig.min = xAxisMin xAxisConfig.max = xAxisMax return { chart: { height: '50%', animation: false, backgroundColor: 'transparent' }, accessibility: { enabled: false }, credits: { enabled: false }, title: { text: null }, legend: { enabled: false }, tooltip: { shared: true, crosshairs: true, xDateFormat: tooltipDateFormat }, plotOptions: { series: { marker: { enabled: false, states: { hover: { enabled: true, radius: 5 } } } } }, xAxis: xAxisConfig, yAxis: { gridLineColor: 'rgba(0, 0, 0, 0.05)' } } } // Temperatur Chart const temperatureOptions = useMemo(() => { // Prüfe, ob Min/Max-Temperaturen angezeigt werden sollen const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 const showMinMax = (timeRange === '7d' || timeRange === '30d' || timeRange === '365d') || (isCustomRange && customDays >= 7) // Bei 7d, 30d, 365d und custom >= 7 Tage: Min/Max-Temperaturen anzeigen if (showMinMax) { const minTemps = sortedData.filter(item => item.min_temperature != null).map(item => item.min_temperature) const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature) // Prüfe, ob Daten vorhanden sind if (minTemps.length === 0 || maxTemps.length === 0) { return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null } }, series: [] } } let yMin = Math.min(...minTemps) let yMax = Math.max(...maxTemps) // Füge einen kleinen Puffer hinzu (2°C oben/unten), um den Platz optimal zu nutzen yMin = Math.floor(yMin - 2) yMax = Math.ceil(yMax + 2) return { ...getCommonOptions(), yAxis: { title: { text: null }, gridLineColor: 'rgba(0, 0, 0, 0.05)', min: yMin, max: yMax, startOnTick: false, endOnTick: false }, series: [ { name: 'Maximaltemperatur', data: withGaps(sortedData.filter(item => item.max_temperature != null).map(item => [new Date(item.datetime).getTime(), item.max_temperature])), color: 'rgb(255, 99, 132)', type: 'line', lineWidth: 2, connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 1, valueSuffix: ' °C' } }, { name: 'Minimaltemperatur', data: withGaps(sortedData.filter(item => item.min_temperature != null).map(item => [new Date(item.datetime).getTime(), item.min_temperature])), color: 'rgb(54, 162, 235)', type: 'line', lineWidth: 2, connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 1, valueSuffix: ' °C' } } ] } } // Standard: Temperatur als Flächendiagramm const temps = sortedData.filter(item => item.temperature != null).map(item => item.temperature) if (temps.length === 0) { return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null } }, series: [] } } const { yMin, yMax } = calcYRange(temps, 5) return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null }, min: yMin, max: yMax }, series: [{ name: 'Temperatur', data: withGaps(sortedData.filter(item => item.temperature != null).map(item => [new Date(item.datetime).getTime(), item.temperature])), color: 'rgb(255, 99, 132)', fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [ [0, 'rgba(255, 99, 132, 0.3)'], [1, 'rgba(255, 99, 132, 0.1)'] ] }, type: 'areaspline', threshold: null, connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 1, valueSuffix: ' °C' } }] } }, [sortedData, temperatureSuffix, timeRange, withGaps]) // Luftfeuchtigkeit Chart const humidityOptions = useMemo(() => { const humidities = sortedData.filter(item => item.humidity != null).map(item => item.humidity) const { yMin, yMax } = calcYRange(humidities, 10) return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null }, min: yMin, max: yMax }, series: [{ name: 'Feuchte', data: withGaps(sortedData.filter(item => item.humidity != null).map(item => [new Date(item.datetime).getTime(), item.humidity])), color: 'rgb(54, 162, 235)', fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [ [0, 'rgba(54, 162, 235, 0.3)'], [1, 'rgba(54, 162, 235, 0.1)'] ] }, type: 'area', connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 0, valueSuffix: ' %' } }] } }, [sortedData, timeRange, withGaps]) // Luftdruck Chart const pressureOptions = useMemo(() => { const pressures = sortedData.filter(item => item.pressure != null).map(item => item.pressure) if (pressures.length === 0) { return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null } }, series: [] } } const { yMin, yMax } = calcYRange(pressures, 20) return { ...getCommonOptions(), yAxis: { ...getCommonOptions().yAxis, title: { text: null }, min: yMin, max: yMax }, series: [{ name: 'Luftdruck', data: withGaps(sortedData.filter(item => item.pressure != null).map(item => [new Date(item.datetime).getTime(), item.pressure])), color: 'rgb(75, 192, 192)', fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [ [0, 'rgba(75, 192, 192, 0.3)'], [1, 'rgba(75, 192, 192, 0.1)'] ] }, type: 'area', connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 0, valueSuffix: ' hPa' } }] } }, [sortedData, timeRange, withGaps]) // Regen Chart (angepasst an Zeitraum) const rainOptions = useMemo(() => { let series = [] let yAxisTitle = 'Regen (mm) / Rate (mm/h)' if (timeRange === '24h') { // 24h: Area Chart mit Regen und Regenrate yAxisTitle = 'Regen (mm) / Rate (mm/h)' series = [{ name: 'Regen', data: sortedData.filter(item => item.rain != null).map(item => [new Date(item.datetime).getTime(), item.rain]), color: 'rgb(54, 162, 235)', fillColor: 'rgba(54, 162, 235, 0.3)', type: 'area', tooltip: { valueDecimals: 1, valueSuffix: ' mm' } }, { name: 'Regenrate', data: sortedData.filter(item => item.rain_rate != null).map(item => [new Date(item.datetime).getTime(), item.rain_rate]), color: 'rgb(59, 130, 246)', dashStyle: 'Dash', type: 'line', tooltip: { valueSuffix: ' mm/h' } }] } else if (timeRange === '7d' || timeRange === '30d') { // 7d/30d: Balkendiagramm mit täglichen Summen yAxisTitle = 'Regen (mm pro Tag)' series = [{ name: 'Regen', data: rainData.filter(item => item.total_rain != null && item.total_rain > 0).map(item => [new Date(item.date).getTime(), item.total_rain]), color: 'rgb(54, 162, 235)', type: 'column', tooltip: { valueDecimals: 1, valueSuffix: ' mm' } }] } else if (timeRange === '365d') { // 365d: Balkendiagramm mit wöchentlichen Summen yAxisTitle = 'Regen (mm pro Woche)' series = [{ name: 'Regen', data: rainData.filter(item => item.total_rain != null && item.total_rain > 0).map(item => [new Date(item.week_start).getTime(), item.total_rain]), color: 'rgb(54, 162, 235)', type: 'column', tooltip: { valueDecimals: 1, valueSuffix: ' mm' } }] } else if (typeof timeRange === 'object' && timeRange.type === 'custom') { // Custom range: tägliche Summen aus sortedData (total_rain ist im daily-aggregated-range enthalten) yAxisTitle = 'Regen (mm pro Tag)' series = [{ name: 'Regen', data: sortedData .filter(item => item.total_rain != null && item.total_rain > 0) .map(item => [new Date(item.datetime).getTime(), item.total_rain]), color: 'rgb(54, 162, 235)', type: 'column', tooltip: { valueDecimals: 1, valueSuffix: ' mm' } }] } return { ...getCommonOptions(), legend: { enabled: series.length > 1, align: 'right', verticalAlign: 'top', floating: true, itemStyle: { fontSize: '11px', fontWeight: 'normal' } }, yAxis: { ...getCommonOptions().yAxis, title: { text: null } }, series } }, [sortedData, rainData, timeRange, withGaps]) // Windgeschwindigkeit Chart const windSpeedOptions = useMemo(() => { // Prüfe, ob Böen angezeigt werden sollen (nicht bei 365d oder custom >= 365 Tage) const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365) const windSpeedSeries = { name: 'Windgeschwindigkeit', data: withGaps(sortedData .filter(item => item.wind_speed != null) .map(item => [new Date(item.datetime).getTime(), item.wind_speed])), color: 'rgb(153, 102, 255)', fillColor: 'rgba(153, 102, 255, 0.1)', type: 'area', connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 1, valueSuffix: ' km/h' } } const gustSeries = { name: 'Böe' + windGustSuffix, data: withGaps(sortedData .filter(item => item.wind_gust != null) .map(item => [new Date(item.datetime).getTime(), item.wind_gust])), color: 'rgba(255, 160, 80, 0.6)', fillColor: 'rgba(255, 160, 80, 0.08)', type: 'area', lineWidth: 1.5, connectNulls: false, gapSize: 2 * 24 * 3600 * 1000, gapUnit: 'value', tooltip: { valueDecimals: 1, valueSuffix: ' km/h' } } const series = hideGusts ? [windSpeedSeries] : [gustSeries, windSpeedSeries] return { ...getCommonOptions(), legend: { enabled: true, align: 'right', verticalAlign: 'top', floating: true, itemStyle: { fontSize: '11px', fontWeight: 'normal' } }, plotOptions: { series: { marker: { enabled: false }, lineWidth: 2 }, line: { step: 'left' // Keine Glättung } }, yAxis: { ...getCommonOptions().yAxis, title: { text: null }, min: 0 }, series } }, [sortedData, timeRange, windGustSuffix, withGaps]) // Windrichtung Chart const windDirOptions = useMemo(() => ({ ...getCommonOptions(), tooltip: { formatter: function() { const dateFormat = timeRange === '24h' ? '%d.%m.%Y %H:%M' : '%d.%m.%Y' const dateStr = Highcharts.dateFormat(dateFormat, this.x) return `${dateStr}
\u25CF ${this.series.name}: ${this.y.toFixed(0)}°` } }, plotOptions: { scatter: { marker: { enabled: true, radius: 2, states: { hover: { enabled: true, radius: 3 } } } } }, yAxis: { ...getCommonOptions().yAxis, title: { text: null }, min: 0, max: 360, tickInterval: 45, labels: { formatter: function() { const directions = { 0: 'N', 45: 'NO', 90: 'O', 135: 'SO', 180: 'S', 225: 'SW', 270: 'W', 315: 'NW', 360: 'N' } return directions[this.value] || '' } } }, series: [{ name: 'Windrichtung', data: sortedData.filter(item => item.wind_dir != null).map(item => [new Date(item.datetime).getTime(), item.wind_dir]), color: 'rgb(54, 162, 235)', type: 'scatter' }] }), [sortedData, timeRange]) // Aktuellste Werte für Übersicht (immer aus den 24h-Daten, Fallback auf sortedData) const current = (sortedCurrentData.length > 0 ? sortedCurrentData[sortedCurrentData.length - 1] : sortedData[sortedData.length - 1]) || {} // Berechne Min/Max für den gewählten Zeitraum const periodStats = useMemo(() => { // Für den gewählten Zeitraum alle Daten verwenden const periodData = sortedData if (periodData.length === 0) { return { minTemp: null, maxTemp: null, minTempTime: null, maxTempTime: null, minHumidity: null, maxHumidity: null, minHumidityTime: null, maxHumidityTime: null, minPressure: null, maxPressure: null, minPressureTime: null, maxPressureTime: null } } // Zeitformat basierend auf Zeitraum const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 let timeFormat = 'dd.MM HH:mm' if (isCustomRange) { timeFormat = customDays < 7 ? 'HH:mm' : 'dd.MM HH:mm' } else { timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm' } // Temperatur const minTempItem = periodData.reduce((min, item) => item.temperature != null && (min === null || item.temperature < min.temperature) ? item : min, null) const maxTempItem = periodData.reduce((max, item) => item.temperature != null && (max === null || item.temperature > max.temperature) ? item : max, null) // Luftfeuchtigkeit const minHumidityItem = periodData.reduce((min, item) => item.humidity != null && (min === null || item.humidity < min.humidity) ? item : min, null) const maxHumidityItem = periodData.reduce((max, item) => item.humidity != null && (max === null || item.humidity > max.humidity) ? item : max, null) // Luftdruck const minPressureItem = periodData.reduce((min, item) => item.pressure != null && (min === null || item.pressure < min.pressure) ? item : min, null) const maxPressureItem = periodData.reduce((max, item) => item.pressure != null && (max === null || item.pressure > max.pressure) ? item : max, null) // Windgeschwindigkeit const maxWindGustItem = periodData.reduce((max, item) => item.wind_gust != null && (max === null || item.wind_gust > max.wind_gust) ? item : max, null) return { minTemp: minTempItem?.temperature ?? null, maxTemp: maxTempItem?.temperature ?? null, minTempTime: minTempItem ? format(new Date(minTempItem.datetime), timeFormat, { locale: de }) : null, maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), timeFormat, { locale: de }) : null, minHumidity: minHumidityItem?.humidity ?? null, maxHumidity: maxHumidityItem?.humidity ?? null, minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), timeFormat, { locale: de }) : null, maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), timeFormat, { locale: de }) : null, minPressure: minPressureItem?.pressure ?? null, maxPressure: maxPressureItem?.pressure ?? null, minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), timeFormat, { locale: de }) : null, maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), timeFormat, { locale: de }) : null, maxWindGust: maxWindGustItem?.wind_gust ?? null, maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), timeFormat, { locale: de }) : null } }, [sortedData, timeRange]) // Regen-Lookup: date-string → total_rain const rainByDate = useMemo(() => { const map = {} rainData.forEach(item => { const key = item.date ? item.date.split('T')[0] : item.week_start ? item.week_start.split('T')[0] : null if (key) map[key] = item.total_rain }) return map }, [rainData]) // Tabellen-Daten: ein Eintrag pro Tag const tableData = useMemo(() => { const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom' const customDays = isCustomRange ? (timeRange.days || 1) : 0 const isDailyAggregated = ['7d', '30d', '365d'].includes(timeRange) || (isCustomRange && customDays >= 7) if (isDailyAggregated) { return sortedData.map(item => { const dateKey = item.datetime.split('T')[0] return { date: format(new Date(item.datetime), 'dd.MM.yyyy', { locale: de }), tempMin: item.min_temperature ?? null, tempMax: item.max_temperature ?? null, humMin: item.min_humidity ?? null, humMax: item.max_humidity ?? null, pressMin: item.min_pressure ?? null, pressMax: item.max_pressure ?? null, rain: item.total_rain ?? rainByDate[dateKey] ?? null, windMax: item.wind_gust ?? null, } }) } else { // Stundenwerte → pro Tag aggregieren const byDay = {} sortedData.forEach(item => { const d = new Date(item.datetime) const dateKey = format(d, 'yyyy-MM-dd') const dateLabel = format(d, 'dd.MM.yyyy', { locale: de }) if (!byDay[dateKey]) { byDay[dateKey] = { date: dateLabel, temps: [], hums: [], pressures: [], rains: [], windGusts: [] } } if (item.temperature != null) byDay[dateKey].temps.push(item.temperature) if (item.humidity != null) byDay[dateKey].hums.push(item.humidity) if (item.pressure != null) byDay[dateKey].pressures.push(item.pressure) if (item.rain != null) byDay[dateKey].rains.push(item.rain) if (item.wind_gust != null) byDay[dateKey].windGusts.push(item.wind_gust) }) const startKey = isCustomRange ? timeRange.start.split('T')[0] : null const endKey = isCustomRange ? timeRange.end.split('T')[0] : null return Object.entries(byDay) .sort(([a], [b]) => a.localeCompare(b)) .filter(([k]) => !startKey || (k >= startKey && k <= endKey)) .map(([, d]) => ({ date: d.date, tempMin: d.temps.length ? Math.min(...d.temps) : null, tempMax: d.temps.length ? Math.max(...d.temps) : null, humMin: d.hums.length ? Math.round(Math.min(...d.hums)) : null, humMax: d.hums.length ? Math.round(Math.max(...d.hums)) : null, pressMin: d.pressures.length ? Math.min(...d.pressures) : null, pressMax: d.pressures.length ? Math.max(...d.pressures) : null, rain: d.rains.length ? Math.max(...d.rains) : null, windMax: d.windGusts.length ? Math.max(...d.windGusts) : null, })) } }, [sortedData, rainByDate, timeRange]) return (
{/* Navigation für Zeitraum-Auswahl */}
{/* Zeitraum-Beschreibung */}
{timeRangeLabel}
{/* Charts Grid / Tabellenansicht */} {showTable ? (
{tableData.map((row, i) => ( ))}
Datum Temperatur
°C
Feuchte
%
Luftdruck
hPa
Regen
mm
Wind-V
max km/h
min max min max min max
{row.date} {row.tempMin != null ? row.tempMin.toFixed(1) : '-'} {row.tempMax != null ? row.tempMax.toFixed(1) : '-'} {row.humMin != null ? row.humMin : '-'} {row.humMax != null ? row.humMax : '-'} {row.pressMin != null ? row.pressMin.toFixed(0) : '-'} {row.pressMax != null ? row.pressMax.toFixed(0) : '-'} {row.rain != null ? row.rain.toFixed(1) : '0'} {row.windMax != null ? row.windMax.toFixed(1) : '-'}
) : (
Aktuell: {current.temperature?.toFixed(1) || '-'}°C

🌡️ Temperatur{temperatureSuffix}[°C]

Min: {periodStats.minTemp?.toFixed(1) || '-'}°C ({periodStats.minTempTime || '-'}) | Max: {periodStats.maxTemp?.toFixed(1) || '-'}°C ({periodStats.maxTempTime || '-'})
Aktuell: {current.pressure?.toFixed(0) || '-'} hPa

🌐 Luftdruck{aggregationSuffix}{(() => { const t = barTrendArrow(current.bar_trend); return t ? {t.arrow} : null })()}[hPa]

Min: {periodStats.minPressure?.toFixed(0) || '-'} hPa ({periodStats.minPressureTime || '-'}) | Max: {periodStats.maxPressure?.toFixed(0) || '-'} hPa ({periodStats.maxPressureTime || '-'})
Aktuell: {current.humidity || '-'}%

💧 Luftfeuchtigkeit{aggregationSuffix}[%]

Min: {periodStats.minHumidity || '-'}% ({periodStats.minHumidityTime || '-'}) | Max: {periodStats.maxHumidity || '-'}% ({periodStats.maxHumidityTime || '-'})
Aktuell: {current.rain?.toFixed(1) || '-'} mm

🌧️ Regen{rainSuffix}[mm]

 
Aktuell: {current.wind_dir ?? '-'}°

🧭 Windrichtung{aggregationSuffix}[°]

 
Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h

💨 Wind{aggregationSuffix}[km/h]

Max: {periodStats.maxWindGust?.toFixed(1) || '-'} km/h ({periodStats.maxWindGustTime || '-'})
)} {/* end showTable ternary */} {/* Modal Anleitung */} {showAnleitung && (
setShowAnleitung(false)}>
e.stopPropagation()}>
Bedienungsanleitung