Files
SpendingAnalysis/docs/plans/2026-02-10-sankey-money-flow-design.md
Andy b04d6d90fa feat: add Sankey "Money Flow" chart to analysis tab
Three-column Sankey diagram (Income → Categories → Tags) rendered with
custom matplotlib bezier paths. Features gradient-fill bands that
interpolate color from source to destination nodes, neon glow effects
on nodes and bands, and a surplus node when income exceeds expenses.

Also fixes QFont::setPointSize warning by switching dark.qss font-size
units from px to pt, preventing Qt delegate code from receiving -1
point sizes during inline cell editing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 22:44:43 -05:00

4.4 KiB

Sankey "Money Flow" Chart — Design

Summary

Add a new "Money Flow" tab to the Analysis view that renders a three-column Sankey diagram showing how income flows through spending categories into needs/wants/savings tags. Rendered entirely with matplotlib using custom bezier path drawing — no new dependencies.

Data Structure

Three columns of nodes connected by curved bands:

Income ──► Categories ──► Tags
  • Left: Single "Income" node — sum of all positive-amount transactions in the filtered period.
  • Middle: One node per spending category — sized by total absolute spend. Plus a "Surplus" node if income exceeds total expenses.
  • Right: Needs, Wants, Savings, Untagged — sized by total absolute spend per tag.

Bands represent dollar amounts flowing between nodes. Thickness is proportional to amount. All values come from AnalysisService queries filtered by the standard _FilterBar (date range, account, person).

Visual Design

Theme Integration (Cyberpunk "Matrix 2026")

  • Background: VOID (#0a0a0f), matching all other charts
  • Axes styled via existing _style_ax() / _style_figure() helpers
  • Color palette: reuses CHART_COLORS, TAG_COLORS, NEON_GREEN, NEON_AMBER

Nodes

  • Thin vertical rectangles (width ~0.02 in axes coords)
  • Fill: node color at 60% opacity
  • Border: node color at full opacity, 1.5px
  • Glow: 2-3 progressively wider/more-transparent copies behind for neon bloom

Colors:

  • Income node: NEON_GREEN (#00ff41)
  • Category nodes: CHART_COLORS palette (same as existing charts)
  • Tag nodes: TAG_COLORS (needs=green, wants=magenta, savings=cyan)
  • Surplus node: NEON_AMBER (#ffb627)
  • Untagged node: TEXT_DIM (#4a5568)

Bands (Flows)

  • Smooth cubic bezier curves (matplotlib Path with CURVE4 codes)
  • Gradient fill: Each band sliced into ~50 vertical strips, color interpolated from source to destination node color
  • Glow effect: A wider, more-transparent copy of each band rendered behind it for neon bloom
  • Alpha: ~0.35 for main band, ~0.08-0.15 for glow passes
  • Sorting: Largest bands drawn first (back), smallest on top

Labels

  • Node name + dollar amount (e.g., "Groceries $1,234")
  • Left column labels: right-aligned, to the left of the node
  • Middle column labels: centered below/above nodes
  • Right column labels: left-aligned, to the right of the node
  • Font: TEXT color (#b8c4d4), fontsize 8
  • Dollar amounts: bold

Layout

  • Three columns at x = 0.1, 0.5, 0.9 (in axes normalized coords)
  • Nodes stacked vertically, sorted largest-on-top
  • Small gaps between nodes (~2% of total height)
  • Axes limits: x=[0, 1], y=[0, 1], all axes/spines hidden

Implementation

New Class: _MoneyFlowTab(QWidget)

Follows the same pattern as _SpendingOverTimeTab, _CategoryBreakdownTab, and _ForecastingTab:

class _MoneyFlowTab(QWidget):
    def __init__(self, session, parent=None):
        # _FilterBar, Figure, FigureCanvasQTAgg, NavigationToolbar2QT
        # Connect filter signals to refresh()

    def refresh(self):
        # 1. Query income total, category totals, tag totals
        # 2. Build node positions and sizes
        # 3. Draw nodes, bands, labels via _draw_sankey()

Helper: _draw_sankey(ax, income, categories, tags)

Separated rendering logic for testability and clarity:

  1. Calculate node heights proportional to amounts
  2. Position nodes in three columns with vertical gaps
  3. For each flow (income→category, category→tag): draw gradient bezier bands with glow
  4. Draw node rectangles with glow
  5. Draw labels

Data Queries

Reuses existing AnalysisService methods:

  • spending_by_category() — for category totals
  • spending_by_tag() — for tag totals
  • Direct query for income total (sum of positive amounts in filtered period)
  • Cross-query: category-to-tag amounts (new query needed — group by category_id + tag)

Tab Placement

4th tab in AnalysisView._tabs: "Money Flow" — inserted after "Category Breakdown", before "Forecasting".

Files Changed

  • src/ui/analysis_view.py — add _MoneyFlowTab class and register in AnalysisView.__init__
  • src/services/analysis.py — add spending_by_category_and_tag() method for the category→tag cross-query

No New Dependencies

Everything uses matplotlib's existing PathPatch, Path, Rectangle, and color utilities — all already available via the matplotlib>=3.8 dependency.