460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
import { useMemo } 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 }) => {
|
||
// 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])
|
||
|
||
// Gemeinsame Chart-Optionen
|
||
const getCommonOptions = () => ({
|
||
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: '%d.%m.%Y %H:%M'
|
||
},
|
||
plotOptions: {
|
||
series: {
|
||
marker: {
|
||
enabled: false,
|
||
states: {
|
||
hover: {
|
||
enabled: true,
|
||
radius: 5
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
xAxis: {
|
||
type: 'datetime',
|
||
tickInterval: 4 * 3600 * 1000, // 4 Stunden in Millisekunden
|
||
labels: {
|
||
format: '{value:%H:%M}',
|
||
align: 'center'
|
||
},
|
||
gridLineWidth: 1,
|
||
gridLineColor: 'rgba(0, 0, 0, 0.1)'
|
||
},
|
||
yAxis: {
|
||
gridLineColor: 'rgba(0, 0, 0, 0.05)'
|
||
}
|
||
})
|
||
|
||
// Temperatur Chart
|
||
const temperatureOptions = useMemo(() => {
|
||
const temps = sortedData.map(item => item.temperature)
|
||
const min = Math.min(...temps)
|
||
const max = Math.max(...temps)
|
||
const range = max - min
|
||
|
||
let yMin = min
|
||
let yMax = max
|
||
|
||
if (range < 10) {
|
||
const center = (max + min) / 2
|
||
yMin = center - 5
|
||
yMax = center + 5
|
||
}
|
||
|
||
return {
|
||
...getCommonOptions(),
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: { text: 'Temperatur (°C)' },
|
||
min: yMin,
|
||
max: yMax
|
||
},
|
||
series: [{
|
||
name: 'Temperatur',
|
||
data: sortedData.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,
|
||
tooltip: {
|
||
valueSuffix: ' °C'
|
||
}
|
||
}]
|
||
}
|
||
}, [sortedData])
|
||
|
||
// Luftfeuchtigkeit Chart
|
||
const humidityOptions = useMemo(() => ({
|
||
...getCommonOptions(),
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: { text: 'Feuchte (%)' },
|
||
min: 40,
|
||
max: 100
|
||
},
|
||
series: [{
|
||
name: 'Feuchte',
|
||
data: sortedData.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',
|
||
tooltip: {
|
||
valueSuffix: ' %'
|
||
}
|
||
}]
|
||
}), [sortedData])
|
||
|
||
// Luftdruck Chart
|
||
const pressureOptions = useMemo(() => {
|
||
const pressures = sortedData.map(item => item.pressure)
|
||
const min = Math.min(...pressures)
|
||
const max = Math.max(...pressures)
|
||
const range = max - min
|
||
|
||
let yMin = min
|
||
let yMax = max
|
||
|
||
if (range < 40) {
|
||
const center = (max + min) / 2
|
||
yMin = center - 20
|
||
yMax = center + 20
|
||
}
|
||
|
||
return {
|
||
...getCommonOptions(),
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: { text: 'Luftdruck (hPa)' },
|
||
min: yMin,
|
||
max: yMax
|
||
},
|
||
series: [{
|
||
name: 'Luftdruck',
|
||
data: sortedData.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',
|
||
tooltip: {
|
||
valueSuffix: ' hPa'
|
||
}
|
||
}]
|
||
}
|
||
}, [sortedData])
|
||
|
||
// Regen Chart
|
||
const rainOptions = useMemo(() => ({
|
||
...getCommonOptions(),
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: { text: 'Regen (mm) / Rate (mm/h)' }
|
||
},
|
||
series: [{
|
||
name: 'Regen',
|
||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
|
||
color: 'rgb(54, 162, 235)',
|
||
fillColor: 'rgba(54, 162, 235, 0.3)',
|
||
type: 'area',
|
||
tooltip: {
|
||
valueSuffix: ' mm'
|
||
}
|
||
}, {
|
||
name: 'Regenrate',
|
||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
|
||
color: 'rgb(59, 130, 246)',
|
||
dashStyle: 'Dash',
|
||
type: 'line',
|
||
tooltip: {
|
||
valueSuffix: ' mm/h'
|
||
}
|
||
}]
|
||
}), [sortedData])
|
||
|
||
// Windgeschwindigkeit Chart
|
||
const windSpeedOptions = useMemo(() => ({
|
||
...getCommonOptions(),
|
||
plotOptions: {
|
||
series: {
|
||
marker: {
|
||
enabled: false
|
||
},
|
||
lineWidth: 2
|
||
},
|
||
line: {
|
||
step: 'left' // Keine Glättung
|
||
}
|
||
},
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: {
|
||
text: 'Windspeed (km/h)',
|
||
style: {
|
||
whiteSpace: 'nowrap'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: 'Windgeschwindigkeit',
|
||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
|
||
color: 'rgb(153, 102, 255)',
|
||
fillColor: 'rgba(153, 102, 255, 0.1)',
|
||
type: 'area',
|
||
tooltip: {
|
||
valueSuffix: ' km/h'
|
||
}
|
||
}, {
|
||
name: 'Windböen',
|
||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_gust]),
|
||
color: 'rgb(255, 159, 64)',
|
||
fillColor: 'rgba(255, 159, 64, 0.1)',
|
||
type: 'area',
|
||
tooltip: {
|
||
valueSuffix: ' km/h'
|
||
}
|
||
}]
|
||
}), [sortedData])
|
||
|
||
// Windrichtung Chart
|
||
const windDirOptions = useMemo(() => ({
|
||
...getCommonOptions(),
|
||
plotOptions: {
|
||
scatter: {
|
||
marker: {
|
||
enabled: true,
|
||
radius: 2,
|
||
states: {
|
||
hover: {
|
||
enabled: true,
|
||
radius: 3
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
...getCommonOptions().yAxis,
|
||
title: { text: 'Windrichtung' },
|
||
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.map(item => [new Date(item.datetime).getTime(), item.wind_dir]),
|
||
color: 'rgb(54, 162, 235)',
|
||
type: 'scatter',
|
||
tooltip: {
|
||
valueSuffix: ' °'
|
||
}
|
||
}]
|
||
}), [sortedData])
|
||
|
||
// Aktuellste Werte für Übersicht
|
||
const current = sortedData[sortedData.length - 1] || {}
|
||
|
||
// Berechne Min/Max für den aktuellen Tag
|
||
const todayStats = useMemo(() => {
|
||
const now = new Date()
|
||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
|
||
const todayData = sortedData.filter(item => {
|
||
const itemDate = new Date(item.datetime)
|
||
return itemDate >= todayStart
|
||
})
|
||
|
||
if (todayData.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
|
||
}
|
||
}
|
||
|
||
// Temperatur
|
||
const minTempItem = todayData.reduce((min, item) =>
|
||
item.temperature != null && (min === null || item.temperature < min.temperature) ? item : min, null)
|
||
const maxTempItem = todayData.reduce((max, item) =>
|
||
item.temperature != null && (max === null || item.temperature > max.temperature) ? item : max, null)
|
||
|
||
// Luftfeuchtigkeit
|
||
const minHumidityItem = todayData.reduce((min, item) =>
|
||
item.humidity != null && (min === null || item.humidity < min.humidity) ? item : min, null)
|
||
const maxHumidityItem = todayData.reduce((max, item) =>
|
||
item.humidity != null && (max === null || item.humidity > max.humidity) ? item : max, null)
|
||
|
||
// Luftdruck
|
||
const minPressureItem = todayData.reduce((min, item) =>
|
||
item.pressure != null && (min === null || item.pressure < min.pressure) ? item : min, null)
|
||
const maxPressureItem = todayData.reduce((max, item) =>
|
||
item.pressure != null && (max === null || item.pressure > max.pressure) ? item : max, null)
|
||
|
||
// Windgeschwindigkeit
|
||
const maxWindGustItem = todayData.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), 'HH:mm', { locale: de }) : null,
|
||
maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), 'HH:mm', { locale: de }) : null,
|
||
minHumidity: minHumidityItem?.humidity ?? null,
|
||
maxHumidity: maxHumidityItem?.humidity ?? null,
|
||
minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
|
||
maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
|
||
minPressure: minPressureItem?.pressure ?? null,
|
||
maxPressure: maxPressureItem?.pressure ?? null,
|
||
minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), 'HH:mm', { locale: de }) : null,
|
||
maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), 'HH:mm', { locale: de }) : null,
|
||
maxWindGust: maxWindGustItem?.wind_gust ?? null,
|
||
maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), 'HH:mm', { locale: de }) : null
|
||
}
|
||
}, [sortedData])
|
||
|
||
return (
|
||
<div className="dashboard">
|
||
{/* Charts Grid */}
|
||
<div className="charts-grid">
|
||
<div className="chart-container">
|
||
<h3>🌡️ Temperatur - Aktuell: {current.temperature?.toFixed(1) || '-'}°C</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
||
</div>
|
||
<div className="chart-stats">
|
||
Min: {todayStats.minTemp?.toFixed(1) || '-'}°C ({todayStats.minTempTime || '-'}) | Max: {todayStats.maxTemp?.toFixed(1) || '-'}°C ({todayStats.maxTempTime || '-'})
|
||
</div>
|
||
</div>
|
||
|
||
<div className="chart-container">
|
||
<h3>🌐 Luftdruck - Aktuell: {current.pressure?.toFixed(1) || '-'} hPa</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
|
||
</div>
|
||
<div className="chart-stats">
|
||
Min: {todayStats.minPressure?.toFixed(1) || '-'} hPa ({todayStats.minPressureTime || '-'}) | Max: {todayStats.maxPressure?.toFixed(1) || '-'} hPa ({todayStats.maxPressureTime || '-'})
|
||
</div>
|
||
</div>
|
||
|
||
<div className="chart-container">
|
||
<h3>💧 Luftfeuchtigkeit - Aktuell: {current.humidity || '-'}%</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
|
||
</div>
|
||
<div className="chart-stats">
|
||
Min: {todayStats.minHumidity || '-'}% ({todayStats.minHumidityTime || '-'}) | Max: {todayStats.maxHumidity || '-'}% ({todayStats.maxHumidityTime || '-'})
|
||
</div>
|
||
</div>
|
||
|
||
<div className="chart-container">
|
||
<h3>🌧️ Regen - Aktuell: {current.rain?.toFixed(1) || '-'} mm</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="chart-container">
|
||
<h3>🧭 Windrichtung - Aktuell: {current.wind_dir ?? '-'}°</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={windDirOptions} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="chart-container">
|
||
<h3>💨 Windspeed - Aktuell: {current.wind_speed?.toFixed(1) || '-'} km/h</h3>
|
||
<div className="chart-wrapper">
|
||
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
|
||
</div>
|
||
<div className="chart-stats">
|
||
Max: {todayStats.maxWindGust?.toFixed(1) || '-'} km/h ({todayStats.maxWindGustTime || '-'})
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div className="dashboard-footer">
|
||
<div className="version-line">
|
||
<div><a href="mailto:rxf@gmx.de">
|
||
mailto:rxf@gmx.de
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<span className="version-full">Version</span>
|
||
<span className="version-short">V</span>
|
||
{' '}{version} – {buildDate}
|
||
</div>
|
||
</div>
|
||
<hr />
|
||
<div className="footer-credits">
|
||
<div className="footer-left">Daten-Erfassung mit einer Davis VantagePro.</div>
|
||
<div className="footer-right">Grafiken erzeugt mit HighCharts</div>
|
||
</div>
|
||
<div className="footer-sponsor">
|
||
Die Wetterstation wurde vom Zeitungsverlag Waiblingen <a href="https://www.zvw.de" target="_blank" rel="noopener noreferrer">www.zvw.de</a> gestiftet.
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default WeatherDashboard
|