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)
|
||||
summaryCard(day: day, metrics: metrics)
|
||||
statsGrid(metrics: metrics)
|
||||
rainTimeline(weather: weather, day: day, isToday: safeIndex == 0)
|
||||
hundeHinweise(day: day, metrics: metrics)
|
||||
schnueffelCard(metrics: metrics)
|
||||
}
|
||||
|
|
@ -187,6 +188,99 @@ struct WetterView: View {
|
|||
.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
|
||||
|
||||
private func hundeHinweise(day: DayWeather, metrics: DayMetrics) -> some View {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue