Skip to content

Chart

Draws bar, line, area, donut, scatter and funnel charts. One config names each series and its color, and the tooltip and legend read it.

Library
What’s this?
Sales by account
This quarter, USD

Installation

pnpm dlx shadcn@latest add @wisty/chart

The CLI installs the Radix or Base UI file to match your project.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • A dashboard, report or record page shows numbers as a picture: a comparison, a trend, a share of a whole, a relationship or the stages of a pipeline.
  • Pick the chart by what the reader should see, then copy the example named in the row. A row another item answers names that item and its example.
Intent The reader should see Example
Comparison Values side by side, with long category names Bar chart
Comparison Each value exactly, with long category names Bar chart with values
Comparison The largest few of many, with the rest stated Bar chart of the top five
Comparison Values side by side, and the records behind the one chosen Bar chart with drill-down
Comparison Values across ordered buckets Column chart
Comparison Two or three groups side by side in each bucket Column chart with groups
Comparison Values against a fixed target Column chart with a target
Comparison Values that can fall below zero Column chart with negative values
Comparison Long names, and what each value is made of Stacked bar chart
Comparison Values side by side, and what each is made of Stacked column chart
Comparison Many categories at a glance, without heavy bars Dot plot
Trending Two to four values over time, in one unit Line chart
Trending One value over time Area chart
Trending One value adding up over time, against a target Area chart with a running total
Trending Two values over time, in two units Combination chart
Composition Each column's parts as shares, when totals differ Stacked column chart to 100%
Composition The share of a whole, two to five parts Donut chart
Composition The share of a whole, small parts as "Others" Donut chart with Others
Composition The share of a whole, with each part's value Chart legend: Campaigns by source
Relationship How two measures move together Scatter plot
Relationship How three measures relate, the third as dot size Chart legend: Bubble chart
Pipeline How many reach each stage Funnel chart
Pipeline How a value got from its start to its end Waterfall chart
Metric One number, with its change Metric: Closed won
Metric One number, colored by the range it falls in Metric: Segment ranges
Metric One number against a target Gauge: Quota attainment
Dashboard Several measures at once, on cards Dashboard
Any A chart whose rows are loading or empty Loading and empty states

Don't use it

  • One number. Use Metric, or Gauge when it has a target.
  • Values the reader will sort, filter or copy. Use Table or Data table.
  • More than four lines or five slices. Split the chart, use a bar chart, or add the smallest parts up into "Others".
  • A third unit. Two units get two titled axes, and a third needs a second chart.
  • A chart type Chart doesn't draw:
Chart Use instead
Treemap A donut chart of four named groups plus "Others"
Heatmap or matrix Data table, with the values in its cells
Map A bar chart by region
Sankey A stacked column chart of each source by target

Usage

Every chart is Recharts composed inside ChartContainer, with a config that names each series and gives it a color.

ChartContainer
└── a Recharts chart, like BarChart
    ├── axes, grid and series, like XAxis and Bar
    ├── ChartTooltip, with content={<ChartTooltipContent />}
    └── ChartLegend, with content={<ChartLegendContent />}

The tooltip and legend read their labels and colors from the config. To format tooltip values, map payload as below, so each row keeps its indicator and name.

import { useId } from "react"
import { Bar, BarChart, XAxis } from "recharts"

import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  type ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from "@/components/ui/chart"

const data = [
  { stage: "Prospecting", amount: 420000 },
  { stage: "Qualification", amount: 310000 },
  { stage: "Negotiation/Review", amount: 180000 },
]

const config = {
  amount: { label: "Amount", color: "var(--chart-1)" },
} satisfies ChartConfig

// Swap en-US and USD for the org's locale and currency.
const full = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
})

