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>
This commit is contained in:
2026-02-10 22:44:43 -05:00
parent 41e1d452e2
commit b04d6d90fa
4 changed files with 497 additions and 11 deletions

View File

@@ -0,0 +1,114 @@
# 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`:
```python
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.