Ranked bars
A ranking as horizontal bars, biggest first: top decline reasons, volume by provider. The value is written at each bar's tip, the bars share one tone, and past the tenth the rest fold into Other. The most common chart merchants ask for. Counts or amounts only; a rate per group is a table.
Anatomy
One row per entity: the label, the bar, and the value at its tip. With drilldown, a 'View payments' column on the right. The bars are sized against the biggest one, so every bar starts at zero and there is no axis to read.
- Insufficient funds3,170
- Do not honor1,779
- Issuer unavailable928
- 3DS failed696
- Expired card402
- Invalid CVV351
- Suspected fraud288
- Limit exceeded204
- Restricted card167
- Invalid account122
- Other138
- 1Label
The entity, up to 10rem, truncated. Other is written in muted text.
- 2Bar
h-5 (20px), square ends, var(--series-1). Its length is the value against the biggest one, from zero.
- 3Value
At the tip, in tabular figures, formatted by formatValue. It is the reading.
- 4Highlight
With highlight, the named entity keeps the tone and the rest turn grey.
- 5Other
Past the tenth, one Other row, always last and in var(--series-other). It never drills.
- 6Drilldown
With onViewPaymentsRow, rows that carry a filter reveal 'View payments' on hover.
Rules
What the bars enforce, and what the caller must not ask of them.
- Pass counts or amounts: declines, payments, volume.
- Let the bars sort and fold. Top ten and Other is the rule.
- Name the entity the question asked about with highlight.
- Put it in a ChartCard body, with the total as the card's value (7,733 declines) and the window in its subtitle.
- Don't pass rates. Other would add up rates, and a rate per group is a table.
- Don't give each bar its own colour: one measure, one tone. The length already says which is bigger.
- Don't draw it with a visible axis or gridlines: the value at the tip is the reading.
- Don't give Other a filter or put it anywhere but last.
Recipes
The common shape: top decline reasons, with the total as the card's value and the window in its subtitle.
Decline reasons
- Insufficient funds3,170
- Do not honor1,779
- Issuer unavailable928
- 3DS failed696
- Expired card402
- Other758
<ChartCard title="Decline reasons" value="7,733" suffix="declines" subtitle="Last 7 days">
<RankedBars
rows={[
{ key: "insufficient_funds", label: "Insufficient funds", value: 3170 },
{ key: "do_not_honor", label: "Do not honor", value: 1779 },
{ key: "issuer_unavailable", label: "Issuer unavailable", value: 928 },
// …
{ key: "others", label: "Others", value: 758 }, // already folded upstream: joins Other
]}
formatValue={(n) => n.toLocaleString("en-US")}
/>
</ChartCard>The question named Stripe: it keeps the tone and the other providers step back to grey.
- dLocal$6.3M
- Stripe$3.1M
- Adyen$2.2M
- Braintree$1.4M
- PayU$891K
// "How much went through Stripe?" names one provider: it keeps the tone.
<RankedBars rows={providers} formatValue={formatMoney} highlight="stripe" />Thirteen reasons in: the ten biggest, then Other, last and in grey, with the sum of the rest.
- Insufficient funds3,170
- Do not honor1,779
- Issuer unavailable928
- 3DS failed696
- Expired card402
- Invalid CVV351
- Suspected fraud288
- Limit exceeded204
- Restricted card167
- Invalid account122
- Other138
// Thirteen reasons in: the ten biggest, then Other, last and in grey.
<RankedBars rows={allThirteenReasons} formatValue={formatCount} otherLabel="Other" />Wire onViewPaymentsRow. Rows with a filter reveal 'View payments' on hover; the column is kept on every row so the bars keep one track.
- Insufficient funds3,170
- Do not honor1,779
- Issuer unavailable928
- 3DS failed696
- Expired card402
- Invalid CVV351
- Suspected fraud288
- Limit exceeded204
- Restricted card167
- Invalid account122
- Other138
<RankedBars
rows={rows} // each row carries filter: { reason: "insufficient_funds" }
formatValue={formatCount}
onViewPaymentsRow={(row) => navigate({ dimensions: row.filter })}
/>Import
Copy this import line at the top of the file where you compose this molecule.
import { RankedBars, type RankedBarsRow } from "@/components/patterns/ranked-bars";Props
Everything else is forwarded to the underlying elements via ...props.
RankedBars
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | RankedBarsRow[] (required) | — | Counts or amounts, in any order: the bars sort them biggest first and fold past the tenth into Other. |
| formatValue | (n: number) => string (required) | — | Formats the value written at each bar's tip, e.g. a count or a currency formatter. |
| highlight | string | — | The key of the entity the question named. It keeps var(--series-1); the other bars turn grey. |
| onViewPaymentsRow | (row: RankedBarsRow) => void | — | Rows with a filter reveal a 'View payments' CTA on hover. Other never drills: it is several entities. |
| otherLabel | string | "Other" | Label of the folded tail. |
| className | string | — | Extra classes on the list. |
RankedBarsRow
| Prop | Type | Default | Description |
|---|---|---|---|
| key | string (required) | — | Stable id of the entity; what highlight matches. |
| label | string (required) | — | Row label. A row labelled Other, Others or Otros joins the folded tail. |
| value | number (required) | — | A count or an amount. Never a rate: Other is a sum. Negative and missing values are left out. |
| 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: RankedBars goes in its body.
- Share barWhen the parts add up to a whole, the share is one bar instead.
- TableA rate per group (approval by provider) is a table, not a ranking.
- ChartThe chart Rules this piece follows.