export function PipelineByStage() {
  const titleId = useId()

  return (
    <Card>
      <CardHeader>
        <CardTitle id={titleId}>Pipeline by stage</CardTitle>
        <CardDescription>This quarter, USD</CardDescription>
      </CardHeader>
      <CardContent>
        <ChartContainer config={config}>
          <BarChart data={data} aria-labelledby={titleId}>
            <XAxis dataKey="stage" tickLine={false} axisLine={false} />
            <ChartTooltip
              content={({ active, payload, label }) => (
                <ChartTooltipContent
                  active={active}
                  label={label}
                  payload={payload?.map((item) => ({
                    ...item,
                    value: full.format(Number(item.value)),
                  }))}
                />
              )}
            />
            <Bar dataKey="amount" fill="var(--color-amount)" />
          </BarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  )
}

Each config key gets --color-<key> inside the chart. Which fields are keys:

  • A series key, the dataKey of a measure like amount, is a config key.
  • In a donut or funnel, nameKey picks a row field, like source. That field's values (Web, Partner Referral) are config keys.
  • A category axis's field, like stage above, needs no entry. Its values are the tick labels.
  • A key can be a value straight from the data, spaces and punctuation included. Paint it with chartColor(key), which returns the var(--color-<key>) the chart defines for it, escaped: fill={chartColor(row.source)}.
  • When a tooltip looks up config[row.segment].label, as the Scatter plot example does, type the field as the config's keys. Rows from a query come as string, so check each value before you build them.

A config entry's icon, an icon component, takes the place of that series' swatch in the legend and its indicator in the tooltip, in the muted text color. hideIcon on ChartLegendContent brings the legend's swatches back.

From a report or query, take the keys from the grouping, the labels from its display values and the colors by position. Keep the four largest groups and add the rest into one "Others" row.

Examples

Column chart

Comparison. Sales by account size as columns in the buckets' own order, with a period switch that moves them against one value axis.

Sales by account size
This year, USD

Stacked bar chart

Comparison. Open pipeline by owner as horizontal bars stacked by stage, the legend below, the total in the tooltip, and a period switch on one axis.

Open pipeline by owner and stage
This quarter, USD

Stacked column chart

Comparison. Sales by account size and product, a color per product, the legend below, the total in the tooltip, and a period switch on one axis.

Sales by account size and product
This year, USD

Dot plot

Comparison. Sales by account type as a dot per row, largest first, the value axis on top, with a period switch that moves dots to their new rank.

Sales by account type
This quarter, USD

Line chart

Trending. Sales and profit by month as two plain lines, the legend below, with a period switch that redraws them on the same scale.

Sales and profit by month
This year, USD

Area chart

Trending. Sales by month as one series, a line with a dot on each point over a flat 20% fill, and a period switch on the same scale.

Sales by month
This year, USD

Combination chart

Trending. Sales as columns on a left axis and open opportunities as a line on a right axis, both titled, with a period switch that keeps both scales.

Sales and open opportunities by month
This year

Donut chart

Composition. Campaigns by source on a thin ring with the total in the middle, and a period switch where slices re-sort and a source leaves.

Campaigns by source
This year

Scatter plot

Relationship. Accounts by employees and sales in one series, a color per segment, with a period switch that moves each account on fixed axes.

Accounts by employees and sales
This year

Funnel chart

Pipeline. Opportunities by stage as a tapered column with each count on its band, the total above and a legend titled Stage, with a period switch.

Opportunities by stage
This quarter

Opportunities: 325

Stage
  • Prospecting: 120 (37%)
  • Qualification: 90 (28%)
  • Proposal/Price Quote: 60 (18%)
  • Negotiation/Review: 35 (11%)
  • Closed Won: 20 (6%)

Waterfall chart

Pipeline. Cash flow from the opening to the closing balance, rises and falls colored by meaning, with a period switch whose steps move when it changes.

Cash flow
This quarter, USD

Dashboard

Three charts on cards sharing one config and one period filter: columns with an average line, every region by name, and sales against a dashed target.

Sales this year and last
Jan to Jun, USD
Sales by region
Jan to Jun, USD
Sales against target
Jan to Jun, USD

Loading and empty states

