Files

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-dashboard-charts-and-layout 02 execute 1
src/components/dashboard/charts/ExpenseDonutChart.tsx
src/components/dashboard/charts/IncomeBarChart.tsx
src/components/dashboard/charts/SpendBarChart.tsx
true
UI-DONUT-01
UI-BAR-01
UI-HBAR-01
truths artifacts key_links
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
path provides exports min_lines
src/components/dashboard/charts/ExpenseDonutChart.tsx Donut pie chart for expense breakdown
ExpenseDonutChart
60
path provides exports min_lines
src/components/dashboard/charts/IncomeBarChart.tsx Vertical grouped bar chart for income budget vs actual
IncomeBarChart
40
path provides exports min_lines
src/components/dashboard/charts/SpendBarChart.tsx Horizontal bar chart for category spend budget vs actual
SpendBarChart
40
from to via pattern
src/components/dashboard/charts/ExpenseDonutChart.tsx @/components/ui/chart ChartContainer + ChartConfig ChartContainer.*config
from to via pattern
src/components/dashboard/charts/IncomeBarChart.tsx @/components/ui/chart ChartContainer + ChartConfig ChartContainer.*config
from to via pattern
src/components/dashboard/charts/SpendBarChart.tsx @/components/ui/chart ChartContainer + ChartConfig ChartContainer.*config
from to via pattern
src/components/dashboard/charts/ExpenseDonutChart.tsx @/lib/format formatCurrency for center label and legend 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/.

<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

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:

export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"

From src/lib/palette.ts:

export const categoryColors: Record<CategoryType, string>
// Values: "var(--color-income)", "var(--color-bill)", etc.

From src/lib/format.ts:

export function formatCurrency(amount: number, currency?: string, locale?: string): string

CSS tokens available in index.css @theme:

--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):

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:

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:

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 <Cell key={entry.type} fill={var(--color-${entry.type}-fill)} />
  • Center label: use <Label> inside <Pie> with content function that checks viewBox && "cx" in viewBox && "cy" in viewBox (per Pitfall 4), then renders <text> with textAnchor="middle" dominantBaseline="middle" and a <tspan className="fill-foreground text-xl font-bold"> showing formatCurrency(totalExpenses, currency). Center label shows total expense amount only (per user decision -- no label text).

Custom legend: Below the chart, render a <ul> with legend items (following the existing pattern from DashboardPage.tsx lines 168-182). Each item shows a color dot (using var(--color-${entry.type}-fill) as background), the label text, and the formatted amount right-aligned. Use the shadcn ChartLegend + ChartLegendContent if it works well with the pie chart nameKey, otherwise use the manual ul-based legend matching the existing codebase pattern. Place legend below the donut (per research recommendation for tight 3-column layout).

Tooltip: Use <ChartTooltip content={<ChartTooltipContent nameKey="type" formatter={(value) => formatCurrency(Number(value), currency)} />} /> for formatted currency tooltips.

Import order: Follow conventions -- React first, then recharts, then @/ imports. cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build ExpenseDonutChart 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 components src/components/dashboard/charts/IncomeBarChart.tsx, src/components/dashboard/charts/SpendBarChart.tsx **IncomeBarChart** (`src/components/dashboard/charts/IncomeBarChart.tsx`):

Props interface:

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)
  • <CartesianGrid vertical={false} /> for horizontal grid lines only
  • <XAxis dataKey="label" tick={{ fontSize: 12 }} /> for category labels
  • <YAxis tick={{ fontSize: 12 }} /> for amount axis
  • Two <Bar> components (NOT stacked -- no stackId): <Bar dataKey="budgeted" fill="var(--color-budgeted)" radius={[4, 4, 0, 0]} /> and <Bar dataKey="actual" radius={[4, 4, 0, 0]}> with <Cell> 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: <ChartTooltip content={<ChartTooltipContent formatter={(value) => formatCurrency(Number(value), currency)} />} />
  • ChartLegend: <ChartLegend content={<ChartLegendContent />} /> for budgeted/actual legend

SpendBarChart (src/components/dashboard/charts/SpendBarChart.tsx):

Props interface:

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 <BarChart> (per Research Pattern 3 and Pitfall 2)
  • <CartesianGrid horizontal={false} /> -- only vertical grid lines for horizontal bar layout
  • <XAxis type="number" hide /> (number axis, hidden)
  • <YAxis type="category" dataKey="label" width={120} tick={{ fontSize: 12 }} /> (category labels on Y axis)
  • Two <Bar> components (NOT stacked): <Bar dataKey="budgeted" fill="var(--color-budgeted)" radius={4} /> and <Bar dataKey="actual" radius={4}> with <Cell> 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 build IncomeBarChart 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/02-dashboard-charts-and-layout/02-02-SUMMARY.md`