Donut card
The canonical Yuno donut chart card (Top providers, Top issuers, Top card brands, Top payment methods, Top card types, Top countries). Wraps ChartCard chrome around an interactive Pie (hover a slice → the corresponding row highlights, center label updates, slice pushes 4px outward), a ranked rows list underneath (dot + name + count + % Badge), and an optional 'View more (N)' button that opens a HORIZONTAL Dialog (donut left, full list right). Passing onViewPaymentsRow enables per-slice + per-row drilldown; the container `actions` slot is suppressed automatically in that mode to avoid competing affordances.
Anatomy
A ChartCard whose body is a two-block stack: (1) the DonutPieBlock — a Recharts Pie with an inner+outer radius that leaves room for a centered pointer-events-none KPI overlay showing the currently-hovered row's name (or the first row's, when nothing is hovered) + its percentage; (2) the DonutRowsList — a ranked ul with the same dot/label rhythm as LegendList, plus the count + % Badge on the right. When expandedRows is longer than the visible individual rows, a 'View more (N)' link appears below the list.
Top providers
- dLocal6.3M43.20%
- Stripe3.1M21.40%
- Adyen2.2M14.96%
- Braintree1.4M9.69%
- PayU890.5K6.11%
- Others672.2K4.64%
- 1Header — Title + info + actions
Inherited from ChartCard. Info tooltip defaults to 'Top by share for {title.toLowerCase()}.'. Actions slot renders the caller's ReactNode UNLESS onViewPaymentsRow is set (per-slice CTA wins to avoid competing affordances).
- 2Donut (Pie block)
Recharts PieChart with innerRadius 55 + outerRadius 90 (card size) or 70 + 120 (Dialog size). paddingAngle 1 between slices, stroke var(--card) 2px so slices don't blur into each other. Hovered slice pushes 4px outward via activeShape; the row highlights via hovered index. Slices with `filter` become clickable when onViewPaymentsRow is set.
- 3Center label
Absolutely-positioned overlay inside the donut hole. Shows [truncated name text-xs muted] + [pct.toFixed(2)% text-2xl semibold]. Uses lg = text-3xl inside the Dialog.
- 4Rows list
Vertical ul. Each row: dot + name (left), formattedCount + secondary Badge with .toFixed(2)% (right). Hover highlights the corresponding slice AND vice-versa (bg-muted/60).
- 5Hover CTA
When onViewPaymentsRow is set and the row carries `filter`, hovering the row swaps count + Badge for a 'View payments' link Button (grid overlay, no shift).
- 6View more (N)
When expandedRows.length > visible individual rows, a centered link Button appears below the list. Opens a Dialog with a HORIZONTAL layout: bigger donut on the left (280px, radii 70/120, center label lg), full row list on the right.
Recipes
Rows are static. No per-row CTA. Actions slot renders whatever the caller passes (usually nothing). Use for donuts that are display-only in prod (Top payment methods, Top card types, Top countries).
Top providers
- dLocal6.3M43.20%
- Stripe3.1M21.40%
- Adyen2.2M14.96%
- Braintree1.4M9.69%
- PayU890.5K6.11%
- Others672.2K4.64%
<DonutCard title="Top providers" rows={rows} />
// rows: DonutRow[] = [
// { name, value, pct, color, filter? },
// { name: "Others", value, pct, color, isOthers: true },
// ]Same as standard, plus a Header actions slot with a ghost icon Button (menu, filter, etc.). Suppressed automatically if you enable drilldown.
Top providers
- dLocal6.3M43.20%
- Stripe3.1M21.40%
- Adyen2.2M14.96%
- Braintree1.4M9.69%
- PayU890.5K6.11%
- Others672.2K4.64%
<DonutCard
title="Top providers"
rows={rows}
actions={
<Button variant="ghost" size="icon">
<DotsThree weight="light" />
</Button>
}
/>Wire onViewPaymentsRow. Rows with `filter` reveal 'View payments' on hover; the corresponding slice becomes clickable and dispatches to the same handler. The Others bucket (isOthers: true) stays non-clickable — no misleading affordance.
Top providers
- dLocal6.3M43.20%
- Stripe3.1M21.40%
- Adyen2.2M14.96%
- Braintree1.4M9.69%
- PayU890.5K6.11%
- Others672.2K4.64%
<DonutCard
title="Top providers"
rows={rows}
onViewPaymentsRow={(row) => navigate({ dimensions: row.filter })}
/>Pass rows (5 individuals + optional Others) AND expandedRows (full individual list). A 'View more (N)' link appears below the card's list. Opens a horizontal Dialog: bigger donut left, full row list right. Common on Top providers / Top issuers / Top card brands.
Top providers
- dLocal6.3M43.20%
- Stripe3.1M21.40%
- Adyen2.2M14.96%
- Braintree1.4M9.69%
- PayU890.5K6.11%
- Others672.2K4.64%
<DonutCard
title="Top providers"
rows={topFive} // 5 individuals + Others bucket
expandedRows={fullList} // 10 individuals, no Others
onViewPaymentsRow={handler} // opens horizontal Dialog
/>Import
Copy this import line at the top of the file where you compose this molecule.
import { DonutCard, type DonutRow } from "@/components/patterns/donut-card";Props
Everything else is forwarded to the underlying elements via ...props.
DonutCard
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string (required) | — | Card title. Rendered inside ChartCard's Header. |
| rows | DonutRow[] (required) | — | Rows shown both as Pie slices and as the ranked list. Prod caps at 6 (5 individuals + optional "Others" bucket). |
| expandedRows | DonutRow[] | — | Full individual row list (no "Others"). When provided and longer than the individual rows in `rows`, the card renders a "View more (N)" button that opens a horizontal Dialog (donut left, full list right). |
| actions | ReactNode | — | Optional custom top-right actions slot in the Header. Suppressed automatically when `onViewPaymentsRow` is set (per-slice CTA wins). |
| onViewPaymentsRow | (row: DonutRow) => void | — | When defined, rows with `filter` reveal a "View payments" CTA on hover and their slices become clickable. Fires with the whole row on click. |
| info | string | "Top by share for {title}." | Info tooltip text on the Header. Falls back to the default sentence. |
| formatCount | (n: number) => string | compact ("1.2K") | Format the row `value`. Pass a currency formatter for volume donuts. |
| className | string | — | Extra classes forwarded to the underlying ChartCard. |
DonutRow
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string (required) | — | Slice + row label. |
| value | number (required) | — | Raw count. Formatted via `formatCount` in the row. |
| pct | number (required) | — | Percentage 0–100. Rendered in the center label + the row Badge. |
| color | string (required) | — | Slice color. Prefer var(--chart-N). |
| isOthers | boolean | — | True if this row is the aggregate "Others" bucket. Omit `filter` on Others rows so they stay non-clickable. |
| filter | Record<string, string | number> | — | Per-row drilldown filter. When present + onViewPaymentsRow wired, both the slice and the row's hover CTA drill down. |
Related
Cross-links to the atoms this molecule composes and sibling patterns.
- Chart cardThe outer chrome. DonutCard is a specialization; ChartCard hosts every other chart-body shape.
- Legend listFor a data card WITHOUT a pie (list-only). Use inside ChartCard's body when you don't need the donut.