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, title: {
hoverRadius: 5, text: null
}
}, },
plugins: {
legend: { legend: {
display: false, enabled: false
}, },
tooltip: { tooltip: {
callbacks: { shared: true,
title: (context) => { crosshairs: true,
const index = context[0].dataIndex xDateFormat: '%d.%m.%Y %H:%M'
return format(new Date(sortedData[index].datetime), 'dd.MM.yyyy HH:mm', { locale: de }) },
plotOptions: {
series: {
marker: {
enabled: false,
states: {
hover: {
enabled: true,
radius: 5
}
} }
} }
} }
}, },
scales: { xAxis: {
x: { type: 'datetime',
grid: { tickInterval: 4 * 3600 * 1000, // 4 Stunden in Millisekunden
display: true, labels: {
color: 'rgba(0, 0, 0, 0.1)', format: '{value:%H:%M}',
align: 'center'
}, },
ticks: { gridLineWidth: 1,
type: 'time', gridLineColor: 'rgba(0, 0, 0, 0.1)'
time: {
unit: 'hour',
stepSize: 4
}, },
ticks: { yAxis: {
autoSkip: false gridLineColor: 'rgba(0, 0, 0, 0.05)'
}
/*
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)',
}
}
}
} }
})
// 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)', let yMin = min
backgroundColor: 'rgba(255, 99, 132, 0.1)', let yMax = max
fill: 'start',
tension: 0.4,
}
]
}
const temperatureOptions = {
...commonOptions,
scales: {
...commonOptions.scales,
y: {
...commonOptions.scales.y,
afterDataLimits: (axis) => {
const range = axis.max - axis.min
if (range < 15) { if (range < 15) {
const center = (axis.max + axis.min) / 2 const center = (max + min) / 2
axis.max = center + 7.5 yMin = center - 7.5
axis.min = center - 7.5 yMax = center + 7.5
}
}
}
}
} }
// Feuchte Chart return {
const humidityData = { ...getCommonOptions(),
labels, yAxis: {
datasets: [ ...getCommonOptions().yAxis,
{ title: { text: 'Temperatur (°C)' },
label: 'Luftfeuchtigkeit (%)', min: yMin,
data: sortedData.map(item => item.humidity), max: yMax
borderColor: 'rgb(54, 162, 235)', },
backgroundColor: 'rgba(54, 162, 235, 0.1)', series: [{
fill: true, name: 'Temperatur',
tension: 0.4, 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])
const humidityOptions = { // Luftfeuchtigkeit Chart
...commonOptions, const humidityOptions = useMemo(() => ({
scales: { ...getCommonOptions(),
...commonOptions.scales, yAxis: {
y: { ...getCommonOptions().yAxis,
...commonOptions.scales.y, title: { text: 'Luftfeuchtigkeit (%)' },
min: 0, min: 0,
max: 100 max: 100
} },
} series: [{
} name: 'Luftfeuchtigkeit',
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.humidity]),
// Druck Chart color: 'rgb(54, 162, 235)',
const pressureData = { fillColor: {
labels, linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
datasets: [ stops: [
{ [0, 'rgba(54, 162, 235, 0.3)'],
label: 'Luftdruck (hPa)', [1, 'rgba(54, 162, 235, 0.1)']
data: sortedData.map(item => item.pressure),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
fill: true,
tension: 0.4,
}
] ]
},
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 < 50) {
const center = (max + min) / 2
yMin = center - 25
yMax = center + 25
} }
const pressureOptions = { return {
...commonOptions, ...getCommonOptions(),
scales: { yAxis: {
...commonOptions.scales, ...getCommonOptions().yAxis,
y: { title: { text: 'Luftdruck (hPa)' },
...commonOptions.scales.y, min: yMin,
afterDataLimits: (axis) => { max: yMax
const range = axis.max - axis.min },
if (range < 50) { series: [{
const center = (axis.max + axis.min) / 2 name: 'Luftdruck',
axis.max = center + 25 data: sortedData.map(item => [new Date(item.datetime).getTime(), item.pressure]),
axis.min = center - 25 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 // Regen Chart
const rainData = { const rainOptions = useMemo(() => ({
labels, ...getCommonOptions(),
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 = {
...commonOptions,
plugins: {
...commonOptions.plugins,
legend: { legend: {
display: true, enabled: true,
position: 'top', 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'
} }
}, {
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 // Windgeschwindigkeit Chart
const windSpeedData = { const windSpeedOptions = useMemo(() => ({
labels, ...getCommonOptions(),
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,
},
{
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,
}
]
}
const windSpeedOptions = {
...commonOptions,
plugins: {
...commonOptions.plugins,
legend: { legend: {
display: true, enabled: true,
position: 'top', align: 'center',
verticalAlign: 'top'
},
plotOptions: {
series: {
marker: {
enabled: false
},
lineWidth: 2
},
line: {
step: 'left' // Keine Glättung
} }
},
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 // 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: { yAxis: {
...commonOptions.scales, ...getCommonOptions().yAxis,
y: { title: { text: 'Windrichtung' },
...commonOptions.scales.y,
min: 0, min: 0,
max: 360, max: 360,
ticks: { tickInterval: 45,
stepSize: 45, labels: {
callback: (value) => { formatter: function() {
if (value === 0 || value === 360) return 'N' const directions = {
if (value === 45) return 'NO' 0: 'N', 45: 'NO', 90: 'O', 135: 'SO',
if (value === 90) return 'O' 180: 'S', 225: 'SW', 270: 'W', 315: 'NW', 360: 'N'
if (value === 135) return 'SO' }
if (value === 180) return 'S' return directions[this.value] || ''
if (value === 225) return 'SW'
if (value === 270) return 'W'
if (value === 315) return 'NW'
return ''
}
}
} }
} }
},
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>