Any intent. A chart card while its rows load, as a skeleton at the chart's size, with no rows, as an empty state, and when they couldn't load, with Retry.

Sales by month
Jan to Jun, USD
Loading chart
Sales by region
Jan to Jun, USD
No sales in this period
Sales by product
Jan to Jun, USD
Sales couldn't load

API reference

PartBuilt on
ChartContainer<div> (opens in a new tab)
ChartStyle
ChartTooltipRecharts Tooltip (opens in a new tab)
ChartTooltipContent<div> (opens in a new tab)
ChartLegendRecharts Legend (opens in a new tab)
ChartLegendContent<div> (opens in a new tab)

ChartContainer

PropTypeDefault
config*ChartConfig
initialDimension{ width: number height: number }{ width: 320, height: 200 }

ChartStyle

PropTypeDefault
id*string
config*ChartConfig

ChartTooltipContent

PropTypeDefault
hideLabelbooleanfalse
hideIndicatorbooleanfalse
indicator"line" | "dot" | "dashed""dot"
nameKeystring
labelKeystring

ChartLegendContent

PropTypeDefault
hideIconbooleanfalse
nameKeystring
verticalAlign"top" | "bottom" | "middle""bottom"

chartColorfunction

The var(--color-<key>) a config key sets, for any key, spaces included.

ParameterTypeDefault
key*string

Returns string.

ChartConfigtype

type ChartConfig = Record<
  string,
  {
    label?: React.ReactNode
    icon?: React.ComponentType
  } & (
    | { color?: string; theme?: never }
    | { color?: never; theme: Record<keyof typeof THEMES, string> }
  )
>

SLDS mapping

SLDS 2 has no chart stylesheet or blueprint. The Charts guidelines set the look, and the tokens set the values.

  • Series colors --chart-1 to --chart-6 are Lightning's default chart palette, in hue and tint pairs: blue, light blue, purple, light purple, teal, light teal. Dark mode takes the darker set a Lightning dashboard draws there.
  • data-chart-palette on any element inside the theme root switches the charts in it to Salesforce's other dashboard palettes, with the names a dashboard's properties use. default and color-safe are categorical, with distinct hues. bluegrass, sunrise, water and watermelon are sequential, light to dark, for ordered buckets.
  • The tooltip is SLDS's hover tooltip on the popover surface. Legend swatches and tooltip dots are SLDS's legend dots.
  • The donut is SLDS's thin ring, starting at 12 o'clock and running clockwise. The funnel is SLDS's: a stacked column, each stage as tall as its value, clipped to a taper.
  • A chart sits on a card, as a Lightning dashboard component does: the card's title, description and footer play its title, subtitle and footer. The title names what's measured ("Sales by account"), never the chart type; the description holds the period and the unit.

Known gaps:

  • Compact numbers print "$620K". Salesforce prints a lowercase "k".
  • Recharts' legend can't show values beside each entry or a size scale. Use Chart legend in HTML beside the chart.
  • Not drawn: treemap, heatmap, map, sankey and origami. When not to use lists what to draw instead.
PropValueSLDSNotes
data-chart-palettedefault, color-safe, bluegrass, sunrise, water, watermelon (added)Dashboard colorPaletteSalesforce's dashboard palettes. Put the attribute on any element inside the theme root to switch every chart in it.
data-chart-swatchsquare (added)Square legend swatches (funnel, treemap)Put it on ChartContainer for charts whose marks are blocks. The legend swatches and the tooltip's dot indicators turn square, so the swatch matches the mark.

Keyboard interactions

Each chart is one tab stop.

Key Action
ArrowRight and ArrowLeft Move along the data and show the tooltip for each point
Escape Hide the tooltip
  • On horizontal bars (layout="vertical"), ArrowLeft moves down the list and ArrowRight moves up. ArrowUp, ArrowDown, Home and End do nothing.
  • The arrows walk only the first Scatter of a chart, so put every row in one.
  • The funnel is one point to Recharts. Focus shows the first stage and the arrows don't move, so draw each stage's count on its band and name each stage with its count in an HTML legend, as the Funnel chart example does.
  • The keys can't choose a mark. To drill into the records behind a bar, take the click from Bar onClick and give keyboard users a second control after the chart, such as a select of the same categories, as the Bar chart with drill-down example does.

