Skip to content

Commit 4dc2b49

Browse files
committed
Add chart granularity picker
1 parent 87ec186 commit 4dc2b49

File tree

7 files changed

+108
-7
lines changed

7 files changed

+108
-7
lines changed

Modules/Sources/JetpackStats/Analytics/StatsEvent.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ public enum StatsEvent {
8383
/// - "to_type": New chart type
8484
case chartTypeChanged
8585

86+
/// Chart granularity changed
87+
/// - Parameters:
88+
/// - "from": Previous granularity (e.g., "day", "week", "automatic")
89+
/// - "to": New granularity
90+
case chartGranularityChanged
91+
8692
/// Chart metric selected
8793
/// - Parameters:
8894
/// - "metric": The metric selected (e.g., "visitors", "views", "likes")
@@ -200,3 +206,16 @@ extension SiteMetric {
200206
}
201207
}
202208
}
209+
210+
extension DateRangeGranularity {
211+
/// Analytics tracking name for the granularity
212+
var analyticsName: String {
213+
switch self {
214+
case .hour: "hour"
215+
case .day: "day"
216+
case .week: "week"
217+
case .month: "month"
218+
case .year: "year"
219+
}
220+
}
221+
}

Modules/Sources/JetpackStats/Cards/ChartCard.swift

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,20 @@ struct ChartCard: View {
180180

181181
@ViewBuilder
182182
private var moreMenuContent: some View {
183+
chartTypeSection
184+
granularitySection
185+
dataSection
186+
EditCardMenuContent(cardViewModel: viewModel)
187+
}
188+
189+
private var chartTypeSection: some View {
183190
Section {
184191
ControlGroup {
185-
ForEach(ChartType.allCases, id: \.self) { type in
192+
ForEach(ChartType.allCases) { type in
186193
Button {
187194
let previousType = viewModel.selectedChartType
188195
viewModel.selectedChartType = type
189196

190-
// Track chart type change
191197
viewModel.tracker?.send(.chartTypeChanged, properties: [
192198
"from_type": previousType.rawValue,
193199
"to_type": type.rawValue
@@ -198,6 +204,39 @@ struct ChartCard: View {
198204
}
199205
}
200206
}
207+
}
208+
209+
private var granularitySection: some View {
210+
Section {
211+
Menu {
212+
granularityButton(for: nil)
213+
let options: [DateRangeGranularity] = [.day, .week, .month, .year]
214+
ForEach(options) { granularity in
215+
granularityButton(for: granularity)
216+
}
217+
} label: {
218+
Label(viewModel.effectiveGranularity.localizedTitle, systemImage: "calendar")
219+
}
220+
}
221+
}
222+
223+
private func granularityButton(for granularity: DateRangeGranularity?) -> some View {
224+
Button {
225+
let previousGranularity = viewModel.selectedGranularity
226+
viewModel.selectedGranularity = granularity
227+
viewModel.tracker?.send(.chartGranularityChanged, properties: [
228+
"from": previousGranularity?.analyticsName ?? "automatic",
229+
"to": granularity?.analyticsName ?? "automatic"
230+
])
231+
} label: {
232+
Label(
233+
granularity?.localizedTitle ?? Strings.Granularity.automatic,
234+
systemImage: viewModel.selectedGranularity == granularity ? "checkmark" : ""
235+
)
236+
}
237+
}
238+
239+
private var dataSection: some View {
201240
Section {
202241
Button {
203242
isShowingRawData = true
@@ -208,7 +247,6 @@ struct ChartCard: View {
208247
Label(Strings.Buttons.learnMore, systemImage: "info.circle")
209248
}
210249
}
211-
EditCardMenuContent(cardViewModel: viewModel)
212250
}
213251

214252
// MARK: - Chart View
@@ -276,10 +314,12 @@ private struct CardGradientBackground: View {
276314
}
277315
}
278316

279-
public enum ChartType: String, CaseIterable, Codable {
317+
public enum ChartType: String, CaseIterable, Identifiable, Codable {
280318
case line
281319
case columns
282320

321+
public var id: String { rawValue }
322+
283323
var localizedTitle: String {
284324
switch self {
285325
case .line: Strings.Chart.lineChart

Modules/Sources/JetpackStats/Cards/ChartCardViewModel.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel {
1414

1515
@Published var isEditing = false
1616
@Published var selectedMetric: SiteMetric
17+
1718
@Published var selectedChartType: ChartType {
1819
didSet {
1920
// Update configuration when chart type changes
@@ -22,14 +23,29 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel {
2223
}
2324
}
2425

26+
@Published var selectedGranularity: DateRangeGranularity? {
27+
didSet {
28+
// Reload data with new granularity
29+
loadData(for: dateRange)
30+
}
31+
}
32+
2533
weak var configurationDelegate: CardConfigurationDelegate?
2634

2735
var dateRange: StatsDateRange {
2836
didSet {
37+
// Reset granularity to automatic when date period changes (but not for adjacent navigation)
38+
if !dateRange.isAdjacent(to: oldValue) {
39+
selectedGranularity = nil
40+
}
2941
loadData(for: dateRange)
3042
}
3143
}
3244

45+
var effectiveGranularity: DateRangeGranularity {
46+
selectedGranularity ?? dateRange.dateInterval.preferredGranularity
47+
}
48+
3349
private let service: any StatsServiceProtocol
3450
let tracker: (any StatsTracker)?
3551

@@ -147,7 +163,7 @@ final class ChartCardViewModel: ObservableObject, TrafficCardViewModel {
147163
private func getSiteStats(dateRange: StatsDateRange) async throws -> [SiteMetric: ChartData] {
148164
var output: [SiteMetric: ChartData] = [:]
149165

150-
let granularity = dateRange.dateInterval.preferredGranularity
166+
let granularity = selectedGranularity ?? dateRange.dateInterval.preferredGranularity
151167

152168
// Fetch both current and previous period data concurrently
153169
async let currentResponseTask = service.getSiteStats(

Modules/Sources/JetpackStats/Strings.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ enum Strings {
3131
static let year = AppLocalizedString("jetpackStats.calendar.year", value: "Year", comment: "Year time period")
3232
}
3333

34+
enum Granularity {
35+
static let automatic = AppLocalizedString("jetpackStats.granularity.automatic", value: "Automatic", comment: "Automatic granularity option")
36+
static let hour = AppLocalizedString("jetpackStats.granularity.hours", value: "Hours", comment: "Hours granularity option")
37+
static let day = AppLocalizedString("jetpackStats.granularity.days", value: "Days", comment: "Days granularity option")
38+
static let week = AppLocalizedString("jetpackStats.granularity.weeks", value: "Weeks", comment: "Weeks granularity option")
39+
static let month = AppLocalizedString("jetpackStats.granularity.months", value: "Months", comment: "Months granularity option")
40+
static let year = AppLocalizedString("jetpackStats.granularity.years", value: "Years", comment: "Years granularity option")
41+
}
42+
3443
enum SiteMetrics {
3544
static let views = AppLocalizedString("jetpackStats.siteMetrics.views", value: "Views", comment: "Site views metric")
3645
static let visitors = AppLocalizedString("jetpackStats.siteMetrics.visitors", value: "Visitors", comment: "Site visitors metric")
@@ -111,6 +120,7 @@ enum Strings {
111120
static let incompleteData = AppLocalizedString("jetpackStats.chart.incompleteData", value: "Might show incomplete data", comment: "Shown when current period data might be incomplete")
112121
static let hourlyDataUnavailable = AppLocalizedString("jetpackStats.chart.hourlyDataNotAvailable", value: "Hourly data not available", comment: "Shown for metrics that don't support hourly data")
113122
static let empty = AppLocalizedString("jetpackStats.chart.dataEmpty", value: "No data for period", comment: "Shown for empty states")
123+
static let granularity = AppLocalizedString("jetpackStats.chart.granularity", value: "Granularity", comment: "Granularity picker label")
114124
}
115125

116126
enum TopListTitles {

Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import Foundation
22

3-
enum DateRangeGranularity: Comparable {
3+
enum DateRangeGranularity: Comparable, CaseIterable, Identifiable {
44
case hour
55
case day
66
case week
77
case month
88
case year
9+
10+
var id: Self { self }
911
}
1012

1113
extension DateInterval {
@@ -21,7 +23,7 @@ extension DateInterval {
2123
if totalDays <= 1 {
2224
return .hour
2325
}
24-
// For ranges 2-90 days: show daily data (2-90 points)
26+
// For ranges 2-31 days: show daily data (2-90 points)
2527
else if totalDays <= 31 {
2628
return .day
2729
}
@@ -40,6 +42,16 @@ extension DateInterval {
4042
}
4143

4244
extension DateRangeGranularity {
45+
var localizedTitle: String {
46+
switch self {
47+
case .hour: Strings.Granularity.hour
48+
case .day: Strings.Granularity.day
49+
case .week: Strings.Granularity.week
50+
case .month: Strings.Granularity.month
51+
case .year: Strings.Granularity.year
52+
}
53+
}
54+
4355
/// Components needed to aggregate data at this granularity
4456
var calendarComponents: Set<Calendar.Component> {
4557
switch self {

WordPress/Classes/Utility/Analytics/WPAnalyticsEvent+JetpackStats.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ extension StatsEvent {
2222
case .chartTypeChanged: .jetpackStatsChartTypeChanged
2323
case .chartMetricSelected: .jetpackStatsChartMetricSelected
2424
case .chartBarSelected: .jetpackStatsChartBarSelected
25+
case .chartGranularityChanged: .jetpackStatsChartGranularityChanged
2526
case .todayCardTapped: .jetpackStatsTodayCardTapped
2627
case .topListItemTapped: .jetpackStatsTopListItemTapped
2728
case .statsTabSelected: .jetpackStatsTabSelected

WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,7 @@ import WordPressShared
657657
case jetpackStatsChartTypeChanged
658658
case jetpackStatsChartMetricSelected
659659
case jetpackStatsChartBarSelected
660+
case jetpackStatsChartGranularityChanged
660661

661662
// Today
662663
case jetpackStatsTodayCardTapped
@@ -1805,6 +1806,8 @@ import WordPressShared
18051806
return "jetpack_stats_chart_metric_selected"
18061807
case .jetpackStatsChartBarSelected:
18071808
return "jetpack_stats_chart_bar_selected"
1809+
case .jetpackStatsChartGranularityChanged:
1810+
return "jetpack_stats_chart_granularity_changed"
18081811

18091812
// Today
18101813
case .jetpackStatsTodayCardTapped:

0 commit comments

Comments
 (0)