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 |
|
true |
|
|
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.mdFrom 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
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 === 0andtotalExpenses === 0: check if this is a "no items at all" case. RenderChartEmptyStatewithemptyMessageprop. Import from./ChartEmptyState. - If data is empty but there may be items with zero amounts: the parent will pass an
allZeroindicator (or the totalExpenses will be 0). When totalExpenses is 0 but the component is rendered, show a single neutral sector (full ring) invar(--color-muted)with$0center label. Use a synthetic data point:[{ type: "empty", value: 1, label: "" }]withfill="var(--color-muted)".
Donut rendering:
- Wrap in
ChartContainerwithconfig={chartConfig}andclassName="min-h-[250px] w-full" - Use
PieChart>PiewithdataKey="value"nameKey="type"innerRadius={60}outerRadius={85}cx="50%"cy="50%" - Active sector hover: maintain
activeIndexstate withuseState(-1). SetactiveShapeto a render function that draws aSectorwithouterRadius + 8(expanded). WireonMouseEnter={(_, index) => setActiveIndex(index)}andonMouseLeave={() => 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 checksviewBox && "cx" in viewBox && "cy" in viewBox(per Pitfall 4), then renders<text>withtextAnchor="middle"dominantBaseline="middle"and a<tspan className="fill-foreground text-xl font-bold">showingformatCurrency(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, renderChartEmptyStatewithemptyMessage - 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
ChartContainerwithclassName="min-h-[250px] w-full" - Use
BarChart(vertical, which is the default -- nolayoutprop 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 -- nostackId):<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: ifentry.actual > entry.budgeted, usefill="var(--color-over-budget)", otherwise usefill="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, renderChartEmptyStatewithemptyMessage - 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
ChartContainerwithclassName="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: ifentry.actual > entry.budgeted, fill is"var(--color-over-budget)"(red accent per user decision), otherwise fill isvar(--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.
<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 buildpasses </success_criteria>