**WIP**
This commit is contained in:
121
api/main.py
121
api/main.py
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import os
|
import os
|
||||||
@@ -47,6 +47,8 @@ app.add_middleware(
|
|||||||
|
|
||||||
# Pydantic Models
|
# Pydantic Models
|
||||||
class WeatherData(BaseModel):
|
class WeatherData(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
datetime: datetime
|
datetime: datetime
|
||||||
temperature: Optional[float] = None
|
temperature: Optional[float] = None
|
||||||
@@ -59,9 +61,6 @@ class WeatherData(BaseModel):
|
|||||||
rain_rate: Optional[float] = None
|
rain_rate: Optional[float] = None
|
||||||
received_at: datetime
|
received_at: datetime
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherStats(BaseModel):
|
class WeatherStats(BaseModel):
|
||||||
avg_temperature: Optional[float] = None
|
avg_temperature: Optional[float] = None
|
||||||
@@ -343,6 +342,120 @@ async def get_rain_data(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/hourly-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
|
||||||
|
async def get_hourly_aggregated_data(
|
||||||
|
days: int = Query(7, ge=1, le=60, description="Anzahl Tage zurück (max 60)")
|
||||||
|
):
|
||||||
|
"""Gibt stündlich aggregierte Wetterdaten zurück (Stundenmittel)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
0 as id,
|
||||||
|
date_trunc('hour', datetime) as datetime,
|
||||||
|
AVG(temperature) as temperature,
|
||||||
|
ROUND(AVG(humidity)) as humidity,
|
||||||
|
AVG(pressure) as pressure,
|
||||||
|
AVG(wind_speed * 1.60934) as wind_speed,
|
||||||
|
MAX(wind_gust * 1.60934) as wind_gust,
|
||||||
|
AVG(wind_dir) as wind_dir,
|
||||||
|
AVG(rain) as rain,
|
||||||
|
AVG(rain_rate) as rain_rate,
|
||||||
|
MAX(received_at) as received_at
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('hour', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (days,))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/daily-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
|
||||||
|
async def get_daily_aggregated_data(
|
||||||
|
days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück (max 730)")
|
||||||
|
):
|
||||||
|
"""Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
0 as id,
|
||||||
|
date_trunc('day', datetime) as datetime,
|
||||||
|
AVG(temperature) as temperature,
|
||||||
|
ROUND(AVG(humidity)) as humidity,
|
||||||
|
AVG(pressure) as pressure,
|
||||||
|
AVG(wind_speed * 1.60934) as wind_speed,
|
||||||
|
MAX(wind_gust * 1.60934) as wind_gust,
|
||||||
|
AVG(wind_dir) as wind_dir,
|
||||||
|
AVG(rain) as rain,
|
||||||
|
AVG(rain_rate) as rain_rate,
|
||||||
|
MAX(received_at) as received_at
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('day', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (days,))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/rain-daily", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
|
async def get_daily_rain_data(
|
||||||
|
days: int = Query(30, ge=1, le=365, description="Anzahl Tage zurück")
|
||||||
|
):
|
||||||
|
"""Gibt tägliche Regensummen zurück"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
date_trunc('day', datetime) as date,
|
||||||
|
SUM(rain) as total_rain
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('day', datetime)
|
||||||
|
ORDER BY date ASC
|
||||||
|
""", (days,))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/rain-weekly", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
|
async def get_weekly_rain_data(
|
||||||
|
days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück")
|
||||||
|
):
|
||||||
|
"""Gibt wöchentliche Regensummen zurück (Woche = Mo-So)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
date_trunc('week', datetime) as week_start,
|
||||||
|
SUM(rain) as total_rain
|
||||||
|
FROM weather_data
|
||||||
|
WHERE datetime >= NOW() - make_interval(days => %s)
|
||||||
|
GROUP BY date_trunc('week', datetime)
|
||||||
|
ORDER BY week_start ASC
|
||||||
|
""", (days,))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -4,35 +4,73 @@ import './App.css'
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [weatherData, setWeatherData] = useState([])
|
const [weatherData, setWeatherData] = useState([])
|
||||||
|
const [rainData, setRainData] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [lastUpdate, setLastUpdate] = useState(null)
|
const [lastUpdate, setLastUpdate] = useState(null)
|
||||||
|
const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
// Prüfe ob eingebettete Daten vorhanden sind (statischer Build)
|
setLoading(true)
|
||||||
if (window.__WEATHER_DATA__) {
|
|
||||||
setWeatherData(window.__WEATHER_DATA__)
|
|
||||||
setLastUpdate(new Date())
|
|
||||||
setLoading(false)
|
|
||||||
} else {
|
|
||||||
// Development oder Production: Daten von API holen
|
|
||||||
// Im Development: localhost:8000
|
|
||||||
// Im Production: /api/ (nginx proxy)
|
|
||||||
const apiUrl = import.meta.env.DEV
|
|
||||||
? 'http://localhost:8000/weather/history?hours=24'
|
|
||||||
: '/api/weather/history?hours=24'
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl)
|
// Prüfe ob eingebettete Daten vorhanden sind (statischer Build)
|
||||||
if (!response.ok) {
|
if (window.__WEATHER_DATA__ && timeRange === '24h') {
|
||||||
throw new Error('API-Fehler: ' + response.status)
|
setWeatherData(window.__WEATHER_DATA__)
|
||||||
}
|
setRainData([])
|
||||||
const data = await response.json()
|
|
||||||
setWeatherData(data)
|
|
||||||
setLastUpdate(new Date())
|
setLastUpdate(new Date())
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API-URLs basierend auf Zeitraum
|
||||||
|
let weatherUrl, rainUrl
|
||||||
|
const baseUrl = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
||||||
|
|
||||||
|
switch (timeRange) {
|
||||||
|
case '24h':
|
||||||
|
weatherUrl = `${baseUrl}/weather/history?hours=24`
|
||||||
|
rainUrl = null
|
||||||
|
break
|
||||||
|
case '7d':
|
||||||
|
weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=7`
|
||||||
|
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
|
||||||
|
break
|
||||||
|
case '30d':
|
||||||
|
weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=30`
|
||||||
|
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
|
||||||
|
break
|
||||||
|
case '365d':
|
||||||
|
weatherUrl = `${baseUrl}/weather/daily-aggregated?days=365`
|
||||||
|
rainUrl = `${baseUrl}/weather/rain-weekly?days=365`
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
weatherUrl = `${baseUrl}/weather/history?hours=24`
|
||||||
|
rainUrl = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wetterdaten laden
|
||||||
|
const weatherResponse = await fetch(weatherUrl)
|
||||||
|
if (!weatherResponse.ok) {
|
||||||
|
throw new Error('API-Fehler: ' + weatherResponse.status)
|
||||||
|
}
|
||||||
|
const weatherDataResult = await weatherResponse.json()
|
||||||
|
setWeatherData(weatherDataResult)
|
||||||
|
|
||||||
|
// Regendaten laden (falls separater Endpunkt)
|
||||||
|
if (rainUrl) {
|
||||||
|
const rainResponse = await fetch(rainUrl)
|
||||||
|
if (rainResponse.ok) {
|
||||||
|
const rainDataResult = await rainResponse.json()
|
||||||
|
setRainData(rainDataResult)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setRainData([])
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastUpdate(new Date())
|
||||||
|
setLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -41,12 +79,12 @@ function App() {
|
|||||||
|
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
||||||
// Automatisches Update alle 5 Minuten (nur im Entwicklungsmodus)
|
// Automatisches Update alle 5 Minuten (nur für 24h und ohne statische Daten)
|
||||||
if (!window.__WEATHER_DATA__) {
|
if (!window.__WEATHER_DATA__ && timeRange === '24h') {
|
||||||
const interval = setInterval(fetchData, 5 * 60 * 1000)
|
const interval = setInterval(fetchData, 5 * 60 * 1000)
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [timeRange])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +136,12 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
<WeatherDashboard data={weatherData} />
|
<WeatherDashboard
|
||||||
|
data={weatherData}
|
||||||
|
rainData={rainData}
|
||||||
|
timeRange={timeRange}
|
||||||
|
onTimeRangeChange={setTimeRange}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,48 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-range-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-nav button {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-nav button:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-nav button.active {
|
||||||
|
background: #0066cc;
|
||||||
|
border-color: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-label {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.current-values {
|
.current-values {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
@@ -137,6 +179,15 @@
|
|||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-range-nav button {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.version-short {
|
.version-short {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,47 @@ Highcharts.setOptions({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const WeatherDashboard = ({ data }) => {
|
const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeChange }) => {
|
||||||
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
|
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
|
||||||
const sortedData = useMemo(() => {
|
const sortedData = useMemo(() => {
|
||||||
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])
|
||||||
|
|
||||||
// Gemeinsame Chart-Optionen
|
// Zeitraum-Label
|
||||||
const getCommonOptions = () => ({
|
const timeRangeLabel = useMemo(() => {
|
||||||
|
switch (timeRange) {
|
||||||
|
case '24h': return 'Die letzten 24 Stunden'
|
||||||
|
case '7d': return 'Die letzten 7 Tage'
|
||||||
|
case '30d': return 'Die letzten 30 Tage'
|
||||||
|
case '365d': return 'Die letzten 365 Tage'
|
||||||
|
default: return 'Die letzten 24 Stunden'
|
||||||
|
}
|
||||||
|
}, [timeRange])
|
||||||
|
|
||||||
|
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
||||||
|
const getCommonOptions = () => {
|
||||||
|
// X-Achsen-Konfiguration basierend auf Zeitraum
|
||||||
|
let xAxisConfig = {
|
||||||
|
type: 'datetime',
|
||||||
|
gridLineWidth: 1,
|
||||||
|
gridLineColor: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (timeRange) {
|
||||||
|
case '24h':
|
||||||
|
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
||||||
|
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
||||||
|
break
|
||||||
|
case '7d':
|
||||||
|
case '30d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
|
break
|
||||||
|
case '365d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%b}', align: 'center' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
chart: {
|
chart: {
|
||||||
height: '50%',
|
height: '50%',
|
||||||
animation: false,
|
animation: false,
|
||||||
@@ -48,7 +81,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
shared: true,
|
shared: true,
|
||||||
crosshairs: true,
|
crosshairs: true,
|
||||||
xDateFormat: '%d.%m.%Y %H:%M'
|
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : '%d.%m.%Y'
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
series: {
|
series: {
|
||||||
@@ -63,20 +96,12 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: xAxisConfig,
|
||||||
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: {
|
yAxis: {
|
||||||
gridLineColor: 'rgba(0, 0, 0, 0.05)'
|
gridLineColor: 'rgba(0, 0, 0, 0.05)'
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Temperatur Chart
|
// Temperatur Chart
|
||||||
const temperatureOptions = useMemo(() => {
|
const temperatureOptions = useMemo(() => {
|
||||||
@@ -192,14 +217,15 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
}
|
}
|
||||||
}, [sortedData])
|
}, [sortedData])
|
||||||
|
|
||||||
// Regen Chart
|
// Regen Chart (angepasst an Zeitraum)
|
||||||
const rainOptions = useMemo(() => ({
|
const rainOptions = useMemo(() => {
|
||||||
...getCommonOptions(),
|
let series = []
|
||||||
yAxis: {
|
let yAxisTitle = 'Regen (mm) / Rate (mm/h)'
|
||||||
...getCommonOptions().yAxis,
|
|
||||||
title: { text: 'Regen (mm) / Rate (mm/h)' }
|
if (timeRange === '24h') {
|
||||||
},
|
// 24h: Area Chart mit Regen und Regenrate
|
||||||
series: [{
|
yAxisTitle = 'Regen (mm) / Rate (mm/h)'
|
||||||
|
series = [{
|
||||||
name: 'Regen',
|
name: 'Regen',
|
||||||
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
|
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
|
||||||
color: 'rgb(54, 162, 235)',
|
color: 'rgb(54, 162, 235)',
|
||||||
@@ -218,7 +244,41 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
valueSuffix: ' mm/h'
|
valueSuffix: ' mm/h'
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}), [sortedData])
|
} else if (timeRange === '7d' || timeRange === '30d') {
|
||||||
|
// 7d/30d: Balkendiagramm mit täglichen Summen
|
||||||
|
yAxisTitle = 'Regen (mm pro Tag)'
|
||||||
|
series = [{
|
||||||
|
name: 'Regen',
|
||||||
|
data: rainData.map(item => [new Date(item.date).getTime(), item.total_rain || 0]),
|
||||||
|
color: 'rgb(54, 162, 235)',
|
||||||
|
type: 'column',
|
||||||
|
tooltip: {
|
||||||
|
valueSuffix: ' mm'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
} else if (timeRange === '365d') {
|
||||||
|
// 365d: Balkendiagramm mit wöchentlichen Summen
|
||||||
|
yAxisTitle = 'Regen (mm pro Woche)'
|
||||||
|
series = [{
|
||||||
|
name: 'Regen',
|
||||||
|
data: rainData.map(item => [new Date(item.week_start).getTime(), item.total_rain || 0]),
|
||||||
|
color: 'rgb(54, 162, 235)',
|
||||||
|
type: 'column',
|
||||||
|
tooltip: {
|
||||||
|
valueSuffix: ' mm'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getCommonOptions(),
|
||||||
|
yAxis: {
|
||||||
|
...getCommonOptions().yAxis,
|
||||||
|
title: { text: yAxisTitle }
|
||||||
|
},
|
||||||
|
series
|
||||||
|
}
|
||||||
|
}, [sortedData, rainData, timeRange])
|
||||||
|
|
||||||
// Windgeschwindigkeit Chart
|
// Windgeschwindigkeit Chart
|
||||||
const windSpeedOptions = useMemo(() => ({
|
const windSpeedOptions = useMemo(() => ({
|
||||||
@@ -311,17 +371,12 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
// Aktuellste Werte für Übersicht
|
// Aktuellste Werte für Übersicht
|
||||||
const current = sortedData[sortedData.length - 1] || {}
|
const current = sortedData[sortedData.length - 1] || {}
|
||||||
|
|
||||||
// Berechne Min/Max für den aktuellen Tag
|
// Berechne Min/Max für den gewählten Zeitraum
|
||||||
const todayStats = useMemo(() => {
|
const periodStats = useMemo(() => {
|
||||||
const now = new Date()
|
// Für den gewählten Zeitraum alle Daten verwenden
|
||||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
const periodData = sortedData
|
||||||
|
|
||||||
const todayData = sortedData.filter(item => {
|
if (periodData.length === 0) {
|
||||||
const itemDate = new Date(item.datetime)
|
|
||||||
return itemDate >= todayStart
|
|
||||||
})
|
|
||||||
|
|
||||||
if (todayData.length === 0) {
|
|
||||||
return {
|
return {
|
||||||
minTemp: null, maxTemp: null, minTempTime: null, maxTempTime: null,
|
minTemp: null, maxTemp: null, minTempTime: null, maxTempTime: null,
|
||||||
minHumidity: null, maxHumidity: null, minHumidityTime: null, maxHumidityTime: null,
|
minHumidity: null, maxHumidity: null, minHumidityTime: null, maxHumidityTime: null,
|
||||||
@@ -329,48 +384,84 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zeitformat basierend auf Zeitraum
|
||||||
|
const timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
|
||||||
|
|
||||||
// Temperatur
|
// Temperatur
|
||||||
const minTempItem = todayData.reduce((min, item) =>
|
const minTempItem = periodData.reduce((min, item) =>
|
||||||
item.temperature != null && (min === null || item.temperature < min.temperature) ? item : min, null)
|
item.temperature != null && (min === null || item.temperature < min.temperature) ? item : min, null)
|
||||||
const maxTempItem = todayData.reduce((max, item) =>
|
const maxTempItem = periodData.reduce((max, item) =>
|
||||||
item.temperature != null && (max === null || item.temperature > max.temperature) ? item : max, null)
|
item.temperature != null && (max === null || item.temperature > max.temperature) ? item : max, null)
|
||||||
|
|
||||||
// Luftfeuchtigkeit
|
// Luftfeuchtigkeit
|
||||||
const minHumidityItem = todayData.reduce((min, item) =>
|
const minHumidityItem = periodData.reduce((min, item) =>
|
||||||
item.humidity != null && (min === null || item.humidity < min.humidity) ? item : min, null)
|
item.humidity != null && (min === null || item.humidity < min.humidity) ? item : min, null)
|
||||||
const maxHumidityItem = todayData.reduce((max, item) =>
|
const maxHumidityItem = periodData.reduce((max, item) =>
|
||||||
item.humidity != null && (max === null || item.humidity > max.humidity) ? item : max, null)
|
item.humidity != null && (max === null || item.humidity > max.humidity) ? item : max, null)
|
||||||
|
|
||||||
// Luftdruck
|
// Luftdruck
|
||||||
const minPressureItem = todayData.reduce((min, item) =>
|
const minPressureItem = periodData.reduce((min, item) =>
|
||||||
item.pressure != null && (min === null || item.pressure < min.pressure) ? item : min, null)
|
item.pressure != null && (min === null || item.pressure < min.pressure) ? item : min, null)
|
||||||
const maxPressureItem = todayData.reduce((max, item) =>
|
const maxPressureItem = periodData.reduce((max, item) =>
|
||||||
item.pressure != null && (max === null || item.pressure > max.pressure) ? item : max, null)
|
item.pressure != null && (max === null || item.pressure > max.pressure) ? item : max, null)
|
||||||
|
|
||||||
// Windgeschwindigkeit
|
// Windgeschwindigkeit
|
||||||
const maxWindGustItem = todayData.reduce((max, item) =>
|
const maxWindGustItem = periodData.reduce((max, item) =>
|
||||||
item.wind_gust != null && (max === null || item.wind_gust > max.wind_gust) ? item : max, null)
|
item.wind_gust != null && (max === null || item.wind_gust > max.wind_gust) ? item : max, null)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
minTemp: minTempItem?.temperature ?? null,
|
minTemp: minTempItem?.temperature ?? null,
|
||||||
maxTemp: maxTempItem?.temperature ?? null,
|
maxTemp: maxTempItem?.temperature ?? null,
|
||||||
minTempTime: minTempItem ? format(new Date(minTempItem.datetime), 'HH:mm', { locale: de }) : null,
|
minTempTime: minTempItem ? format(new Date(minTempItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), 'HH:mm', { locale: de }) : null,
|
maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
minHumidity: minHumidityItem?.humidity ?? null,
|
minHumidity: minHumidityItem?.humidity ?? null,
|
||||||
maxHumidity: maxHumidityItem?.humidity ?? null,
|
maxHumidity: maxHumidityItem?.humidity ?? null,
|
||||||
minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
|
minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
|
maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
minPressure: minPressureItem?.pressure ?? null,
|
minPressure: minPressureItem?.pressure ?? null,
|
||||||
maxPressure: maxPressureItem?.pressure ?? null,
|
maxPressure: maxPressureItem?.pressure ?? null,
|
||||||
minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), 'HH:mm', { locale: de }) : null,
|
minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), 'HH:mm', { locale: de }) : null,
|
maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), timeFormat, { locale: de }) : null,
|
||||||
maxWindGust: maxWindGustItem?.wind_gust ?? null,
|
maxWindGust: maxWindGustItem?.wind_gust ?? null,
|
||||||
maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), 'HH:mm', { locale: de }) : null
|
maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), timeFormat, { locale: de }) : null
|
||||||
}
|
}
|
||||||
}, [sortedData])
|
}, [sortedData, timeRange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard">
|
<div className="dashboard">
|
||||||
|
{/* Navigation für Zeitraum-Auswahl */}
|
||||||
|
<div className="time-range-nav">
|
||||||
|
<button
|
||||||
|
className={timeRange === '24h' ? 'active' : ''}
|
||||||
|
onClick={() => onTimeRangeChange('24h')}
|
||||||
|
>
|
||||||
|
24 Stunden
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={timeRange === '7d' ? 'active' : ''}
|
||||||
|
onClick={() => onTimeRangeChange('7d')}
|
||||||
|
>
|
||||||
|
7 Tage
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={timeRange === '30d' ? 'active' : ''}
|
||||||
|
onClick={() => onTimeRangeChange('30d')}
|
||||||
|
>
|
||||||
|
30 Tage
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={timeRange === '365d' ? 'active' : ''}
|
||||||
|
onClick={() => onTimeRangeChange('365d')}
|
||||||
|
>
|
||||||
|
365 Tage
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zeitraum-Beschreibung */}
|
||||||
|
<div className="time-range-label">
|
||||||
|
{timeRangeLabel}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Charts Grid */}
|
{/* Charts Grid */}
|
||||||
<div className="charts-grid">
|
<div className="charts-grid">
|
||||||
<div className="chart-container">
|
<div className="chart-container">
|
||||||
@@ -379,7 +470,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="chart-stats">
|
<div className="chart-stats">
|
||||||
Min: {todayStats.minTemp?.toFixed(1) || '-'}°C ({todayStats.minTempTime || '-'}) | Max: {todayStats.maxTemp?.toFixed(1) || '-'}°C ({todayStats.maxTempTime || '-'})
|
Min: {periodStats.minTemp?.toFixed(1) || '-'}°C ({periodStats.minTempTime || '-'}) | Max: {periodStats.maxTemp?.toFixed(1) || '-'}°C ({periodStats.maxTempTime || '-'})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -389,7 +480,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
|
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="chart-stats">
|
<div className="chart-stats">
|
||||||
Min: {todayStats.minPressure?.toFixed(1) || '-'} hPa ({todayStats.minPressureTime || '-'}) | Max: {todayStats.maxPressure?.toFixed(1) || '-'} hPa ({todayStats.maxPressureTime || '-'})
|
Min: {periodStats.minPressure?.toFixed(1) || '-'} hPa ({periodStats.minPressureTime || '-'}) | Max: {periodStats.maxPressure?.toFixed(1) || '-'} hPa ({periodStats.maxPressureTime || '-'})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -399,7 +490,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
|
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="chart-stats">
|
<div className="chart-stats">
|
||||||
Min: {todayStats.minHumidity || '-'}% ({todayStats.minHumidityTime || '-'}) | Max: {todayStats.maxHumidity || '-'}% ({todayStats.maxHumidityTime || '-'})
|
Min: {periodStats.minHumidity || '-'}% ({periodStats.minHumidityTime || '-'}) | Max: {periodStats.maxHumidity || '-'}% ({periodStats.maxHumidityTime || '-'})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -423,7 +514,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
|
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
|
||||||
</div>
|
</div>
|
||||||
<div className="chart-stats">
|
<div className="chart-stats">
|
||||||
Max: {todayStats.maxWindGust?.toFixed(1) || '-'} km/h ({todayStats.maxWindGustTime || '-'})
|
Max: {periodStats.maxWindGust?.toFixed(1) || '-'} km/h ({periodStats.maxWindGustTime || '-'})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user