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>
115 lines
4.4 KiB
Markdown
115 lines
4.4 KiB
Markdown
# 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.
|