Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-26 18:00:48 -06:00
parent cdb13136fc
commit c809eafd19
4 changed files with 102 additions and 2 deletions

View File

@ -202,6 +202,9 @@
"comment" : "One of the times displayed in the reminder subtitle when daily reminders are enabled.", "comment" : "One of the times displayed in the reminder subtitle when daily reminders are enabled.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"7-Day Avg" : {
"comment" : "Title for an insight card showing the 7-day average completion percentage."
},
"7-Day Trend" : { "7-Day Trend" : {
"comment" : "A heading for the 7-day trend section of an insight detail sheet.", "comment" : "A heading for the 7-day trend section of an insight detail sheet.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -401,6 +404,18 @@
"comment" : "Title for a stat card displaying the longest streak of days in an arc.", "comment" : "Title for a stat card displaying the longest streak of days in an arc.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Best Ritual" : {
"comment" : "Title for an insight card showing the highest-performing ritual by completion rate."
},
"Your highest-performing ritual by completion rate in the current arc. Keep it up!" : {
"comment" : "Explanation for the Best Ritual insight card."
},
"No active rituals" : {
"comment" : "Caption for the Best Ritual insight card when there are no active rituals."
},
"Start a ritual to see which one you complete most consistently." : {
"comment" : "Explanation for the Best Ritual insight card when there are no active rituals."
},
"Body scan for tension" : { "Body scan for tension" : {
"comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.", "comment" : "Habit title for a mindfulness ritual that involves scanning the body for tension.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2469,6 +2484,15 @@
"comment" : "Label for a breakdown item showing the total number of check-ins made by the user.", "comment" : "Label for a breakdown item showing the total number of check-ins made by the user.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Total Check-ins" : {
"comment" : "Title for an insight card showing the total number of habits completed all-time."
},
"All-time habits completed" : {
"comment" : "Caption for the Total Check-ins insight card."
},
"The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change." : {
"comment" : "Explanation for the Total Check-ins insight card."
},
"Track your streaks, progress, and trends over time." : { "Track your streaks, progress, and trends over time." : {
"comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to use the app's insights feature.", "comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to use the app's insights feature.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -2520,6 +2544,12 @@
"comment" : "Name of a ritual preset that users can add to their collection, focusing on preparing for a fresh week.", "comment" : "Name of a ritual preset that users can add to their collection, focusing on preparing for a fresh week.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Weekly average" : {
"comment" : "Caption for the 7-Day Avg insight card."
},
"Your average completion rate over the last 7 days. This smooths out daily fluctuations and shows your typical consistency." : {
"comment" : "Explanation for the 7-Day Avg insight card."
},
"Welcome to Rituals" : { "Welcome to Rituals" : {
"comment" : "The title of the welcome screen in the setup wizard.", "comment" : "The title of the welcome screen in the setup wizard.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true

View File

@ -452,6 +452,33 @@ final class RitualStore: RitualStoreProviding {
BreakdownItem(label: point.label, value: "\(Int(point.value * 100))%") BreakdownItem(label: point.label, value: "\(Int(point.value * 100))%")
} }
// 7-day average
let weeklyAvg = weeklyAverageCompletion()
// Total habits completed all-time
let totalHabitsAllTime = rituals
.flatMap { $0.arcs ?? [] }
.flatMap { $0.habits ?? [] }
.reduce(0) { $0 + $1.completedDayIDs.count }
// Best ritual by completion rate
let bestRitualInfo: (title: String, rate: Int)? = {
var best: (title: String, rate: Int)?
for ritual in currentRituals {
guard let arc = ritual.currentArc else { continue }
let habits = arc.habits ?? []
guard !habits.isEmpty else { continue }
let totalCheckIns = habits.reduce(0) { $0 + $1.completedDayIDs.count }
let possibleCheckIns = habits.count * arc.dayIndex(for: Date())
guard possibleCheckIns > 0 else { continue }
let rate = Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100)
if best == nil || rate > best!.rate {
best = (ritual.title, rate)
}
}
return best
}()
return [ return [
InsightCard( InsightCard(
title: String(localized: "Active"), title: String(localized: "Active"),
@ -493,7 +520,42 @@ final class RitualStore: RitualStoreProviding {
explanation: String(localized: "This counts every unique calendar day where you completed at least one habit. It's calculated by scanning all your habit check-ins across all rituals and counting the distinct days. For example, if you checked in on Monday, skipped Tuesday, then checked in Wednesday and Thursday, your Days Active would be 3."), explanation: String(localized: "This counts every unique calendar day where you completed at least one habit. It's calculated by scanning all your habit check-ins across all rituals and counting the distinct days. For example, if you checked in on Monday, skipped Tuesday, then checked in Wednesday and Thursday, your Days Active would be 3."),
symbolName: "calendar", symbolName: "calendar",
breakdown: daysActiveBreakdown() breakdown: daysActiveBreakdown()
) ),
InsightCard(
title: String(localized: "7-Day Avg"),
value: "\(weeklyAvg)%",
caption: String(localized: "Weekly average"),
explanation: String(localized: "Your average completion rate over the last 7 days. This smooths out daily fluctuations and shows your typical consistency."),
symbolName: "chart.line.uptrend.xyaxis",
breakdown: trendBreakdown,
trendData: trendData
),
InsightCard(
title: String(localized: "Total Check-ins"),
value: "\(totalHabitsAllTime)",
caption: String(localized: "All-time habits completed"),
explanation: String(localized: "The total number of habit check-ins you've made since you started using Rituals. Every check-in counts toward building lasting change."),
symbolName: "checkmark.seal.fill"
),
{
if let best = bestRitualInfo {
return InsightCard(
title: String(localized: "Best Ritual"),
value: "\(best.rate)%",
caption: best.title,
explanation: String(localized: "Your highest-performing ritual by completion rate in the current arc. Keep it up!"),
symbolName: "star.fill"
)
} else {
return InsightCard(
title: String(localized: "Best Ritual"),
value: "",
caption: String(localized: "No active rituals"),
explanation: String(localized: "Start a ritual to see which one you complete most consistently."),
symbolName: "star.fill"
)
}
}()
] ]
} }

View File

@ -50,6 +50,7 @@ struct InsightCardView: View {
} }
// Show full-width mini bar chart at bottom (Athlytic style) // Show full-width mini bar chart at bottom (Athlytic style)
// Always reserve consistent height for chart area to keep cards aligned
if let trendData = card.trendData, !trendData.isEmpty { if let trendData = card.trendData, !trendData.isEmpty {
Chart(trendData) { point in Chart(trendData) { point in
BarMark( BarMark(
@ -68,6 +69,10 @@ struct InsightCardView: View {
.chartYAxis(.hidden) .chartYAxis(.hidden)
.frame(height: 20) .frame(height: 20)
.accessibilityHidden(true) .accessibilityHidden(true)
} else {
// Placeholder to maintain consistent card height
Spacer()
.frame(height: 20)
} }
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)

View File

@ -55,12 +55,15 @@ Rituals is a paid, offline-first habit tracker built around customizable "ritual
- Grouped habit list by ritual - Grouped habit list by ritual
### Insights Tab ### Insights Tab
- Tappable insight cards with detail sheets - Tappable insight cards with full-screen detail sheets
- **Active Rituals**: Count with per-ritual breakdown - **Active Rituals**: Count with per-ritual breakdown
- **Streak**: Current and longest streak tracking - **Streak**: Current and longest streak tracking
- **Habits Today**: Completed count with per-ritual breakdown - **Habits Today**: Completed count with per-ritual breakdown
- **Completion**: Today's percentage with 7-day trend chart - **Completion**: Today's percentage with 7-day trend chart
- **Days Active**: Total active days with detailed breakdown (first check-in, most recent, per-ritual counts) - **Days Active**: Total active days with detailed breakdown (first check-in, most recent, per-ritual counts)
- **7-Day Avg**: Weekly average completion percentage with trend chart
- **Total Check-ins**: All-time habit completions across all rituals
- **Best Ritual**: Highest-performing ritual by completion rate in the current arc
- Trend indicators (up/down/stable) with week-over-week comparison - Trend indicators (up/down/stable) with week-over-week comparison
- Contextual tips based on performance - Contextual tips based on performance