Accessibility

  • Name every chart from visible text: aria-labelledby on the chart root, pointing at the CardTitle of the card it sits on. Don't use the title prop, which draws an SVG title that browsers may show on hover.
  • Moving with the arrow keys announces nothing. Put the key numbers in HTML near the chart, as a table or a sentence, when exact values matter.
  • A Pie adds a second tab stop unless it has rootTabIndex={-1}.
  • Color never carries meaning alone. Label the series, dash targets, give changes a sign, and name a scatter's group in its tooltip.
  • The palettes are Salesforce's as published. The lighter tints sit under 3:1 on white, so label them and check the series you draw against your background.

Motion

  • Series animate in on mount and when the data changes. Keep the chart mounted and its series keys stable, so it moves from the old values to the new; don't key a chart by period.
  • A period switch keeps one value axis for every period: getNiceTickValues over all the periods' rows, passed as ticks and as the top of domain. Marks then move against a steady scale, and periods compare honestly; Recharts doesn't animate an axis that rescales.
  • Bars match old to new by their category with animationMatchBy={matchByDataKey("size")}, so a category that comes or goes grows in or shrinks out instead of the bars beside it shifting values.
  • Lines, areas and donuts take animationDuration={800} and animationEasing="cubic-bezier(0.42,0,0.58,1)", and a Pie also takes animationBegin={0}. Recharts' named easings don't match CSS: its ease-in-out is CSS's ease-out.
  • The tooltip fades in and moves between points in 0.1s.
  • A leaving stacked column collapses as one block, and each donut slice slides from its old angles to its new ones, so a slice that changes rank slides past its neighbour and a leaving slice sweeps shut where it was, each through the example's animationInterpolateFn; Recharts' defaults split the stack into slivers and snap the ring to the new order.
  • A waterfall's steps grow from their own start, not from zero, through the example's animationInterpolateFn; a step that leaves drops its label at once.
  • With reduced motion, series and tooltip don't animate.

Responsive behavior

  • ChartContainer fills its width at 16:9 and redraws on resize. Inside a flex parent it has no width of its own, so give it w-full.
  • Under 24rem, as in a third of a dashboard, 16:9 leaves too little plot. Put @container on the figure or card and @max-sm:aspect-square on ChartContainer.
  • A donut with its legend on the right takes mx-auto aspect-[3/2] w-full max-w-md; with the legend below, mx-auto aspect-square w-full max-w-72. A funnel takes aspect-[4/3] w-full beside its legend, which moves under it in a narrow card.
  • A legend on the right needs about 400px. In a narrow card, put it below: a legend is a prop, so the donut example reads useChartWidth() and moves it under 384px.
  • On an ordered axis (months, size buckets), thin crowded ticks evenly with interval="equidistantPreserveStart". Never thin names that aren't ordered, like accounts: interval={0} and a tick that wraps them, as the bar chart's does. When every name must show, as a waterfall's steps must, use interval={0} and a tick that wraps names under their column and turns them 45° below about 56px a column.

Performance

  • Each chart is one SVG, and Recharts adds about 100 kB gzipped to the bundle.
  • Hundreds of points draw fine. Lightning turns hover off above 200 points; aggregate or bin the data before that.
  • Keep data and the config outside the component or in state, so Recharts doesn't animate the same series again on every render.

Rules

