Skip to content

Refactor to make "time" a first-class grouping/subgrouping variable#27

Merged
wesm merged 23 commits intomainfrom
first-class-time-grouping
Nov 5, 2025
Merged

Refactor to make "time" a first-class grouping/subgrouping variable#27
wesm merged 23 commits intomainfrom
first-class-time-grouping

Conversation

@wesm
Copy link
Owner

@wesm wesm commented Nov 5, 2025

This change allows you to treat time (year or month) as a first-class grouping characteristic, allowing you to answer such questions as "how much did I spend at Starbucks on my Chase Sapphire card by Month"? Previously, time was handled as a top-level filter and so you would use the arrow to navigate from month to month. This change makes it a first-class citizen that you can drill down into and group by.

image

wesm and others added 23 commits November 3, 2025 21:49
Add TIME as a first-class aggregate dimension alongside Merchant/Category/
Group/Account. This is Phase 1 of transforming time from a filter into a
top-level aggregate that can be drilled into and sub-grouped by.

Changes:
- Add ViewMode.TIME enum value
- Add TimeGranularity enum (YEAR, MONTH)
- Add SortMode.TIME_PERIOD for chronological sorting
- Add state fields: time_granularity, selected_time_year, selected_time_month
- Add time helper methods: is_time_period_selected(), clear_time_selection(),
  toggle_time_granularity()
- Update cycle_grouping(): Merchant → Category → Group → Account → TIME → Merchant
- Update cycle_sub_grouping(): Include TIME in sub-grouping cycle
- Update is_drilled_down() to include time selections
- Update clear_drill_down_and_selection() to clear time selections
- Update NavigationState to save time_granularity
- Update 3 tests to account for TIME in sub-grouping cycle

All existing tests pass (29 sub-grouping tests).

Next phases: time aggregation, drill-down, UI integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add navigate_time_period() method to enable arrow key navigation when drilled
into a specific time period. This method handles both year and month navigation
with proper wrapping around year boundaries.

Changes:
- Add navigate_time_period() method in state.py
  - Navigates forward/backward through years or months
  - Handles Dec → Jan year wrapping
  - Returns formatted period description ("2024", "Mar 2024")
  - Returns None when not drilled into time period

- Add 15 comprehensive tests in TestTimeNavigation class:
  - Time period selection checking
  - Granularity toggling
  - Year navigation (forward/backward)
  - Month navigation (forward/backward)
  - Month wrapping around year boundaries (Dec ↔ Jan)
  - Edge cases and null handling

All 15 new tests pass.

Next: Phase 2 - time aggregation in data layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add time-based aggregation with year/month granularity and gap filling
to ensure continuous time series display.

Changes:
- Add aggregate_by_time() in data_manager.py
  - Groups transactions by year or month
  - Excludes hidden transactions from totals (consistent with other aggregations)
  - Returns DataFrame with time_period_display, year, month, count, total

- Add _fill_time_gaps() helper method
  - Fills gaps between earliest and latest period with zeros
  - For years: fills missing years (e.g., 2023, 2025 → fills 2024)
  - For months: fills only between actual min/max months (Jan, Mar → fills Feb only)
  - Ensures continuous time series for better visualization

- Add _generate_all_months() helper
  - Generates all months in a year range for gap filling

- Add format_time_period() in formatters.py
  - Formats year: "2024"
  - Formats month: "Mar 2024"
  - Static method for consistency with existing formatters

- Add 6 comprehensive tests in TestTimeAggregation class:
  - Basic year/month aggregation
  - Gap filling for years and months
  - Empty DataFrame handling
  - Hidden transaction exclusion from totals

All tests pass (6 new tests, 905 total).

Next: Phase 3 - wire up TIME view in controller.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Integrate TIME view into controller with full aggregation, sorting, and
display support. TIME view now shows in the g-cycle and displays a table
of time periods with count and total columns.

Changes in app_controller.py:
- Add switch_to_time_view() method
- Add _prepare_time_aggregate_view() to aggregate and prepare TIME view data
- Add _prepare_time_view_data() for TIME-specific table formatting
- Update refresh_view() to handle ViewMode.TIME
- Update is_aggregate_view check to include TIME
- Add TIME support in sub-grouping (Merchants > Amazon > by Year)
- Update get_next_sort_field() for TIME view: Period → Count → Amount → Period
- Update _get_action_hints() to show y/t granularity toggle in TIME view
- Add TIME to switch_to_detail_view navigation history check

Changes in data_manager.py:
- Add type assertions for year min/max to satisfy pyright

Changes in formatters.py:
- Add format_time_period() static method for display formatting

Changes in state.py:
- Add type assertion in navigate_time_period() for type safety

