Highcharts sieht viel besser aus
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user