Rows and color

  • Rows hold raw numbers (620000, never 620 "in thousands"), in the order drawn: largest first for comparisons and shares, buckets in their own order.
  • Picklist values are data, written as Salesforce labels them: "Closed Won", "Proposal/Price Quote", "Partner Referral". A measure's name is sentence case: "Closed won" for the amount.
  • One config per dashboard, satisfies ChartConfig, with color or theme per series. Never a colors array.
  • Colors go --chart-1 onward in order, as Lightning's dashboards draw them. The values of one grouping (stages, products, segments) take the palette in order, so three stages are blue, light blue and purple. Two different measures take --chart-1 and --chart-3; the same measure in an earlier period takes its tint, --chart-2.
  • The slice or bar that folds the small groups ("Others") takes --chart-other, a neutral, and goes last.
  • Paint with fill="var(--color-<key>)", or chartColor(key) for a key from data, never Cell or a hex value. Choose data-chart-palette per dashboard, not per chart.
  • --success and --destructive only for marks that mean good or bad, with a sign or a word.
  • At most four lines, five slices and about 12 bar categories.

Axes and marks

  • tickLine={false} axisLine={false} on every axis, tickMargin={8} on category axes, width="auto" on Y axes. Value axes start at zero.
  • margin={{ top: 12 }} at least, or Recharts pushes the top tick's label below its gridline to keep it inside the chart.
  • Horizontal bars and dot plots put the value axis on top and use CartesianGrid horizontal={false}; every other chart uses vertical={false}.
  • Title an axis only when the card's description doesn't carry its unit. A rotated title takes textAnchor: "middle" so it centers on the axis. Two units get two titled YAxis, the second on the right; never three.
  • Lines are type="linear" and strokeWidth={2}. One series over time is an Area with fillOpacity={0.2} and dots; two to four are Lines with dot={false}.
  • cursor={false} on scatter, dot plot, donut and funnel.
  • A waterfall shows a start, the steps and an end, never the parts of one number. Its rows are signed changes and totals; a stated total that doesn't match the steps before it adds a "Not accounted for" step, never a corrected one.
  • A waterfall's steps go rises first, then falls, each largest first, or in time order, and in the same order in every period, so a filter moves them instead of shuffling them. Keep to about 10 steps and fold the smallest into "Other". One waterfall per card: they don't work as small multiples.

Legend, tooltip and labels

  • No legend for one series. Otherwise <ChartLegend itemSorter={null} content={<ChartLegendContent />} /> below the chart, since Recharts sorts by name without it. Every legend entry must be a config key, or it shows a swatch with no text.
  • Every legend reads swatch, then name. A legend of a field's values (stages, products, sources) starts with the field's name in muted text, as Lightning titles its legends; a legend of measures (Sales, Profit) has none. Put it in the legend's content, before ChartLegendContent.
  • A donut's or funnel's legend sits on its right, top-aligned; every other chart's sits below. In a narrow card, every legend goes below.
  • A scatter with groups builds its legend from the groups: content={() => <ChartLegendContent payload={legend} />}.
  • Charts whose marks are blocks, like the funnel, take data-chart-swatch="square" on ChartContainer, so the legend and tooltip swatches match the marks.
  • Format with a fixed locale: compact for ticks and labels ("$620K"), full for tooltips. Pass compact.format, never the formatter object. formatter on ChartTooltipContent replaces the whole row.
  • A fixed target is a dashed ReferenceLine with ifOverflow="extendDomain", or a target above the data disappears. Label it where no bar crosses the line. A target that changes by period is a dashed series. Only targets and forecasts are dashed.
  • Value labels sit outside the bar: <LabelList className="fill-foreground" /> at right or top. When every bar has one, hide the value axis and the grid. A waterfall keeps both, since its labels hide in a narrow card.
  • A donut's total is a Label with position="center" whose content centers on x + width / 2 and y + height / 2. A "cx" in viewBox check draws nothing.

Names and states

  • Keep the chart's box while rows load: aria-busy="true" on the card, a Skeleton at the chart's aspect ratio and an sr-only "Loading chart". Never empty axes or fake series.
  • No rows is an Empty in the same box that says what's missing and what to change. An error is the same Empty with the message and a Retry button.
  • Never pass activeShape.