---
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 componentsrc/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 `cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run buildExpenseDonutChart renders a donut with center total label, active sector expansion on hover, custom legend below, CSS variable fills, and handles empty/zero-amount states. Build passes.Task 2: Create IncomeBarChart and SpendBarChart componentssrc/components/dashboard/charts/IncomeBarChart.tsx, src/components/dashboard/charts/SpendBarChart.tsx
**IncomeBarChart** (`src/components/dashboard/charts/IncomeBarChart.tsx`):
Props interface:
```typescript
interface IncomeBarChartProps {
data: Array<{ label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
```
- If `data.length === 0`, render `ChartEmptyState` with `emptyMessage`
- ChartConfig: `{ budgeted: { label: "Budgeted" (from props or hardcode), color: "var(--color-budget-bar-bg)" }, actual: { label: "Actual", color: "var(--color-income-fill)" } } satisfies ChartConfig`
- Wrap in `ChartContainer` with `className="min-h-[250px] w-full"`
- Use `BarChart` (vertical, which is the default -- no `layout` prop needed)
- `` for horizontal grid lines only
- `` for category labels
- `` for amount axis
- Two `` components (NOT stacked -- no `stackId`): `` and `` with `` per entry: if `entry.actual > entry.budgeted`, use `fill="var(--color-over-budget)"`, otherwise use `fill="var(--color-income-fill)"` (per user decision: actual bars vivid, over-budget bars red)
- Tooltip: ` formatCurrency(Number(value), currency)} />} />`
- ChartLegend: `} />` for budgeted/actual legend
**SpendBarChart** (`src/components/dashboard/charts/SpendBarChart.tsx`):
Props interface:
```typescript
interface SpendBarChartProps {
data: Array<{ type: string; label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
```
- If `data.length === 0`, render `ChartEmptyState` with `emptyMessage`
- ChartConfig: `{ budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" }, actual: { label: "Actual", color: "var(--color-muted-foreground)" } } satisfies ChartConfig` (base color for actual; overridden per-cell)
- Wrap in `ChartContainer` with `className="min-h-[250px] w-full"`
- **CRITICAL: Horizontal bars via `layout="vertical"`** on `` (per Research Pattern 3 and Pitfall 2)
- `` -- only vertical grid lines for horizontal bar layout
- `` (number axis, hidden)
- `` (category labels on Y axis)
- Two `` components (NOT stacked): `` and `` with `` per entry: if `entry.actual > entry.budgeted`, fill is `"var(--color-over-budget)"` (red accent per user decision), otherwise fill is `var(--color-${entry.type}-fill)` (vivid category color)
- Tooltip and Legend same pattern as IncomeBarChart
- The actual bar naturally extending past the budgeted bar IS the over-budget visual indicator (per Research Pattern 5)
**Both components:** Follow project import conventions. Use named exports. Accept `t()` translations via props or use i18n keys in config labels.
cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run buildIncomeBarChart renders grouped vertical bars (budgeted muted, actual vivid) with over-budget red fill. SpendBarChart renders horizontal bars via layout="vertical" with per-cell over-budget coloring. Both handle empty data with ChartEmptyState. Build passes.
- `bun run build` passes with no type errors
- All three chart files exist in `src/components/dashboard/charts/`
- Each chart uses `ChartContainer` as its outer wrapper (not raw `ResponsiveContainer`)
- No hardcoded hex color values -- all colors via CSS variables
- Each chart handles empty data gracefully (ChartEmptyState or neutral ring)
- ExpenseDonutChart has center label with formatted currency, active hover, and legend
- IncomeBarChart has two grouped (not stacked) bars
- SpendBarChart uses `layout="vertical"` with swapped axis types
- ExpenseDonutChart renders donut with center total, hover expansion, and custom legend using CSS variable fills
- IncomeBarChart renders grouped vertical bars comparing budgeted (muted) vs actual (vivid) for income
- SpendBarChart renders horizontal bars comparing budget vs actual by category type with over-budget red accent
- All charts handle zero-data and empty states
- `bun run build` passes