Files
wetter_station/frontend/src/components/WeatherDashboard.jsx
rxf 0b9d21c24c V1.1.0 Responsive
Footer angepasst
Lauffähigkeit auf Server verbessert
deploy.sh mit for loop
2026-03-22 18:44:22 +01:00

460 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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