docs(02): research phase domain

This commit is contained in:
2026-03-16 12:50:24 +01:00
parent f548e7bbb7
commit 7346a6a125

View File

@@ -0,0 +1,510 @@
# Phase 2: Dashboard Charts and Layout - Research
**Researched:** 2026-03-16
**Domain:** Recharts 2.x chart components (Donut/PieChart, BarChart, Horizontal BarChart), shadcn/ui ChartContainer integration, React Router URL state management
**Confidence:** HIGH
## Summary
Phase 2 replaces the existing flat pie chart and progress bars on the dashboard with three rich chart components -- an expense donut chart with center label and active sector, a grouped vertical bar chart for income budgeted vs actual, and a horizontal bar chart for category-type budget vs actual spending. All charts must consume CSS variable tokens from the established OKLCH palette (no hardcoded hex values) and handle empty states gracefully. Month navigation via URL search params enables shareable links and browser history navigation.
The project uses **Recharts 2.15.4** (not 3.x as the roadmap loosely referenced). This is important because Recharts 2.x has working `<Label>` support inside `<Pie>` for center text, and the established `chart.tsx` from shadcn/ui with the `initialDimension` patch is already configured. The `ChartContainer` + `ChartConfig` pattern from shadcn/ui provides the theme-aware wrapper -- all chart colors flow through `ChartConfig` entries referencing CSS variables, which ChartStyle injects as scoped `--color-{key}` custom properties.
**Primary recommendation:** Build each chart as an isolated presentational component in `src/components/dashboard/charts/`, wire them into a refactored `DashboardContent` with a 3-column responsive grid, and manage month selection state with `useSearchParams` from `react-router-dom`.
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- 3-column grid on desktop -- donut, vertical bar, and horizontal bar charts side by side in a single row
- Each chart wrapped in its own Card component (consistent with StatCard pattern)
- Visual order: SummaryStrip first, chart row second, QuickAdd button moved below charts
- Month navigation: Arrow buttons for prev/next month plus a clickable month label that opens a dropdown for direct jump to any available month
- Month stored in URL search params (e.g. `/dashboard?month=2026-02`) -- enables sharing links and browser back button
- When navigating to a month with no budget: show the month page with an empty prompt ("No budget for this month") and a create/generate option
- Dropdown lists all months that have budgets; arrow buttons allow navigating beyond existing budgets (showing empty prompt)
- When a chart has no data: show a muted placeholder (greyed-out chart outline with text overlay) inside the chart card
- Donut chart with zero amounts (budget exists, nothing spent): show empty ring in neutral/muted color with $0 center label
- When a brand new budget has no items at all: show individual placeholders per chart card independently (no combined empty state)
- Center label shows total expense amount only (formatted currency, no label text)
- Active sector expands on hover
- Uses existing category color CSS variables from palette.ts
- Vertical bar chart (income): budgeted bars in muted/lighter shade, actual bars in vivid category color -- emphasizes actuals
- Horizontal bar chart (category type spend): budgeted bars muted, actual bars vivid
- Over-budget indicator: actual bar extends past budgeted mark with red accent (over-budget semantic color) -- visually flags overspending
- All bars consume CSS variable tokens (no hardcoded hex values)
### Claude's Discretion
- Responsive breakpoints for chart row collapse (3-col to stacked)
- Donut legend placement (below vs right side of chart)
- Chart tooltip content and formatting
- Exact spacing and typography within chart cards
- DashboardSkeleton updates for new layout
- Data memoization strategy (useMemo vs derived state)
- Month navigation placement (PageShell CTA slot vs own row)
### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| UI-DASH-01 | Redesign dashboard with hybrid layout -- summary cards, charts, and collapsible category sections with budget/actual columns | This phase delivers the charts layer: 3-column chart grid below SummaryStrip, month navigation, responsive layout. Collapsible sections are Phase 3. |
| UI-BAR-01 | Add bar chart comparing income budget vs actual | Vertical BarChart with grouped bars (budgeted muted, actual vivid), using ChartContainer + ChartConfig pattern |
| UI-HBAR-01 | Add horizontal bar chart comparing spend budget vs actual by category type | Horizontal BarChart via `layout="vertical"` on `<BarChart>`, swapped axis types, over-budget red accent via Cell conditional fill |
| UI-DONUT-01 | Improve donut chart for expense category breakdown with richer styling | PieChart with innerRadius/outerRadius, activeShape for hover expansion, center Label for total, custom legend, category fill colors from CSS variables |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| recharts | 2.15.4 | All chart rendering (PieChart, BarChart) | Already installed, shadcn/ui chart.tsx built on it |
| react-router-dom | 7.13.1 | `useSearchParams` for month URL state | Already installed, provides shareable URL state |
| react | 19.2.4 | `useMemo` for data derivation memoization | Already installed |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @/components/ui/chart | shadcn | ChartContainer, ChartConfig, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent | Wrap every chart for theme-aware color injection |
| @/lib/palette | project | categoryColors map (CSS variable references) | Feed into ChartConfig color entries |
| @/lib/format | project | formatCurrency for tooltip/label values | All monetary displays in charts |
| lucide-react | 0.577.0 | ChevronLeft, ChevronRight icons for month nav | Month navigation arrows |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Recharts 2.x | Recharts 3.x | v3 has broken `<Label>` in PieChart (issue #5985); stay on 2.15.4 |
| useSearchParams | useState | Would lose URL shareability and browser back/forward; user explicitly chose URL params |
| Custom tooltip | ChartTooltipContent from shadcn | shadcn's built-in tooltip handles config labels/colors automatically; prefer it |
**Installation:**
```bash
# No new packages needed -- all dependencies already installed
```
## Architecture Patterns
### Recommended Project Structure
```
src/
components/
dashboard/
charts/
ExpenseDonutChart.tsx # Donut pie chart with center label
IncomeBarChart.tsx # Vertical grouped bar (budgeted vs actual)
SpendBarChart.tsx # Horizontal bar (budget vs actual by category)
ChartEmptyState.tsx # Shared muted placeholder for no-data charts
MonthNavigator.tsx # Prev/Next arrows + month dropdown
SummaryStrip.tsx # (existing)
StatCard.tsx # (existing)
DashboardSkeleton.tsx # (existing -- needs update for 3-col chart row)
hooks/
useMonthParam.ts # Custom hook wrapping useSearchParams for month state
pages/
DashboardPage.tsx # Refactored: month param -> budget lookup -> DashboardContent
lib/
palette.ts # (existing -- categoryColors, add chartFillColors)
format.ts # (existing -- formatCurrency)
```
### Pattern 1: ChartContainer + ChartConfig Color Injection
**What:** shadcn's `ChartContainer` reads a `ChartConfig` object and injects scoped `--color-{key}` CSS custom properties via a `<style>` tag. Chart components then reference these as `fill="var(--color-{key})"`.
**When to use:** Every chart in this phase.
**Example:**
```typescript
// Source: shadcn/ui chart docs + project's chart.tsx
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import type { ChartConfig } from "@/components/ui/chart"
const chartConfig = {
bill: {
label: "Bills",
color: "var(--color-bill-fill)", // references index.css @theme token
},
variable_expense: {
label: "Variable Expenses",
color: "var(--color-variable-expense-fill)",
},
// ... other category types
} satisfies ChartConfig
// In JSX:
<ChartContainer config={chartConfig} className="min-h-[250px] w-full">
<PieChart>
<Pie data={data} dataKey="value" nameKey="type" fill="var(--color-bill)" />
<ChartTooltip content={<ChartTooltipContent />} />
</PieChart>
</ChartContainer>
```
### Pattern 2: Donut Chart with Active Shape + Center Label
**What:** `<Pie>` with `innerRadius`/`outerRadius` creates donut shape. `activeIndex` + `activeShape` prop renders expanded sector on hover. `<Label>` placed inside `<Pie>` renders center text.
**When to use:** ExpenseDonutChart component.
**Example:**
```typescript
// Source: Recharts 2.x API docs, recharts/recharts#191
import { PieChart, Pie, Cell, Sector, Label } from "recharts"
// Active shape: renders the hovered sector with larger outerRadius
const renderActiveShape = (props: any) => {
const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props
return (
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius + 8}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
)
}
// In component:
const [activeIndex, setActiveIndex] = useState(-1)
<Pie
data={pieData}
dataKey="value"
nameKey="type"
innerRadius={60}
outerRadius={85}
activeIndex={activeIndex}
activeShape={renderActiveShape}
onMouseEnter={(_, index) => setActiveIndex(index)}
onMouseLeave={() => setActiveIndex(-1)}
>
{pieData.map((entry) => (
<Cell key={entry.type} fill={`var(--color-${entry.type}-fill)`} />
))}
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
<tspan className="fill-foreground text-xl font-bold">
{formatCurrency(totalExpenses, currency)}
</tspan>
</text>
)
}
}}
/>
</Pie>
```
### Pattern 3: Horizontal Bar Chart via layout="vertical"
**What:** Recharts uses `layout="vertical"` on `<BarChart>` to produce horizontal bars. Axes must be swapped: `XAxis type="number"` and `YAxis type="category"`.
**When to use:** SpendBarChart (budget vs actual by category type).
**Example:**
```typescript
// Source: Recharts API docs, shadcn/ui chart-bar-horizontal pattern
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"
<BarChart layout="vertical" data={spendData}>
<CartesianGrid horizontal={false} />
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="label"
width={120}
tick={{ fontSize: 12 }}
/>
<Bar dataKey="budgeted" fill="var(--color-budgeted)" radius={4} />
<Bar dataKey="actual" fill="var(--color-actual)" radius={4}>
{spendData.map((entry, index) => (
<Cell
key={index}
fill={entry.actual > entry.budgeted
? "var(--color-over-budget)"
: `var(--color-${entry.type}-fill)`
}
/>
))}
</Bar>
</BarChart>
```
### Pattern 4: Month Navigation with URL Search Params
**What:** `useSearchParams` stores the selected month as `?month=YYYY-MM` in the URL. A custom `useMonthParam` hook parses the param, falls back to current month, and provides setter functions.
**When to use:** DashboardPage and MonthNavigator.
**Example:**
```typescript
// Source: React Router v7 docs
import { useSearchParams } from "react-router-dom"
function useMonthParam() {
const [searchParams, setSearchParams] = useSearchParams()
const monthParam = searchParams.get("month")
const now = new Date()
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`
const month = monthParam || currentMonth // "YYYY-MM"
const setMonth = (newMonth: string) => {
setSearchParams((prev) => {
prev.set("month", newMonth)
return prev
})
}
const navigateMonth = (delta: number) => {
const [year, mo] = month.split("-").map(Number)
const d = new Date(year, mo - 1 + delta, 1)
const next = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
setMonth(next)
}
return { month, setMonth, navigateMonth }
}
```
### Pattern 5: Over-Budget Visual Indicator
**What:** For bar charts, when actual exceeds budgeted, the actual bar uses `--color-over-budget` (red) fill via `<Cell>` conditional coloring. The bar naturally extends past the budgeted bar length, providing a visual overshoot.
**When to use:** Both IncomeBarChart and SpendBarChart.
**Key detail:** With grouped (non-stacked) bars, the actual bar and budgeted bar render side by side. The actual bar being taller/longer than the budgeted bar IS the visual indicator. Adding a red fill when over-budget reinforces the message.
### Anti-Patterns to Avoid
- **Wrapping Recharts in abstractions:** shadcn/ui explicitly says "we do not wrap Recharts." Use Recharts components directly, only adding ChartContainer/ChartTooltip as enhancement wrappers.
- **Hardcoded hex colors in charts:** All colors must flow through CSS variables via ChartConfig. The existing `categoryColors` in palette.ts already uses `var(--color-*)` references.
- **Using ResponsiveContainer directly:** The project's `chart.tsx` already wraps it inside `ChartContainer` with the `initialDimension` patch. Using raw `ResponsiveContainer` bypasses the fix and causes `width(-1)` console warnings.
- **Stacking bars when grouped is needed:** For income bar chart, budgeted and actual should be side-by-side (grouped), not stacked. Do NOT use `stackId` -- just place two `<Bar>` components without it.
- **Putting month state in React state:** User decision requires URL search params. Using `useState` for month would lose shareability and browser back/forward support.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Chart color theming | Custom CSS injection per chart | `ChartContainer` + `ChartConfig` from chart.tsx | Handles theme injection, dark mode, and scoped CSS variables automatically |
| Responsive chart sizing | Manual resize observers | `ChartContainer` (wraps `ResponsiveContainer` with `initialDimension` patch) | Already solves the SSR/initial-render sizing bug |
| Chart tooltips | Custom div overlays on hover | `<ChartTooltip content={<ChartTooltipContent />} />` | Pre-styled, reads ChartConfig labels, handles positioning |
| Currency formatting | `toFixed(2)` or template literals | `formatCurrency(amount, currency)` from lib/format.ts | Handles locale-aware formatting (Intl.NumberFormat) |
| Month arithmetic | Manual date string manipulation | `new Date(year, month + delta, 1)` | Handles year rollover (Dec to Jan) automatically |
| Category color lookup | Switch statements or if/else chains | `categoryColors[type]` from palette.ts | Single source of truth, already uses CSS variable references |
**Key insight:** The shadcn chart.tsx component is the critical integration layer. It provides ChartContainer (with the initialDimension fix), ChartConfig (color theming), ChartTooltip/Content (pre-styled tooltips), and ChartLegend/Content (pre-styled legends). Every chart MUST use ChartContainer as its outer wrapper.
## Common Pitfalls
### Pitfall 1: Recharts width(-1) Console Warnings
**What goes wrong:** Charts render with 0 or negative width on initial mount, producing console warnings and invisible charts.
**Why it happens:** `ResponsiveContainer` measures parent on mount; if parent has no explicit dimensions yet, width resolves to 0 or -1.
**How to avoid:** Always use `ChartContainer` from chart.tsx (which sets `initialDimension={{ width: 320, height: 200 }}`). Also set `className="min-h-[250px] w-full"` on ChartContainer.
**Warning signs:** Console warns `width(-1)` or chart appears blank on first render.
### Pitfall 2: Horizontal Bar Chart Axis Confusion
**What goes wrong:** Setting `layout="vertical"` but leaving XAxis/YAxis in default configuration produces broken or invisible bars.
**Why it happens:** Recharts naming is counterintuitive -- `layout="vertical"` means bars go **horizontal**. When layout is vertical, XAxis must be `type="number"` and YAxis must be `type="category"`.
**How to avoid:** Always pair `layout="vertical"` with `<XAxis type="number" />` and `<YAxis type="category" dataKey="label" />`.
**Warning signs:** Bars not visible, axis labels missing, or bars rendering as tiny dots.
### Pitfall 3: ChartConfig Keys Must Match Data Keys
**What goes wrong:** Tooltip labels show raw dataKey names instead of formatted labels, or colors don't apply.
**Why it happens:** `ChartConfig` keys are used to look up labels and colors. If the config key doesn't match the `dataKey` or `nameKey` used in the chart, the lookup fails silently.
**How to avoid:** Ensure ChartConfig keys exactly match the `dataKey` and `nameKey` values used on `<Bar>`, `<Pie>`, and tooltip/legend `nameKey` props.
**Warning signs:** Tooltips showing "budgeted" instead of "Budgeted Amount", or missing color dots in legend.
### Pitfall 4: Pie Chart Label Positioning with viewBox
**What goes wrong:** Center label text does not appear or appears at wrong position.
**Why it happens:** In Recharts 2.x, the `<Label>` component inside `<Pie>` receives a `viewBox` prop with `cx`/`cy` coordinates. If the content function doesn't destructure and check for these, the text won't render.
**How to avoid:** Always check `viewBox && "cx" in viewBox && "cy" in viewBox` before rendering the `<text>` element in the Label content function.
**Warning signs:** Donut chart renders but center is empty.
### Pitfall 5: useSearchParams Replaces All Params
**What goes wrong:** Setting one search param wipes out others.
**Why it happens:** `setSearchParams({ month: "2026-03" })` replaces ALL params. If other params existed, they're gone.
**How to avoid:** Use the callback form: `setSearchParams(prev => { prev.set("month", value); return prev })`. This preserves existing params.
**Warning signs:** Other URL params disappearing when changing month.
### Pitfall 6: Empty Array Passed to PieChart
**What goes wrong:** Recharts throws errors or renders broken SVG when `<Pie data={[]}/>` is used.
**Why it happens:** Recharts expects at least one data point for proper SVG path calculation.
**How to avoid:** Conditionally render the chart only when data exists, or show the ChartEmptyState placeholder when data is empty.
**Warning signs:** Console errors about NaN or invalid SVG path.
### Pitfall 7: Month Param Mismatch with Budget start_date Format
**What goes wrong:** Budget lookup fails even though the correct month is selected.
**Why it happens:** URL param is `YYYY-MM` but `budget.start_date` is `YYYY-MM-DD`. Comparison must use `startsWith` prefix matching.
**How to avoid:** Use `budget.start_date.startsWith(monthParam)` for matching, consistent with the existing `currentMonthStart` helper pattern.
**Warning signs:** "No budget" message shown for a month that has a budget.
## Code Examples
Verified patterns from the project codebase and official sources:
### ChartConfig for Category Colors (Using Existing CSS Variables)
```typescript
// Source: project index.css @theme tokens + palette.ts pattern
import type { ChartConfig } from "@/components/ui/chart"
// For donut chart -- uses fill variants (lighter for chart fills)
export const expenseChartConfig = {
bill: { label: "Bills", color: "var(--color-bill-fill)" },
variable_expense: { label: "Variable Expenses", color: "var(--color-variable-expense-fill)" },
debt: { label: "Debts", color: "var(--color-debt-fill)" },
saving: { label: "Savings", color: "var(--color-saving-fill)" },
investment: { label: "Investments", color: "var(--color-investment-fill)" },
} satisfies ChartConfig
// For income bar chart -- budgeted (muted) vs actual (vivid)
export const incomeBarConfig = {
budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
actual: { label: "Actual", color: "var(--color-income-fill)" },
} satisfies ChartConfig
// For spend bar chart -- same muted/vivid pattern, per-cell override for over-budget
export const spendBarConfig = {
budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
actual: { label: "Actual", color: "var(--color-muted-foreground)" }, // base; overridden per-cell
} satisfies ChartConfig
```
### Memoized Data Derivation Pattern
```typescript
// Source: React useMemo best practice for derived chart data
const pieData = useMemo(() => {
return EXPENSE_TYPES.map((type) => {
const total = items
.filter((i) => i.category?.type === type)
.reduce((sum, i) => sum + i.actual_amount, 0)
return { type, value: total, label: t(`categories.types.${type}`) }
}).filter((d) => d.value > 0)
}, [items, t])
const totalExpenses = useMemo(() => {
return items
.filter((i) => i.category?.type !== "income")
.reduce((sum, i) => sum + i.actual_amount, 0)
}, [items])
```
### Budget Lookup by Month Param
```typescript
// Source: project DashboardPage.tsx existing pattern + useSearchParams
const { month } = useMonthParam() // "YYYY-MM"
const { budgets, loading } = useBudgets()
const currentBudget = useMemo(() => {
return budgets.find((b) => b.start_date.startsWith(month))
}, [budgets, month])
// Month dropdown options: all months that have budgets
const availableMonths = useMemo(() => {
return budgets.map((b) => b.start_date.slice(0, 7)) // "YYYY-MM"
}, [budgets])
```
### Empty Donut State (Zero Amounts)
```typescript
// When budget exists but all actuals are 0: show neutral ring
const hasExpenseData = pieData.length > 0
const allZero = items
.filter((i) => i.category?.type !== "income")
.every((i) => i.actual_amount === 0)
// If allZero but items exist: render single neutral sector with $0 center
// If no items at all: render ChartEmptyState placeholder
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Raw `ResponsiveContainer` | `ChartContainer` with `initialDimension` | shadcn/ui chart.tsx (Phase 1 patch) | Eliminates width(-1) warnings |
| Hardcoded hex colors in charts | CSS variable tokens via ChartConfig | Phase 1 OKLCH token system | Theme-aware, dark-mode-ready charts |
| Month in React state | Month in URL search params | Phase 2 (this phase) | Shareable links, browser history |
| Single pie chart + progress bars | 3-chart dashboard (donut + 2 bar charts) | Phase 2 (this phase) | Richer financial visualization |
**Deprecated/outdated:**
- Recharts 3.x `<Label>` in PieChart: Broken in v3.0 (issue #5985). The project is on 2.15.4 where it works correctly -- do NOT upgrade.
- Direct `style={{ backgroundColor: categoryColors[type] }}`: The existing DashboardContent uses inline styles for legend dots. Charts should use ChartConfig + `fill="var(--color-*)"` instead.
## Open Questions
1. **Donut legend placement decision**
- What we know: User left this to Claude's discretion (below vs right side of chart)
- What's unclear: In a 3-column layout, right-side legend may be too cramped
- Recommendation: Place legend below the donut chart. In a tight 3-column grid, vertical space is more available than horizontal. Use the shadcn `ChartLegendContent` with `verticalAlign="bottom"` or a custom legend matching the existing li-based pattern.
2. **Month navigation placement**
- What we know: User left this to Claude's discretion (PageShell CTA slot vs own row)
- What's unclear: PageShell has an `action` slot that renders top-right
- Recommendation: Use PageShell `action` slot for the MonthNavigator component. This keeps the dashboard title and month selector on the same row, saving vertical space and following the established PageShell pattern.
3. **Responsive breakpoint for chart collapse**
- What we know: User wants 3-col on desktop, stacked on smaller screens
- What's unclear: Exact breakpoint (md? lg? xl?)
- Recommendation: Use `lg:grid-cols-3` (1024px+) for 3-column, `md:grid-cols-2` for 2-column (donut + one bar side-by-side, third stacks below), single column below md. This matches the existing `lg:grid-cols-3` breakpoint used by SummaryStrip.
4. **Muted bar color for "budgeted" amounts**
- What we know: The existing `--color-budget-bar-bg: oklch(0.92 0.01 260)` token is available
- What's unclear: Whether this is visually distinct enough next to vivid category fills
- Recommendation: Use `--color-budget-bar-bg` for budgeted bars. It is intentionally muted (low chroma, high lightness) to recede behind vivid actual bars. If too subtle, a slightly darker variant can be added to index.css.
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | None installed |
| Config file | None |
| Quick run command | `bun run build` (type-check + build) |
| Full suite command | `bun run build && bun run lint` |
### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| UI-DONUT-01 | Expense donut chart renders with center label, active hover, custom legend | manual-only | Visual inspection in browser | N/A |
| UI-BAR-01 | Vertical bar chart shows income budgeted vs actual | manual-only | Visual inspection in browser | N/A |
| UI-HBAR-01 | Horizontal bar chart shows category spend budget vs actual | manual-only | Visual inspection in browser | N/A |
| UI-DASH-01 | 3-column chart layout, month navigation, empty states | manual-only | Visual inspection in browser | N/A |
**Justification for manual-only:** All requirements are visual/UI-specific (chart rendering, hover interactions, layout, responsive breakpoints). No test framework is installed, and adding one is out of scope for this phase. Type-checking via `bun run build` catches structural errors.
### Sampling Rate
- **Per task commit:** `bun run build` (catches type errors and import issues)
- **Per wave merge:** `bun run build && bun run lint`
- **Phase gate:** Build passes + visual verification of all 3 charts + month navigation
### Wave 0 Gaps
- No test infrastructure exists in the project
- Visual/chart testing would require a framework like Playwright or Storybook (out of scope for this milestone)
- `bun run build` serves as the automated quality gate
## Sources
### Primary (HIGH confidence)
- Project codebase: `src/components/ui/chart.tsx` -- ChartContainer with initialDimension patch, ChartConfig type, ChartTooltip/Legend components
- Project codebase: `src/lib/palette.ts` -- categoryColors using CSS variable references
- Project codebase: `src/index.css` -- OKLCH color tokens including chart fills and semantic status colors
- Project codebase: `package.json` -- recharts 2.15.4, react-router-dom 7.13.1
- [Recharts Pie API docs](https://recharts.github.io/en-US/api/Pie/) -- activeShape, activeIndex, innerRadius/outerRadius
- [Recharts Bar API docs](https://recharts.github.io/en-US/api/Bar/) -- stackId, layout, Cell
- [shadcn/ui Chart docs](https://ui.shadcn.com/docs/components/radix/chart) -- ChartContainer, ChartConfig, ChartTooltip patterns
- [React Router useSearchParams](https://reactrouter.com/api/hooks/useSearchParams) -- URL state management API
### Secondary (MEDIUM confidence)
- [shadcn Bar Charts gallery](https://ui.shadcn.com/charts/bar) -- horizontal bar chart pattern with layout="vertical"
- [shadcn Donut Active pattern](https://www.shadcn.io/patterns/chart-pie-donut-active) -- activeIndex/activeShape with Sector expansion
- [Recharts 2.x PieChart demo source](https://github.com/recharts/recharts/blob/2.x/demo/component/PieChart.tsx) -- renderActiveShape reference implementation
- [Recharts GitHub issue #191](https://github.com/recharts/recharts/issues/191) -- center label in PieChart approaches
- [Recharts GitHub issue #5985](https://github.com/recharts/recharts/issues/5985) -- Label broken in Recharts 3.0 (confirms 2.x works)
### Tertiary (LOW confidence)
- None -- all findings verified with primary or secondary sources
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH -- all libraries already installed and in use; Recharts 2.15.4 API verified
- Architecture: HIGH -- builds on established project patterns (PageShell, SummaryStrip, ChartContainer, palette.ts)
- Pitfalls: HIGH -- verified via official docs, GitHub issues, and project-specific chart.tsx code
**Research date:** 2026-03-16
**Valid until:** 2026-04-16 (stable -- Recharts 2.x is mature, no breaking changes expected)