Progress Visualization — Feature & Testing Guide
Overview
Progress visualization gives users visual insight into their goal progress through bar charts, pace indicators, streak bars, habit calendars, and momentum metrics. The feature spans the Goal Detail page (per-goal charts and streak data) and the Dashboard (aggregate momentum and activity). All chart data originates from the CalculateGoalProgress backend interaction, which groups goal events and habit completions into time-bucketed data points.
Progress Chart
What It Shows
A bar chart on the Goal Detail page displaying event frequency over time — how often the user logged progress updates.
Time Periods
The chart supports three grouping modes, toggled via period selector buttons:
| Period | Grouping | Visible Points | Label Format |
|---|---|---|---|
| Day | Calendar day | Last 30 days | "Mar 15" |
| Week | ISO week | Last 13 weeks | "Mar 15 - Mar 21" |
| Month | Calendar month | Last 6 months | "Mar 2025" |
Stats Row
Below the period selector, three metrics display:
- Total: Count of events in the lookback period
- Avg/Week: Calculated weekly average
- Pace: PaceIndicator badge (see below)
Visual Details
- Blue bars for periods with events, gray bars for empty periods
- Bar height scales proportionally to the maximum count
- Minimum 2px height for empty bars (visual baseline)
- Hover/tap tooltip shows label and exact count
- Empty state displays "No progress data yet" when no events exist
- Loading state shows pulse animation skeleton
Data Flow
useProgressData(goalId)composable firesGOAL_PROGRESS_DATA_QUERY- Backend
CalculateGoalProgressinteraction groups events by period - Returns
dataPoints[],pace,totalEvents,averagePerWeek ProgressChartcomponent renders the data with period toggling
Pace Indicator
What It Shows
A colored pill badge indicating whether the user is ahead, on track, or behind their expected update frequency.
Pace Values
| Pace | Color | Icon | Condition |
|---|---|---|---|
| Ahead | Emerald | TrendingUp | Actual/expected ratio ≥ 1.2 |
| On Track | Blue | Minus | Ratio between 0.8 and 1.19 |
| Behind | Amber | TrendingDown | Ratio < 0.8 |
Calculation (Backend)
expected_events = lookback_days / days_to_update
ratio = actual_events / expected_events- If the goal has no
days_to_updatesetting, pace defaults to"on_track" - The lookback period defaults to 90 days
Streak Visualization
What It Shows
A side-by-side comparison of current streak vs longest streak for habit goals. Rendered on the Goal Detail page only when the goal has a recurrence type.
Visual Details
- Two horizontal progress bars
- Current streak: orange-to-red gradient bar
- Longest streak: purple-to-indigo gradient bar
- Bar width proportional to max(current, longest) — the larger streak fills 100%
- Labels show day counts (e.g., "12 days")
- Animated transitions (700ms ease) when values change
Data Source
goal.habitStreak— current consecutive streakgoal.longestHabitStreak— all-time best streak- Both returned directly from the
GOAL_QUERY
Goal Progress Card
GoalProgress.vue — the single-card progress display this section used to document — is deleted (OBJ-3561). The Goal Detail page's progress display is now split across GoalHero.vue (percentage + status pills in the hero) and the right-rail GoalDetailsCard.vue (target due date). The goal description (goal.content) renders separately, inline in the page's main content column. For the shared progress-percentage derivation these consume (habit → roadmap → target-amount → not-tracked precedence), see goals.md § Progress Tracking.
Habit Calendar
What It Shows
A 30-day grid calendar showing daily habit completion history. Rendered on the Goal Detail page for habit goals.
Grid Layout
- 7-column grid (Monday through Sunday)
- 30-day lookback from today
- Day labels: M, T, W, T, F, S, S
Color Coding
| Color | Meaning |
|---|---|
| Emerald (green) | Completed normally |
| Blue | Completed with streak freeze |
| Slate (gray) | Missed day |
| Ring highlight | Today's date |
Interactions
- Hover/tap on any day shows a tooltip with the date and status
- Legend below the grid explains color coding
Data Source
goal.habitCompletions[]fromGOAL_QUERY- Each completion has
completedDate(YYYY-MM-DD) andstreakFreezeUsed(boolean)
Habit Completion Ring
What It Shows
A circular SVG progress indicator showing a habit's completion rate. Used in the Full Dashboard's "Today's practice" widget (TodayActions.vue) and on the Goal Detail habit tracking section.
Color Thresholds
| Completion Rate | Color |
|---|---|
| ≥ 80% | Green |
| 50–79% | Amber |
| < 50% | Red |
Props
| Prop | Default | Description |
|---|---|---|
completed | — | Number of completions |
total | — | Total expected completions |
size | 40 | Ring diameter in pixels |
strokeWidth | 3 | Circle stroke width |
Visual Details
- SVG circle with stroke-dashoffset animation
- Center text shows rounded percentage
- Slot for custom inner content
- 500ms ease-out transition
Momentum tiles (Dashboard)
DashboardHero.vue — rendered for every goal-having user, not gated to 3+ goals. Full per-tile contract, tap behavior, and microcopy: dashboard.md § DashboardHero. Summary for QA purposes:
What It Shows
A grid of momentum tiles at the top of the Dashboard. Which variant renders depends on active goal count, not a fixed 3+ threshold:
| Active goals | Variant | Tiles |
|---|---|---|
| 1–2 (sparse) | 4-tile | this week · completed · active · total progress |
| 3+ (hierarchy) | "honest" 3-tile | this week · day streak · active goals |
(0 goals or a first-run new-goal state render neither — see dashboard.md's branch list.)
Metrics
| Metric | Icon | Description | Tap behavior |
|---|---|---|---|
| This week | Zap | Distinct calendar days active this ISO week, capped at 7 | Opens explanation tooltip |
| Day streak (3-tile only) | Streak metaphor icon | Consecutive sign-in days | Opens StreakDetailsModal |
| Completed (4-tile only) | Trophy | Count of completed goals over total | Opens explanation tooltip |
| Active / Active goals | Target | Count of in-progress goals | Opens explanation tooltip |
| Total progress (4-tile only) | Mini progress ring | completed / (completed + active) as a percentage — structurally 0% for habit-only users | Opens explanation tooltip |
Visual Details
- CSS grid layout (
grid-cols-2 md:grid-cols-4for the 4-tile variant,grid-cols-3for the 3-tile "honest" variant) — no dividers - Every non-streak tile carries a corner ⓘ marker and opens a tap-to-toggle tooltip with per-tile microcopy on tap; the
day streaktile carries a corner chevron instead and opens a modal, never a tooltip - Mini progress ring for the
total progresstile only (4-tile variant) - Responsive: 2-up on mobile, 4-up at
md:for the 4-tile variant; the 3-tile variant stays 3-up at every width
Shape of Your Week (Dashboard)
What It Shows
WeekShapeChart.vue — a bar-chart widget showing this week's daily activity, rendered in the Full Dashboard's right rail (desktop) / feed (mobile). This is a real standalone component (not an inline Dashboard.vue section); its input data (dailyPercents, todayIndex, weeklyCheckIns, strongestDayLabel) is computed in Dashboard.vue from an activityDays computed property and passed down as props.
Layout
- Header: "Shape of your week"
- 7 bars (Mon–Sun), height proportional to that day's activity percentage; today's bar is visually distinguished
- Day labels below the bars
- Caption line summarizing the week, or an empty-state line when there's no data yet
Today's Habit Check-In (Dashboard)
What It Shows
There is no standalone "Today's Habits" widget component. Habit check-in on the Dashboard happens in one of two places, depending on dashboard mode — see Dashboard — habit check-in surfaces in the Habits guide for the current layout:
- Focus Mode (default): each due habit is its own full-card step (
HabitCard.vue) in theStreamlinedDashboardcarousel, with a single Check In button - Full Dashboard: a "Today's practice" list widget (
TodayActions.vue) with one row and one check-in button per due habit
Interactions
- Tap the check-in button to complete today's habit
- Button shows loading/spinner state during the mutation
- Button reflects a "done" state after success (green check icon or "Done for today" caption, depending on surface)
- Emits a
checkedIn/habitCheckedInevent that triggers a Dashboard refetch
See habits.md for full habit check-in behavior.
Backend: CalculateGoalProgress Interaction
Input
| Param | Type | Default | Description |
|---|---|---|---|
goal_id | String | — | Goal's public ID |
period | String | "week" | Grouping: "day", "week", or "month" |
lookback_days | Integer | 90 | How far back to query |
Output
{
data_points: [
{ label: "Mar 15", count: 3, date: "2025-03-15" },
{ label: "Mar 8 - Mar 14", count: 5, date: "2025-03-08" },
...
],
pace: "on_track", # "ahead" | "on_track" | "behind"
total_events: 42,
average_per_week: 3.5,
streak_data: [ # Only for habit goals
{ label: "Mar 15", count: 1, date: "2025-03-15" },
...
]
}Grouping Logic
- Day:
events.group_by { |e| e.created_at.to_date }→ one entry per day - Week: Group by ISO week → label as "Mar 15 - Mar 21"
- Month: Group by year-month → label as "Mar 2025"
Pace Calculation
- If
days_to_updateis nil → return"on_track" expected = lookback_days / days_to_updateratio = total_events / expected- Ratio ≥ 1.2 →
"ahead", 0.8–1.19 →"on_track", < 0.8 →"behind"
Streak Data (Habits Only)
- Queries
goal.habit_completionswithin the lookback period - Maps each to
{ label, count, date }where count is 1 (normal) or 0 (freeze used) - Ordered by date ascending
Composables
useProgressData
useProgressData(goalId: string) → {
period: Ref<string> // "day" | "week" | "month"
lookbackDays: Ref<number> // default 90
loading: Ref<boolean>
error: Ref<any>
dataPoints: ComputedRef<DataPoint[]>
pace: ComputedRef<string>
totalEvents: ComputedRef<number>
averagePerWeek: ComputedRef<number>
streakData: ComputedRef<DataPoint[]>
setPeriod: (period: string) => void
refetch: () => void
}- Fires
GOAL_PROGRESS_DATA_QUERYwith cache-and-network policy - Automatically refetches when period or lookbackDays change
There is no useProgressAnimation (or similar) composable — the Goal Detail page's progress percentage comes from useGoalProgress (see goals.md § Progress Tracking), not this composable.
Testing the Feature
Manual Testing Checklist
1. Progress Chart — Default View
- [ ] Navigate to a goal detail page with several logged events
- [ ] Verify ProgressChart section appears
- [ ] Verify bar chart renders with blue bars
- [ ] Verify "Week" period is selected by default
- [ ] Verify stats row shows Total, Avg/Week, and Pace
2. Progress Chart — Period Switching
- [ ] Tap Day → verify chart updates to daily granularity (up to 30 bars)
- [ ] Tap Month → verify chart updates to monthly granularity (up to 6 bars)
- [ ] Tap Week → verify return to weekly view (up to 13 bars)
- [ ] Verify labels change format: "Mar 15" (day), "Mar 15 - Mar 21" (week), "Mar 2025" (month)
3. Progress Chart — Tooltips
- [ ] Hover/tap on a bar → verify tooltip shows label and count
- [ ] Hover/tap on an empty bar → verify tooltip shows "0"
4. Progress Chart — Empty State
- [ ] Navigate to a goal with no events
- [ ] Verify "No progress data yet" empty state displays
- [ ] Verify no chart bars render
5. Progress Chart — Loading State
- [ ] On slow connection, verify skeleton pulse animation appears
- [ ] Verify chart renders after data loads
6. Pace Indicator
- [ ] For a goal with frequent updates: verify "Ahead" badge (emerald, TrendingUp)
- [ ] For a goal with moderate updates: verify "On Track" badge (blue, Minus)
- [ ] For a goal with sparse updates: verify "Behind" badge (amber, TrendingDown)
- [ ] For a goal with no
daysToUpdatesetting: verify "On Track" default
7. Streak Visualization
- [ ] Navigate to a habit goal with a streak
- [ ] Verify StreakVisualization shows two bars: Current and Longest
- [ ] Verify current streak bar uses orange-to-red gradient
- [ ] Verify longest streak bar uses purple-to-indigo gradient
- [ ] Verify bars are proportional (larger streak fills 100%)
- [ ] Verify labels show day counts
8. Streak Visualization — Non-habit Goals
- [ ] Navigate to a non-habit goal
- [ ] Verify StreakVisualization does NOT render
9. Goal Progress Card
- [ ] Navigate to a goal with 50% progress
- [ ] Verify the hero shows "50%"
- [ ] Verify the target due date displays in the
GoalDetailsCardright-rail card
10. Habit Calendar
- [ ] Navigate to a daily habit with check-in history
- [ ] Verify 30-day grid displays with 7 columns
- [ ] Verify completed days show in emerald (green)
- [ ] Verify streak-freeze days show in blue
- [ ] Verify missed days show in gray
- [ ] Verify today has a ring highlight
- [ ] Hover on a day → verify tooltip shows date and status
- [ ] Verify legend shows below the grid
11. Habit Completion Ring
- [ ] On the Full Dashboard's "Today's practice" widget, verify completion rings render per habit row
- [ ] Verify ring color: green (≥80%), amber (50–79%), red (<50%)
- [ ] Verify center text shows percentage
- [ ] Verify ring progresses proportionally
12. Momentum tiles (DashboardHero)
- [ ] Have 3+ active goals, navigate to Dashboard → verify the "honest" 3-tile variant renders: this week, day streak, active goals
- [ ] Have 1–2 active goals, navigate to Dashboard → verify the 4-tile variant renders instead: this week, completed, active, total progress
- [ ] Tap a non-streak tile (mobile viewport) → verify an explanation tooltip opens; tap again → verify it closes
- [ ] Tap the day streak tile → verify
StreakDetailsModalopens (not a tooltip) - [ ] With 0 active goals: verify neither variant renders (empty-state dashboard instead)
13. Shape of Your Week
- [ ] On Dashboard (Full Dashboard, active user state), verify the "Shape of your week" bar chart renders
- [ ] Verify 7 bars for Mon–Sun, with today visually distinguished
- [ ] Check in today → verify today's bar updates
- [ ] Verify the caption line reflects this week's activity
14. Today's Habit Check-In
- [ ] On Dashboard, verify due habits appear with check-in buttons (Focus Mode card or Full Dashboard "Today's practice" row)
- [ ] Tap check-in → verify loading spinner appears
- [ ] Verify button changes to green check after success
- [ ] Verify completion ring updates
- [ ] Verify streak count increments
- [ ] Tap again → verify button is disabled (no double check-in)
15. Animation Quality
- [ ] Verify progress bar animation is smooth (no jank)
- [ ] Verify streak bar transitions animate on data change
- [ ] Verify completion ring animates stroke on load
Backend Unit Tests
cd rails_api
# Interaction specs
bundle exec rspec spec/interactions/goal_tracking/calculate_goal_progress_spec.rb
# GraphQL specs
bundle exec rspec spec/interactions/goal_tracking/calculate_goal_progress_spec.rbFrontend Unit Tests
cd ionic_frontend
npm run test:unit -- --run -t "ProgressChart"
npm run test:unit -- --run -t "PaceIndicator"
npm run test:unit -- --run -t "StreakVisualization"
npm run test:unit -- --run -t "HabitCalendar"
npm run test:unit -- --run -t "HabitCompletionRing"
npm run test:unit -- --run -t "MomentumStat"
npm run test:unit -- --run -t "DashboardHero"
npm run test:unit -- --run -t "WeekShapeChart"
npm run test:unit -- --run -t "HabitCard"
npm run test:unit -- --run -t "useProgressData"GraphQL API Reference
Queries
query GoalProgressDataQuery($id: ID!, $period: String, $lookbackDays: Int) {
goalProgressData(id: $id, period: $period, lookbackDays: $lookbackDays) {
dataPoints {
label # "Mar 15", "Mar 15 - Mar 21", or "Mar 2025"
count # Number of events in this period
date # ISO date string for period start
}
pace # "ahead" | "on_track" | "behind"
totalEvents # Total count in lookback period
averagePerWeek # Calculated weekly average (Float)
streakData { # Habit goals only
label
count # 1 for normal completion, 0 for freeze
date
}
}
}Related Goal Fields
query GoalQuery($id: ID!) {
goal(id: $id) {
# Progress fields
currentAmount
targetAmount
# Habit visualization fields
habitStreak
longestHabitStreak
streakFreezesAvailable
streakFreezesUsed
checkedInToday
dueToday
completionRate
habitCompletions {
completedDate
streakFreezeUsed
}
}
}Known Behaviors & Edge Cases
Period-dependent data limits: The chart shows at most 30 data points (day), 13 (week), or 6 (month). Older data is truncated. The lookback window is 90 days by default.
Pace defaults to "on_track": If a goal has no
days_to_updatesetting (null), pace always returns "on_track" regardless of event frequency. This prevents misleading indicators on goals without an update cadence.Streak data is habit-only: The
streakDatafield in the response is empty for non-habit goals. The StreakVisualization component only renders whengoal.recurrenceTypeis set.Goal Progress Card retired:
GoalProgress.vue(and its dedicated mount-animation) is deleted (OBJ-3561) — the Goal Detail page's progress percentage now renders viaGoalHero.vue, sourced fromuseGoalProgress. See Goal Progress Card above.Bar height minimum: Empty periods still render a 2px bar for visual continuity. This prevents the chart from looking broken when data is sparse.
Completion ring color thresholds: The ring color changes at exactly 80% (green), 50% (amber), and below 50% (red). These thresholds are hardcoded in
HabitCompletionRing.Momentum tile variant selection:
DashboardHerorenders for any user with 1+ active goals — 1–2 goals get the 4-tile sparse variant, 3+ get the "honest" 3-tile hierarchy variant (see Momentum tiles above). It is not a 3+-goals-only component; only 0 active goals (or a first-run new-goal state) skips it entirely.Weekly check-ins calculation:
weeklyCheckInscounts distinct calendar days the user was active in the current ISO week (Monday–Sunday), capped at 7.Streak vs sign-in streak: The
DashboardHeroday streaktile shows the user's sign-in streak (consecutive days visiting the app). Habit streaks are per-goal and shown on individual goal detail pages. These are different numbers.Cache-and-network policy:
useProgressDatauses Apollo'scache-and-networkfetch policy — it shows cached data immediately while fetching fresh data in the background. This prevents chart flicker on navigation.Habit check-in idempotency: Tapping the check-in button twice on the same day is safe — the backend returns the existing completion without creating a duplicate (unique index on
[goal_id, completed_date]). The button becomes disabled (or the card advances) after the first successful check-in.
Last updated: 2026-09-09 (OBJ-3583: retargeted "Goal Progress Card" onto GoalHero.vue/GoalDetailsCard.vue and removed the dead GoalProgress.vue references — component deleted in OBJ-3561; corrected an inaccurate "days remaining"/description claim in the replacement text per review)