import { DensityScatterChart } from "@elabs-ai/components-charts";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-chartsPaste 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
Used inside
Pairs with
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
| Prop | Type | Description |
|---|---|---|
datarequired | DensityScatterData | Columnar (preferred past ~50k) or rows. |
xDataKey | string | Row key for x when `data` is rows. Default `"x"`. Also the intent `field` for x ranges. |
xKey | string | Row 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. |
yDataKey | string | Row key for y when `data` is rows. Default `"y"`. |
yKey | string | Row key for y when `data` is rows. @deprecated Since 6.0.0, use `yDataKey`. Removed in 7.0.0. |
valueKeys | readonly string[] | Row keys lifted as numeric columns (rows input only). |
categoryKeys | readonly string[] | Row keys lifted as categorical columns (rows input only). |
zones | readonly DensityZone[] | Zones on the axes, inner → outer. |
outside | DensityOutsideZone | Label/colour for points matching no zone. |
colorBy | DensityColorBy | What colours the dots. Default `{ kind: "zone" }` (or `"density"` without zones). |
shapeBy | DensityShapeBy | Dot 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)`. |
valueKey | string | The value column whose cell mean the tooltip reports (a `colorBy: value` uses its own key). Unset: no mean row. |
cellSize | number | Bin size in CSS px. Default 5. Smaller = finer colour field, more cells. |
underlay | number | Underlay threshold (points per cell) — a faint body under dense areas. `0` turns it off. Default 4. |
pointRadius | number | Dot radius in CSS px at the home view; grows slightly with zoom. Default 1.35. |
zoom | boolean | Wheel zoom + drag pan. Default `true`. Off, whatever this says, under a host policy with `active: false` (`ChartConfigProvider` `interactions`). |
domain | Partial<DensityView> | The full-data window. Default: the data extent padded 3%. |
view | DensityView | |
defaultView | DensityView | |
onViewChange | (view: DensityView) => void | |
selection | DensityScatterSelection | |
defaultSelection | DensityScatterSelection | |
onSelectionChange | (selection: DensityScatterSelection) => void | |
selectionGestures | readonly 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) => void | Fires one intent per committed gesture. |
selectionField | string | Field name carried in x-range intents. Default `xDataKey`. |
selectionFieldY | string | Y-range intents carry this field. Default `yDataKey`. |
selectionToolbar | "auto" | "none" | `"auto"` (default): the toolbar shows when gestures are listed; `"none"` hides it. |
legend | ContainerLegendProp | Container legend (RM-118). `true` → `{ interactive: "toggle" }`. `{ values: true }` prints each entry's point count (per zone, category or the one density/value class). |
xLabel | ReactNode | Axis titles. |
yLabel | ReactNode | |
formatX | (value: number) => string | |
formatY | (value: number) => string | |
xAxis | DensityAxisOptions | Per-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). |
yAxis | DensityAxisOptions | |
minimap | boolean | The 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). |
sizeKey | string | Per-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] | |
pointOpacity | number | Multiplies the dots' opacity (0–1). Default `1`. |
densityFloor | number | The 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`. |
selectionTool | ChartSelectionToolMode | The 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>) => void | A 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>) => void | A 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 | undefined | Extra tooltip content for one hovered dot: a title (e.g. the row's dimension value) and rows appended after the coordinates. |
showLassoShape | boolean | Keep the committed lasso outline drawn while its constraint is active (default `true`). `false`: the dimmed / highlighted dots alone show it. |
formatValue | (value: number) => string | |
plotHeight | Responsive<ChartPlotHeight> | |
aspectRatio | string | |
margin | number | Partial<Margin> | Space around the plot: one number for every side, or per side. |
accessibleLabel | string | |
accessibleDescription | string | |
messages | DensityScatterLabels & ChartMessages | This 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.`. |
labels | DensityScatterLabels | @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) => void | Per-frame statistics (stories, diagnostics). |
renderer | "webgl" | "canvas2d" | Force the Canvas-2D path (tests, screenshots). |
zoneTags | boolean | In-plot zone tags (named buttons that select a zone). Default `true`. |
statLines | readonly 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) => ReactNode | A 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. |
hiddenKeys | ReadonlySet<string> | Hidden classes, controlled. Keys are zone ids / category labels. |
onHiddenKeysChange | (keys: ReadonlySet<string>) => void | |
onLegendItemClick | (key: string, event: ReactMouseEvent | ReactKeyboardEvent) => void | A 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). |
status | ChartStatus | Loading 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"`. |
palette | ChartPalette | Colour ramp for `colorBy={{ kind: "category" }}` classes (RM-186), through `resolvePalette`. Unset: the twelve series tokens, as before. Zones keep their own `color`. |