Wetter: stündliche Niederschlags-Timeline (WeatherKit hourlyForecast)
Säulen-Diagramm zwischen Stats-Grid und Hunde-Hinweisen: - Heute: nächste 12h ab jetzt - Andere Tage: 06–22h des Tages - Säulenhöhe aus precipitationAmount in mm, Farbgradient aus Intensität (chance + mm). Header zeigt durchschnittliche Regenwahrscheinlichkeit oder 'trocken' wenn < 5%.
This commit is contained in:
parent
70e0a238e9
commit
5c4754caff
1 changed files with 94 additions and 0 deletions
|
|
@ -54,6 +54,7 @@ struct WetterView: View {
|
||||||
gassiScoreBadge(metrics: metrics)
|
gassiScoreBadge(metrics: metrics)
|
||||||
summaryCard(day: day, metrics: metrics)
|
summaryCard(day: day, metrics: metrics)
|
||||||
statsGrid(metrics: metrics)
|
statsGrid(metrics: metrics)
|
||||||
|
rainTimeline(weather: weather, day: day, isToday: safeIndex == 0)
|
||||||
hundeHinweise(day: day, metrics: metrics)
|
hundeHinweise(day: day, metrics: metrics)
|
||||||
schnueffelCard(metrics: metrics)
|
schnueffelCard(metrics: metrics)
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +188,99 @@ struct WetterView: View {
|
||||||
.background(.background.secondary, in: RoundedRectangle(cornerRadius: 12))
|
.background(.background.secondary, in: RoundedRectangle(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Niederschlags-Timeline
|
||||||
|
|
||||||
|
private func rainTimeline(weather: Weather, day: DayWeather, isToday: Bool) -> some View {
|
||||||
|
let hours = relevantHours(weather: weather, day: day, isToday: isToday)
|
||||||
|
let maxMm = max(hours.map { $0.precipitationAmount.converted(to: .millimeters).value }.max() ?? 0, 0.5)
|
||||||
|
let totalChance = hours.reduce(0.0) { $0 + $1.precipitationChance } / Double(max(1, hours.count))
|
||||||
|
|
||||||
|
return VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack {
|
||||||
|
Label("Niederschlag", systemImage: "umbrella.fill")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
Spacer()
|
||||||
|
Text(headerLabel(isToday: isToday, totalChance: totalChance))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hours.isEmpty {
|
||||||
|
Text("Keine stündlichen Daten verfügbar.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(alignment: .bottom, spacing: 6) {
|
||||||
|
ForEach(Array(hours.enumerated()), id: \.offset) { _, hour in
|
||||||
|
rainBar(for: hour, scaleMm: maxMm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 110)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.background(.background.secondary, in: RoundedRectangle(cornerRadius: 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func relevantHours(weather: Weather, day: DayWeather, isToday: Bool) -> [HourWeather] {
|
||||||
|
let all = weather.hourlyForecast
|
||||||
|
if isToday {
|
||||||
|
// Ab jetzt die nächsten 12 Stunden
|
||||||
|
return Array(all.filter { $0.date >= .now }.prefix(12))
|
||||||
|
}
|
||||||
|
// Der ausgewählte Tag, 06:00 bis 22:00
|
||||||
|
let cal = Calendar.current
|
||||||
|
return all
|
||||||
|
.filter { cal.isDate($0.date, inSameDayAs: day.date) }
|
||||||
|
.filter {
|
||||||
|
let h = cal.component(.hour, from: $0.date)
|
||||||
|
return h >= 6 && h <= 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rainBar(for hour: HourWeather, scaleMm: Double) -> some View {
|
||||||
|
let chance = hour.precipitationChance
|
||||||
|
let mm = hour.precipitationAmount.converted(to: .millimeters).value
|
||||||
|
// Höhe primär aus mm, mindestens 6 (auch bei 0 sichtbarer Bar-Sockel)
|
||||||
|
let height: CGFloat = max(6, CGFloat(mm / scaleMm) * 50)
|
||||||
|
let intensity: Double = min(1.0, chance + mm / 5)
|
||||||
|
let baseColor = Color.blue
|
||||||
|
return VStack(spacing: 3) {
|
||||||
|
Text("\(Int((chance * 100).rounded()))%")
|
||||||
|
.font(.caption2.monospacedDigit())
|
||||||
|
.foregroundStyle(chance >= 0.3 ? .primary : .secondary)
|
||||||
|
Rectangle()
|
||||||
|
.fill(LinearGradient(
|
||||||
|
colors: [baseColor.opacity(0.4 + 0.6 * intensity), baseColor.opacity(0.2 + 0.2 * intensity)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
))
|
||||||
|
.frame(width: 22, height: height)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
|
Text(hourLabel(hour.date))
|
||||||
|
.font(.caption2.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(height: 100, alignment: .bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hourLabel(_ date: Date) -> String {
|
||||||
|
let f = DateFormatter()
|
||||||
|
f.locale = Locale(identifier: "de_DE")
|
||||||
|
f.dateFormat = "HH"
|
||||||
|
return "\(f.string(from: date)):00"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func headerLabel(isToday: Bool, totalChance: Double) -> String {
|
||||||
|
let pct = Int((totalChance * 100).rounded())
|
||||||
|
let prefix = isToday ? "nächste 12 h" : "06–22 h"
|
||||||
|
if totalChance < 0.05 { return "\(prefix) · trocken" }
|
||||||
|
return "\(prefix) · ⌀ \(pct) % Regen"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Hunde-Hinweise
|
// MARK: - Hunde-Hinweise
|
||||||
|
|
||||||
private func hundeHinweise(day: DayWeather, metrics: DayMetrics) -> some View {
|
private func hundeHinweise(day: DayWeather, metrics: DayMetrics) -> some View {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue