--- phase: 02-dashboard-charts-and-layout plan: 02 type: execute wave: 1 depends_on: [] files_modified: - src/components/dashboard/charts/ExpenseDonutChart.tsx - src/components/dashboard/charts/IncomeBarChart.tsx - src/components/dashboard/charts/SpendBarChart.tsx autonomous: true requirements: [UI-DONUT-01, UI-BAR-01, UI-HBAR-01] must_haves: truths: - "Donut chart renders expense data by category type with center total label and active sector hover expansion" - "Donut chart shows custom legend with category colors and formatted amounts" - "Donut chart shows neutral empty ring with $0 center when all actuals are zero" - "Vertical bar chart renders grouped budgeted vs actual bars for income with muted/vivid color distinction" - "Horizontal bar chart renders budget vs actual spending by category type with over-budget red accent" - "All three charts consume CSS variable tokens via ChartConfig -- no hardcoded hex values" - "All three charts handle empty data by rendering ChartEmptyState placeholder" artifacts: - path: "src/components/dashboard/charts/ExpenseDonutChart.tsx" provides: "Donut pie chart for expense breakdown" exports: ["ExpenseDonutChart"] min_lines: 60 - path: "src/components/dashboard/charts/IncomeBarChart.tsx" provides: "Vertical grouped bar chart for income budget vs actual" exports: ["IncomeBarChart"] min_lines: 40 - path: "src/components/dashboard/charts/SpendBarChart.tsx" provides: "Horizontal bar chart for category spend budget vs actual" exports: ["SpendBarChart"] min_lines: 40 key_links: - from: "src/components/dashboard/charts/ExpenseDonutChart.tsx" to: "@/components/ui/chart" via: "ChartContainer + ChartConfig" pattern: "ChartContainer.*config" - from: "src/components/dashboard/charts/IncomeBarChart.tsx" to: "@/components/ui/chart" via: "ChartContainer + ChartConfig" pattern: "ChartContainer.*config" - from: "src/components/dashboard/charts/SpendBarChart.tsx" to: "@/components/ui/chart" via: "ChartContainer + ChartConfig" pattern: "ChartContainer.*config" - from: "src/components/dashboard/charts/ExpenseDonutChart.tsx" to: "@/lib/format" via: "formatCurrency for center label and legend" pattern: "formatCurrency" --- Build the three chart components -- ExpenseDonutChart, IncomeBarChart, and SpendBarChart -- as isolated presentational components. Purpose: These are the core visual deliverables of Phase 2. Each chart is self-contained, receives pre-computed data as props, uses ChartContainer/ChartConfig from shadcn for CSS-variable-driven color theming, and handles its own empty state. Plan 03 will wire them into the dashboard layout. Output: Three chart component files in `src/components/dashboard/charts/`. @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-dashboard-charts-and-layout/02-CONTEXT.md @.planning/phases/02-dashboard-charts-and-layout/02-RESEARCH.md From src/components/ui/chart.tsx: ```typescript export type ChartConfig = { [k in string]: { label?: React.ReactNode; icon?: React.ComponentType } & ({ color?: string; theme?: never } | { color?: never; theme: Record<"light" | "dark", string> }) } export function ChartContainer({ config, className, children, ...props }: { config: ChartConfig; children: ReactNode } & ComponentProps<"div">): JSX.Element export const ChartTooltip: typeof RechartsPrimitive.Tooltip export function ChartTooltipContent({ nameKey, ...props }: TooltipProps & { nameKey?: string }): JSX.Element export const ChartLegend: typeof RechartsPrimitive.Legend export function ChartLegendContent({ nameKey, payload, verticalAlign, ...props }: LegendProps & { nameKey?: string }): JSX.Element ``` From src/lib/types.ts: ```typescript export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment" ``` From src/lib/palette.ts: ```typescript export const categoryColors: Record // Values: "var(--color-income)", "var(--color-bill)", etc. ``` From src/lib/format.ts: ```typescript export function formatCurrency(amount: number, currency?: string, locale?: string): string ``` CSS tokens available in index.css @theme: ```css --color-income-fill: oklch(0.68 0.19 155); --color-bill-fill: oklch(0.65 0.19 25); --color-variable-expense-fill: oklch(0.70 0.18 50); --color-debt-fill: oklch(0.60 0.20 355); --color-saving-fill: oklch(0.68 0.18 220); --color-investment-fill: oklch(0.65 0.18 285); --color-over-budget: oklch(0.55 0.20 25); --color-on-budget: oklch(0.50 0.17 155); --color-budget-bar-bg: oklch(0.92 0.01 260); ``` From src/pages/DashboardPage.tsx (existing data patterns): ```typescript const EXPENSE_TYPES: CategoryType[] = ["bill", "variable_expense", "debt", "saving", "investment"] // pieData shape: { name: string, value: number, type: CategoryType }[] // totalExpenses: number ``` Task 1: Create ExpenseDonutChart component src/components/dashboard/charts/ExpenseDonutChart.tsx Create `src/components/dashboard/charts/ExpenseDonutChart.tsx`. If the `charts/` directory was not created by Plan 01 yet (parallel wave), create it. **Props interface:** ```typescript interface ExpenseDonutChartProps { data: Array<{ type: string; value: number; label: string }> totalExpenses: number currency: string emptyMessage: string // i18n-driven, passed from parent } ``` **ChartConfig:** Build config from data entries, mapping each `type` to its fill color: ```typescript const chartConfig = useMemo(() => { const config: ChartConfig = {} for (const entry of data) { config[entry.type] = { label: entry.label, color: `var(--color-${entry.type}-fill)`, } } return config }, [data]) ``` **Empty/Zero states:** - If `data.length === 0` and `totalExpenses === 0`: check if this is a "no items at all" case. Render `ChartEmptyState` with `emptyMessage` prop. Import from `./ChartEmptyState`. - If data is empty but there may be items with zero amounts: the parent will pass an `allZero` indicator (or the totalExpenses will be 0). When totalExpenses is 0 but the component is rendered, show a single neutral sector (full ring) in `var(--color-muted)` with `$0` center label. Use a synthetic data point: `[{ type: "empty", value: 1, label: "" }]` with `fill="var(--color-muted)"`. **Donut rendering:** - Wrap in `ChartContainer` with `config={chartConfig}` and `className="min-h-[250px] w-full"` - Use `PieChart` > `Pie` with `dataKey="value"` `nameKey="type"` `innerRadius={60}` `outerRadius={85}` `cx="50%"` `cy="50%"` - Active sector hover: maintain `activeIndex` state with `useState(-1)`. Set `activeShape` to a render function that draws a `Sector` with `outerRadius + 8` (expanded). Wire `onMouseEnter={(_, index) => setActiveIndex(index)}` and `onMouseLeave={() => setActiveIndex(-1)}` per Research Pattern 2. - Cell coloring: map data entries to `` - Center label: use `