This commit is contained in:
rxf
2026-03-22 20:09:44 +01:00
parent 0b9d21c24c
commit c471c0e33a
4 changed files with 426 additions and 128 deletions

View File

@@ -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)

View File

@@ -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 {
setLoading(true)
// Prüfe ob eingebettete Daten vorhanden sind (statischer Build) // Prüfe ob eingebettete Daten vorhanden sind (statischer Build)
if (window.__WEATHER_DATA__) { if (window.__WEATHER_DATA__ && timeRange === '24h') {
setWeatherData(window.__WEATHER_DATA__) setWeatherData(window.__WEATHER_DATA__)
setRainData([])
setLastUpdate(new Date()) setLastUpdate(new Date())
setLoading(false) setLoading(false)
} else { return
// 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)
if (!response.ok) {
throw new Error('API-Fehler: ' + response.status)
}
const data = await response.json()
setWeatherData(data)
setLastUpdate(new Date())
setLoading(false)
} }
// 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>
) )

View File

@@ -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;
} }

View File

@@ -20,63 +20,88 @@ 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])
// Zeitraum-Label
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 // Gemeinsame Chart-Optionen (angepasst an Zeitraum)
const getCommonOptions = () => ({ const getCommonOptions = () => {
chart: { // X-Achsen-Konfiguration basierend auf Zeitraum
height: '50%', let xAxisConfig = {
animation: false, type: 'datetime',
backgroundColor: 'transparent' gridLineWidth: 1,
}, gridLineColor: 'rgba(0, 0, 0, 0.1)'
accessibility: { }
enabled: false
}, switch (timeRange) {
credits: { case '24h':
enabled: false xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
}, xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
title: { break
text: null case '7d':
}, case '30d':
legend: { xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
enabled: false break
}, case '365d':
tooltip: { xAxisConfig.labels = { format: '{value:%b}', align: 'center' }
shared: true, break
crosshairs: true, }
xDateFormat: '%d.%m.%Y %H:%M'
}, return {
plotOptions: { chart: {
series: { height: '50%',
marker: { animation: false,
enabled: false, backgroundColor: 'transparent'
states: { },
hover: { accessibility: {
enabled: true, enabled: false
radius: 5 },
credits: {
enabled: false
},
title: {
text: null
},
legend: {
enabled: false
},
tooltip: {
shared: true,
crosshairs: true,
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : '%d.%m.%Y'
},
plotOptions: {
series: {
marker: {
enabled: false,
states: {
hover: {
enabled: true,
radius: 5
}
} }
} }
} }
}
},
xAxis: {
type: 'datetime',
tickInterval: 4 * 3600 * 1000, // 4 Stunden in Millisekunden
labels: {
format: '{value:%H:%M}',
align: 'center'
}, },
gridLineWidth: 1, xAxis: xAxisConfig,
gridLineColor: 'rgba(0, 0, 0, 0.1)' yAxis: {
}, gridLineColor: 'rgba(0, 0, 0, 0.05)'
yAxis: { }
gridLineColor: 'rgba(0, 0, 0, 0.05)'
} }
}) }
// Temperatur Chart // Temperatur Chart
const temperatureOptions = useMemo(() => { const temperatureOptions = useMemo(() => {
@@ -192,33 +217,68 @@ 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)'
name: 'Regen', series = [{
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]), name: 'Regen',
color: 'rgb(54, 162, 235)', data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
fillColor: 'rgba(54, 162, 235, 0.3)', color: 'rgb(54, 162, 235)',
type: 'area', fillColor: 'rgba(54, 162, 235, 0.3)',
tooltip: { type: 'area',
valueSuffix: ' mm' tooltip: {
} valueSuffix: ' mm'
}, { }
name: 'Regenrate', }, {
data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]), name: 'Regenrate',
color: 'rgb(59, 130, 246)', data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
dashStyle: 'Dash', color: 'rgb(59, 130, 246)',
type: 'line', dashStyle: 'Dash',
tooltip: { type: 'line',
valueSuffix: ' mm/h' tooltip: {
} 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,66 +371,97 @@ 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 => {
const itemDate = new Date(item.datetime)
return itemDate >= todayStart
})
if (todayData.length === 0) { if (periodData.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,
minPressure: null, maxPressure: null, minPressureTime: null, maxPressureTime: null minPressure: null, maxPressure: null, minPressureTime: null, maxPressureTime: null
} }
} }
// 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>