trends: staggered per-series line draw-in

The single-waterline trace-in was invisible with sparse data (2 points draw
in one step). Replace it with a per-series waterline: each station's line now
traces left->right on first appearance, series revealed 0.18s apart in a
cascade so the draw is clearly visible. Newly-added favourites get a full
reveal immediately on reload; Reduce Motion still jump-cuts. Range/mode
morphs and numeric headline transition unchanged.
This commit is contained in:
FuelBoard Contributor
2026-08-20 18:40:09 +01:00
parent 69c3c17606
commit 508ccc3802
+29 -11
View File
@@ -35,10 +35,12 @@ struct TrendsView: View {
@State private var loadFailed = false @State private var loadFailed = false
@State private var firstSnapshot: String? @State private var firstSnapshot: String?
/// Lines trace-in leftright on first appearance by revealing an /// Per-series draw-in waterline: maps stationID how many leading points
/// increasing prefix of each series' points. Stays at the full count after /// are revealed, so each line traces leftright on first appearance.
/// the first reveal so range/mode switches morph instead of re-tracing. /// Series reveal in a short staggered cascade so the draw is clearly
@State private var revealCount: Int = 0 /// visible even with only 2 points per line. Stays full after the first
/// reveal so range/mode switches morph instead of re-tracing.
@State private var revealed: [String: Int] = [:]
/// Seeded from `selectedFuel` (the fuel the tab was on) so the sheet /// Seeded from `selectedFuel` (the fuel the tab was on) so the sheet
/// opens where the user was same pattern as FavouritesView. /// opens where the user was same pattern as FavouritesView.
@@ -171,16 +173,32 @@ struct TrendsView: View {
withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.35)) { withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.35)) {
series = fetched series = fetched
} }
// Newly-added favourites have no reveal entry yet surface them
// fully so their line isn't left blank by the draw-in waterline.
for h in fetched where revealed[h.stationID] == nil {
revealed[h.stationID] = h.points.count
}
} else { } else {
series = fetched series = fetched
let maxPoints = fetched.reduce(0) { max($0, $1.points.count) } let hasPoints = fetched.contains { !$0.points.isEmpty }
if maxPoints > 0 { if hasPoints {
if reduceMotion { if reduceMotion {
revealCount = maxPoints var all: [String: Int] = [:]
for h in fetched { all[h.stationID] = h.points.count }
revealed = all
} else { } else {
revealCount = 0 revealed = [:]
let cascadeNS = UInt64(0.18 * 1_000_000_000)
for (i, h) in fetched.enumerated() {
let sid = h.stationID
let total = h.points.count
Task {
try? await Task.sleep(nanoseconds: UInt64(i) * cascadeNS)
guard !Task.isCancelled else { return }
withAnimation(.easeOut(duration: 0.5)) { withAnimation(.easeOut(duration: 0.5)) {
revealCount = maxPoints revealed[sid] = total
}
}
} }
} }
} }
@@ -419,13 +437,13 @@ struct TrendsView: View {
// type-checker's budget.) // type-checker's budget.)
Chart { Chart {
ForEach(displaySeries) { history in ForEach(displaySeries) { history in
ForEach(history.points.prefix(revealCount)) { point in ForEach(history.points.prefix(revealed[history.stationID] ?? 0)) { point in
areaMark(point, series: history.name, areaMark(point, series: history.name,
color: seriesColor(index(of: history.stationID))) color: seriesColor(index(of: history.stationID)))
} }
} }
ForEach(displaySeries) { history in ForEach(displaySeries) { history in
ForEach(history.points.prefix(revealCount)) { point in ForEach(history.points.prefix(revealed[history.stationID] ?? 0)) { point in
lineMark(point, series: history.name, lineMark(point, series: history.name,
color: seriesColor(index(of: history.stationID))) color: seriesColor(index(of: history.stationID)))
} }