From b3afe45ee09a122ec68d2e3d37d555906f185f32 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 15:39:35 +0200 Subject: [PATCH 1/4] feat(model): add fillingMissingDays and zero factory on DailySummary Adds `DailySummary.zero(for:)` factory and `[DailySummary].fillingMissingDays(from:to:)` extension to pad date ranges with zero-value entries for missing days. Closes #241 --- .../InputMetrics/Models/DailySummary.swift | 20 +++++++++++++++++++ .../InputMetrics/Utilities/DateHelper.swift | 1 + 2 files changed, 21 insertions(+) diff --git a/InputMetrics/InputMetrics/Models/DailySummary.swift b/InputMetrics/InputMetrics/Models/DailySummary.swift index 94929ac..1252279 100644 --- a/InputMetrics/InputMetrics/Models/DailySummary.swift +++ b/InputMetrics/InputMetrics/Models/DailySummary.swift @@ -52,4 +52,24 @@ struct DailySummary: Codable, FetchableRecord, PersistableRecord { static let peakMouseSpeed = Column(CodingKeys.peakMouseSpeed) static let peakWPM = Column(CodingKeys.peakWPM) } + + static func zero(for date: String) -> DailySummary { + DailySummary(date: date, mouseDistancePx: 0, mouseClicksLeft: 0, mouseClicksRight: 0, mouseClicksMiddle: 0, keystrokes: 0, scrollDistanceVertical: 0, scrollDistanceHorizontal: 0) + } +} + +extension [DailySummary] { + func fillingMissingDays(from start: Date, to end: Date) -> [DailySummary] { + let calendar = Calendar.current + let formatter = DateHelper.self + let existing = Dictionary(uniqueKeysWithValues: self.map { ($0.date, $0) }) + var result: [DailySummary] = [] + var current = start + while current <= end { + let key = formatter.string(from: current) + result.append(existing[key] ?? .zero(for: key)) + current = calendar.date(byAdding: .day, value: 1, to: current) ?? current + } + return result + } } diff --git a/InputMetrics/InputMetrics/Utilities/DateHelper.swift b/InputMetrics/InputMetrics/Utilities/DateHelper.swift index adb16f9..95a5a1f 100644 --- a/InputMetrics/InputMetrics/Utilities/DateHelper.swift +++ b/InputMetrics/InputMetrics/Utilities/DateHelper.swift @@ -19,4 +19,5 @@ enum DateHelper { static func date(from string: String) -> Date? { formatter.date(from: string) } + } From 3528546d542c2773a2e2eccd3cd3a04ca0af26a8 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 15:39:41 +0200 Subject: [PATCH 2/4] fix(viewmodel): fill missing days in week/month chart data Replaces duplicated private fillMissingDays implementations with a call to the shared [DailySummary].fillingMissingDays extension for both MouseStatsViewModel and KeyboardStatsViewModel. --- .../InputMetrics/ViewModels/KeyboardStatsViewModel.swift | 4 ++++ .../InputMetrics/ViewModels/MouseStatsViewModel.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/InputMetrics/InputMetrics/ViewModels/KeyboardStatsViewModel.swift b/InputMetrics/InputMetrics/ViewModels/KeyboardStatsViewModel.swift index a7466f4..e48df16 100644 --- a/InputMetrics/InputMetrics/ViewModels/KeyboardStatsViewModel.swift +++ b/InputMetrics/InputMetrics/ViewModels/KeyboardStatsViewModel.swift @@ -86,6 +86,10 @@ final class KeyboardStatsViewModel { } } + if selectedRange == .week || selectedRange == .month { + chartData = chartData.fillingMissingDays(from: startDate, to: selectedDate) + } + if selectedRange == .year { chartData = aggregateByWeek(chartData) } diff --git a/InputMetrics/InputMetrics/ViewModels/MouseStatsViewModel.swift b/InputMetrics/InputMetrics/ViewModels/MouseStatsViewModel.swift index 0dcd70e..ccc34ea 100644 --- a/InputMetrics/InputMetrics/ViewModels/MouseStatsViewModel.swift +++ b/InputMetrics/InputMetrics/ViewModels/MouseStatsViewModel.swift @@ -81,6 +81,10 @@ final class MouseStatsViewModel { } } + if selectedRange == .week || selectedRange == .month { + chartData = chartData.fillingMissingDays(from: startDate, to: selectedDate) + } + if selectedRange == .year { chartData = aggregateByWeek(chartData) } From b875b9cb4729d8269d8523ed0bd313a6b0d2a8ca Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 15:39:48 +0200 Subject: [PATCH 3/4] fix(chart): use raw date string as x key to prevent duplicate series Using formatted weekday names (EEE) as x keys caused Swift Charts to create a second series when 8 data points mapped to only 7 unique labels. Switching to yyyy-MM-dd as the data key guarantees uniqueness; a chartXAxis modifier handles display formatting. --- .../InputMetrics/Views/ChartView.swift | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/InputMetrics/InputMetrics/Views/ChartView.swift b/InputMetrics/InputMetrics/Views/ChartView.swift index 56b9769..f27a6bd 100644 --- a/InputMetrics/InputMetrics/Views/ChartView.swift +++ b/InputMetrics/InputMetrics/Views/ChartView.swift @@ -26,32 +26,31 @@ struct ChartView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } else { Chart(data, id: \.date) { item in - let label = formatLabel(from: item.date) let value = metricValue(for: item) - LineMark( - x: .value("Date", label), + AreaMark( + x: .value("Date", item.date), y: .value("Value", value) ) - .foregroundStyle(Color.blue) + .foregroundStyle(Color.blue.opacity(0.15)) .interpolationMethod(.catmullRom) - AreaMark( - x: .value("Date", label), + LineMark( + x: .value("Date", item.date), y: .value("Value", value) ) - .foregroundStyle(Color.blue.opacity(0.1)) + .foregroundStyle(Color.blue) .interpolationMethod(.catmullRom) PointMark( - x: .value("Date", label), + x: .value("Date", item.date), y: .value("Value", value) ) .foregroundStyle(Color.blue) .symbolSize(30) - if hoveredLabel == label { - RuleMark(x: .value("Date", label)) + if hoveredLabel == item.date { + RuleMark(x: .value("Date", item.date)) .foregroundStyle(Color.secondary.opacity(0.3)) .annotation(position: .top, spacing: 4) { Text(formattedTooltip(value: value)) @@ -63,6 +62,18 @@ struct ChartView: View { } } } + .chartLegend(.hidden) + .chartXAxis { + AxisMarks { value in + AxisValueLabel { + if let dateStr = value.as(String.self) { + Text(formatLabel(from: dateStr)) + } + } + AxisGridLine() + AxisTick() + } + } .accessibilityElement(children: .ignore) .accessibilityLabel("\(chartTitle) chart for \(range == .week ? "this week" : range == .month ? "this month" : "this year")") .chartYAxisLabel(yAxisLabel) From 5ce23c805a391e3517102dfdff6412ca73fe9655 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 30 Mar 2026 15:42:29 +0200 Subject: [PATCH 4/4] style(utils): remove spurious blank line in DateHelper --- InputMetrics/InputMetrics/Utilities/DateHelper.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/InputMetrics/InputMetrics/Utilities/DateHelper.swift b/InputMetrics/InputMetrics/Utilities/DateHelper.swift index 95a5a1f..adb16f9 100644 --- a/InputMetrics/InputMetrics/Utilities/DateHelper.swift +++ b/InputMetrics/InputMetrics/Utilities/DateHelper.swift @@ -19,5 +19,4 @@ enum DateHelper { static func date(from string: String) -> Date? { formatter.date(from: string) } - }