# Donut / Pie Chart — Implementation Guide Requires **recharts** (install from the web app directory; see SKILL.md Step 2). --- ## Data structure Charts expect an array of objects with `name`, `value`, and `color`: ```ts interface ChartData { name: string; value: number; color: string; } ``` --- ## Donut chart component Create at `components/DonutChart.tsx`: ```tsx import React from "react"; import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; import { Card } from "@/components/ui/card"; interface ChartData { name: string; value: number; color: string; } interface DonutChartProps { title: string; data: ChartData[]; } export const DonutChart: React.FC = ({ title, data }) => { const total = data.reduce((sum, item) => sum + item.value, 0); const mainPercentage = total > 0 ? Math.round((data[0]?.value / total) * 100) : 0; return (

{title}

{data.map((entry, index) => ( ))} {/* Center label */}
{mainPercentage}%
{/* Legend */}
{data.map((item, index) => (
{item.name}
))}
); }; ``` --- ## Key Recharts concepts | Component | Purpose | |-----------|---------| | `ResponsiveContainer` | Wraps chart to make it fill its parent's width | | `PieChart` | Chart container for pie/donut | | `Pie` | The data ring; `innerRadius` > 0 makes it a donut | | `Cell` | Individual segment; accepts `fill` color | | `paddingAngle` | Gap between segments (degrees) | ### Donut vs Pie | Property | Donut | Pie | |----------|-------|-----| | `innerRadius` | `> 0` (e.g. `70`) | `0` | | Center label | Yes, positioned absolutely | Not typical | --- ## Preparing chart data from raw records Transform API data into the `ChartData[]` format before passing to the chart: ```tsx const CATEGORIES = ["Plumbing", "HVAC", "Electrical"] as const; const OTHER_LABEL = "Other"; const COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"]; const chartData = useMemo(() => { const counts: Record = {}; CATEGORIES.forEach((c) => (counts[c] = 0)); counts[OTHER_LABEL] = 0; records.forEach((record) => { const type = record.category; if (CATEGORIES.includes(type as (typeof CATEGORIES)[number])) { counts[type]++; } else { counts[OTHER_LABEL]++; } }); return [ ...CATEGORIES.map((name, i) => ({ name, value: counts[name], color: COLORS[i] })), { name: OTHER_LABEL, value: counts[OTHER_LABEL], color: COLORS[CATEGORIES.length] }, ]; }, [records]); ``` --- ## Color palette recommendations | Use case | Colors | |----------|--------| | Categorical (4 items) | `#7C3AED` `#EC4899` `#14B8A6` `#06B6D4` | | Status (3 items) | `#22C55E` `#F59E0B` `#EF4444` (green/amber/red) | | Sequential | Use opacity variants of one hue: `#7C3AED` at 100%, 75%, 50%, 25% | Keep chart colors consistent with the app's design system. Define them as constants, not inline values. --- ## Other chart types For **bar charts** and **line / area charts**, see `bar-line-chart.md` in this directory. --- ## Accessibility - Always include a text legend (not just colors). - Chart should be wrapped in a section with a visible heading. - For critical data, provide a text summary or table alternative. - Use sufficient color contrast between segments. - Consider `prefers-reduced-motion` for chart animations. --- ## Common mistakes | Mistake | Fix | |---------|-----| | Missing `ResponsiveContainer` | Chart won't resize; always wrap in `ResponsiveContainer` | | Fixed width/height on `PieChart` | Let `ResponsiveContainer` control sizing | | No legend | Add a grid legend below the chart | | Inline colors | Extract to constants for consistency | | No fallback for empty data | Show "No data" message when `data` is empty |