Scatter chart
Points by two numbers, bubbles sized by area, groups, log axes, quadrants and trend lines; rows it can't place are counted. Same props on both engines.
Install
Pick one engine. Both builds take the same props, so switching later changes only the import path.
Recharts
npx shadcn@latest add https://beautifulcharts.dev/r/instrument-scatter-chart-recharts.jsonimport { ScatterChart } from "@/components/beautiful-charts/themes/instrument/recharts/scatter-chart";
npm dependencies: recharts@^3.10.1
Apache ECharts
npx shadcn@latest add https://beautifulcharts.dev/r/instrument-scatter-chart-echarts.jsonimport { ScatterChart } from "@/components/beautiful-charts/themes/instrument/echarts/scatter-chart";
npm dependencies: echarts@^6.1.0
Example
A complete client module. Paste it into your app after installing the Recharts build; for ECharts, change recharts to echarts in the import.
"use client";
import { ScatterChart } from "@/components/beautiful-charts/themes/instrument/recharts/scatter-chart";
import type { ChartConfig } from "@/components/beautiful-charts/core/contracts";
const data = [
{ team: "Atlas", cycle: 3.1, bugs: 4, size: 12 }, { team: "Beacon", cycle: 2.2, bugs: 2, size: 7 },
{ team: "Cobalt", cycle: 5.4, bugs: 11, size: 20 }, { team: "Drift", cycle: 2.8, bugs: 3, size: 9 },
{ team: "Ember", cycle: 4.0, bugs: 7, size: 15 }, { team: "Fjord", cycle: null, bugs: 1, size: 5 }, // not drawn, and counted
];
// Entries for the fields set labels and formats; bubbles are sized by area, so twice the people looks twice as big.
const config = {
cycle: { label: "Cycle time", format: (days: number) => `${days} d` },
bugs: { label: "Bugs" },
size: { label: "People" },
} satisfies ChartConfig;
export function CycleTimeAndBugs() {
return <ScatterChart data={data} xKey="cycle" yKey="bugs" sizeKey="size" nameKey="team" config={config} trend labelPoints="top" title="Longer cycles, more bugs" />;
}
Scatter chart props
Generated from ScatterChartProps in the installed source, so this table and your editor's hints always agree. * marks a required prop.
| Prop | Type | Description |
|---|---|---|
| xKey* | keyof TDatum & string | The field for the horizontal position (a number). |
| yKey* | keyof TDatum & string | The field for the vertical position (a number). |
| sizeKey | keyof TDatum & string | A field that sizes each point by area, making a bubble chart. |
| groupKey | keyof TDatum & string | A field that sorts points into coloured groups. Group names are the keys of config. |
| nameKey | keyof TDatum & string | A field naming each point, for tooltips, the table and labels. |
| labelPoints | string[] | "top" | Label these points on the chart (by name), or the largest few ("top"). |
| xScale | "linear" | "log" | "linear" (default) or "log" (powers of ten). |
| yScale | "linear" | "log" | "linear" (default) or "log" (powers of ten). |
| trend | boolean | Draw a least-squares trend line per group (or for all points without groups). |
| quadrants | { x: number; y: number; labels?: [string, string, string, string] } | Split the plot into quadrants at these values, with optional labels (top-left, top-right, bottom-left, bottom-right). |
| maxRadius | number | Largest bubble radius in px (default 22). |
| radius | number | Point radius when there's no sizeKey, in px (default 4.5). |
| grid | boolean | Draw grid lines behind the plot. On by default. |
| xAxis | AxisOptions | Horizontal axis: hide it, format ticks, or add a label (defaults to the x field's label). |
| yAxis | AxisOptions | Vertical axis: hide it, format ticks, or add a label (defaults to the y field's label). |
Recharts build only
Native Recharts children render inside the chart with your original rows, so ReferenceArea, Label or a custom Customized layer work as they do in any Recharts chart.
| Prop | Type | Description |
|---|---|---|
| children | ReactNode | Recharts children render inside the same ScatterChart, which receives your original rows. |
Apache ECharts build only
Escape hatches into the engine. Your option is deep-merged over the generated one, so you can change one nested setting without replacing a whole section.
| Prop | Type | Description |
|---|---|---|
| renderer | EChartsRenderer | "canvas" (default, best for many points) or "svg". |
| option | OptionOverride | Deep-merged over the generated option, or (generated) => option. Points are custom series "points" (one item per point); trend lines are "trends". |
Series config
One entry per series key in config. shadcn's ChartConfig entries work as they are. Give a series at most one of color, theme or colors; TypeScript rejects more than one. This table lists only the options this chart reads.
| Prop | Type | Description |
|---|---|---|
| label | ReactNode | Name shown in the legend, tooltip and table. Defaults to the key, humanised. |
| ariaLabel | string | Plain text for tables, summaries and announcements when label is not a string. |
| icon | ComponentType<{ className?: string }> | An icon component (e.g. from lucide-react) shown in place of the swatch in the legend and tooltip. |
| format | (value: number) => string | Formats this series' values everywhere: tooltip, legend, table, readout. |
| color | ColorValue | One colour: any CSS colour or var(--chart-1), or { light, dark }. |
| theme | never | shadcn's per-theme colours, { light, dark }. |
| colors | never | Gradient stops from base to value end, per theme. The last stop is the series' single colour. |
Events
Every callback, and the payload types it receives. Indexes always point into the data you passed, even when a brush or grouping shows a subset.
| Prop | Type | Description |
|---|---|---|
| onSelectedChange | (selection: Selection<TDatum> | null) => void | Called when a row is selected or cleared, by pointer, keyboard or the data table. |
| onSelectedSeriesChange | (key: string | null) => void | Called when the selected series changes (legend "select" action, or a click on a series). |
| onHiddenChange | (hidden: string[]) => void | Called when the legend hides or shows a series. |
| onActiveChange | (index: number | null) => void | Called as the hovered or focused row changes, or null when it leaves. |
| onReady | (chart: EChartsType) => void | Called once with the ECharts instance, e.g. to add event listeners. |
export type Selection<TDatum> = {
/** Index into the data you passed in, even when a brush shows a subset. */
dataIndex: number;
datum: TDatum;
seriesKey: string | null;
source: "pointer" | "keyboard" | "table";
/** For a grouped segment (a donut's "Other"): the indexes of every row it stands for. `dataIndex` is the largest. */
members?: number[] | undefined;
};
export type TooltipContext<TDatum> = {
datum: TDatum;
/** Index into the data you passed in. */
dataIndex: number;
/** Visible series at this index. `value` is your raw value; `share` is its part of the row in 100% mode. */
series: { key: string; label: ReactNode; value: number | null; share?: number | null | undefined; color: string; selected: boolean }[];
};Parts and styling hooks
Each part carries a data-slot attribute. Theme rules inside the chart are one attribute selector deep, so a selector such as .my-chart [data-slot="chart-title"] overrides them. These are the slots in the files this chart installs; parts a chart doesn't use are simply absent from its markup.
chartchart-bodychart-descriptionchart-eyebrowchart-footerchart-headerchart-headingchart-legendchart-legend-itemchart-legend-labelchart-legend-swatchchart-legend-valuechart-livechart-mainchart-plotchart-readoutchart-statchart-stat-notechart-stat-valuechart-statechart-state-boxchart-state-messagechart-statschart-tablechart-table-scrollchart-takeawaychart-titlechart-tooltipodometerodometer-digitodometer-glyphsodometer-reelodometer-signpartial-notereadout-deltareadout-unitreadout-valuesr-onlytooltip-indicatortooltip-rowtooltip-titletooltip-valueProps every chart shares
Header, states, legend, tooltip, formatting, motion, sound and controlled interaction state. They work the same on every Instrument chart and both engines. The guide explains colours, controlled state and accessibility.
Show all 34 shared props
| Prop | Type | Description |
|---|---|---|
| data* | TDatum[] | Your rows, as they are. Missing values (null, undefined, NaN) stay missing and are never drawn as zero. |
| locale | string | BCP 47 locale. Defaults to "en-US" so server and client render the same text. |
| timeZone | string | IANA time zone for dates. Defaults to "UTC" so server and client agree. |
| config | ChartConfig | Per-series label, colour, icon, format and style, keyed by series key. shadcn ChartConfig works as it is. |
| eyebrow | ReactNode | Small label above the title. |
| title | ReactNode | Card title. A string also names the chart for screen readers. |
| description | ReactNode | A line under the title. |
| takeaway | ReactNode | ((summary: string) => ReactNode) | false | The sentence under the title. Computed from the data by default; pass text, a function of the computed sentence, or false to hide it. |
| ariaDescription | string | Accessible summary for screen readers. Computed from the data by default. |
| motion | MotionPreference | "full" (default), "subtle" or "off". Reduced-motion settings always win. |
| motionOptions | MotionOptions | When the entrance plays. |
| sound | boolean | Sound cues on hover, scrub and select. Plays only if the visitor has also turned sound on for the page. |
| density | Density | "comfortable" (default) or "compact" for dashboards. |
| legend | LegendOptions | false | Legend position, alignment, shape and click action, or false to hide it. |
| tooltip | TooltipOptions<TDatum> | false | Tooltip position, indicator, cursor and custom content, or false to turn it off. |
| valueFormatter | (value: number) => string | Values formatted for display. Series formatters in config win. |
| loading | boolean | Show the loading state. Over existing data, the old marks stay dimmed instead of vanishing. |
| error | string | null | An error message. Shows the error state in place of the plot. |
| emptyState | ReactNode | Shown instead of the plot when there is no data. |
| loadingState | ReactNode | Shown while loading is true and there is no data yet. |
| errorState | ReactNode | Shown when error is set. |
| selected | number | null | Controlled and uncontrolled interaction state. Point selection (selected) pins one row by its index in data. Series selection (selectedSeries) picks one series and dims the others. They are separate: picking a row doesn't pick a series. |
| defaultSelected | number | null | Initial selected row when uncontrolled. |
| selectedSeries | string | null | Controlled series selection: this series stays lit and the others dim. |
| defaultSelectedSeries | string | null | Initial series selection when uncontrolled. |
| defaultHidden | string[] | Series hidden at first when uncontrolled. |
| activeIndex | number | null | Controlled hover or focus index, for syncing several charts on one cursor. |
| palette | "theme" | "shadcn" | Series colours: "theme" (default) uses the theme's designed palette; "shadcn" uses your --chart-1..5. --bc-series-N always wins. You can also opt a whole section in with the class bc-use-shadcn-colors. |
| table | boolean | Show the "Show data table" disclosure. On by default. |
| stats | ChartStat[] | A row of figures under the title, for dashboard cards and blocks. |
| footer | ReactNode | A line or action at the bottom of the card, e.g. a source note or a link. |
| className | string | Classes on the card, for spacing and layout. |
| height | number | Height of the plot area in px. The frame sizes around it. |