diff --git a/FuelBoard/FavouritesView.swift b/FuelBoard/FavouritesView.swift index eef6576..b814cdc 100644 --- a/FuelBoard/FavouritesView.swift +++ b/FuelBoard/FavouritesView.swift @@ -161,6 +161,13 @@ struct FavouritesView: View { priceDisplayStyle: priceDisplayStyle ) } + .onAppear { + // QA hook: launch with `-showTrends` to open the sheet + // without a tap (same pattern as -showKeySheet). + if ProcessInfo.processInfo.arguments.contains("-showTrends") { + showTrends = true + } + } } } } diff --git a/FuelBoard/TrendsView.swift b/FuelBoard/TrendsView.swift index bdc2a24..6d00adb 100644 --- a/FuelBoard/TrendsView.swift +++ b/FuelBoard/TrendsView.swift @@ -175,6 +175,7 @@ struct TrendsView: View { VStack(spacing: 12) { chart legend + legendFooter } } } @@ -256,10 +257,41 @@ struct TrendsView: View { Text(history.name) .font(.caption) .lineLimit(1) + if let avg = FuelHistoryStore.averagePence(history.points) { + Text(legendFigure(avg)) + .font(.caption) + .foregroundStyle(.secondary) + .monospacedDigit() + } Spacer() } } } .padding(.horizontal, 4) } + + /// The bracket figure in the chart key: absolute pence in Price mode, + /// signed pence above the day's cheapest in vs-cheapest mode — always + /// pence, matching the list rows (the y-axis follows the display toggle). + private func legendFigure(_ pence: Double) -> String { + switch mode { + case .price: + return String(format: "%.1fp", pence) + case .vsCheapest: + return pence > 0 ? String(format: "+%.1fp", pence) : String(format: "%.1fp", pence) + } + } + + /// One-line descriptor under the key so the brackets are self-explanatory. + private var legendFooter: some View { + Group { + if mode == .price { + Text("Average price over the days shown") + } else { + Text("Average pence above the day's cheapest favourite") + } + } + .font(.caption2) + .foregroundStyle(.secondary) + } } diff --git a/FuelBoard/en.lproj/Localizable.strings b/FuelBoard/en.lproj/Localizable.strings index 5ebe00d..08b4c3d 100644 --- a/FuelBoard/en.lproj/Localizable.strings +++ b/FuelBoard/en.lproj/Localizable.strings @@ -156,3 +156,5 @@ "No price history yet" = "No price history yet"; "First snapshot %@ — a few days are needed to draw a trend." = "First snapshot %@ — a few days are needed to draw a trend."; "Prices are recorded each day FuelBoard's relay runs — check back in a few days." = "Prices are recorded each day FuelBoard's relay runs — check back in a few days."; +"Average price over the days shown" = "Average price over the days shown"; +"Average pence above the day's cheapest favourite" = "Average pence above the day's cheapest favourite"; diff --git a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelHistoryTests.swift b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelHistoryTests.swift index 3780d29..876fb7e 100644 --- a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelHistoryTests.swift +++ b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelHistoryTests.swift @@ -164,6 +164,29 @@ final class FuelHistoryTests: XCTestCase { XCTAssertEqual(pruned.count, days.count) } + // MARK: Chart-key average + + func testAveragePenceEmptyIsNil() { + XCTAssertNil(FuelHistoryStore.averagePence([])) + } + + func testAveragePenceSinglePoint() { + let d = FuelHistoryStore.date(fromDay: "2026-08-15")! + XCTAssertEqual(FuelHistoryStore.averagePence([PricePoint(date: d, pence: 156.7)]), 156.7) + } + + func testAveragePenceMultiplePoints() { + let d1 = FuelHistoryStore.date(fromDay: "2026-08-15")! + let d2 = FuelHistoryStore.date(fromDay: "2026-08-16")! + // (153.9 + 156.7) / 2 = 155.3 — exact in binary? 153.9+156.7=310.6, /2=155.3 + let avg = FuelHistoryStore.averagePence([ + PricePoint(date: d1, pence: 153.9), + PricePoint(date: d2, pence: 156.7), + ]) + XCTAssertNotNil(avg) + XCTAssertEqual(avg!, 155.3, accuracy: 0.0001) + } + // MARK: Mirror URLs func testHistoryFileURLKeepsBaseLastSegment() { diff --git a/Shared/FuelHistory.swift b/Shared/FuelHistory.swift index d8cd5b8..77114b6 100644 --- a/Shared/FuelHistory.swift +++ b/Shared/FuelHistory.swift @@ -148,6 +148,13 @@ enum FuelHistoryStore { } } + /// Average pence over a series' points; nil when there are no points + /// (a station with no data shows no bracket figure in the chart key). + static func averagePence(_ points: [PricePoint]) -> Double? { + guard !points.isEmpty else { return nil } + return points.reduce(0) { $0 + $1.pence } / Double(points.count) + } + /// Rebase every station's series so each day's CHEAPEST favourite sits at /// 0 and the others show signed pence above it (mirrors the list's /// baseline delta pattern). Days where a station has no point are gaps.