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>
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_COLORSpalette (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
PathwithCURVE4codes) - 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:
TEXTcolor (#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:
- Calculate node heights proportional to amounts
- Position nodes in three columns with vertical gaps
- For each flow (income→category, category→tag): draw gradient bezier bands with glow
- Draw node rectangles with glow
- Draw labels
Data Queries
Reuses existing AnalysisService methods:
spending_by_category()— for category totalsspending_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_MoneyFlowTabclass and register inAnalysisView.__init__src/services/analysis.py— addspending_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.