Skip to main content

DensityScatterChart

Distributionchart

A point plot for 10⁵–10⁶ rows: every point drawn, its colour the density around it, zones on the axes classifying it, and an intersection selection (x range × y range × lasso × zone). WebGL dots, screen-space bins, Canvas-2D fallback.

import { DensityScatterChart } from "@elabs-ai/components-charts";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.

Add the brand-ui DensityScatterChart chart (from @elabs-ai/components-charts) to this project.

1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`.
2. Look DensityScatterChart up before writing code: `npx -y @elabs-ai/components-cli docs DensityScatterChart` prints its real props, variants, what it pairs with and what to avoid.
3. If @elabs-ai/components-charts is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-charts`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`.
4. Use it here: [the screen or file, and what it should show]
5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop.
6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.

Use it for

  • hundreds of thousands of rows of two continuous measures — where the points pile up, and which zone each falls in

Avoid

  • under ~20k rows — use ScatterChart, which keeps labels, shapes and per-point marks
  • Under ~20k points — ScatterChart has labels, shapes, trends and per-point marks this canvas path cannot.
  • Handing 10⁵ rows as objects — pass columnar data ({ x, y, values }) or pay for the per-row objects before the first pixel.
  • A second hue for density — density is lightness on the class hue; a rainbow ramp misreads as categories.
  • Averaging once a size limit is hit — the chart's point is that it never does; bin size stays in screen pixels, not data units.

Works with

Examples

18 live examples

Loading

`status="loading"` (RM-185): a skeleton fills the same plot box the ready chart would use, at every width, so nothing moves once the data lands.

Stat Lines Per Zone

Checkbox Legend

One Million Points

One million points — the budget story. The picture is the same; only the readout changes.

Color By Value

Density Only

Color By Category

Shape By Category

Shape by a categorical column — a second channel beside colour (WCAG 1.4.1). Here colour AND shape read the same bins, so the legend swatches are the glyphs themselves; a named `shapes` map pins a glyph to a value and the rest take the cycle. Works on the WebGL and the Canvas-2D path alike.

Shape By Category Color By Zone

Shape by one column while colour comes from the zones: the dots carry both channels; the legend keeps the zones (the host draws a shape key of its own from `dealShapes`).

Keyboard Range Selection

Selection is an intersection. The play function drives the KEYBOARD path — the x-range slider thumbs are `role="slider"` buttons in the axis gutter — and asserts the intent that leaves the chart and the live-region announcement.

Thumb Hit Bounds Across Widths

Review fix4: the four range thumbs (x-start/x-end/y-start/y-end) must stay inside their own chart root and clear of each other at every width — the review before this one only checked ~900px, where the bug (x-start/y-start overlapping 10×10px at the bottom-left corner; x-end/y-end reaching past the root's own edges) was invisible.

Legend Hide And Select

The legend (RM-118): a plain click hides/shows a zone (`aria-pressed`); a Shift- or Ctrl-click SELECTS that zone as a constraint instead. Each zone also carries a named tag inside the plot that selects it on a plain click.

Toolbar Tools

The toolbar (RM-145) is the way into the plot gestures: Pointer pans and zooms, Range turns a drag in the plot into an x AND a y range at once, Lasso is freehand. The play function switches tools through the real buttons.

Controlled View

A controlled view: the host owns the window, the chart eases into every change.

In Chart Frame

Inside `ChartFrame`: title, description, notes and the frame's own chrome.

Canvas 2 D Fallback

The Canvas-2D fallback, forced — what a browser without WebGL gets. Same picture, slower.

Zone Shapes

API

DensityScatterChart

PropTypeDescription
datarequiredDensityScatterDataColumnar (preferred past ~50k) or rows.
xDataKeystringRow key for x when `data` is rows. Default `"x"`. Also the intent `field` for x ranges.
xKeystringRow key for x when `data` is rows. Also the intent `field` for x ranges. @deprecated Since 6.0.0, use `xDataKey`. Removed in 7.0.0.
yDataKeystringRow key for y when `data` is rows. Default `"y"`.
yKeystringRow key for y when `data` is rows. @deprecated Since 6.0.0, use `yDataKey`. Removed in 7.0.0.
valueKeysreadonly string[]Row keys lifted as numeric columns (rows input only).
categoryKeysreadonly string[]Row keys lifted as categorical columns (rows input only).
zonesreadonly DensityZone[]Zones on the axes, inner → outer.
outsideDensityOutsideZoneLabel/colour for points matching no zone.
colorByDensityColorByWhat colours the dots. Default `{ kind: "zone" }` (or `"density"` without zones).
shapeByDensityShapeByDot glyph per value of a category column (`DENSITY_SHAPES`). Independent of `colorBy` — shape by one column, colour by zone or another column. Unset: every dot is a circle. When the legend colours by the SAME column, its swatches take the glyphs; a host that colours by something else draws its own shape key from `dealShapes(labels, shapeBy)`.
valueKeystringThe value column whose cell mean the tooltip reports (a `colorBy: value` uses its own key). Unset: no mean row.
cellSizenumberBin size in CSS px. Default 5. Smaller = finer colour field, more cells.
underlaynumberUnderlay threshold (points per cell) — a faint body under dense areas. `0` turns it off. Default 4.
pointRadiusnumberDot radius in CSS px at the home view; grows slightly with zoom. Default 1.35.
zoombooleanWheel zoom + drag pan. Default `true`. Off, whatever this says, under a host policy with `active: false` (`ChartConfigProvider` `interactions`).
domainPartial<DensityView>The full-data window. Default: the data extent padded 3%.
viewDensityView
defaultViewDensityView
onViewChange(view: DensityView) => void
selectionDensityScatterSelection
defaultSelectionDensityScatterSelection
onSelectionChange(selection: DensityScatterSelection) => void
selectionGesturesreadonly ChartSelectionGesture[]Gestures to enable (ADR 0040). `"range"`: drag on an axis gutter (one axis), or — with the toolbar's Range tool — a drag in the plot that sets the x AND the y range at once; keyboard: `role="slider"` thumbs in each gutter. `"lasso"`: the toolbar's Lasso tool, freehand in the plot. The toolbar (`ChartSelectionToolbar`: Pointer / Range / Lasso) mounts above the plot, or in `ChartFrame`'s action slot when framed. Unset: no gesture layer, no toolbar, DOM byte-identical.
onSelectionIntent(intent: ChartSelectionIntent) => voidFires one intent per committed gesture.
selectionFieldstringField name carried in x-range intents. Default `xDataKey`.
selectionFieldYstringY-range intents carry this field. Default `yDataKey`.
selectionToolbar"auto" | "none"`"auto"` (default): the toolbar shows when gestures are listed; `"none"` hides it.
legendContainerLegendPropContainer legend (RM-118). `true` → `{ interactive: "toggle" }`. `{ values: true }` prints each entry's point count (per zone, category or the one density/value class).
xLabelReactNodeAxis titles.
yLabelReactNode
formatX(value: number) => string
formatY(value: number) => string
xAxisDensityAxisOptionsPer-axis presentation. `labels: false` drops the tick labels (grid lines stay); `tickSpacing` is the target distance between ticks and grid lines in CSS px (default 90 on x, 60 on y).
yAxisDensityAxisOptions
minimapbooleanThe overview in the plot's bottom-right corner while zoomed: every point at the home window plus the current window as a frame; drag in it to pan. Default `true` (shown only when zoom is on and the plot is large enough).
zoomControlsPlacement"top-end" | "bottom-end"Where the + / − / reset buttons sit while zoomed. `"top-end"` (default) or `"bottom-end"` — stacked above the minimap, clear of host chrome that overlays the top-right corner (e.g. an embedding app's object menu).
sizeKeystringPer-point size: a `data` value column. Dots scale by area between `sizeRange[0]` and `sizeRange[1]` (CSS px radii, default `[1.2, 6]`).
sizeRange[number, number]
pointOpacitynumberMultiplies the dots' opacity (0–1). Default `1`.
densityFloornumberThe lightest a lone dot is drawn, 0–1 along its class ramp (background → class colour). Raise it to keep sparse dots — the outside class above all — clearly visible; `1` draws every dot in its full class colour. Default `0.32`.
selectionToolChartSelectionToolModeThe active selection tool, controlled — for a host that renders its own tool switch (e.g. a BI host's selection toolbar). Unset: the toolbar owns it.
onPointClick(index: number, event: ReactPointerEvent<HTMLDivElement>) => voidA click (no drag) on a dot: the nearest visible point within its radius, by row index. Lets a host select that row's category.
onBackgroundClick(event: ReactPointerEvent<HTMLDivElement>) => voidA click (no drag) on the plot that hits no dot — e.g. a BI host opening its selection session (and toolbar) the way a native chart does.
describePoint(index: number) => DensityPointDescription | undefinedExtra tooltip content for one hovered dot: a title (e.g. the row's dimension value) and rows appended after the coordinates.
showLassoShapebooleanKeep the committed lasso outline drawn while its constraint is active (default `true`). `false`: the dimmed / highlighted dots alone show it.
formatValue(value: number) => string
plotHeightResponsive<ChartPlotHeight>
aspectRatiostring
marginnumber | Partial<Margin>Space around the plot: one number for every side, or per side.
accessibleLabelstring
accessibleDescriptionstring
messagesDensityScatterLabels & ChartMessagesThis chart's own words (the `messages` group, RM-191): the word-bag keys of `DensityScatterLabels` (tooltip headings, zone and selection names, the live-region template), and overrides of the catalogue's `charts.*` keys the shared parts it renders print (legend, loading text), for this chart only. The two key sets never collide: a catalogue key always starts with `charts.`.
labelsDensityScatterLabels@deprecated Use `messages` — the same object, the same keys (RM-191, ADR 0042 A.1). Still read until 7.0.0, with one development warning; when both are set, `messages` wins.
onFrame(stats: DensityFrameStats) => voidPer-frame statistics (stories, diagnostics).
renderer"webgl" | "canvas2d"Force the Canvas-2D path (tests, screenshots).
zoneTagsbooleanIn-plot zone tags (named buttons that select a zone). Default `true`.
statLinesreadonly DensityStatLine[]Average / median / standard-deviation reference lines, over all points or one per colour class (zone or category). Per-class lines take their class's outline colour and run across that class's points; overall lines take `--chart-foreground`. Each statistic has its own dash, every line is tagged in the plot, and all of them are restated in the accessible description. Hidden classes get no line.
renderOverlay(context: DensityOverlayContext) => ReactNodeA host layer drawn over the plot (above the zone outlines, below the tooltip) — e.g. an editor for the zones. It receives the current window, the plot box and both projections; it re-renders on every view change. Pointer events reach the chart unless the layer handles them itself.
hiddenKeysReadonlySet<string>Hidden classes, controlled. Keys are zone ids / category labels.
onHiddenKeysChange(keys: ReadonlySet<string>) => void
onLegendItemClick(key: string, event: ReactMouseEvent | ReactKeyboardEvent) => voidA click on a legend entry, handed to the host instead of the chart's own behaviour. With `legend={{ toggleControl: "checkbox" }}` the checkboxes still hide and show classes, so an entry click is free for, say, a BI engine's own selection. Unset, an entry click toggles the class (or, in checkbox mode, selects its zone).
statusChartStatusLoading vs ready (RM-185). `"loading"` shows a skeleton in the plot box the chart will fill, with one polite status message, until the data is ready. Default: `"ready"`.
paletteChartPaletteColour ramp for `colorBy={{ kind: "category" }}` classes (RM-186), through `resolvePalette`. Unset: the twelve series tokens, as before. Zones keep their own `color`.

Also accepts: Omit<HTMLAttributes<HTMLDivElement>, "onSelect" | "onSelectionChange">, FrameSizeGroupProps, Pick<ChartStateGroupProps, "status">