From 4dc2b494c5811b858c3cd449e0236abd22be3c03 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Dec 2025 11:29:07 -0500 Subject: [PATCH 1/3] Add chart granularity picker --- .../JetpackStats/Analytics/StatsEvent.swift | 19 ++++++++ .../JetpackStats/Cards/ChartCard.swift | 48 +++++++++++++++++-- .../Cards/ChartCardViewModel.swift | 18 ++++++- Modules/Sources/JetpackStats/Strings.swift | 10 ++++ .../Utilities/DateRangeGranularity.swift | 16 ++++++- .../WPAnalyticsEvent+JetpackStats.swift | 1 + .../Utility/Analytics/WPAnalyticsEvent.swift | 3 ++ 7 files changed, 108 insertions(+), 7 deletions(-) diff --git a/Modules/Sources/JetpackStats/Analytics/StatsEvent.swift b/Modules/Sources/JetpackStats/Analytics/StatsEvent.swift index 909c91cce10d..97b9f5b35b73 100644 --- a/Modules/Sources/JetpackStats/Analytics/StatsEvent.swift +++ b/Modules/Sources/JetpackStats/Analytics/StatsEvent.swift @@ -83,6 +83,12 @@ public enum StatsEvent { /// - "to_type": New chart type case chartTypeChanged + /// Chart granularity changed + /// - Parameters: + /// - "from": Previous granularity (e.g., "day", "week", "automatic") + /// - "to": New granularity + case chartGranularityChanged + /// Chart metric selected /// - Parameters: /// - "metric": The metric selected (e.g., "visitors", "views", "likes") @@ -200,3 +206,16 @@ extension SiteMetric { } } } + +extension DateRangeGranularity { + /// Analytics tracking name for the granularity + var analyticsName: String { + switch self { + case .hour: "hour" + case .day: "day" + case .week: "week" + case .month: "month" + case .year: "year" + } + } +} diff --git a/Modules/Sources/JetpackStats/Cards/ChartCard.swift b/Modules/Sources/JetpackStats/Cards/ChartCard.swift index 9d5b290edd68..e7529211aae0 100644 --- a/Modules/Sources/JetpackStats/Cards/ChartCard.swift +++ b/Modules/Sources/JetpackStats/Cards/ChartCard.swift @@ -180,14 +180,20 @@ struct ChartCard: View { @ViewBuilder private var moreMenuContent: some View { + chartTypeSection + granularitySection + dataSection + EditCardMenuContent(cardViewModel: viewModel) + } + + private var chartTypeSection: some View { Section { ControlGroup { - ForEach(ChartType.allCases, id: \.self) { type in + ForEach(ChartType.allCases) { type in Button { let previousType = viewModel.selectedChartType viewModel.selectedChartType = type - // Track chart type change viewModel.tracker?.send(.chartTypeChanged, properties: [ "from_type": previousType.rawValue, "to_type": type.rawValue @@ -198,6 +204,39 @@ struct ChartCard: View { } } } + } + + private var granularitySection: some View { + Section { + Menu { + granularityButton(for: nil) + let options: [DateRangeGranularity] = [.day, .week, .month, .year] + ForEach(options) { granularity in + granularityButton(for: granularity) + } + } label: { + Label(viewModel.effectiveGranularity.localizedTitle, systemImage: "calendar") + } + } + } + + private func granularityButton(for granularity: DateRangeGranularity?) -> some View { + Button { + let previousGranularity = viewModel.selectedGranularity + viewModel.selectedGranularity = granularity + viewModel.tracker?.send(.chartGranularityChanged, properties: [ + "from": previousGranularity?.analyticsName ?? "automatic", + "to": granularity?.analyticsName ?? "automatic" + ]) + } label: { + Label( + granularity?.localizedTitle ?? Strings.Granularity.automatic, + systemImage: viewModel.selectedGranularity == granularity ? "checkmark" : "" + ) + } + } + + private var dataSection: some View { Section { Button { isShowingRawData = true @@ -208,7 +247,6 @@ struct ChartCard: View { Label(Strings.Buttons.learnMore, systemImage: "info.circle") } } - EditCardMenuContent(cardViewModel: viewModel) } // MARK: - Chart View @@ -276,10 +314,12 @@ private struct CardGradientBackground: View { } } -public enum ChartType: String, CaseIterable, Codable { +public enum ChartType: String, CaseIterable, Identifiable, Codable { case line case columns + public var id: String { rawValue } + var localizedTitle: String { switch self { case .line: Strings.Chart.lineChart diff --git a/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift b/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift index 9a28a6a90c59..bb2ca9f3cb0a 100644 --- a/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift +++ b/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift @@ -14,6 +14,7 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel { @Published var isEditing = false @Published var selectedMetric: SiteMetric + @Published var selectedChartType: ChartType { didSet { // Update configuration when chart type changes @@ -22,14 +23,29 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel { } } + @Published var selectedGranularity: DateRangeGranularity? { + didSet { + // Reload data with new granularity + loadData(for: dateRange) + } + } + weak var configurationDelegate: CardConfigurationDelegate? var dateRange: StatsDateRange { didSet { + // Reset granularity to automatic when date period changes (but not for adjacent navigation) + if !dateRange.isAdjacent(to: oldValue) { + selectedGranularity = nil + } loadData(for: dateRange) } } + var effectiveGranularity: DateRangeGranularity { + selectedGranularity ?? dateRange.dateInterval.preferredGranularity + } + private let service: any StatsServiceProtocol let tracker: (any StatsTracker)? @@ -147,7 +163,7 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel { private func getSiteStats(dateRange: StatsDateRange) async throws -> [SiteMetric: ChartData] { var output: [SiteMetric: ChartData] = [:] - let granularity = dateRange.dateInterval.preferredGranularity + let granularity = selectedGranularity ?? dateRange.dateInterval.preferredGranularity // Fetch both current and previous period data concurrently async let currentResponseTask = service.getSiteStats( diff --git a/Modules/Sources/JetpackStats/Strings.swift b/Modules/Sources/JetpackStats/Strings.swift index ad550b8881b4..6cc86cf6311b 100644 --- a/Modules/Sources/JetpackStats/Strings.swift +++ b/Modules/Sources/JetpackStats/Strings.swift @@ -31,6 +31,15 @@ enum Strings { static let year = AppLocalizedString("jetpackStats.calendar.year", value: "Year", comment: "Year time period") } + enum Granularity { + static let automatic = AppLocalizedString("jetpackStats.granularity.automatic", value: "Automatic", comment: "Automatic granularity option") + static let hour = AppLocalizedString("jetpackStats.granularity.hours", value: "Hours", comment: "Hours granularity option") + static let day = AppLocalizedString("jetpackStats.granularity.days", value: "Days", comment: "Days granularity option") + static let week = AppLocalizedString("jetpackStats.granularity.weeks", value: "Weeks", comment: "Weeks granularity option") + static let month = AppLocalizedString("jetpackStats.granularity.months", value: "Months", comment: "Months granularity option") + static let year = AppLocalizedString("jetpackStats.granularity.years", value: "Years", comment: "Years granularity option") + } + enum SiteMetrics { static let views = AppLocalizedString("jetpackStats.siteMetrics.views", value: "Views", comment: "Site views metric") static let visitors = AppLocalizedString("jetpackStats.siteMetrics.visitors", value: "Visitors", comment: "Site visitors metric") @@ -111,6 +120,7 @@ enum Strings { static let incompleteData = AppLocalizedString("jetpackStats.chart.incompleteData", value: "Might show incomplete data", comment: "Shown when current period data might be incomplete") static let hourlyDataUnavailable = AppLocalizedString("jetpackStats.chart.hourlyDataNotAvailable", value: "Hourly data not available", comment: "Shown for metrics that don't support hourly data") static let empty = AppLocalizedString("jetpackStats.chart.dataEmpty", value: "No data for period", comment: "Shown for empty states") + static let granularity = AppLocalizedString("jetpackStats.chart.granularity", value: "Granularity", comment: "Granularity picker label") } enum TopListTitles { diff --git a/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift b/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift index 70c0a5e0380f..f2dbc022a92c 100644 --- a/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift +++ b/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift @@ -1,11 +1,13 @@ import Foundation -enum DateRangeGranularity: Comparable { +enum DateRangeGranularity: Comparable, CaseIterable, Identifiable { case hour case day case week case month case year + + var id: Self { self } } extension DateInterval { @@ -21,7 +23,7 @@ extension DateInterval { if totalDays <= 1 { return .hour } - // For ranges 2-90 days: show daily data (2-90 points) + // For ranges 2-31 days: show daily data (2-90 points) else if totalDays <= 31 { return .day } @@ -40,6 +42,16 @@ extension DateInterval { } extension DateRangeGranularity { + var localizedTitle: String { + switch self { + case .hour: Strings.Granularity.hour + case .day: Strings.Granularity.day + case .week: Strings.Granularity.week + case .month: Strings.Granularity.month + case .year: Strings.Granularity.year + } + } + /// Components needed to aggregate data at this granularity var calendarComponents: Set { switch self { diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent+JetpackStats.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent+JetpackStats.swift index 5b20d3b80067..fa1fbd2b0b9b 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent+JetpackStats.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent+JetpackStats.swift @@ -22,6 +22,7 @@ extension StatsEvent { case .chartTypeChanged: .jetpackStatsChartTypeChanged case .chartMetricSelected: .jetpackStatsChartMetricSelected case .chartBarSelected: .jetpackStatsChartBarSelected + case .chartGranularityChanged: .jetpackStatsChartGranularityChanged case .todayCardTapped: .jetpackStatsTodayCardTapped case .topListItemTapped: .jetpackStatsTopListItemTapped case .statsTabSelected: .jetpackStatsTabSelected diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index 7ac3c8b0d3b2..519e37ddc2a0 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -657,6 +657,7 @@ import WordPressShared case jetpackStatsChartTypeChanged case jetpackStatsChartMetricSelected case jetpackStatsChartBarSelected + case jetpackStatsChartGranularityChanged // Today case jetpackStatsTodayCardTapped @@ -1805,6 +1806,8 @@ import WordPressShared return "jetpack_stats_chart_metric_selected" case .jetpackStatsChartBarSelected: return "jetpack_stats_chart_bar_selected" + case .jetpackStatsChartGranularityChanged: + return "jetpack_stats_chart_granularity_changed" // Today case .jetpackStatsTodayCardTapped: From f6b1c68560ed31b52e771543149d8d0e06fbac8f Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Dec 2025 13:12:53 -0500 Subject: [PATCH 2/3] Simplify mapDataPoints --- .../Cards/ChartCardViewModel.swift | 7 +- .../Cards/StandaloneChartCard.swift | 7 +- .../Services/Data/DataPoint.swift | 29 ++-- .../JetpackStatsTests/DataPointTests.swift | 158 ++++++++++++------ 4 files changed, 122 insertions(+), 79 deletions(-) diff --git a/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift b/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift index bb2ca9f3cb0a..298f466494ba 100644 --- a/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift +++ b/Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift @@ -183,11 +183,8 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel { // Map previous data to align with current period dates so they // are displayed on the same timeline on the charts. let mappedPreviousDataPoints = DataPoint.mapDataPoints( - previousDataPoints, - from: dateRange.effectiveComparisonInterval, - to: dateRange.dateInterval, - component: dateRange.component, - calendar: dateRange.calendar + currentData: dataPoints, + previousData: previousDataPoints ) output[metric] = ChartData( diff --git a/Modules/Sources/JetpackStats/Cards/StandaloneChartCard.swift b/Modules/Sources/JetpackStats/Cards/StandaloneChartCard.swift index bc0704bac352..57375770e0b0 100644 --- a/Modules/Sources/JetpackStats/Cards/StandaloneChartCard.swift +++ b/Modules/Sources/JetpackStats/Cards/StandaloneChartCard.swift @@ -282,11 +282,8 @@ private func generateChartData( // Map previous data points to current period dates for overlay let mappedPreviousData = DataPoint.mapDataPoints( - previousPeriod.dataPoints, - from: previousDateInterval, - to: dateRange.dateInterval, - component: dateRange.component, - calendar: calendar + currentData: currentPeriod.dataPoints, + previousData: previousPeriod.dataPoints ) return ChartData( diff --git a/Modules/Sources/JetpackStats/Services/Data/DataPoint.swift b/Modules/Sources/JetpackStats/Services/Data/DataPoint.swift index 038af162f8a1..f51883c87c52 100644 --- a/Modules/Sources/JetpackStats/Services/Data/DataPoint.swift +++ b/Modules/Sources/JetpackStats/Services/Data/DataPoint.swift @@ -12,27 +12,20 @@ struct DataPoint: Identifiable, Sendable { extension DataPoint { /// Maps previous period data points to align with current period dates. + /// Takes the dates from current data and replaces values with corresponding previous data values. + /// Arrays are aligned from the end - if lengths differ, the beginning of the longer array is skipped. /// - Parameters: - /// - previousData: The data points from the previous period - /// - from: The date interval of the previous period - /// - to: The date interval of the current period - /// - component: The calendar component to use for date calculations - /// - calendar: The calendar to use for date calculations - /// - Returns: An array of data points with dates shifted to align with the current period + /// - currentData: The data points from the current period (provides dates) + /// - previousData: The data points from the previous period (provides values) + /// - Returns: An array of data points with current dates and previous values static func mapDataPoints( - _ dataPoits: [DataPoint], - from: DateInterval, - to: DateInterval, - component: Calendar.Component, - calendar: Calendar + currentData: [DataPoint], + previousData: [DataPoint] ) -> [DataPoint] { - let offset = calendar.dateComponents([component], from: from.start, to: to.start).value(for: component) ?? 0 - return dataPoits.map { dataPoint in - DataPoint( - date: calendar.date(byAdding: component, value: offset, to: dataPoint.date) ?? dataPoint.date, - value: dataPoint.value - ) - } + // reversing to align by the last item in case there is a mismatch in the number of items + zip(currentData.reversed(), previousData.reversed()).map { current, previous in + DataPoint(date: current.date, value: previous.value) + }.reversed() } static func getTotalValue(for dataPoints: [DataPoint], metric: SiteMetric) -> Int? { diff --git a/Modules/Tests/JetpackStatsTests/DataPointTests.swift b/Modules/Tests/JetpackStatsTests/DataPointTests.swift index 720bed4869ce..c63bfac7e9e3 100644 --- a/Modules/Tests/JetpackStatsTests/DataPointTests.swift +++ b/Modules/Tests/JetpackStatsTests/DataPointTests.swift @@ -4,18 +4,18 @@ import Foundation @Suite struct DataPointTests { - let calendar = Calendar.mock(timeZone: .eastern) - @Test("Maps previous data to current period with simple day offset") func testMapPreviousDataToCurrentSimpleDayOffset() { // GIVEN - let previousStart = Date("2025-01-01T00:00:00-03:00") - let previousEnd = Date("2025-01-08T00:00:00-03:00") - let previousRange = DateInterval(start: previousStart, end: previousEnd) - - let currentStart = Date("2025-01-08T00:00:00-03:00") - let currentEnd = Date("2025-01-15T00:00:00-03:00") - let currentRange = DateInterval(start: currentStart, end: currentEnd) + let currentData = [ + DataPoint(date: Date("2025-01-08T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-09T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-10T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-11T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-12T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-13T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-14T00:00:00-03:00"), value: 0) + ] let previousData = [ DataPoint(date: Date("2025-01-01T00:00:00-03:00"), value: 100), @@ -29,11 +29,8 @@ struct DataPointTests { // WHEN let mappedData = DataPoint.mapDataPoints( - previousData, - from: previousRange, - to: currentRange, - component: .day, - calendar: calendar + currentData: currentData, + previousData: previousData ) // THEN @@ -49,13 +46,11 @@ struct DataPointTests { @Test("Maps previous month data to current month") func testMapPreviousMonthDataToCurrent() { // GIVEN - let previousStart = Date("2024-12-01T00:00:00-03:00") - let previousEnd = Date("2025-01-01T00:00:00-03:00") - let previousRange = DateInterval(start: previousStart, end: previousEnd) - - let currentStart = Date("2025-01-01T00:00:00-03:00") - let currentEnd = Date("2025-02-01T00:00:00-03:00") - let currentRange = DateInterval(start: currentStart, end: currentEnd) + let currentData = [ + DataPoint(date: Date("2025-01-01T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-15T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-31T00:00:00-03:00"), value: 0) + ] let previousData = [ DataPoint(date: Date("2024-12-01T00:00:00-03:00"), value: 1000), @@ -65,11 +60,8 @@ struct DataPointTests { // WHEN let mappedData = DataPoint.mapDataPoints( - previousData, - from: previousRange, - to: currentRange, - component: .month, - calendar: calendar + currentData: currentData, + previousData: previousData ) // THEN @@ -85,39 +77,67 @@ struct DataPointTests { @Test("Maps with empty previous data") func testMapEmptyPreviousData() { // GIVEN - let previousRange = DateInterval( - start: Date("2025-01-01T00:00:00-03:00"), - end: Date("2025-01-08T00:00:00-03:00") - ) - let currentRange = DateInterval( - start: Date("2025-01-08T00:00:00-03:00"), - end: Date("2025-01-15T00:00:00-03:00") - ) + let currentData = [ + DataPoint(date: Date("2025-01-08T00:00:00-03:00"), value: 100), + DataPoint(date: Date("2025-01-09T00:00:00-03:00"), value: 200), + DataPoint(date: Date("2025-01-10T00:00:00-03:00"), value: 300) + ] let previousData: [DataPoint] = [] // WHEN let mappedData = DataPoint.mapDataPoints( - previousData, - from: previousRange, - to: currentRange, - component: .day, - calendar: calendar + currentData: currentData, + previousData: previousData ) // THEN #expect(mappedData.isEmpty) } + @Test("Maps with mismatched array lengths, aligned from the end") + func testMapMismatchedArrayLengths() { + // GIVEN - current has 6 items, previous has 5 items + let currentData = [ + DataPoint(date: Date("2025-01-08T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-09T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-10T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-11T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-12T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-13T00:00:00-03:00"), value: 0) + ] + + let previousData = [ + DataPoint(date: Date("2025-01-01T00:00:00-03:00"), value: 100), + DataPoint(date: Date("2025-01-02T00:00:00-03:00"), value: 200), + DataPoint(date: Date("2025-01-03T00:00:00-03:00"), value: 300), + DataPoint(date: Date("2025-01-04T00:00:00-03:00"), value: 400), + DataPoint(date: Date("2025-01-05T00:00:00-03:00"), value: 500) + ] + + // WHEN + let mappedData = DataPoint.mapDataPoints( + currentData: currentData, + previousData: previousData + ) + + // THEN - should have 5 items (min count), aligned from the end + // current[1..5] paired with previous[0..4] + #expect(mappedData.count == 5) + #expect(mappedData[0].date == Date("2025-01-09T00:00:00-03:00")) + #expect(mappedData[0].value == 100) + #expect(mappedData[1].date == Date("2025-01-10T00:00:00-03:00")) + #expect(mappedData[1].value == 200) + #expect(mappedData[4].date == Date("2025-01-13T00:00:00-03:00")) + #expect(mappedData[4].value == 500) + } + @Test("Maps year-over-year comparison") func testMapYearOverYearComparison() { // GIVEN - let previousStart = Date("2024-01-01T00:00:00-03:00") - let previousEnd = Date("2024-01-08T00:00:00-03:00") - let previousRange = DateInterval(start: previousStart, end: previousEnd) - - let currentStart = Date("2025-01-01T00:00:00-03:00") - let currentEnd = Date("2025-01-08T00:00:00-03:00") - let currentRange = DateInterval(start: currentStart, end: currentEnd) + let currentData = [ + DataPoint(date: Date("2025-01-01T00:00:00-03:00"), value: 0), + DataPoint(date: Date("2025-01-07T00:00:00-03:00"), value: 0) + ] let previousData = [ DataPoint(date: Date("2024-01-01T00:00:00-03:00"), value: 1000), @@ -126,11 +146,8 @@ struct DataPointTests { // WHEN let mappedData = DataPoint.mapDataPoints( - previousData, - from: previousRange, - to: currentRange, - component: .year, - calendar: calendar + currentData: currentData, + previousData: previousData ) // THEN @@ -140,4 +157,43 @@ struct DataPointTests { #expect(mappedData[1].date == Date("2025-01-07T00:00:00-03:00")) #expect(mappedData[1].value == 2000) } + + @Test("Maps previous week data to current week") + func testMapPreviousWeekDataToCurrent() { + // GIVEN + let currentData = [ + DataPoint(date: Date("2025-11-17T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-11-24T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-12-01T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-12-08T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-12-15T05:00:00+00:00"), value: 0) + ] + + let previousData = [ + DataPoint(date: Date("2025-10-20T04:00:00+00:00"), value: 3), + DataPoint(date: Date("2025-10-27T04:00:00+00:00"), value: 5), + DataPoint(date: Date("2025-11-03T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-11-10T05:00:00+00:00"), value: 0), + DataPoint(date: Date("2025-11-17T05:00:00+00:00"), value: 1) + ] + + // WHEN + let mappedData = DataPoint.mapDataPoints( + currentData: currentData, + previousData: previousData + ) + + // THEN + #expect(mappedData.count == 5) + #expect(mappedData[0].date == Date("2025-11-17T05:00:00+00:00")) + #expect(mappedData[0].value == 3) + #expect(mappedData[1].date == Date("2025-11-24T05:00:00+00:00")) + #expect(mappedData[1].value == 5) + #expect(mappedData[2].date == Date("2025-12-01T05:00:00+00:00")) + #expect(mappedData[2].value == 0) + #expect(mappedData[3].date == Date("2025-12-08T05:00:00+00:00")) + #expect(mappedData[3].value == 0) + #expect(mappedData[4].date == Date("2025-12-15T05:00:00+00:00")) + #expect(mappedData[4].value == 1) + } } From 2e5a74d81e709cc668ecaa0d943fe8ffbc2df7cd Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Tue, 23 Dec 2025 13:38:08 -0500 Subject: [PATCH 3/3] Update release notes --- .../Sources/JetpackStats/Utilities/DateRangeGranularity.swift | 2 +- RELEASE-NOTES.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift b/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift index f2dbc022a92c..1588f000b53e 100644 --- a/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift +++ b/Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift @@ -23,7 +23,7 @@ extension DateInterval { if totalDays <= 1 { return .hour } - // For ranges 2-31 days: show daily data (2-90 points) + // For ranges 2-31 days: show daily data (2-31 points) else if totalDays <= 31 { return .day } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 67a3b481ab33..2c1aa2ae1814 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,8 @@ * [**] Reader: Add "Freshly Pressed" to Discover [#24828] * [**] Intelligence: Expand AI-based features to more locales [#25034] * [*] Fix previewing posts on WordPress.com atomic sites [#25045] +* [*] Stats: Add days/weeks/months/years picker for bar and line charts [#25093] +* [*] Stats: Fix an issue with weekly data for the comparison period sometimes not being shown correctly [#25093] 26.5 -----