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"
},
"dependencies": {
"chart.js": "^4.4.1",
"chartjs-adapter-date-fns": "^3.0.0",
"date-fns": "^3.3.1",
"highcharts": "^11.4.0",
"highcharts-react-official": "^3.2.1",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1"
},
"devDependencies": {

View File

@@ -1,33 +1,22 @@
import { useMemo } from 'react'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
TimeScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js'
import 'chartjs-adapter-date-fns'
import { Line } from 'react-chartjs-2'
import Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'
import './WeatherDashboard.css'
ChartJS.register(
CategoryScale,
LinearScale,
TimeScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
)
// 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)
@@ -35,292 +24,289 @@ const WeatherDashboard = ({ data }) => {
return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
}, [data])
// Labels für X-Achse (Zeit)
const labels = useMemo(() => {
return sortedData.map(item =>
format(new Date(item.datetime), 'HH:mm', { locale: de })
)
}, [sortedData])
// Chart-Konfiguration
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
// Gemeinsame Chart-Optionen
const getCommonOptions = () => ({
chart: {
height: 250,
animation: false,
backgroundColor: 'transparent'
},
elements: {
point: {
radius: 0,
hitRadius: 10,
hoverRadius: 5,
}
credits: {
enabled: false
},
plugins: {
legend: {
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 })
}
}
}
title: {
text: null
},
scales: {
x: {
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.1)',
},
ticks: {
type: 'time',
time: {
unit: 'hour',
stepSize: 4
},
ticks: {
autoSkip: false
}
/*
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 })
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
}
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
const temperatureData = {
labels,
datasets: [
{
label: 'Temperatur (°C)',
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 = useMemo(() => {
const temps = sortedData.map(item => item.temperature)
const min = Math.min(...temps)
const max = Math.max(...temps)
const range = max - min
const temperatureOptions = {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
afterDataLimits: (axis) => {
const range = axis.max - axis.min
if (range < 15) {
const center = (axis.max + axis.min) / 2
axis.max = center + 7.5
axis.min = center - 7.5
}
let yMin = min
let yMax = max
if (range < 15) {
const center = (max + min) / 2
yMin = center - 7.5
yMax = center + 7.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: 'area',
threshold: yMin,
tooltip: {
valueSuffix: ' °C'
}
}
}]
}
}
}, [sortedData])
// Feuchte Chart
const humidityData = {
labels,
datasets: [
{
label: 'Luftfeuchtigkeit (%)',
data: sortedData.map(item => item.humidity),
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
fill: true,
tension: 0.4,
// Luftfeuchtigkeit Chart
const humidityOptions = useMemo(() => ({
...getCommonOptions(),
yAxis: {
...getCommonOptions().yAxis,
title: { text: 'Luftfeuchtigkeit (%)' },
min: 0,
max: 100
},
series: [{
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 = {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
min: 0,
max: 100
}
// 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 < 50) {
const center = (max + min) / 2
yMin = center - 25
yMax = center + 25
}
}
// Druck Chart
const pressureData = {
labels,
datasets: [
{
label: 'Luftdruck (hPa)',
data: sortedData.map(item => item.pressure),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
fill: true,
tension: 0.4,
}
]
}
const pressureOptions = {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
afterDataLimits: (axis) => {
const range = axis.max - axis.min
if (range < 50) {
const center = (axis.max + axis.min) / 2
axis.max = center + 25
axis.min = center - 25
}
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 rainData = {
labels,
datasets: [
{
label: 'Regen (mm)',
data: sortedData.map(item => item.rain),
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.3)',
fill: true,
tension: 0.4,
},
{
label: 'Regenrate (mm/h)',
data: sortedData.map(item => item.rain_rate),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderDash: [5, 5],
fill: false,
tension: 0.4,
const rainOptions = useMemo(() => ({
...getCommonOptions(),
legend: {
enabled: true,
align: 'center',
verticalAlign: 'top'
},
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'
}
]
}
const rainOptions = {
...commonOptions,
plugins: {
...commonOptions.plugins,
legend: {
display: true,
position: 'top',
}, {
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 windSpeedData = {
labels,
datasets: [
{
label: 'Windgeschwindigkeit (km/h)',
data: sortedData.map(item => item.wind_speed),
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
fill: true,
tension: 0,
const windSpeedOptions = useMemo(() => ({
...getCommonOptions(),
legend: {
enabled: true,
align: 'center',
verticalAlign: 'top'
},
plotOptions: {
series: {
marker: {
enabled: false
},
lineWidth: 2
},
{
label: 'Windböen (km/h)',
data: sortedData.map(item => item.wind_gust),
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
fill: true,
tension: 0,
line: {
step: 'left' // Keine Glättung
}
]
}
const windSpeedOptions = {
...commonOptions,
plugins: {
...commonOptions.plugins,
legend: {
display: true,
position: 'top',
},
yAxis: {
...getCommonOptions().yAxis,
title: { text: 'Windgeschwindigkeit (km/h)' }
},
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 windDirData = {
labels,
datasets: [
{
label: 'Windrichtung (°)',
data: sortedData.map(item => item.wind_dir),
borderColor: 'rgb(255, 205, 86)',
backgroundColor: 'rgb(255, 205, 86)',
pointRadius: 4,
pointHoverRadius: 6,
showLine: false,
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 ''
const windDirOptions = useMemo(() => ({
...getCommonOptions(),
plotOptions: {
scatter: {
marker: {
enabled: true,
radius: 4,
states: {
hover: {
enabled: true,
radius: 6
}
}
}
}
}
}
},
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
const current = sortedData[sortedData.length - 1] || {}
@@ -356,42 +342,42 @@ const WeatherDashboard = ({ data }) => {
<div className="chart-container">
<h3>🌡 Temperatur</h3>
<div className="chart-wrapper">
<Line data={temperatureData} options={temperatureOptions} />
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
</div>
</div>
<div className="chart-container">
<h3>💧 Luftfeuchtigkeit</h3>
<div className="chart-wrapper">
<Line data={humidityData} options={humidityOptions} />
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
</div>
</div>
<div className="chart-container">
<h3>🌐 Luftdruck</h3>
<div className="chart-wrapper">
<Line data={pressureData} options={pressureOptions} />
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
</div>
</div>
<div className="chart-container">
<h3>🌧 Regen</h3>
<div className="chart-wrapper">
<Line data={rainData} options={rainOptions} />
<HighchartsReact highcharts={Highcharts} options={rainOptions} />
</div>
</div>
<div className="chart-container">
<h3>💨 Windgeschwindigkeit</h3>
<div className="chart-wrapper">
<Line data={windSpeedData} options={windSpeedOptions} />
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div>
</div>
<div className="chart-container">
<h3>🧭 Windrichtung</h3>
<div className="chart-wrapper">
<Line data={windDirData} options={windDirOptions} />
<HighchartsReact highcharts={Highcharts} options={windDirOptions} />
</div>
</div>
</div>