Share bar
Parts of a whole as one horizontal bar that adds up to the total, with the ranked list under it: dot, label, value and share. It replaces the donut in a new flow, because a share reads faster as lengths on one line than as angles around a ring. Counts or amounts only.
Anatomy
Two parts. The bar: one segment per entity, sized by its value, with a 1px gap that shows the surface behind it. The list: a LegendList row per segment. Hovering a segment lights its row, and hovering a row dims the other segments.
- Card$9.2M54.4%
- Bank transfer$5.5M32.5%
- Wallet$1.5M8.9%
- Cash voucher$720K4.3%
- 1Bar
h-4 (16px), rounded-full, full width. Segments grow in proportion to their value, biggest first, or in the caller's order with order="given".
- 2Segment
The entity's colour, var(--series-N), given by the caller. Never picked by position.
- 3Gap
1px between segments. It is the surface itself, so the bar reads on a card and on a panel alike.
- 4Other
Past the sixth entity, the rest folds into one Other segment and row, always var(--series-other) and always last. A row the caller already named Other joins it. Other never drills: it is several entities.
- 5List row
LegendList: dot, label, the value in muted tabular figures, and the share in a secondary Badge with one decimal.
- 6Drilldown
With onViewPaymentsRow, rows that carry a filter reveal 'View payments' on hover.
Rules
What the bar enforces, and what the caller must not ask of it.
- Pass counts or amounts: payments, volume, disputes.
- Give every row its entity's colour, var(--series-N), the same one it wears on every other chart.
- Let the bar sort and fold. Six segments and Other is the rule.
- For parts that have their own order, like payment states, pass them in that order (byPaymentState) with order="given": Refunded never sits next to the red.
- Put it in a ChartCard body, or straight into a narrow panel.
- Don't pass rates. An approval rate per provider does not add up to anything: that is a ranked list or a table.
- Don't colour by position in the list: a filter that removes a neighbour would repaint every survivor.
- Don't use a donut for a new flow. The donut recipes copy Insights and are marked as production mirrors.
- Don't give Other a filter or a colour of its own.
Recipes
The common shape: a share of approved volume by payment method, inside a ChartCard.
Approved volume by payment method
- Card$9.2M54.4%
- Bank transfer$5.5M32.5%
- Wallet$1.5M8.9%
- Cash voucher$720K4.3%
<ChartCard title="Approved volume by payment method">
<ShareBar
rows={[
{ key: "card", label: "Card", value: 9_200_000, color: "var(--series-1)" },
{ key: "wallet", label: "Wallet", value: 1_500_000, color: "var(--series-2)" },
{ key: "bank", label: "Bank transfer", value: 5_500_000, color: "var(--series-3)" },
{ key: "cash", label: "Cash voucher", value: 720_000, color: "var(--series-4)" },
]}
formatValue={formatMoney}
/>
</ChartCard>Eight providers in: the bar keeps the six biggest and folds the rest into Other, in grey. The shares still add up to 100%.
Approved volume by provider
- dLocal$6.3M43.2%
- Stripe$3.1M21.4%
- Adyen$2.2M15.0%
- Braintree$1.4M9.7%
- PayU$891K6.1%
- Worldpay$410K2.8%
- Other$261K1.8%
// Eight providers in, six segments and Other out. Past the sixth, rows fold
// into Other, drawn in --series-other whatever colour they carried.
<ShareBar rows={providers} formatValue={formatMoney} otherLabel="Other" />Statuses sorted with byPaymentState and drawn with order="given": each state in its own colour (paymentStateColour), Declined and Error one red series, Refunded never next to the red, and the states the launch does not name folded into Other, last and grey.
Payment status distribution
- Succeeded1,20469.2%
- Refunded1589.1%
- Pending543.1%
- Declined / error22212.8%
- Other1015.8%
import { ShareBar, type ShareBarRow } from "@/components/patterns/share-bar";
import { byPaymentState, paymentStateColour, paymentStateSeries } from "@/lib/payment-state";
// Counts by status, as the payments list reads them.
const counts: [string, number][] = [["Succeeded", 1204], ["Declined", 180], ["Refunded", 158], ["Error", 42] /* … */];
// One row per series: Declined and Error are one red series, unnamed states are Other.
const SERIES_LABEL: Partial<Record<string, string>> = { failed: "Declined / error", other: "Other" };
const rows: ShareBarRow[] = Object.values(
counts.reduce<Record<string, ShareBarRow>>((acc, [status, value]) => {
const series = paymentStateSeries(status);
const label = SERIES_LABEL[series] ?? status;
acc[series] = { key: series, label, value: (acc[series]?.value ?? 0) + value, color: paymentStateColour(status) };
return acc;
}, {}),
).sort((a, b) => byPaymentState(a.key, b.key));
// order="given": the states keep their order, so Refunded never sits next to the red.
<ShareBar rows={rows} order="given" formatValue={(n) => n.toLocaleString("en-US")} />Wire onViewPaymentsRow. Rows with a filter reveal 'View payments' on hover; Other never does.
- Card$9.2M54.4%
- Bank transfer$5.5M32.5%
- Wallet$1.5M8.9%
- Cash voucher$720K4.3%
<ShareBar
rows={rows} // each row carries filter: { method: "card" }
formatValue={formatMoney}
onViewPaymentsRow={(row) => navigate({ dimensions: row.filter })}
/>At 420px the bar keeps every segment and the list keeps every row. Nothing is cut, because a bar whose list is missing rows no longer adds up visibly.
- Card$9.2M54.4%
- Bank transfer$5.5M32.5%
- Wallet$1.5M8.9%
- Cash voucher$720K4.3%
<div className="w-[420px]">
<ShareBar rows={rows} formatValue={formatMoney} />
</div>Import
Copy this import line at the top of the file where you compose this molecule.
import { ShareBar, type ShareBarRow } from "@/components/patterns/share-bar";Props
Everything else is forwarded to the underlying elements via ...props.
ShareBar
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | ShareBarRow[] (required) | — | Counts or amounts. The bar sorts them biggest first (or keeps their order with order="given") and folds past the sixth into Other. |
| formatValue | (n: number) => string (required) | — | Formats each row's value in the list, e.g. a currency or a count formatter. |
| onViewPaymentsRow | (row: ShareBarRow) => void | — | Rows with a filter reveal a 'View payments' CTA on hover. Other never drills: it is a sum of several entities. |
| otherLabel | string | "Other" | Label of the folded tail. |
| order | "size" | "given" | "size" | "size": biggest first. "given": the rows' own order, for parts that have one, like payment states sorted with byPaymentState. Either way a row named Other joins the folded tail and is last. |
| className | string | — | Extra classes on the wrapper. |
ShareBarRow
| Prop | Type | Default | Description |
|---|---|---|---|
| key | string (required) | — | Stable id of the entity. |
| label | string (required) | — | Segment and row label. |
| value | number (required) | — | A count or an amount. Never a rate: rates do not add up. Zero and negative values are left out. |
| color | string (required) | — | The entity's colour, var(--series-1..6), from entityColours() (@/lib/entity-colour) asked by name. Required on purpose: ShareBar never assigns colour by position. |
| filter | Record<string, string | number> | — | Per-row drilldown filter, used with onViewPaymentsRow. |
Related
Cross-links to the atoms this molecule composes and sibling patterns.
- Chart cardThe usual host: ShareBar goes in its body.
- Legend listThe list under the bar. Use it alone for a ranked list of rates.
- Donut cardThe Insights donuts, marked as production mirrors. ShareBar is what a new flow uses instead.
- ColorsThe Series section: --series-1..6 and --series-other.