import { useState, type PointerEvent, type ReactNode } from "react"; import { Pie } from "@visx/shape"; import { Group } from "@visx/group"; import { localPoint } from "@visx/event"; import { ChartTooltip, TooltipRow } from "./ChartTooltip"; export interface DonutBreakdownPart { key: string; label: string; /** Positive magnitude; parts should sum to (or near) the slice's own value. */ value: number; /** Name of an HSL colour token, e.g. "--chart-green". */ colorVar: string; } export interface DonutSlice { name: string; value: number; color: string; /** What this slice's value is made of, e.g. cost basis vs. gain. Shown as a * small segmented bar and a value per part in the hover tooltip; omit when * there's nothing to break the slice down into. */ breakdown?: DonutBreakdownPart[]; } interface Props { data: DonutSlice[]; size: number; thickness?: number; formatValue: (n: number) => string; /** Centre readout when nothing is hovered, e.g. the total. */ centerLabel: string; centerValue: ReactNode; /** Highlight a slice from outside (e.g. the table row being hovered). When * given, it takes over from the donut's own hover state. */ active?: string | null; /** Called when the pointer or focus moves onto / off a slice. */ onActiveChange?: (name: string | null) => void; } /** A thin bar split by explicit parts (not auto-assigned identity colours, * unlike WeightBar) — used for a value's own composition, e.g. principal vs * gain, where colour means direction rather than "which entity". */ function CompositionBar({ parts, total }: { parts: DonutBreakdownPart[]; total: number }) { const shown = parts.filter((p) => p.value !== 0); if (shown.length === 0 || total <= 0) return null; return (