Features:
- TIME view displays: Period | Count | Total (no Top Category)
- Sort by time period (chronological), count, or amount
- Granularity toggle hint shows in status bar (y/t keys)
- Sub-grouping by time works (e.g., Merchants > Amazon > by Year)

All 128 tests pass (122 existing + 6 time aggregation tests).

Next: Phase 4 - drill-down into time periods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Enable drilling into time periods from TIME view, with full breadcrumb
navigation and Escape key support for going back through time hierarchy.

Changes in state.py:
- Update drill_down() to handle TIME view
  - Parse year: "2024" → selected_time_year = 2024
  - Parse month: "Mar 2024" → selected_time_year = 2024, selected_time_month = 3
  - Supports multi-level: Time > 2024 > Merchants > Amazon

- Update get_filtered_df() to apply time period filters
  - Filter by selected_time_year
  - Filter by selected_time_month (if set)
  - Combines with other dimension filters

- Update go_back() to restore time selections from history
  - Restores selected_time_year, selected_time_month, time_granularity
  - Fallback clears time last (month before year)

- Update get_breadcrumb() to show time in navigation path
  - TIME view: "Years" or "Months"
  - Time drill-down: "Time > 2024" or "Time > Mar 2024"
  - Multi-level: "Time > 2024 > Merchants > Amazon"
  - Time sub-grouping: "Merchants > Amazon (by Year)"
  - Remove old time_frame filter indicator (replaced by drill-down)

- Update NavigationState to save time fields
  - Add selected_time_year, selected_time_month, time_granularity
  - Ensures proper restoration when pressing Escape

Changes in app_controller.py:
- Update switch_to_detail_view() to include TIME in history check

Navigation flow examples:
- Time > 2024 → Esc → Years (aggregate view)
- Time > Mar 2024 → Esc → Months (aggregate view)
- Time > 2024 > Merchants > Amazon → Esc → Time > 2024 > Merchants
- Merchants > Amazon (by Year) → Drill "2024" → Time > 2024 > Merchants > Amazon

All type checks pass (0 errors).

Next: Phase 5 - keyboard bindings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Wire up keyboard shortcuts for TIME view granularity toggling, time period
clearing, and arrow key navigation. Update all failing tests to match new
breadcrumb behavior (time only shown when drilled in, not as filter).

Changes in app.py:
- Update y/t/a key bindings to new actions
  - y: toggle_year_granularity (only in TIME view)
  - t: toggle_month_granularity (only in TIME view)
  - a: clear_time_period (clear time drill-down, like Escape shortcut)

- Add action handlers:
  - action_toggle_year_granularity() - switches to year view
  - action_toggle_month_granularity() - switches to month view
  - action_clear_time_period() - clears time selection

- Update arrow key handlers:
  - action_prev_period() - navigate backward when drilled into time
  - action_next_period() - navigate forward when drilled into time
  - Do nothing when not in time drill-down (simplified from old behavior)

- Add TimeGranularity import

Changes in app_controller.py:
- Add toggle_time_granularity() method
- Update cycle_grouping() docstring

Changes in keybindings.py:
- Update g key description to include TIME
- Update y/t/a keys for new granularity behavior
- Update arrow key descriptions

Test fixes in test_state.py (7 tests):
- Update 6 breadcrumb tests to confirm timeframe NOT shown
  - test_breadcrumb_with_this_year_timeframe
  - test_breadcrumb_with_this_month_timeframe
  - test_breadcrumb_with_custom_single_month
  - test_breadcrumb_with_custom_date_range
  - test_breadcrumb_shows_sub_grouping
  - test_breadcrumb_multi_level_drill_down

Test fixes in test_app_controller.py (1 test):
- Update test_cycle_grouping_account_to_merchant_field_sort
  - Now: ACCOUNT → TIME → MERCHANT (includes TIME in cycle)

All 920 tests pass.

Next: Phase 6 - documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update navigation guide to document the new TIME dimension, granularity
toggling, time drill-down, and time-based analysis workflows.

Changes in docs/guide/navigation.md:
- Update cycle order: Merchant → Category → Group → Account → TIME → Merchant
- Add TIME to aggregate views table
- Add "Time as an Aggregate Dimension" section
  - Explains TIME view concept
  - Documents y/t granularity toggle
  - Shows drill-down into years/months

- Add "Time + Other Dimensions" section
  - Time-first analysis (Time > 2024 > Merchants)
  - Dimension-first with time sub-grouping (Merchants > Amazon > by Year)

- Update "Navigate Between Time Periods" section
  - Arrow keys navigate when drilled into time
  - 'a' key clears time selection

- Add time-based use cases:
  - "How has my spending changed over time?"
  - "How has my Amazon spending trended?"

- Update Quick Reference table
  - y/t keys toggle granularity
  - a key clears time period
  - Arrow keys navigate time periods

