From d4a5f1b1c9b53f3d04bc5de304fbc04507640e0c Mon Sep 17 00:00:00 2001 From: rxf Date: Mon, 30 Mar 2026 11:43:06 +0200 Subject: [PATCH] Temperatur mit Min/Max --- api/main.py | 71 ++++++---- frontend/src/App.jsx | 2 +- frontend/src/components/WeatherDashboard.jsx | 128 ++++++++++++++++++- 3 files changed, 172 insertions(+), 29 deletions(-) diff --git a/api/main.py b/api/main.py index 68cc9cf..8260e2e 100644 --- a/api/main.py +++ b/api/main.py @@ -375,11 +375,11 @@ async def get_hourly_aggregated_data( conn.close() -@app.get("/weather/daily-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"]) +@app.get("/weather/daily-aggregated", response_model=List[dict], 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)""" + """Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel mit Min/Max-Temperaturen)""" conn = get_db_connection() try: with conn.cursor() as cursor: @@ -387,17 +387,15 @@ async def get_daily_aggregated_data( if days >= 365: 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 + AVG(temperature)::float as temperature, + MIN(temperature)::float as min_temperature, + MAX(temperature)::float as max_temperature, + ROUND(AVG(humidity))::int as humidity, + AVG(pressure)::float as pressure, + AVG(wind_speed * 1.60934)::float as wind_speed, + MAX(wind_gust * 1.60934)::float as wind_gust, + AVG(wind_dir)::float as wind_dir FROM weather_data GROUP BY date_trunc('day', datetime) ORDER BY datetime ASC @@ -405,17 +403,15 @@ async def get_daily_aggregated_data( else: 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 + AVG(temperature)::float as temperature, + MIN(temperature)::float as min_temperature, + MAX(temperature)::float as max_temperature, + ROUND(AVG(humidity))::int as humidity, + AVG(pressure)::float as pressure, + AVG(wind_speed * 1.60934)::float as wind_speed, + MAX(wind_gust * 1.60934)::float as wind_gust, + AVG(wind_dir)::float as wind_dir FROM weather_data WHERE datetime >= NOW() - make_interval(days => %s) GROUP BY date_trunc('day', datetime) @@ -428,6 +424,37 @@ async def get_daily_aggregated_data( conn.close() +@app.get("/weather/daily-with-minmax", response_model=List[dict], tags=["Aggregated Data"]) +async def get_daily_with_minmax_data( + days: int = Query(30, ge=1, le=90, description="Anzahl Tage zurück (max 90)") +): + """Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen zurück""" + conn = get_db_connection() + try: + with conn.cursor() as cursor: + cursor.execute(""" + SELECT + date_trunc('day', datetime) as datetime, + AVG(temperature)::float as temperature, + MIN(temperature)::float as min_temperature, + MAX(temperature)::float as max_temperature, + ROUND(AVG(humidity))::int as humidity, + AVG(pressure)::float as pressure, + AVG(wind_speed * 1.60934)::float as wind_speed, + MAX(wind_gust * 1.60934)::float as wind_gust, + AVG(wind_dir)::float as wind_dir + 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") diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a4dd92a..3793a96 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -40,7 +40,7 @@ function App() { rainUrl = `${baseUrl}/weather/rain-daily?days=7` break case '30d': - weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=30` + weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=30` rainUrl = `${baseUrl}/weather/rain-daily?days=30` break case '365d': diff --git a/frontend/src/components/WeatherDashboard.jsx b/frontend/src/components/WeatherDashboard.jsx index 159f622..9579dd3 100644 --- a/frontend/src/components/WeatherDashboard.jsx +++ b/frontend/src/components/WeatherDashboard.jsx @@ -46,8 +46,8 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' const aggregationSuffix = useMemo(() => { switch (timeRange) { case '7d': - case '30d': return ' (Stundenmittel)' + case '30d': case '365d': return ' (Tagesmittel)' default: @@ -55,6 +55,30 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } }, [timeRange]) + // Spezieller Suffix für Temperatur bei 30d/365d + const temperatureSuffix = useMemo(() => { + switch (timeRange) { + case '7d': + return ' (Stundenmittel)' + case '30d': + case '365d': + return ' (Tages-Min/Max)' + default: + return '' + } + }, [timeRange]) + + // Spezieller Suffix für Windböen bei 30d/365d + const windGustSuffix = useMemo(() => { + switch (timeRange) { + case '30d': + case '365d': + return ' (TagesMax)' + default: + return '' + } + }, [timeRange]) + // Gemeinsame Chart-Optionen (angepasst an Zeitraum) const getCommonOptions = () => { // X-Achsen-Konfiguration basierend auf Zeitraum @@ -123,7 +147,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' tooltip: { shared: true, crosshairs: true, - xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' || timeRange === '30d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y') + xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y') }, plotOptions: { series: { @@ -147,7 +171,87 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' // Temperatur Chart const temperatureOptions = useMemo(() => { + // Bei 30d und 365d: Min/Max-Temperaturen anzeigen + if (timeRange === '30d' || timeRange === '365d') { + const minTemps = sortedData.filter(item => item.min_temperature != null).map(item => item.min_temperature) + const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature) + + // Prüfe, ob Daten vorhanden sind + if (minTemps.length === 0 || maxTemps.length === 0) { + return { + ...getCommonOptions(), + yAxis: { + ...getCommonOptions().yAxis, + title: { text: null } + }, + series: [] + } + } + + let yMin = Math.min(...minTemps) + let yMax = Math.max(...maxTemps) + + // Füge einen kleinen Puffer hinzu (2°C oben/unten), um den Platz optimal zu nutzen + yMin = Math.floor(yMin - 2) + yMax = Math.ceil(yMax + 2) + + return { + ...getCommonOptions(), + yAxis: { + title: { text: null }, + gridLineColor: 'rgba(0, 0, 0, 0.05)', + min: yMin, + max: yMax, + startOnTick: false, + endOnTick: false + }, + series: [ + { + name: 'Maximaltemperatur', + data: sortedData.filter(item => item.max_temperature != null).map(item => [new Date(item.datetime).getTime(), item.max_temperature]), + color: 'rgb(255, 99, 132)', + type: 'line', + lineWidth: 2, + connectNulls: false, + gapSize: 2 * 24 * 3600 * 1000, + gapUnit: 'value', + tooltip: { + valueDecimals: 1, + valueSuffix: ' °C' + } + }, + { + name: 'Minimaltemperatur', + data: sortedData.filter(item => item.min_temperature != null).map(item => [new Date(item.datetime).getTime(), item.min_temperature]), + color: 'rgb(54, 162, 235)', + type: 'line', + lineWidth: 2, + connectNulls: false, + gapSize: 2 * 24 * 3600 * 1000, + gapUnit: 'value', + tooltip: { + valueDecimals: 1, + valueSuffix: ' °C' + } + } + ] + } + } + + // Standard: Temperatur als Flächendiagramm const temps = sortedData.filter(item => item.temperature != null).map(item => item.temperature) + + if (temps.length === 0) { + return { + ...getCommonOptions(), + yAxis: { + ...getCommonOptions().yAxis, + title: { text: null } + }, + series: [] + } + } + const min = Math.min(...temps) const max = Math.max(...temps) const range = max - min @@ -191,7 +295,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' } }] } - }, [sortedData, aggregationSuffix]) + }, [sortedData, temperatureSuffix, timeRange]) // Luftfeuchtigkeit Chart const humidityOptions = useMemo(() => ({ @@ -227,6 +331,18 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' // Luftdruck Chart const pressureOptions = useMemo(() => { const pressures = sortedData.filter(item => item.pressure != null).map(item => item.pressure) + + if (pressures.length === 0) { + return { + ...getCommonOptions(), + yAxis: { + ...getCommonOptions().yAxis, + title: { text: null } + }, + series: [] + } + } + const min = Math.min(...pressures) const max = Math.max(...pressures) const range = max - min @@ -370,7 +486,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' valueSuffix: ' km/h' } }, { - name: 'Windböen', + name: 'Böe' + windGustSuffix, data: sortedData .filter(item => item.wind_gust != null) .map(item => [new Date(item.datetime).getTime(), item.wind_gust]), @@ -405,7 +521,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = ' }, series } - }, [sortedData, timeRange]) + }, [sortedData, timeRange, windGustSuffix]) // Windrichtung Chart const windDirOptions = useMemo(() => ({ @@ -558,7 +674,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
Aktuell: {current.temperature?.toFixed(1) || '-'}°C
-

🌡️ Temperatur{aggregationSuffix}[°C]

+

🌡️ Temperatur{temperatureSuffix}[°C]