Highcharts sieht viel besser aus

This commit is contained in:
rxf
2026-02-08 19:44:49 +01:00
parent 251b21fa4f
commit 2fc4bd9db6
2 changed files with 271 additions and 286 deletions

View File

@@ -9,11 +9,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"chart.js": "^4.4.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"highcharts": "^11.4.0",
"highcharts-react-official": "^3.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,33 +1,22 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { import Highcharts from 'highcharts'
Chart as ChartJS, import HighchartsReact from 'highcharts-react-official'
CategoryScale,
LinearScale,
TimeScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js'
import 'chartjs-adapter-date-fns'
import { Line } from 'react-chartjs-2'
import { format } from 'date-fns' import { format } from 'date-fns'
import { de } from 'date-fns/locale' import { de } from 'date-fns/locale'
import './WeatherDashboard.css' import './WeatherDashboard.css'
ChartJS.register( // Deutsche Lokalisierung für Highcharts
CategoryScale, Highcharts.setOptions({
LinearScale, lang: {
TimeScale, months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
PointElement, shortMonths: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
LineElement, weekdays: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
Title, resetZoom: 'Zoom zurücksetzen'
Tooltip, },
Legend, time: {
Filler useUTC: false
) }
})
const WeatherDashboard = ({ data }) => { const WeatherDashboard = ({ data }) => {
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst) // Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
@@ -35,292 +24,289 @@ const WeatherDashboard = ({ data }) => {
return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime)) return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
}, [data]) }, [data])
// Labels für X-Achse (Zeit) // Gemeinsame Chart-Optionen
const labels = useMemo(() => { const getCommonOptions = () => ({
return sortedData.map(item => chart: {
format(new Date(item.datetime), 'HH:mm', { locale: de }) height: 250,
) animation: false,
}, [sortedData]) backgroundColor: 'transparent'
// Chart-Konfiguration
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
}, },
elements: { credits: {
point: { enabled: false
radius: 0,
hitRadius: 10,
hoverRadius: 5,
}
}, },
plugins: { title: {
legend: { text: null
display: false,
},
tooltip: {
callbacks: {
title: (context) => {
const index = context[0].dataIndex
return format(new Date(sortedData[index].datetime), 'dd.MM.yyyy HH:mm', { locale: de })
}
}
}
}, },
scales: { legend: {
x: { enabled: false
grid: { },
display: true, tooltip: {
color: 'rgba(0, 0, 0, 0.1)', shared: true,
}, crosshairs: true,
ticks: { xDateFormat: '%d.%m.%Y %H:%M'
type: 'time', },
time: { plotOptions: {
unit: 'hour', series: {
stepSize: 4 marker: {
}, enabled: false,
ticks: { states: {
autoSkip: false hover: {
} enabled: true,
/* radius: 5
maxRotation: 0,
autoSkip: false,
callback: function(value, index) {
if (sortedData.length === 0) return ''
const date = new Date(sortedData[index]?.datetime)
const hours = date.getHours()
const minutes = date.getMinutes()
// Berechne die nächste 4-Stunden-Zeit
const nearestFourHour = Math.round(hours / 4) * 4
// Wenn die Stunde durch 4 teilbar ist UND die Minuten <= 2 sind (also 00:00, 00:05 zählen),
// dann ist dies der Datenpunkt, der der 4-Stunden-Zeit am nächsten liegt
if (hours % 4 === 0 && minutes <= 2) {
return format(new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, 0), 'HH:mm', { locale: de })
} }
return ''
} }
*/
}
},
y: {
grid: {
color: 'rgba(0, 0, 0, 0.05)',
} }
} }
},
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 // Temperatur Chart
const temperatureData = { const temperatureOptions = useMemo(() => {
labels, const temps = sortedData.map(item => item.temperature)
datasets: [ const min = Math.min(...temps)
{ const max = Math.max(...temps)
label: 'Temperatur (°C)', const range = max - min
data: sortedData.map(item => item.temperature),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
fill: 'start',
tension: 0.4,
}
]
}
const temperatureOptions = { let yMin = min
...commonOptions, let yMax = max
scales: {
...commonOptions.scales, if (range < 15) {
y: { const center = (max + min) / 2
...commonOptions.scales.y, yMin = center - 7.5
afterDataLimits: (axis) => { yMax = center + 7.5
const range = axis.max - axis.min }
if (range < 15) {
const center = (axis.max + axis.min) / 2 return {
axis.max = center + 7.5 ...getCommonOptions(),
axis.min = center - 7.5 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: 'area',
threshold: yMin,
tooltip: {
valueSuffix: ' °C'
} }
} }]
} }
} }, [sortedData])
// Feuchte Chart // Luftfeuchtigkeit Chart
const humidityData = { const humidityOptions = useMemo(() => ({
labels, ...getCommonOptions(),
datasets: [ yAxis: {
{ ...getCommonOptions().yAxis,
label: 'Luftfeuchtigkeit (%)', title: { text: 'Luftfeuchtigkeit (%)' },
data: sortedData.map(item => item.humidity), min: 0,
borderColor: 'rgb(54, 162, 235)', max: 100
backgroundColor: 'rgba(54, 162, 235, 0.1)', },
fill: true, series: [{
tension: 0.4, name: 'Luftfeuchtigkeit',
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])
const humidityOptions = { // Luftdruck Chart
...commonOptions, const pressureOptions = useMemo(() => {
scales: { const pressures = sortedData.map(item => item.pressure)
...commonOptions.scales, const min = Math.min(...pressures)
y: { const max = Math.max(...pressures)
...commonOptions.scales.y, const range = max - min
min: 0,
max: 100 let yMin = min
} let yMax = max
if (range < 50) {
const center = (max + min) / 2
yMin = center - 25
yMax = center + 25
} }
}
// Druck Chart return {
const pressureData = { ...getCommonOptions(),
labels, yAxis: {
datasets: [ ...getCommonOptions().yAxis,
{ title: { text: 'Luftdruck (hPa)' },
label: 'Luftdruck (hPa)', min: yMin,
data: sortedData.map(item => item.pressure), max: yMax
borderColor: 'rgb(75, 192, 192)', },
backgroundColor: 'rgba(75, 192, 192, 0.1)', series: [{
fill: true, name: 'Luftdruck',
tension: 0.4, 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: [
const pressureOptions = { [0, 'rgba(75, 192, 192, 0.3)'],
...commonOptions, [1, 'rgba(75, 192, 192, 0.1)']
scales: { ]
...commonOptions.scales, },
y: { type: 'area',
...commonOptions.scales.y, tooltip: {
afterDataLimits: (axis) => { valueSuffix: ' hPa'
const range = axis.max - axis.min
if (range < 50) {
const center = (axis.max + axis.min) / 2
axis.max = center + 25
axis.min = center - 25
}
} }
} }]
} }
} }, [sortedData])
// Regen Chart // Regen Chart
const rainData = { const rainOptions = useMemo(() => ({
labels, ...getCommonOptions(),
datasets: [ legend: {
{ enabled: true,
label: 'Regen (mm)', align: 'center',
data: sortedData.map(item => item.rain), verticalAlign: 'top'
borderColor: 'rgb(54, 162, 235)', },
backgroundColor: 'rgba(54, 162, 235, 0.3)', yAxis: {
fill: true, ...getCommonOptions().yAxis,
tension: 0.4, title: { text: 'Regen (mm) / Rate (mm/h)' }
}, },
{ series: [{
label: 'Regenrate (mm/h)', name: 'Regen',
data: sortedData.map(item => item.rain_rate), data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
borderColor: 'rgb(59, 130, 246)', color: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(59, 130, 246, 0.1)', fillColor: 'rgba(54, 162, 235, 0.3)',
borderDash: [5, 5], type: 'area',
fill: false, tooltip: {
tension: 0.4, valueSuffix: ' mm'
} }
] }, {
} name: 'Regenrate',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
const rainOptions = { color: 'rgb(59, 130, 246)',
...commonOptions, dashStyle: 'Dash',
plugins: { type: 'line',
...commonOptions.plugins, tooltip: {
legend: { valueSuffix: ' mm/h'
display: true,
position: 'top',
} }
} }]
} }), [sortedData])
// Windgeschwindigkeit Chart // Windgeschwindigkeit Chart
const windSpeedData = { const windSpeedOptions = useMemo(() => ({
labels, ...getCommonOptions(),
datasets: [ legend: {
{ enabled: true,
label: 'Windgeschwindigkeit (km/h)', align: 'center',
data: sortedData.map(item => item.wind_speed), verticalAlign: 'top'
borderColor: 'rgb(153, 102, 255)', },
backgroundColor: 'rgba(153, 102, 255, 0.1)', plotOptions: {
fill: true, series: {
tension: 0, marker: {
enabled: false
},
lineWidth: 2
}, },
{ line: {
label: 'Windböen (km/h)', step: 'left' // Keine Glättung
data: sortedData.map(item => item.wind_gust),
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
fill: true,
tension: 0,
} }
] },
} yAxis: {
...getCommonOptions().yAxis,
const windSpeedOptions = { title: { text: 'Windgeschwindigkeit (km/h)' }
...commonOptions, },
plugins: { series: [{
...commonOptions.plugins, name: 'Windgeschwindigkeit',
legend: { data: sortedData.map(item => [new Date(item.datetime).getTime(), item.wind_speed]),
display: true, color: 'rgb(153, 102, 255)',
position: 'top', 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 // Windrichtung Chart
const windDirData = { const windDirOptions = useMemo(() => ({
labels, ...getCommonOptions(),
datasets: [ plotOptions: {
{ scatter: {
label: 'Windrichtung (°)', marker: {
data: sortedData.map(item => item.wind_dir), enabled: true,
borderColor: 'rgb(255, 205, 86)', radius: 4,
backgroundColor: 'rgb(255, 205, 86)', states: {
pointRadius: 4, hover: {
pointHoverRadius: 6, enabled: true,
showLine: false, radius: 6
fill: false, }
}
]
}
const windDirOptions = {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
min: 0,
max: 360,
ticks: {
stepSize: 45,
callback: (value) => {
if (value === 0 || value === 360) return 'N'
if (value === 45) return 'NO'
if (value === 90) return 'O'
if (value === 135) return 'SO'
if (value === 180) return 'S'
if (value === 225) return 'SW'
if (value === 270) return 'W'
if (value === 315) return 'NW'
return ''
} }
} }
} }
} },
} 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(255, 205, 86)',
type: 'scatter',
tooltip: {
valueSuffix: ' °'
}
}]
}), [sortedData])
// Aktuellste Werte für Übersicht // Aktuellste Werte für Übersicht
const current = sortedData[sortedData.length - 1] || {} const current = sortedData[sortedData.length - 1] || {}
@@ -356,42 +342,42 @@ const WeatherDashboard = ({ data }) => {
<div className="chart-container"> <div className="chart-container">
<h3>🌡 Temperatur</h3> <h3>🌡 Temperatur</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={temperatureData} options={temperatureOptions} /> <HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
</div> </div>
</div> </div>
<div className="chart-container"> <div className="chart-container">
<h3>💧 Luftfeuchtigkeit</h3> <h3>💧 Luftfeuchtigkeit</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={humidityData} options={humidityOptions} /> <HighchartsReact highcharts={Highcharts} options={humidityOptions} />
</div> </div>
</div> </div>
<div className="chart-container"> <div className="chart-container">
<h3>🌐 Luftdruck</h3> <h3>🌐 Luftdruck</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={pressureData} options={pressureOptions} /> <HighchartsReact highcharts={Highcharts} options={pressureOptions} />
</div> </div>
</div> </div>
<div className="chart-container"> <div className="chart-container">
<h3>🌧 Regen</h3> <h3>🌧 Regen</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={rainData} options={rainOptions} /> <HighchartsReact highcharts={Highcharts} options={rainOptions} />
</div> </div>
</div> </div>
<div className="chart-container"> <div className="chart-container">
<h3>💨 Windgeschwindigkeit</h3> <h3>💨 Windgeschwindigkeit</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={windSpeedData} options={windSpeedOptions} /> <HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div> </div>
</div> </div>
<div className="chart-container"> <div className="chart-container">
<h3>🧭 Windrichtung</h3> <h3>🧭 Windrichtung</h3>
<div className="chart-wrapper"> <div className="chart-wrapper">
<Line data={windDirData} options={windDirOptions} /> <HighchartsReact highcharts={Highcharts} options={windDirOptions} />
</div> </div>
</div> </div>
</div> </div>