- Remove old time filter documentation
  - TIME dimension replaces old filter-based time navigation
  - Command-line args (--year, --month) now documented as data loading shortcuts

All markdown formatting checks pass.
All 920 tests pass.

Feature complete! TIME is now a first-class aggregate dimension.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fix two critical bugs discovered during manual testing:

1. Drill-down from TIME view didn't work
   - on_data_table_row_selected() checked for MERCHANT/CATEGORY/GROUP/ACCOUNT
   - Missing ViewMode.TIME in the check
   - Pressing Enter on a year/month row had no effect
   - Fixed: Added TIME to view mode check in app.py:1767

2. Sub-grouping by time crashed with ColumnNotFoundError
   - Set field_name = "time_period" but column is "time_period_display"
   - Sort tried to use non-existent "time_period" column
   - Fixed: Use "time_period_display" as field_name in app_controller.py:251
   - Also handle sort_col mapping for "time_period" → "time_period_display"

Both features now work correctly:
- Can drill into years/months from TIME view (Time > 2024 works)
- Can sub-group by time (Merchants > Amazon > by Year works)

All 920 tests pass.
All code quality checks pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Multi-year demo data:
- Update DemoDataGenerator to support generating data across multiple years
- Change API from year parameter to start_year and years parameters
- Default to 3 years of data (2023-2025) to enable multi-year TIME views
- Update all transaction generator methods to accept year parameter
- Update DemoBackend and tests to use new API

TIME drill-down fixes:
- Fix sort field mapping when drilling down from TIME view to details
  (map time_period to date for transaction sorting)
- Add time_period_display to AggregationField type and formatters
- Support TIME sub-grouping within merchant/category drill-downs

All 922 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Use start_year and years parameters instead of deprecated year parameter.
Default to 3 years of data (2023-2025) in demo mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add date range display to TIME view breadcrumb for clarity when viewing
time-based aggregations without drill-down.

Changes:
- Add current_data_start_date and current_data_end_date to AppState
- Compute and store date range in refresh_view() from filtered transactions
- Update breadcrumb to show range in ISO-8601 format when available
  Example: "Months (2023-01-01 to 2025-12-31)"

All 922 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Demo mode now generates 3 years of data (2023-2025) but was being filtered
to only show THIS_YEAR (2025) by default. This change sets the time_frame
to ALL_TIME in demo mode so all generated data is visible.

This enables users to immediately explore multi-year TIME views and
drill-downs without needing to manually change the time filter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the TimeFrame enum (ALL_TIME, THIS_YEAR, THIS_MONTH, CUSTOM) in favor
of using TIME view as the primary way to navigate time periods. This simplifies
the codebase and removes conceptual overlap between timeframe filtering and
TIME drill-down.

Changes:
- Remove TimeFrame enum from state.py
- Remove time_frame field from AppState
- Remove set_timeframe() method and related facade methods
- Update time navigation methods to directly set start_date/end_date
- Remove 17 obsolete tests for timeframe-specific functionality
- Simplify breadcrumb generation and state save/restore

Time filtering now works through:
1. Direct date range (start_date, end_date)
2. TIME view for aggregating by year/month
3. TIME drill-down for viewing specific periods

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…down

Display the date range in breadcrumbs for all aggregate views (Merchants,
Categories, Groups, Accounts, and TIME) when there's no active time drill-down.
This provides clarity about what time period is being aggregated across all views.

Example breadcrumbs:
- "Merchants (2023-01-01 to 2025-12-31)"
- "Categories (2023-01-01 to 2025-12-31)"
- "Years (2023-01-01 to 2025-12-31)"

The date range only hides when drilling down into a specific time period
(e.g., "Time > 2024 > Jan 2024").

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The _initialize_view() method was hardcoded to filter to the current year,
which prevented demo mode from showing the full 3 years of generated data.

Now demo mode skips the date filter initialization (start_date and end_date
remain None), showing all transactions from 2023-2025. Real backends still
default to current year filtering for performance.

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The TIME view was showing just the currency symbol (e.g., "$") as the
column header instead of "Total ($)" like other aggregate views. This
change makes the TIME view column header consistent with the standard
format used in Merchants, Categories, Groups, and Accounts views.

Before: Period | Count | $
After:  Period | Count | Total ($)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove default date filtering from _initialize_view() so all backends
(Amazon, Monarch, YNAB, Demo) show all their data when first loaded.

Key points:
- The --year and --since flags control API FETCHING (to reduce data load
  from Monarch/YNAB), not view filtering
- Amazon mode never needs time filtering since data is local
- All modes now show complete dataset on startup
- Users can drill into specific time periods using TIME view
- The 'a' key clears time drill-down (already implemented)

