diff --git a/src/components/dashboard/charts/ChartEmptyState.tsx b/src/components/dashboard/charts/ChartEmptyState.tsx
new file mode 100644
index 0000000..5df74a1
--- /dev/null
+++ b/src/components/dashboard/charts/ChartEmptyState.tsx
@@ -0,0 +1,19 @@
+import { cn } from "@/lib/utils"
+
+interface ChartEmptyStateProps {
+ message: string
+ className?: string
+}
+
+export function ChartEmptyState({ message, className }: ChartEmptyStateProps) {
+ return (
+
+ )
+}
diff --git a/src/components/dashboard/charts/ExpenseDonutChart.tsx b/src/components/dashboard/charts/ExpenseDonutChart.tsx
new file mode 100644
index 0000000..dc55ba7
--- /dev/null
+++ b/src/components/dashboard/charts/ExpenseDonutChart.tsx
@@ -0,0 +1,156 @@
+import { useMemo, useState } from "react"
+import { PieChart, Pie, Cell, Sector, Label } from "recharts"
+import type { PieSectorDataItem } from "recharts/types/polar/Pie"
+import {
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart"
+import type { ChartConfig } from "@/components/ui/chart"
+import { ChartEmptyState } from "./ChartEmptyState"
+import { formatCurrency } from "@/lib/format"
+
+interface ExpenseDonutChartProps {
+ data: Array<{ type: string; value: number; label: string }>
+ totalExpenses: number
+ currency: string
+ emptyMessage: string
+}
+
+function renderActiveShape(props: PieSectorDataItem) {
+ const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } =
+ props
+ return (
+
+ )
+}
+
+export function ExpenseDonutChart({
+ data,
+ totalExpenses,
+ currency,
+ emptyMessage,
+}: ExpenseDonutChartProps) {
+ const [activeIndex, setActiveIndex] = useState(-1)
+
+ 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])
+
+ // No data at all: show empty state placeholder
+ if (data.length === 0 && totalExpenses === 0) {
+ return
+ }
+
+ // Zero-amount state: budget exists but all actuals are zero
+ const isAllZero = totalExpenses === 0
+ const displayData = isAllZero
+ ? [{ type: "empty", value: 1, label: "" }]
+ : data
+ const displayConfig: ChartConfig = isAllZero
+ ? { empty: { label: "", color: "var(--color-muted)" } }
+ : chartConfig
+
+ return (
+
+
+
+
+ formatCurrency(Number(value), currency)
+ }
+ />
+ }
+ />
+ setActiveIndex(index)
+ }
+ onMouseLeave={
+ isAllZero ? undefined : () => setActiveIndex(-1)
+ }
+ >
+ {displayData.map((entry) => (
+ |
+ ))}
+
+
+
+
+ {/* Custom legend below the donut */}
+ {!isAllZero && data.length > 0 && (
+
+ {data.map((entry) => (
+ -
+
+ {entry.label}
+
+ {formatCurrency(entry.value, currency)}
+
+
+ ))}
+
+ )}
+
+ )
+}