Bereichswahl dazu
This commit is contained in:
68
api/main.py
68
api/main.py
@@ -514,6 +514,74 @@ async def get_weekly_rain_data(
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/hourly-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
|
async def get_hourly_aggregated_range(
|
||||||
|
start: datetime = Query(..., description="Startdatum (ISO 8601)"),
|
||||||
|
end: datetime = Query(..., description="Enddatum (ISO 8601)")
|
||||||
|
):
|
||||||
|
"""Gibt stündlich aggregierte Wetterdaten für einen bestimmten Zeitraum zurück"""
|
||||||
|
if start >= end:
|
||||||
|
raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
date_trunc('hour', datetime) as datetime,
|
||||||
|
AVG(temperature)::float as 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 BETWEEN %s AND %s
|
||||||
|
GROUP BY date_trunc('hour', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (start, end))
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
return [dict(row) for row in results]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/weather/daily-aggregated-range", response_model=List[dict], tags=["Aggregated Data"])
|
||||||
|
async def get_daily_aggregated_range(
|
||||||
|
start: datetime = Query(..., description="Startdatum (ISO 8601)"),
|
||||||
|
end: datetime = Query(..., description="Enddatum (ISO 8601)")
|
||||||
|
):
|
||||||
|
"""Gibt täglich aggregierte Wetterdaten mit Min/Max-Temperaturen für einen bestimmten Zeitraum zurück"""
|
||||||
|
if start >= end:
|
||||||
|
raise HTTPException(status_code=400, detail="Startdatum muss vor Enddatum liegen")
|
||||||
|
|
||||||
|
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 BETWEEN %s AND %s
|
||||||
|
GROUP BY date_trunc('day', datetime)
|
||||||
|
ORDER BY datetime ASC
|
||||||
|
""", (start, end))
|
||||||
|
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)
|
||||||
|
|||||||
@@ -9,7 +9,19 @@ function App() {
|
|||||||
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'
|
const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d', oder {type: 'custom', start, end, days}
|
||||||
|
|
||||||
|
// Handler für Zeitbereich-Änderungen
|
||||||
|
const handleTimeRangeChange = (range, customParams) => {
|
||||||
|
if (range === 'custom' && customParams) {
|
||||||
|
const start = new Date(customParams.start)
|
||||||
|
const end = new Date(customParams.end)
|
||||||
|
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24))
|
||||||
|
setTimeRange({ type: 'custom', start: customParams.start, end: customParams.end, days })
|
||||||
|
} else {
|
||||||
|
setTimeRange(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -30,26 +42,44 @@ function App() {
|
|||||||
let weatherUrl, rainUrl
|
let weatherUrl, rainUrl
|
||||||
const baseUrl = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
const baseUrl = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
|
||||||
|
|
||||||
switch (timeRange) {
|
// Benutzerdefinierter Zeitbereich
|
||||||
case '24h':
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
weatherUrl = `${baseUrl}/weather/history?hours=24`
|
const start = encodeURIComponent(timeRange.start)
|
||||||
rainUrl = null
|
const end = encodeURIComponent(timeRange.end)
|
||||||
break
|
const days = timeRange.days || 1
|
||||||
case '7d':
|
|
||||||
weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=7`
|
if (days >= 7) {
|
||||||
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
|
// >= 7 Tage: Tagesaggregation mit Min/Max verwenden
|
||||||
break
|
weatherUrl = `${baseUrl}/weather/daily-aggregated-range?start=${start}&end=${end}`
|
||||||
case '30d':
|
rainUrl = null // TODO: Regen-Aggregation für Range implementieren
|
||||||
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=30`
|
} else {
|
||||||
rainUrl = `${baseUrl}/weather/rain-daily?days=30`
|
// < 7 Tage: Stundenaggregation verwenden
|
||||||
break
|
weatherUrl = `${baseUrl}/weather/hourly-aggregated-range?start=${start}&end=${end}`
|
||||||
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
|
rainUrl = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vordefinierte Zeitbereiche
|
||||||
|
switch (timeRange) {
|
||||||
|
case '24h':
|
||||||
|
weatherUrl = `${baseUrl}/weather/history?hours=24`
|
||||||
|
rainUrl = null
|
||||||
|
break
|
||||||
|
case '7d':
|
||||||
|
weatherUrl = `${baseUrl}/weather/daily-with-minmax?days=7`
|
||||||
|
rainUrl = `${baseUrl}/weather/rain-daily?days=7`
|
||||||
|
break
|
||||||
|
case '30d':
|
||||||
|
weatherUrl = `${baseUrl}/weather/daily-with-minmax?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
|
// Wetterdaten laden
|
||||||
@@ -155,7 +185,7 @@ function App() {
|
|||||||
currentData={currentWeatherData}
|
currentData={currentWeatherData}
|
||||||
rainData={rainData}
|
rainData={rainData}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
onTimeRangeChange={setTimeRange}
|
onTimeRangeChange={handleTimeRangeChange}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -246,3 +246,135 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="datetime-local"] {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="datetime-local"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #fee;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #c00;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-info {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-info p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-apply {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-apply:hover {
|
||||||
|
background: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import Highcharts from 'highcharts'
|
import Highcharts from 'highcharts'
|
||||||
import HighchartsReact from 'highcharts-react-official'
|
import HighchartsReact from 'highcharts-react-official'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
@@ -21,6 +21,86 @@ Highcharts.setOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange }) => {
|
const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '24h', onTimeRangeChange }) => {
|
||||||
|
// State für benutzerdefinierten Zeitbereich
|
||||||
|
const [showCustomRangeModal, setShowCustomRangeModal] = useState(false)
|
||||||
|
const [customStartDate, setCustomStartDate] = useState('')
|
||||||
|
const [customEndDate, setCustomEndDate] = useState('')
|
||||||
|
const [customError, setCustomError] = useState('')
|
||||||
|
|
||||||
|
// Handler für benutzerdefinierten Zeitbereich
|
||||||
|
const handleOpenCustomRange = () => {
|
||||||
|
// Versuche gespeicherten Zeitbereich zu laden
|
||||||
|
try {
|
||||||
|
const savedRange = localStorage.getItem('customTimeRange')
|
||||||
|
if (savedRange) {
|
||||||
|
const { start, end } = JSON.parse(savedRange)
|
||||||
|
setCustomStartDate(start)
|
||||||
|
setCustomEndDate(end)
|
||||||
|
} else {
|
||||||
|
// Setze Standardwerte (letzte 7 Tage)
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
setCustomStartDate(format(start, "yyyy-MM-dd'T'HH:mm"))
|
||||||
|
setCustomEndDate(format(end, "yyyy-MM-dd'T'HH:mm"))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Bei Fehler: Standardwerte verwenden
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
setCustomStartDate(format(start, "yyyy-MM-dd'T'HH:mm"))
|
||||||
|
setCustomEndDate(format(end, "yyyy-MM-dd'T'HH:mm"))
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomError('')
|
||||||
|
setShowCustomRangeModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyCustomRange = () => {
|
||||||
|
// Validierung
|
||||||
|
const start = new Date(customStartDate)
|
||||||
|
const end = new Date(customEndDate)
|
||||||
|
|
||||||
|
if (!customStartDate || !customEndDate) {
|
||||||
|
setCustomError('Bitte Start- und Endzeit auswählen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffHours = (end - start) / (1000 * 60 * 60)
|
||||||
|
const diffDays = diffHours / 24
|
||||||
|
|
||||||
|
if (diffHours < 1) {
|
||||||
|
setCustomError('Endzeit muss mindestens 1 Stunde nach der Startzeit liegen')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffDays > 365) {
|
||||||
|
setCustomError('Maximaler Zeitraum ist 1 Jahr (365 Tage)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeitbereich im localStorage speichern
|
||||||
|
try {
|
||||||
|
localStorage.setItem('customTimeRange', JSON.stringify({
|
||||||
|
start: customStartDate,
|
||||||
|
end: customEndDate
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
// Fehler beim Speichern ignorieren
|
||||||
|
console.warn('Konnte Zeitbereich nicht speichern:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anwenden
|
||||||
|
onTimeRangeChange('custom', { start: customStartDate, end: customEndDate })
|
||||||
|
setShowCustomRangeModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelCustomRange = () => {
|
||||||
|
setShowCustomRangeModal(false)
|
||||||
|
setCustomError('')
|
||||||
|
}
|
||||||
|
|
||||||
// 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))
|
||||||
@@ -33,6 +113,11 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Zeitraum-Label
|
// Zeitraum-Label
|
||||||
const timeRangeLabel = useMemo(() => {
|
const timeRangeLabel = useMemo(() => {
|
||||||
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
|
const start = new Date(timeRange.start)
|
||||||
|
const end = new Date(timeRange.end)
|
||||||
|
return `${format(start, 'dd.MM.yyyy HH:mm', { locale: de })} - ${format(end, 'dd.MM.yyyy HH:mm', { locale: de })}`
|
||||||
|
}
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '24h': return 'Die letzten 24 Stunden'
|
case '24h': return 'Die letzten 24 Stunden'
|
||||||
case '7d': return 'Die letzten 7 Tage'
|
case '7d': return 'Die letzten 7 Tage'
|
||||||
@@ -44,9 +129,18 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Aggregations-Zusatz für Chart-Titel
|
// Aggregations-Zusatz für Chart-Titel
|
||||||
const aggregationSuffix = useMemo(() => {
|
const aggregationSuffix = useMemo(() => {
|
||||||
|
// Custom range: basierend auf days
|
||||||
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
|
const days = timeRange.days || 1
|
||||||
|
if (days >= 7) {
|
||||||
|
return ' (Tagesmittel)'
|
||||||
|
} else {
|
||||||
|
return ' (Stundenmittel)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vordefinierte Bereiche
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '7d':
|
case '7d':
|
||||||
return ' (Stundenmittel)'
|
|
||||||
case '30d':
|
case '30d':
|
||||||
case '365d':
|
case '365d':
|
||||||
return ' (Tagesmittel)'
|
return ' (Tagesmittel)'
|
||||||
@@ -57,9 +151,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Spezieller Suffix für Temperatur bei 30d/365d
|
// Spezieller Suffix für Temperatur bei 30d/365d
|
||||||
const temperatureSuffix = useMemo(() => {
|
const temperatureSuffix = useMemo(() => {
|
||||||
|
// Custom range: basierend auf days
|
||||||
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
|
const days = timeRange.days || 1
|
||||||
|
if (days >= 7) {
|
||||||
|
return ' (Tages-Min/Max)'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
// Vordefinierte Bereiche
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
case '7d':
|
case '7d':
|
||||||
return ' (Stundenmittel)'
|
|
||||||
case '30d':
|
case '30d':
|
||||||
case '365d':
|
case '365d':
|
||||||
return ' (Tages-Min/Max)'
|
return ' (Tages-Min/Max)'
|
||||||
@@ -70,7 +172,17 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Spezieller Suffix für Windböen bei 30d/365d
|
// Spezieller Suffix für Windböen bei 30d/365d
|
||||||
const windGustSuffix = useMemo(() => {
|
const windGustSuffix = useMemo(() => {
|
||||||
|
// Custom range: basierend auf days
|
||||||
|
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
|
||||||
|
const days = timeRange.days || 1
|
||||||
|
if (days >= 7) {
|
||||||
|
return ' (TagesMax)'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
// Vordefinierte Bereiche
|
||||||
switch (timeRange) {
|
switch (timeRange) {
|
||||||
|
case '7d':
|
||||||
case '30d':
|
case '30d':
|
||||||
case '365d':
|
case '365d':
|
||||||
return ' (TagesMax)'
|
return ' (TagesMax)'
|
||||||
@@ -81,6 +193,10 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
// Gemeinsame Chart-Optionen (angepasst an Zeitraum)
|
||||||
const getCommonOptions = () => {
|
const getCommonOptions = () => {
|
||||||
|
// Prüfe, ob es ein custom range ist
|
||||||
|
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
|
||||||
|
const customDays = isCustomRange ? (timeRange.days || 1) : 0
|
||||||
|
|
||||||
// X-Achsen-Konfiguration basierend auf Zeitraum
|
// X-Achsen-Konfiguration basierend auf Zeitraum
|
||||||
let xAxisConfig = {
|
let xAxisConfig = {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
@@ -91,35 +207,66 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
// Zeitspanne für X-Achse berechnen (für festen Zeitrahmen)
|
// Zeitspanne für X-Achse berechnen (für festen Zeitrahmen)
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
let xAxisMin, xAxisMax
|
let xAxisMin, xAxisMax
|
||||||
|
let tooltipDateFormat = '%d.%m.%Y'
|
||||||
|
|
||||||
switch (timeRange) {
|
if (isCustomRange) {
|
||||||
case '24h':
|
// Custom range Konfiguration - Min/Max aus Daten nehmen
|
||||||
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
if (customDays >= 7) {
|
||||||
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
|
||||||
xAxisMin = now - 24 * 3600 * 1000
|
|
||||||
xAxisMax = now
|
|
||||||
break
|
|
||||||
case '7d':
|
|
||||||
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
xAxisMin = now - 7 * 24 * 3600 * 1000
|
tooltipDateFormat = '%d.%m.%Y'
|
||||||
xAxisMax = now
|
} else {
|
||||||
break
|
xAxisConfig.labels = { format: '{value:%d.%m %H:%M}', align: 'center' }
|
||||||
case '30d':
|
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
||||||
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
}
|
||||||
xAxisMin = now - 30 * 24 * 3600 * 1000
|
// X-Achsen-Bereich aus den tatsächlichen Daten bestimmen
|
||||||
xAxisMax = now
|
if (sortedData.length > 0) {
|
||||||
break
|
xAxisMin = new Date(sortedData[0].datetime).getTime()
|
||||||
case '365d':
|
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
|
||||||
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
|
} else {
|
||||||
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
|
xAxisMin = null
|
||||||
if (sortedData.length > 0) {
|
xAxisMax = null
|
||||||
xAxisMin = new Date(sortedData[0].datetime).getTime()
|
}
|
||||||
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
|
} else {
|
||||||
} else {
|
// Vordefinierte Bereiche
|
||||||
xAxisMin = null
|
switch (timeRange) {
|
||||||
xAxisMax = null
|
case '24h':
|
||||||
}
|
xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
|
||||||
break
|
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
||||||
|
xAxisMin = now - 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
|
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
||||||
|
break
|
||||||
|
case '7d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
|
xAxisMin = now - 7 * 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
|
tooltipDateFormat = '%d.%m.%Y - %Hh'
|
||||||
|
break
|
||||||
|
case '30d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
|
||||||
|
xAxisMin = now - 30 * 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
|
tooltipDateFormat = '%d.%m.%Y'
|
||||||
|
break
|
||||||
|
case '365d':
|
||||||
|
xAxisConfig.labels = { format: '{value:%b %Y}', align: 'center' }
|
||||||
|
tooltipDateFormat = '%b %Y'
|
||||||
|
// Bei 365d: Min/Max aus vorhandenen Daten berechnen
|
||||||
|
if (sortedData.length > 0) {
|
||||||
|
xAxisMin = new Date(sortedData[0].datetime).getTime()
|
||||||
|
xAxisMax = new Date(sortedData[sortedData.length - 1].datetime).getTime()
|
||||||
|
} else {
|
||||||
|
xAxisMin = null
|
||||||
|
xAxisMax = null
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
xAxisConfig.tickInterval = 4 * 3600 * 1000
|
||||||
|
xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
|
||||||
|
xAxisMin = now - 24 * 3600 * 1000
|
||||||
|
xAxisMax = now
|
||||||
|
tooltipDateFormat = '%d.%m.%Y %H:%M'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Min/Max für X-Achse setzen
|
// Min/Max für X-Achse setzen
|
||||||
@@ -147,7 +294,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
shared: true,
|
shared: true,
|
||||||
crosshairs: true,
|
crosshairs: true,
|
||||||
xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : (timeRange === '7d' ? '%d.%m.%Y - %Hh' : '%d.%m.%Y')
|
xDateFormat: tooltipDateFormat
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
series: {
|
series: {
|
||||||
@@ -171,8 +318,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Temperatur Chart
|
// Temperatur Chart
|
||||||
const temperatureOptions = useMemo(() => {
|
const temperatureOptions = useMemo(() => {
|
||||||
// Bei 30d und 365d: Min/Max-Temperaturen anzeigen
|
// Prüfe, ob Min/Max-Temperaturen angezeigt werden sollen
|
||||||
if (timeRange === '30d' || timeRange === '365d') {
|
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
|
||||||
|
const customDays = isCustomRange ? (timeRange.days || 1) : 0
|
||||||
|
const showMinMax = (timeRange === '7d' || timeRange === '30d' || timeRange === '365d') || (isCustomRange && customDays >= 7)
|
||||||
|
|
||||||
|
// Bei 7d, 30d, 365d und custom >= 7 Tage: Min/Max-Temperaturen anzeigen
|
||||||
|
if (showMinMax) {
|
||||||
const minTemps = sortedData.filter(item => item.min_temperature != null).map(item => item.min_temperature)
|
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)
|
const maxTemps = sortedData.filter(item => item.max_temperature != null).map(item => item.max_temperature)
|
||||||
|
|
||||||
@@ -326,7 +478,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
valueSuffix: ' %'
|
valueSuffix: ' %'
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}), [sortedData])
|
}), [sortedData, timeRange])
|
||||||
|
|
||||||
// Luftdruck Chart
|
// Luftdruck Chart
|
||||||
const pressureOptions = useMemo(() => {
|
const pressureOptions = useMemo(() => {
|
||||||
@@ -385,7 +537,7 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}, [sortedData])
|
}, [sortedData, timeRange])
|
||||||
|
|
||||||
// Regen Chart (angepasst an Zeitraum)
|
// Regen Chart (angepasst an Zeitraum)
|
||||||
const rainOptions = useMemo(() => {
|
const rainOptions = useMemo(() => {
|
||||||
@@ -452,8 +604,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
// Windgeschwindigkeit Chart
|
// Windgeschwindigkeit Chart
|
||||||
const windSpeedOptions = useMemo(() => {
|
const windSpeedOptions = useMemo(() => {
|
||||||
// Bei 365d nur Windgeschwindigkeit, keine Böen
|
// Prüfe, ob Böen angezeigt werden sollen (nicht bei 365d oder custom >= 365 Tage)
|
||||||
const series = timeRange === '365d'
|
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
|
||||||
|
const customDays = isCustomRange ? (timeRange.days || 1) : 0
|
||||||
|
const hideGusts = (timeRange === '365d') || (isCustomRange && customDays >= 365)
|
||||||
|
|
||||||
|
// Bei 365d und custom >= 365 Tage: nur Windgeschwindigkeit, keine Böen
|
||||||
|
const series = hideGusts
|
||||||
? [{
|
? [{
|
||||||
name: 'Windgeschwindigkeit',
|
name: 'Windgeschwindigkeit',
|
||||||
data: sortedData
|
data: sortedData
|
||||||
@@ -588,7 +745,15 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Zeitformat basierend auf Zeitraum
|
// Zeitformat basierend auf Zeitraum
|
||||||
const timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
|
const isCustomRange = typeof timeRange === 'object' && timeRange.type === 'custom'
|
||||||
|
const customDays = isCustomRange ? (timeRange.days || 1) : 0
|
||||||
|
let timeFormat = 'dd.MM HH:mm'
|
||||||
|
|
||||||
|
if (isCustomRange) {
|
||||||
|
timeFormat = customDays < 7 ? 'HH:mm' : 'dd.MM HH:mm'
|
||||||
|
} else {
|
||||||
|
timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
|
||||||
|
}
|
||||||
|
|
||||||
// Temperatur
|
// Temperatur
|
||||||
const minTempItem = periodData.reduce((min, item) =>
|
const minTempItem = periodData.reduce((min, item) =>
|
||||||
@@ -662,6 +827,13 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
<span className="time-range-full">365 Tage</span>
|
<span className="time-range-full">365 Tage</span>
|
||||||
<span className="time-range-short">365d</span>
|
<span className="time-range-short">365d</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={(typeof timeRange === 'object' && timeRange.type === 'custom') ? 'active' : ''}
|
||||||
|
onClick={handleOpenCustomRange}
|
||||||
|
>
|
||||||
|
<span className="time-range-full">Bereich</span>
|
||||||
|
<span className="time-range-short">Bereich</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zeitraum-Beschreibung */}
|
{/* Zeitraum-Beschreibung */}
|
||||||
@@ -747,6 +919,55 @@ const WeatherDashboard = ({ data, currentData = [], rainData = [], timeRange = '
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal für benutzerdefinierten Zeitbereich */}
|
||||||
|
{showCustomRangeModal && (
|
||||||
|
<div className="modal-overlay" onClick={handleCancelCustomRange}>
|
||||||
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>Benutzerdefinierten Zeitbereich wählen</h2>
|
||||||
|
|
||||||
|
<div className="modal-form">
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="startDate">Startzeit:</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="startDate"
|
||||||
|
value={customStartDate}
|
||||||
|
onChange={(e) => setCustomStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="endDate">Endzeit:</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="endDate"
|
||||||
|
value={customEndDate}
|
||||||
|
onChange={(e) => setCustomEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customError && (
|
||||||
|
<div className="error-message">{customError}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="modal-info">
|
||||||
|
<p>• Endzeit muss mindestens 1 Stunde nach der Startzeit liegen</p>
|
||||||
|
<p>• Maximaler Zeitraum: 1 Jahr (365 Tage)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-buttons">
|
||||||
|
<button className="btn-cancel" onClick={handleCancelCustomRange}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button className="btn-apply" onClick={handleApplyCustomRange}>
|
||||||
|
Anwenden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="dashboard-footer">
|
<div className="dashboard-footer">
|
||||||
<div className="version-line">
|
<div className="version-line">
|
||||||
|
|||||||
Reference in New Issue
Block a user