This fixes the issue where Amazon mode was showing only 2025 data.

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
README.md updates:
- Document TIME as 5th aggregation dimension alongside Merchants/Categories/Groups/Accounts
- Update "Analyze spending" workflow to include TIME view navigation
- Clarify that --year/--since only control API fetching, not view filtering
- Update keyboard shortcuts: t/y toggle granularity, 'a' clears time drill-down
- Emphasize ability to combine dimensions (sub-group by Time within other drill-downs)

Screenshot generation script updates:
- Add 4 new TIME view screenshots:
  * cycle-5-time-years: TIME view aggregated by years
  * time-view-months: TIME view aggregated by months
  * time-drill-down-year: Drilled into specific year
  * time-drill-down-month: Drilled into specific month
- Update all DemoBackend() calls to use new API (start_year, years parameters)
- All screenshots now use 3 years of demo data (2023-2025)

All markdown formatting validated with markdownlint.
All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When cycling through views with 'g', now resets sort to sensible defaults:
- TIME view: Always TIME_PERIOD ASC (chronological order)
- Leaving TIME → other views: Always AMOUNT DESC (highest spending first)
- Between non-TIME views: Preserve COUNT/AMOUNT or update field-based sorts

This fixes the issue where cycling TIME → MERCHANT would incorrectly
sort by MERCHANT instead of AMOUNT.

Changes:
- moneyflow/state.py: Update cycle_grouping() to reset sort when entering/leaving TIME
- tests/test_app_controller.py: Update test to expect AMOUNT DESC after leaving TIME

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Major updates across 3 documentation files to reflect TIME as first-class
dimension and removal of TimeFrame concept:

navigation.md:
- Add TIME view screenshots (by years and by months)
- Add TIME drill-down screenshots (year and month)
- Update sub-grouping cycle to include Time (Category → Group → Account → Time → Detail)
- Correct command-line flags: --year/--since only control API fetching, not filtering
- Add note explaining API fetching vs view filtering distinction

keyboard-shortcuts.md:
- Update view cycle to include Time (Merchant → ... → Time → Merchant)
- Replace old time filter keys with new TIME view controls:
  * y = toggle to year granularity (not "current year")
  * t = toggle to month granularity (not "current month")
  * a = clear time drill-down (not "all time")
- Update "Monthly spending review" workflow to use TIME view
- Fix incorrect use of ++u++ key (changed to ++d++ for detail view)
- Update cheat sheet to reflect new TIME behavior

quickstart.md:
- Update demo mode description: ~3,000 transactions across 3 years (2023-2025)
- Add TIME to view cycle list
- Add TIME view controls (y/t toggle granularity)
- Clarify --year flag controls API fetching, not view filtering
- Add note that all fetched data is visible by default

All markdown formatting validated with markdownlint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Treat TIME as a standard dimension that was always part of the system,
not as a new feature. Removed "includes TIME" callouts and special
emphasis to make documentation read naturally.

Changes:
- README.md: "Multiple aggregation dimensions" instead of "5 dimensions"
- README.md: Remove "Time dimension" bullet point (already listed in dimensions)
- quickstart.md: Remove "including TIME dimension" callout
- quickstart.md: Remove bold emphasis on **Time**

All markdown formatting validated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The y/t keys to toggle year/month granularity were only working in the
top-level TIME view, not when sub-grouping by Time within a drill-down
(e.g., "Merchants > Amazon (by Year)").

Fix: Check for both TIME view AND time sub-grouping mode when determining
if granularity toggle should be enabled.

Now works in both contexts:
- Top-level TIME view (view_mode == TIME)
- Sub-grouping by time (sub_grouping_mode == TIME)

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When sub-grouping by Time with month granularity, periods were displaying
as just the year (e.g., "2024") instead of the formatted month + year
(e.g., "Mar 2024").

Root cause: format_aggregation_rows() was using the raw time_period_display
column value without formatting it. The time_period_display column contains
"2024" or "2024-03" for sorting, but needs to be formatted for display.

Fix: Added special handling in format_aggregation_rows() to detect when
group_by_field is "time_period_display" and format it using the year and
month values with format_time_period().

Now correctly displays:
- Year granularity: "2023", "2024", "2025"
- Month granularity: "Jan 2023", "Feb 2023", "Mar 2023", etc.

Works in both top-level TIME view and time sub-grouping within drill-downs.

All 905 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link

Deploying moneyflow with  Cloudflare Pages  Cloudflare Pages

Latest commit: f0ec6aa
Status: ✅  Deploy successful!
Preview URL: https://29d3d3ac.moneyflow-6wi.pages.dev
Branch Preview URL: https://first-class-time-grouping.moneyflow-6wi.pages.dev

View logs

@wesm wesm merged commit 0914309 into main Nov 5, 2025
7 checks passed
@wesm wesm deleted the first-class-time-grouping branch November 5, 2025 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant