@antadesign/anta 0.3.29 → 0.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3771 @@
1
+ # Plot
2
+
3
+ A canvas-based plotting library for React, Preact, and plain JavaScript.
4
+ Compose plots using built-in and custom [series](#series), with configurable [axes](#axes), [scales](#scales), and [styles](#styling).
5
+ Explore your data through [tooltip](#tooltips), [selection](#selection-callbacks), and [zoom](#zoom-and-pan) interactions.
6
+
7
+ ## Series types
8
+
9
+ Select a series type below to see its options and examples.
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ ## Composed examples
21
+
22
+ Combine multiple series in one plot to compare values, show trends, and add context.
23
+
24
+
25
+
26
+ ```ts
27
+ import { area, line, rule, scatter, type PlotArgs, type Series } from '@antadesign/plot'
28
+ import '@antadesign/plot/elements/a-plot'
29
+ import type { APlotElement } from '@antadesign/plot/browser'
30
+
31
+ function overviewExample(): PlotArgs<Node> {
32
+ // Synthetic data, sampled every 15 minutes. No randomness: edits keep the same data.
33
+ const forecast = Array.from({ length: 97 }, (_, i) => {
34
+ const hour = i / 4
35
+ const morning = 150 * Math.exp(-(((hour - 9) / 2.8) ** 2))
36
+ const evening = 240 * Math.exp(-(((hour - 17) / 3.4) ** 2))
37
+ const expected = 85 + morning + evening
38
+ const spread = 22 + 12 * Math.sin(hour / 5) ** 2
39
+ return { hour, expected, low: expected - spread, high: expected + spread }
40
+ })
41
+
42
+ const observations = forecast.map((point, i) => {
43
+ const burst = i >= 61 && i <= 68 ? 65 * Math.sin((i - 60) / 9 * Math.PI) : 0
44
+ const actual = Math.round(point.expected + 13 * Math.sin(i * 1.8) + 8 * Math.cos(i * 0.7) + burst)
45
+ return { ...point, actual, outside: actual > point.high || actual < point.low }
46
+ })
47
+
48
+ const trend = observations.map((point, i) => {
49
+ const window = observations.slice(Math.max(0, i - 3), Math.min(observations.length, i + 4))
50
+ return { hour: point.hour, average: window.reduce((sum, row) => sum + row.actual, 0) / window.length }
51
+ })
52
+
53
+ const meanExpected = forecast.reduce((sum, point) => sum + point.expected, 0) / forecast.length
54
+
55
+ const series: Series<Node>[] = [
56
+ area<Node>({
57
+ data: forecast,
58
+ x: 'hour',
59
+ y: 'high',
60
+ y2: 'low',
61
+ color: { light: '#ede9fe', dark: '#30274e' },
62
+ hoverable: false,
63
+ }),
64
+ rule<Node>({
65
+ y: meanExpected,
66
+ color: { light: '#d97706', dark: '#fbbf24' },
67
+ width: 2,
68
+ dash: [8, 4],
69
+ hoverable: false,
70
+ }),
71
+ line<Node>({
72
+ data: forecast,
73
+ x: 'hour',
74
+ y: 'expected',
75
+ color: { light: '#8b5cf6', dark: '#c4b5fd' },
76
+ width: 2,
77
+ dash: [5, 5],
78
+ tooltip: ({ x, y }) => document.createTextNode('Forecast at ' + x + 'h: ' + Math.round(Number(y)) + ' requests/s'),
79
+ }),
80
+ line<Node>({
81
+ data: trend,
82
+ x: 'hour',
83
+ y: 'average',
84
+ color: { light: '#0891b2', dark: '#67e8f9' },
85
+ width: 3,
86
+ tooltip: ({ x, y }) => document.createTextNode('Trend at ' + x + 'h: ' + Math.round(Number(y)) + ' requests/s'),
87
+ }),
88
+ scatter<Node>({
89
+ data: observations.filter(point => !point.outside),
90
+ x: 'hour',
91
+ y: 'actual',
92
+ color: { light: '#0891b2', dark: '#67e8f9' },
93
+ size: 4,
94
+ tooltip: ({ x, y }) => document.createTextNode('Observed at ' + x + 'h: ' + y + ' requests/s'),
95
+ }),
96
+ scatter<Node>({
97
+ data: observations.filter(point => point.outside),
98
+ x: 'hour',
99
+ y: 'actual',
100
+ color: { light: '#e76f51', dark: '#fda489' },
101
+ mark: 'diamond',
102
+ size: 9,
103
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 1 },
104
+ tooltip: ({ x, y }) => document.createTextNode('Outside forecast range: ' + x + 'h · ' + y + ' requests/s'),
105
+ }),
106
+ ]
107
+
108
+ const options = {
109
+
110
+ height: 400,
111
+ border: false,
112
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
113
+ background: { light: '#ffffff', dark: '#151b28' },
114
+ theme_invert: false,
115
+ }
116
+
117
+ const axis = {
118
+
119
+ x: { min: 0, max: 24, label: 'Hour' },
120
+
121
+ y: { min: 0, max: 440, label: 'Requests/s' },
122
+
123
+ }
124
+
125
+ const margin = 12
126
+
127
+ const grid = false
128
+
129
+ const zoom_pan = { x: true, y: true, modifier: false }
130
+
131
+ // Replace the object to apply a change; Plot does not observe in-place mutation.
132
+ const plotArgs: PlotArgs<Node> = { ...options, series, axis, margin, grid, zoom_pan }
133
+
134
+ return plotArgs
135
+ }
136
+
137
+ const plot = document.createElement('a-plot') as APlotElement
138
+ const args = overviewExample()
139
+ plot.plotArgs = {
140
+ ...args,
141
+ height: 280,
142
+ margin: { top: 12, right: 16, bottom: 42, left: 74 },
143
+ zoom_pan: false,
144
+ }
145
+ document.body.append(plot)
146
+ ```
147
+
148
+
149
+
150
+ ```ts
151
+ import { bar, scatter, type PlotArgs } from '@antadesign/plot'
152
+ import '@antadesign/plot/elements/a-plot'
153
+ import type { APlotElement } from '@antadesign/plot/browser'
154
+
155
+ function energyExample(): PlotArgs<Node> {
156
+ const data = [
157
+ { x: 'A', solar: 28, wind: 35, demand: 72 },
158
+ { x: 'B', solar: 48, wind: 18, demand: 54 },
159
+ { x: 'C', solar: 19, wind: 25, demand: 62 },
160
+ { x: 'D', solar: 36, wind: 42, demand: 69 },
161
+ { x: 'E', solar: 52, wind: 12, demand: 78 },
162
+ { x: 'F', solar: 24, wind: 30, demand: 46 },
163
+ { x: 'G', solar: 41, wind: 28, demand: 58 },
164
+ { x: 'H', solar: 16, wind: 22, demand: 51 },
165
+ ]
166
+ return {
167
+ series: [
168
+ bar<Node>({ data, y: ['solar', 'wind'],
169
+ color: [{ light: '#fbbf24', dark: '#d97706' }, { light: '#2dd4bf', dark: '#0d9488' }],
170
+ inset: 5, border_radius: 3,
171
+ tooltip: ({ x, y, label }) => document.createTextNode('Site ' + x + ' · ' + label + ': ' + y + ' kW') }),
172
+ scatter<Node>({ data, y: 'demand', mark: 'diamond', size: 7,
173
+ tooltip: ({ x, y }) => document.createTextNode('Site ' + x + ' · Demand: ' + y + ' kW'),
174
+ color: { light: '#7c3aed', dark: '#c4b5fd' },
175
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 1.5 } }),
176
+ ],
177
+ axis: { x: { label: 'Site' }, y: { min: 0, max: 90, label: 'Power (kW)' } },
178
+ background: { light: '#ffffff', dark: '#151b28' },
179
+ border: false,
180
+ grid: false,
181
+ zoom_pan: false,
182
+ }
183
+ }
184
+
185
+ const plot = document.createElement('a-plot') as APlotElement
186
+ const args = energyExample()
187
+ plot.plotArgs = {
188
+ ...args,
189
+ height: 280,
190
+ margin: { top: 12, right: 16, bottom: 42, left: 58 },
191
+ zoom_pan: false,
192
+ }
193
+ document.body.append(plot)
194
+ ```
195
+
196
+
197
+
198
+ ```ts
199
+ import { area, rect, line, type PlotArgs } from '@antadesign/plot'
200
+ import '@antadesign/plot/elements/a-plot'
201
+ import type { APlotElement } from '@antadesign/plot/browser'
202
+
203
+ function latencyExample(): PlotArgs<Node> {
204
+ const density = (x: number) => 48 * Math.exp(-(((x - 65) / 24) ** 2))
205
+ + 19 * Math.exp(-(((x - 135) / 38) ** 2))
206
+ const bins = Array.from({ length: 24 }, (_, i) => {
207
+ const x = i * 10
208
+ return { x: x + 1, x2: x + 9, y: density(x + 5) * (0.8 + 0.3 * Math.sin(i * 2.3) ** 2), y2: 0 }
209
+ })
210
+ const curve = Array.from({ length: 121 }, (_, i) => ({ x: i * 2, y: density(i * 2) }))
211
+ return {
212
+ series: [
213
+ area<Node>({ data: curve, y2: 0, color: { light: '#fce7f3', dark: '#452039' }, hoverable: false }),
214
+ rect<Node>({ data: bins, color: { light: '#f472b6', dark: '#be185d' },
215
+ tooltip: ({ row, y }) => document.createTextNode(
216
+ (Number(row.x) - 1) + '–' + (Number(row.x2) + 1) + ' ms · ' + Math.round(Number(y)) + ' requests',
217
+ ) }),
218
+ line<Node>({ data: curve, color: { light: '#9d174d', dark: '#fbcfe8' }, width: 2.5,
219
+ tooltip: ({ x, y }) => document.createTextNode('Distribution at ' + x + ' ms: ' + Number(y).toFixed(1) + ' requests') }),
220
+
221
+ ],
222
+ axis: { x: { min: 0, max: 240, label: 'Latency (ms)' }, y: { min: 0, max: 60, label: 'Requests' } },
223
+ background: { light: '#ffffff', dark: '#151b28' },
224
+ border: false,
225
+ grid: false,
226
+ zoom_pan: false,
227
+ }
228
+ }
229
+
230
+ const plot = document.createElement('a-plot') as APlotElement
231
+ const args = latencyExample()
232
+ plot.plotArgs = {
233
+ ...args,
234
+ height: 280,
235
+ margin: { top: 12, right: 16, bottom: 42, left: 58 },
236
+ zoom_pan: false,
237
+ }
238
+ document.body.append(plot)
239
+ ```
240
+
241
+
242
+
243
+ ```ts
244
+ import { area, rect, line, scatter, rule, type PlotArgs } from '@antadesign/plot'
245
+ import '@antadesign/plot/elements/a-plot'
246
+ import type { APlotElement } from '@antadesign/plot/browser'
247
+
248
+ function mixedExample(): PlotArgs<Node> {
249
+ const points = [
250
+ { x: 12, y: 74 }, { x: 25, y: 57 }, { x: 39, y: 85 }, { x: 48, y: 18 },
251
+ { x: 62, y: 70 }, { x: 73, y: 37 }, { x: 86.5, y: 86 }, { x: 92, y: 22 },
252
+ { x: 15, y: 27 }, { x: 36, y: 64 }, { x: 57, y: 88 },
253
+ ]
254
+ const paths = [
255
+ [
256
+ { x: 25, y: 0 }, { x: 36, y: 19 }, { x: 41, y: 15 }, { x: 56, y: 37 },
257
+ { x: 65, y: 23 }, { x: 72, y: 48 }, { x: 76, y: 44 }, { x: 93, y: 63 }, { x: 100, y: 68 },
258
+ ],
259
+ [
260
+ { x: 0, y: 73 }, { x: 9, y: 87 }, { x: 13, y: 84 }, { x: 20, y: 92 },
261
+ { x: 29, y: 79 }, { x: 39, y: 96 }, { x: 42, y: 93 }, { x: 48, y: 100 },
262
+ ],
263
+ ]
264
+ // Sample cosine transitions to round each turn without changing its height.
265
+ const smoothPaths = paths.map(path => path.flatMap((point, i) => {
266
+ const next = path[i + 1]
267
+ if (!next) return [point]
268
+ return Array.from({ length: 16 }, (_, j) => {
269
+ const t = j / 16
270
+ const blend = (1 - Math.cos(Math.PI * t)) / 2
271
+ return { x: point.x + (next.x - point.x) * t, y: point.y + (next.y - point.y) * blend }
272
+ })
273
+ }))
274
+ const colors = [
275
+ { light: '#7c3aed', dark: '#c4b5fd' },
276
+ { light: '#e11d48', dark: '#fda4af' },
277
+ { light: '#0284c7', dark: '#7dd3fc' },
278
+ { light: '#d97706', dark: '#fcd34d' },
279
+ ]
280
+ return {
281
+ series: [
282
+ area<Node>({ data: Array.from({ length: 81 }, (_, i) => {
283
+ const x = i * 1.25
284
+ const center = 50 + 7 * Math.sin(i / 9) + 3 * Math.cos(i / 4)
285
+ const spread = 3 + 1.5 * Math.sin(i / 11) ** 2
286
+ return { x, y: center + spread, y2: center - spread }
287
+ }), color: { light: '#99f6e4', dark: '#115e59' },
288
+ stroke: { color: { light: '#14b8a6', dark: '#5eead4' }, width: 1 } }),
289
+ ...points.map((point, i) => scatter<Node>({ data: [point], size: [7, 12, 21, 10, 15, 9, 26, 13, 11, 8, 14][i],
290
+ mark: 'circle', color: colors[i % colors.length],
291
+ tooltip: ({ x, y }) => document.createTextNode('Point ' + (i + 1) + ' · (' + x + ', ' + y + ')'),
292
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 1.5 } })),
293
+ rect<Node>({ data: [{ x: 32, x2: 44, y: 31, y2: 47 }],
294
+ color: { light: '#ddd6fe', dark: '#5b21b6' },
295
+ stroke: { color: { light: '#8b5cf6', dark: '#c4b5fd' }, width: 1.5 } }),
296
+ rect<Node>({ data: [{ x: 76, x2: 97, y: 76, y2: 96 }],
297
+ color: { light: 'rgba(254, 215, 170, 0.55)', dark: 'rgba(154, 52, 18, 0.55)' },
298
+ stroke: { color: { light: '#f97316', dark: '#fdba74' }, width: 1.5 } }),
299
+ ...smoothPaths.map((data, i) => line<Node>({ data, width: 2.5, dash: i === 1 ? [7, 5] : [],
300
+ tooltip: ({ x, y }) => document.createTextNode(
301
+ 'Line ' + (i + 1) + ' · (' + Number(x).toFixed(1) + ', ' + Number(y).toFixed(1) + ')',
302
+ ),
303
+ color: i % 2 === 0 ? { light: '#0d9488', dark: '#5eead4' }
304
+ : { light: '#f97316', dark: '#fdba74' },
305
+ })),
306
+
307
+ rule<Node>({ y: 10, color: { light: '#0284c7', dark: '#7dd3fc' }, width: 1.5, dash: [6, 4] }),
308
+ ],
309
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
310
+ background: { light: '#ffffff', dark: '#151b28' },
311
+ border: false,
312
+ grid: false,
313
+ zoom_pan: false,
314
+ }
315
+ }
316
+
317
+ const plot = document.createElement('a-plot') as APlotElement
318
+ const args = mixedExample()
319
+ plot.plotArgs = {
320
+ ...args,
321
+ height: 280,
322
+ margin: { top: 12, right: 16, bottom: 42, left: 58 },
323
+ zoom_pan: false,
324
+ }
325
+ document.body.append(plot)
326
+ ```
327
+
328
+ ## Install
329
+
330
+ Install the package to use plots in React, Preact, or plain JavaScript.
331
+
332
+ ```sh
333
+ npm install @antadesign/plot # or pnpm / bun
334
+ ```
335
+
336
+ Import `Plot` from `/components` and series factories from `@antadesign/plot`.
337
+ React 19 is a peer dependency. For Preact, use `preact/compat` aliases.
338
+
339
+ ```tsx
340
+ import { Plot } from '@antadesign/plot/components'
341
+ import { line } from '@antadesign/plot'
342
+ import '@antadesign/plot/elements/a-plot-surface'
343
+ import '@antadesign/anta/elements/a-tooltip'
344
+
345
+ const plotArgs = {
346
+ height: 300,
347
+ series: [line({ data: [{ x: 1, y: 3 }, { x: 2, y: 5 }, { x: 3, y: 4 }] })],
348
+ }
349
+
350
+ function Example() {
351
+ return <Plot plotArgs={plotArgs} />
352
+ }
353
+ ```
354
+
355
+ `Plot` is a function component that uses Anta's configured JSX factory and hooks.
356
+ It needs no DOM registration. Import the surface and tooltip registration modules
357
+ once in your browser entry; their elements install their styles automatically.
358
+ `Plot` handles canvas setup, drawing, interactions, and cleanup.
359
+
360
+ By default, `Plot` fills its parent. Give the parent a height or set `plotArgs.height`,
361
+ as above. Custom renderers supply their hooks through Anta's `configure()`.
362
+ With a worker renderer and a DOM bridge that transfers the surface's canvases,
363
+ `Plot` runs in the worker while `a-plot-surface` stays in the browser.
364
+
365
+ For plain JavaScript or TypeScript, import `@antadesign/plot/elements/a-plot`, then use `<a-plot>` and assign its `plotArgs` property.
366
+
367
+ For custom host integrations, `/components` also exports `PlotSurface`.
368
+ Register it with `@antadesign/plot/elements/a-plot-surface`; your host manages
369
+ controllers, drawing, and tooltip content.
370
+
371
+ ## Plot arguments
372
+
373
+ `PlotArgs` configures the whole plot: its series, axes, and presentation. Pass it
374
+ as the required `plotArgs` prop to `<Plot plotArgs={plotArgs} />`, or assign it to
375
+ the `plotArgs` property on `<a-plot>`. Replace the object to apply changes;
376
+ in-place mutation is not observed.
377
+
378
+ Title, background, and grid
379
+
380
+ Set a title, reserve space with `margin`, and style the plot background and horizontal grid lines.
381
+
382
+ ```ts
383
+ import { scatter, type PlotArgs } from '@antadesign/plot'
384
+ import '@antadesign/plot/elements/a-plot'
385
+ import type { APlotElement } from '@antadesign/plot/browser'
386
+
387
+ const examplePoints = [
388
+ { x: 8, y: 14 }, { x: 36, y: 40 }, { x: 52, y: 32 },
389
+ { x: 44, y: 59 }, { x: 64, y: 54 }, { x: 92, y: 86 },
390
+ ]
391
+
392
+ function presentation(): PlotArgs<Node> {
393
+ return {
394
+ series: [scatter<Node>({ data: examplePoints, size: 3,
395
+ color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
396
+ title: { text: 'Plot title', size: 16, color: { light: '#374151', dark: '#e5e7eb' } },
397
+ height: 260,
398
+ margin: { top: 38, right: 16, bottom: 42, left: 52 },
399
+ background: { light: '#f3f4f6', dark: '#202124' },
400
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
401
+ grid: { x: false, y: true },
402
+ border: true,
403
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
404
+ zoom_pan: false,
405
+ }
406
+ }
407
+
408
+ const plot = document.createElement('a-plot') as APlotElement
409
+ plot.plotArgs = presentation()
410
+ document.body.append(plot)
411
+ ```
412
+
413
+ Border and spacing
414
+
415
+ Set `border: true` and `grid: false` to frame the plot without grid lines. Use `margin` to reserve space around the axes and their labels.
416
+
417
+ ```ts
418
+ import { scatter, type PlotArgs } from '@antadesign/plot'
419
+ import '@antadesign/plot/elements/a-plot'
420
+ import type { APlotElement } from '@antadesign/plot/browser'
421
+
422
+ const examplePoints = [
423
+ { x: 8, y: 14 }, { x: 36, y: 40 }, { x: 52, y: 32 },
424
+ { x: 44, y: 59 }, { x: 64, y: 54 }, { x: 92, y: 86 },
425
+ ]
426
+
427
+ function spacing(): PlotArgs<Node> {
428
+ return {
429
+ series: [scatter<Node>({ data: examplePoints, size: 3,
430
+ color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
431
+ height: 260,
432
+ margin: { top: 40, right: 40, bottom: 68, left: 88 },
433
+ background: { light: '#ffffff', dark: '#202124' },
434
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
435
+ border: true,
436
+ grid: false,
437
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
438
+ zoom_pan: false,
439
+ }
440
+ }
441
+
442
+ const plot = document.createElement('a-plot') as APlotElement
443
+ plot.plotArgs = spacing()
444
+ document.body.append(plot)
445
+ ```
446
+
447
+ | Field | Type | Default | Description |
448
+ |---|---|---|---|
449
+ | [`series`](#series) | `Series[]` | Required | What to draw, in paint order. Build each one with a factory. |
450
+ | [`axis?`](#axes) | `{ x?: AxisArgs, y?: AxisArgs }` | Inferred from data | Per-axis scale, label, ticks, and domain. |
451
+ | [`title?`](#title) | `string \| { text, size?, color? }` | None | Plot title. |
452
+ | `width?` | `number` | Container width | Canvas width in pixels. Overrides the wrapper's CSS width. |
453
+ | `height?` | `number` | Parent height for `Plot`; `300` for `<a-plot>` | Canvas height in pixels. Overrides the host's CSS height. |
454
+ | [`margin?`](#margins) | `number \| { top?, right?, bottom?, left? }` | `60` per side; `2` without axes or title | Space reserved around the plot area for axes and title. |
455
+ | `border?` | `boolean` | `true` | Draws a border around the plot area. |
456
+ | [`grid?`](#grid) | `boolean \| { x?, y? }` | `false` | Grid lines, per axis. |
457
+ | [`chrome_color?`](#theme-colors) | `string \| { light, dark }` | Theme defaults | Color for axis lines, tick marks, grid lines, and the plot border. Text colors are configured separately. |
458
+ | [`background?`](#theme-colors) | `boolean \| string \| { light, dark }` | White | Plot fill. `false` paints none; `true` and an absent value use `#fff`. |
459
+ | [`theme_invert?`](#theme-colors) | `boolean` | Automatic | Forces dark-mode inversion on or off. Absent means invert unless a series color or the background is a `{ light, dark }` pair. |
460
+ | [`zoom_pan?`](#zoom-and-pan) | `boolean \| { x?, y?, modifier? }` | Both axes, Ctrl required | Require Ctrl for zoom and pan by default. Set `modifier: false` to allow gestures without Ctrl. Set both axes to `false` to disable zoom and pan. |
461
+ | [`viewport?`](#viewport) | `{ x?, y?, key? }` | Full domain | Requested starting window per axis. Applied at mount and on each `key` change, clamped to the full domain. |
462
+ | [`on_viewport_change?`](#viewport-changes) | `(change: ViewportChange) => void` | None | Fires with each axis's current window and full extent after a gesture or reset. |
463
+
464
+ A zoomed plot shows a **Reset zoom** button in the plot area; it
465
+ disappears once the view is back to its full extent.
466
+
467
+ ### Axes
468
+
469
+ `axis.x` and `axis.y` both accept `AxisArgs`. Configure them independently.
470
+ Omitted fields are inferred from the series or use the documented defaults.
471
+ [`ThemeColor`](#theme-colors) accepts a CSS color string or a `{ light, dark }` pair.
472
+
473
+ Logarithmic scale
474
+
475
+ Use a logarithmic axis to compare values across orders of magnitude. Format large tick values with a `k` suffix.
476
+
477
+ ```ts
478
+ import { scatter, type PlotArgs } from '@antadesign/plot'
479
+ import '@antadesign/plot/elements/a-plot'
480
+ import type { APlotElement } from '@antadesign/plot/browser'
481
+
482
+ const examplePoints = [
483
+ { x: 8, y: 14 }, { x: 36, y: 40 }, { x: 52, y: 32 },
484
+ { x: 44, y: 59 }, { x: 64, y: 54 }, { x: 92, y: 86 },
485
+ ]
486
+
487
+ function logarithmic(): PlotArgs<Node> {
488
+ const data = examplePoints.map(({ x, y }) => ({ x, y: 10 ** (y / 20 - 1) }))
489
+ return {
490
+ series: [scatter<Node>({ data, size: 3,
491
+ color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
492
+ height: 260,
493
+ margin: { top: 12, right: 14, bottom: 44, left: 80 },
494
+ axis: {
495
+ x: { min: 0, max: 100, label: 'Batch' },
496
+ y: { scale: 'log', min: 0.1, max: 10000, label: 'Duration (ms)',
497
+ tick_label: { format: value => Number(value) >= 1000 ? Number(value) / 1000 + 'k' : String(value) } },
498
+ },
499
+ background: { light: '#ffffff', dark: '#151b28' },
500
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
501
+ grid: { x: false, y: true },
502
+ border: true,
503
+ zoom_pan: false,
504
+ }
505
+ }
506
+
507
+ const plot = document.createElement('a-plot') as APlotElement
508
+ plot.plotArgs = logarithmic()
509
+ document.body.append(plot)
510
+ ```
511
+
512
+ Time and tick formatting
513
+
514
+ Plot timestamps on a UTC axis and format the horizontal ticks as hours and minutes. Add a percent suffix to the vertical ticks.
515
+
516
+ ```ts
517
+ import { scatter, type PlotArgs } from '@antadesign/plot'
518
+ import '@antadesign/plot/elements/a-plot'
519
+ import type { APlotElement } from '@antadesign/plot/browser'
520
+
521
+ const examplePoints = [
522
+ { x: 8, y: 14 }, { x: 36, y: 40 }, { x: 52, y: 32 },
523
+ { x: 44, y: 59 }, { x: 64, y: 54 }, { x: 92, y: 86 },
524
+ ]
525
+
526
+ function timeAxis(): PlotArgs<Node> {
527
+ const start = Date.UTC(2026, 0, 12)
528
+ const data = examplePoints.map(({ x, y }) => ({ x: start + x / 100 * 86400000, y }))
529
+ return {
530
+ series: [scatter<Node>({ data, size: 3,
531
+ color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
532
+ height: 260,
533
+ margin: { top: 12, right: 16, bottom: 44, left: 76 },
534
+ axis: {
535
+ x: { scale: 'utc', min: start, max: start + 86400000, label: 'Time (UTC)', padding: 8,
536
+ tick_label: { format: value => new Date(Number(value)).toISOString().slice(11, 16) } },
537
+ y: { min: 0, max: 100, label: 'Utilization', tick_mark: false,
538
+ tick_label: { format: value => value + '%' } },
539
+ },
540
+ background: { light: '#ffffff', dark: '#151b28' },
541
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
542
+ grid: { x: true, y: false },
543
+ border: true,
544
+ zoom_pan: false,
545
+ }
546
+ }
547
+
548
+ const plot = document.createElement('a-plot') as APlotElement
549
+ plot.plotArgs = timeAxis()
550
+ document.body.append(plot)
551
+ ```
552
+
553
+ | Field | Type | Default | Description |
554
+ |---|---|---|---|
555
+ | [`scale?`](#scales) | `'linear' \| 'log' \| 'category' \| 'time' \| 'utc'` | Inferred | Numeric data uses a linear scale; string data uses categories. Time scales take timestamps in milliseconds; `time` formats in local time and `utc` in UTC. |
556
+ | [`label?`](#axis-labels) | `LabelArg` | Shared field name, if available | Axis name, as text or an object with presentation options. Use `''` to omit it. |
557
+ | `min?` | `number` | Inferred from data | Lower continuous-domain bound. |
558
+ | `max?` | `number` | Inferred from data | Upper continuous-domain bound. |
559
+ | `categories?` | `string[]` | Inferred from data | Explicit category list and order; also declares a categorical axis. |
560
+ | `padding?` | `number` | `0` | Pixel padding at both ends of the axis, inside the plot area. |
561
+ | `padding_left?` | `number` | `padding` | Left-end padding for `axis.x`. |
562
+ | `padding_right?` | `number` | `padding` | Right-end padding for `axis.x`. |
563
+ | `padding_top?` | `number` | `padding` | Top-end padding for `axis.y`. |
564
+ | `padding_bottom?` | `number` | `padding` | Bottom-end padding for `axis.y`. |
565
+ | `grid_align?` | `'center' \| 'edge'` | `'center'` | Position grid lines at category centers or boundaries. Categorical axes only. |
566
+ | `line?` | `boolean` | `true` | Draw the axis line. |
567
+ | `hidden?` | `boolean` | `false` | Hide the axis presentation while retaining its scale. |
568
+ | [`tick_label?`](#tick-labels) | `TickLabelArg` | Automatic formatting | Tick text formatting, size, and color. |
569
+ | `tick_mark?` | `boolean` | `true` | Draw tick marks. Does not hide tick labels. |
570
+
571
+ ### Series
572
+
573
+ A series describes a set of data and how to draw it. Pass one or more series
574
+ in `plotArgs.series`; they are drawn in array order. Import the series
575
+ functions from `@antadesign/plot`.
576
+
577
+ The default column describes omitted fields. Examples may override these defaults.
578
+ See [Data fields](#data-fields), [Series colors](#series-colors), [Strokes](#strokes),
579
+ and [Mark shapes](#mark-shapes) for shared configuration types.
580
+ For callbacks, see [Tooltips](#tooltips) and [Selection callbacks](#selection-callbacks).
581
+
582
+ #### Scatter
583
+
584
+ Draw a mark at each data point. Use `size`, [`mark`](#mark-shapes), and [`stroke`](#strokes) to control
585
+ its appearance. Set `tooltip: true` for a tooltip showing the point's values.
586
+
587
+ Size and color from data
588
+
589
+ Use a size accessor to vary mark diameter and a color accessor to highlight larger values. An outline keeps overlapping bubbles distinct.
590
+
591
+ ```ts
592
+ import { scatter, type APlotElement } from '@antadesign/plot/browser'
593
+ import '@antadesign/plot/elements/a-plot'
594
+
595
+ const plot = document.createElement('a-plot') as APlotElement
596
+
597
+ const data = Array.from({ length: 24 }, (_, i) => ({
598
+ x: 12 + (i * 17 % 83),
599
+ y: 15 + (i * 31 % 72),
600
+ volume: 20 + (i * 43 % 180),
601
+ }))
602
+
603
+ plot.plotArgs = {
604
+ series: [scatter({
605
+ data,
606
+ size: row => (5 + Math.sqrt(Number(row.volume)) * 1.8) * 1.3,
607
+ color: row => Number(row.volume) > 120
608
+ ? { light: '#e76f51', dark: '#fda489' }
609
+ : { light: '#0891b2', dark: '#67e8f9' },
610
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 2 },
611
+ tooltip: ({ row }) => document.createTextNode('Volume: ' + row.volume),
612
+ })],
613
+ height: 260,
614
+ margin: { top: 12, right: 12, bottom: 28, left: 36 },
615
+ axis: { x: { min: 0, max: 110, label: '' }, y: { min: 0, max: 100, label: '' } },
616
+ background: { light: '#ffffff', dark: '#151b28' },
617
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
618
+ border: false,
619
+ zoom_pan: false,
620
+ }
621
+
622
+ document.body.append(plot)
623
+ ```
624
+
625
+ Mark shapes and outlines
626
+
627
+ Use different marks to distinguish groups without relying on color alone. Each series sets its own shape, size, and stroke.
628
+
629
+ ```ts
630
+ import { scatter, type APlotElement } from '@antadesign/plot/browser'
631
+ import '@antadesign/plot/elements/a-plot'
632
+
633
+ const plot = document.createElement('a-plot') as APlotElement
634
+
635
+ const data = Array.from({ length: 18 }, (_, i) => ({
636
+ x: 10 + i * 4.5,
637
+ y: 18 + i * 2.5 + Math.sin(i * 2.1) * 12,
638
+ }))
639
+
640
+ plot.plotArgs = {
641
+ series: [
642
+ scatter({
643
+ data,
644
+ mark: 'square',
645
+ size: 11,
646
+ color: { light: '#c4b5fd', dark: '#8b5cf6' },
647
+ stroke: { color: { light: '#6d28d9', dark: '#ddd6fe' }, width: 2 },
648
+ tooltip: ({ x, y }) => document.createTextNode('Squares: ' + x + ', ' + Math.round(Number(y))),
649
+ }),
650
+ scatter({
651
+ data: data.map(point => ({ x: point.x + 5, y: 95 - point.y })),
652
+ mark: 'diamond',
653
+ size: 13,
654
+ color: { light: '#99f6e4', dark: '#0d9488' },
655
+ stroke: { color: { light: '#0f766e', dark: '#5eead4' }, width: 2 },
656
+ tooltip: ({ x, y }) => document.createTextNode('Diamonds: ' + x + ', ' + Math.round(Number(y))),
657
+ }),
658
+ ],
659
+ height: 260,
660
+ margin: { top: 12, right: 12, bottom: 28, left: 36 },
661
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
662
+ background: { light: '#ffffff', dark: '#151b28' },
663
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
664
+ border: false,
665
+ zoom_pan: false,
666
+ }
667
+
668
+ document.body.append(plot)
669
+ ```
670
+
671
+ Scatter arguments
672
+
673
+ | Field | Type | Default | Description |
674
+ |---|---|---|---|
675
+ | `data` | `Record<string, unknown>[]` | Required | Rows used to build the series. |
676
+ | `x?` | [`FieldArg`](#data-fields) | `'x'` | Horizontal field name or accessor. |
677
+ | `y?` | [`FieldArg`](#data-fields) | `'y'` | Vertical field name or accessor. |
678
+ | `color?` | [`ColorArg`](#series-colors) | Black | Series color; theme pairs provide separate light and dark colors. |
679
+ | `size?` | `number \| ((row, index) => number)` | `5` | Mark diameter in pixels, or a per-row accessor. A row’s `size` takes precedence. |
680
+ | [`mark?`](#mark-shapes) | `MarkShape` | `'circle'` | Mark shape: circle, square, diamond, or triangle. |
681
+ | [`stroke?`](#strokes) | `StrokeArg` | None | Outline color and optional width. |
682
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
683
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
684
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
685
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
686
+
687
+ ```ts
688
+ import { scatter } from '@antadesign/plot'
689
+
690
+ const series = [scatter({
691
+ data: [{ time: 1, value: 3 }, { time: 2, value: 5 }, { time: 3, value: 4 }],
692
+ x: 'time',
693
+ y: 'value',
694
+ color: (row) => Number(row.value) >= 5 ? 'coral' : 'steelblue',
695
+ size: (row) => Number(row.value) * 2,
696
+ mark: 'diamond',
697
+ stroke: { color: 'navy', width: 1 },
698
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
699
+ on_select: ({ row }) => console.log('Selected row:', row),
700
+ hoverable: true,
701
+ highlight: true,
702
+ })]
703
+ ```
704
+
705
+ #### Line
706
+
707
+ Connect points in data order. Set `width` and `dash` for the line, or add
708
+ [`mark`](#mark-shapes) and `mark_size` to show the individual points.
709
+
710
+ Solid and dashed lines
711
+
712
+ Compare measured values with a forecast. A solid line follows each fluctuation, while a dashed line distinguishes the expected trend.
713
+
714
+ ```ts
715
+ import { line, type APlotElement } from '@antadesign/plot/browser'
716
+ import '@antadesign/plot/elements/a-plot'
717
+
718
+ const plot = document.createElement('a-plot') as APlotElement
719
+
720
+ const data = Array.from({ length: 97 }, (_, i) => ({
721
+ x: i / 4,
722
+ expected: 45 + 22 * Math.sin(i / 18) + 12 * Math.cos(i / 7),
723
+ measured: 45 + 22 * Math.sin(i / 18) + 12 * Math.cos(i / 7)
724
+ + 7 * Math.sin(i * 1.1) + 4 * Math.cos(i * 2.3),
725
+ }))
726
+
727
+ plot.plotArgs = {
728
+ series: [
729
+ line<Node>({
730
+ data,
731
+ y: 'expected',
732
+ color: { light: '#c2410c', dark: '#fdba74' },
733
+ width: 2,
734
+ dash: [7, 5],
735
+ tooltip: ({ y }) => document.createTextNode('Expected: ' + Math.round(Number(y))),
736
+ }),
737
+ line<Node>({
738
+ data,
739
+ y: 'measured',
740
+ color: { light: '#0891b2', dark: '#67e8f9' },
741
+ width: 2,
742
+ tooltip: ({ y }) => document.createTextNode('Measured: ' + Math.round(Number(y))),
743
+ }),
744
+ ],
745
+ height: 260,
746
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
747
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 100, label: '' } },
748
+ background: { light: '#ffffff', dark: '#151b28' },
749
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
750
+ grid: { x: false, y: true },
751
+ border: false,
752
+ zoom_pan: false,
753
+ }
754
+
755
+ document.body.append(plot)
756
+ ```
757
+
758
+ Point marks and outlines
759
+
760
+ Show individual samples with diamond marks. Set the line width, mark size, and mark outline independently to keep each measurement visible.
761
+
762
+ ```ts
763
+ import { line, type APlotElement } from '@antadesign/plot/browser'
764
+ import '@antadesign/plot/elements/a-plot'
765
+
766
+ const plot = document.createElement('a-plot') as APlotElement
767
+
768
+ const data = Array.from({ length: 13 }, (_, i) => ({
769
+ x: i,
770
+ y: 35 + i * 2 + 18 * Math.sin(i * 0.8) + 8 * Math.cos(i * 1.7),
771
+ }))
772
+
773
+ plot.plotArgs = {
774
+ series: [line<Node>({
775
+ data,
776
+ color: { light: '#7c3aed', dark: '#c4b5fd' },
777
+ width: 3,
778
+ mark: 'diamond',
779
+ mark_size: 10,
780
+ mark_stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 2 },
781
+ tooltip: ({ x, y }) => document.createTextNode('Sample ' + x + ': ' + Math.round(Number(y))),
782
+ })],
783
+ height: 260,
784
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
785
+ axis: { x: { min: -0.5, max: 12.5, label: '' }, y: { min: 0, max: 100, label: '' } },
786
+ background: { light: '#ffffff', dark: '#151b28' },
787
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
788
+ grid: { x: false, y: true },
789
+ border: false,
790
+ zoom_pan: false,
791
+ }
792
+
793
+ document.body.append(plot)
794
+ ```
795
+
796
+ Line arguments
797
+
798
+ | Field | Type | Default | Description |
799
+ |---|---|---|---|
800
+ | `data` | `Record<string, unknown>[]` | Required | Rows used to build the series. |
801
+ | `x?` | [`FieldArg`](#data-fields) | `'x'` | Horizontal field name or accessor. |
802
+ | `y?` | [`FieldArg`](#data-fields) | `'y'` | Vertical field name or accessor. |
803
+ | `color?` | [`ThemeColor`](#theme-colors) | Black | Series color; theme pairs provide separate light and dark colors. |
804
+ | `width?` | `number` | `2` | Line width in pixels. |
805
+ | `dash?` | `number[]` | Solid | Alternating dash and gap lengths in pixels. |
806
+ | [`mark?`](#mark-shapes) | `MarkShape` | None | Shape drawn at each point. |
807
+ | `mark_size?` | `number` | `6` | Point marker diameter in pixels. |
808
+ | [`mark_stroke?`](#strokes) | `StrokeArg` | None | Outline for the point markers. |
809
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
810
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
811
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
812
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
813
+
814
+ ```ts
815
+ import { line } from '@antadesign/plot'
816
+
817
+ const series = [line({
818
+ data: [{ time: 1, value: 3 }, { time: 2, value: 5 }, { time: 3, value: 4 }],
819
+ x: 'time',
820
+ y: 'value',
821
+ color: { light: 'steelblue', dark: 'lightskyblue' },
822
+ width: 2,
823
+ dash: [6, 3],
824
+ mark: 'circle',
825
+ mark_size: 8,
826
+ mark_stroke: { color: 'navy', width: 1 },
827
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
828
+ on_select: ({ row }) => console.log('Selected row:', row),
829
+ hoverable: true,
830
+ highlight: true,
831
+ })]
832
+ ```
833
+
834
+ #### Bar
835
+
836
+ Draw one bar per category. Use a categorical field on one axis and a numeric
837
+ field on the other. Categories on `x` produce vertical bars; categories on
838
+ `y` produce horizontal bars.
839
+
840
+ Rounded bars and data colors
841
+
842
+ Set corner rounding and spacing independently. A color accessor highlights days with more than 80 requests per second.
843
+
844
+ ```ts
845
+ import { bar, type APlotElement } from '@antadesign/plot/browser'
846
+ import '@antadesign/plot/elements/a-plot'
847
+
848
+ const plot = document.createElement('a-plot') as APlotElement
849
+
850
+ const data = [
851
+ { day: 'Mon', requests: 42 },
852
+ { day: 'Tue', requests: 68 },
853
+ { day: 'Wed', requests: 91 },
854
+ { day: 'Thu', requests: 57 },
855
+ { day: 'Fri', requests: 84 },
856
+ { day: 'Sat', requests: 36 },
857
+ { day: 'Sun', requests: 24 },
858
+ ]
859
+
860
+ plot.plotArgs = {
861
+ series: [bar<Node>({
862
+ data,
863
+ x: 'day',
864
+ y: 'requests',
865
+ border_radius: 6,
866
+ inset: 5,
867
+ color: row => Number(row.requests) > 80
868
+ ? { light: '#c2410c', dark: '#fdba74' }
869
+ : { light: '#0891b2', dark: '#67e8f9' },
870
+ hover_span_x: true,
871
+ tooltip: ({ x, y }) => document.createTextNode(x + ': ' + y + ' requests/s'),
872
+ })],
873
+ height: 260,
874
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
875
+ axis: { x: { label: '' }, y: { min: 0, max: 100, label: '' } },
876
+ background: { light: '#ffffff', dark: '#151b28' },
877
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
878
+ grid: { x: false, y: true },
879
+ border: false,
880
+ zoom_pan: false,
881
+ }
882
+
883
+ document.body.append(plot)
884
+ ```
885
+
886
+ Horizontal bars
887
+
888
+ Place categories on the y-axis to leave room for their names. Hover anywhere along a category’s row to inspect its value.
889
+
890
+ ```ts
891
+ import { bar, type APlotElement } from '@antadesign/plot/browser'
892
+ import '@antadesign/plot/elements/a-plot'
893
+
894
+ const plot = document.createElement('a-plot') as APlotElement
895
+
896
+ const data = [
897
+ { service: 'Search', requests: 92 },
898
+ { service: 'Feed', requests: 78 },
899
+ { service: 'Checkout', requests: 64 },
900
+ { service: 'Profile', requests: 47 },
901
+ { service: 'Upload', requests: 31 },
902
+ ]
903
+
904
+ plot.plotArgs = {
905
+ series: [bar<Node>({
906
+ data,
907
+ x: 'requests',
908
+ y: 'service',
909
+ border_radius: 4,
910
+ inset: 8,
911
+ color: { light: '#7c3aed', dark: '#c4b5fd' },
912
+ hover_span_y: true,
913
+ tooltip: ({ x, y }) => document.createTextNode(y + ': ' + x + ' requests/s'),
914
+ })],
915
+ height: 260,
916
+ margin: { top: 5, right: 8, bottom: 20, left: 78 },
917
+ axis: { x: { min: 0, max: 100, label: '' }, y: { label: '' } },
918
+ background: { light: '#ffffff', dark: '#151b28' },
919
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
920
+ grid: { x: true, y: false },
921
+ border: false,
922
+ zoom_pan: false,
923
+ }
924
+
925
+ document.body.append(plot)
926
+ ```
927
+
928
+ Positive and negative values
929
+
930
+ Bars extend from zero in either direction. Use a color accessor to distinguish monthly gains from losses.
931
+
932
+ ```ts
933
+ import { bar, type APlotElement } from '@antadesign/plot/browser'
934
+ import '@antadesign/plot/elements/a-plot'
935
+
936
+ const plot = document.createElement('a-plot') as APlotElement
937
+
938
+ const data = [
939
+ { month: 'Jan', change: 24 },
940
+ { month: 'Feb', change: -16 },
941
+ { month: 'Mar', change: 38 },
942
+ { month: 'Apr', change: 12 },
943
+ { month: 'May', change: -28 },
944
+ { month: 'Jun', change: -9 },
945
+ { month: 'Jul', change: 31 },
946
+ ]
947
+
948
+ plot.plotArgs = {
949
+ series: [bar<Node>({
950
+ data,
951
+ x: 'month',
952
+ y: 'change',
953
+ inset: 6,
954
+ color: row => Number(row.change) >= 0
955
+ ? { light: '#0d9488', dark: '#5eead4' }
956
+ : { light: '#c2410c', dark: '#fdba74' },
957
+ hover_span_x: true,
958
+ tooltip: ({ x, y }) => document.createTextNode(x + ': ' + (Number(y) > 0 ? '+' : '') + y + '%'),
959
+ })],
960
+ height: 260,
961
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
962
+ axis: { x: { label: '' }, y: { min: -40, max: 40, label: '' } },
963
+ background: { light: '#ffffff', dark: '#151b28' },
964
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
965
+ grid: { x: false, y: true },
966
+ border: false,
967
+ zoom_pan: false,
968
+ }
969
+
970
+ document.body.append(plot)
971
+ ```
972
+
973
+ Custom category order
974
+
975
+ Set `axis.x.categories` to order bars by severity, independently of the source row order.
976
+
977
+ ```ts
978
+ import { bar, type PlotArgs } from '@antadesign/plot'
979
+ import '@antadesign/plot/elements/a-plot'
980
+ import type { APlotElement } from '@antadesign/plot/browser'
981
+
982
+ function categoryOrder(): PlotArgs<Node> {
983
+ const data = [
984
+ { severity: 'High', count: 28 }, { severity: 'Low', count: 46 },
985
+ { severity: 'Critical', count: 12 }, { severity: 'Medium', count: 35 },
986
+ ]
987
+ return {
988
+ series: [bar<Node>({ data, x: 'severity', y: 'count', inset: 10, border_radius: 3,
989
+ color: { light: '#64748b', dark: '#94a3b8' },
990
+ tooltip: ({ x, y }) => document.createTextNode(x + ': ' + y + ' issues'),
991
+ })],
992
+ height: 260,
993
+ margin: { top: 5, right: 12, bottom: 24, left: 30 },
994
+ axis: {
995
+ x: { categories: ['Critical', 'High', 'Medium', 'Low'], label: '' },
996
+ y: { min: 0, max: 50, label: '' },
997
+ },
998
+ background: { light: '#ffffff', dark: '#151b28' },
999
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1000
+ grid: { x: false, y: true },
1001
+ border: false,
1002
+ zoom_pan: false,
1003
+ }
1004
+ }
1005
+
1006
+ const plot = document.createElement('a-plot') as APlotElement
1007
+ plot.plotArgs = categoryOrder()
1008
+ document.body.append(plot)
1009
+ ```
1010
+
1011
+ Bar arguments
1012
+
1013
+ | Field | Type | Default | Description |
1014
+ |---|---|---|---|
1015
+ | `data` | `Record<string, unknown>[]` | Required | Rows used to build the series. |
1016
+ | `x?` | [`FieldArg \| string[]`](#data-fields) | `'x'` | Category or value field. An array of value fields creates horizontal stacks. |
1017
+ | `y?` | [`FieldArg \| string[]`](#data-fields) | `'y'` | Category or value field. An array of value fields creates vertical stacks. |
1018
+ | `color?` | [`ColorArg`](#series-colors)` \| ``ThemeColor``[]` | Black | Uniform or per-row color; a color array maps to stacked fields in order. |
1019
+ | `border_radius?` | `number` | `0` | Bar corner radius in pixels. |
1020
+ | `inset?` | `number` | One sixth of band width | Inset on each side of a categorical band, in pixels. |
1021
+ | `min_size?` | `number` | `2` | Minimum rendered value length in pixels. |
1022
+ | `hover_span_x?` | `boolean` | `false` | Extend the horizontal hit region across the categorical band. |
1023
+ | `hover_span_y?` | `boolean` | `false` | Extend the vertical hit region across the categorical band. |
1024
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
1025
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
1026
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
1027
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
1028
+
1029
+ ```ts
1030
+ import { bar } from '@antadesign/plot'
1031
+
1032
+ const series = [bar({
1033
+ data: [{ group: 'A', count: 8 }, { group: 'B', count: 12 }],
1034
+ x: 'group',
1035
+ y: 'count',
1036
+ color: { light: 'steelblue', dark: 'lightskyblue' },
1037
+ border_radius: 3,
1038
+ inset: 4,
1039
+ min_size: 2,
1040
+ hover_span_x: true,
1041
+ hover_span_y: false,
1042
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
1043
+ on_select: ({ row }) => console.log('Selected row:', row),
1044
+ hoverable: true,
1045
+ highlight: true,
1046
+ })]
1047
+ ```
1048
+
1049
+ ##### Stacked bars
1050
+
1051
+ Pass an array of value-field names to stack them within each category.
1052
+ Use `y` for vertical stacks or `x` for horizontal stacks. An array of
1053
+ colors assigns one color to each field, in the same order.
1054
+
1055
+ Stacked values
1056
+
1057
+ Pass multiple value fields to stack them within each category. Colors follow the field order; hover a segment to see its contribution.
1058
+
1059
+ ```ts
1060
+ import { bar, type APlotElement } from '@antadesign/plot/browser'
1061
+ import '@antadesign/plot/elements/a-plot'
1062
+
1063
+ const plot = document.createElement('a-plot') as APlotElement
1064
+
1065
+ const data = [
1066
+ { day: 'Mon', network: 18, compute: 42, storage: 24 },
1067
+ { day: 'Tue', network: 24, compute: 32, storage: 18 },
1068
+ { day: 'Wed', network: 12, compute: 56, storage: 28 },
1069
+ { day: 'Thu', network: 28, compute: 24, storage: 16 },
1070
+ { day: 'Fri', network: 16, compute: 48, storage: 22 },
1071
+ ]
1072
+
1073
+ plot.plotArgs = {
1074
+ series: [bar<Node>({
1075
+ data,
1076
+ x: 'day',
1077
+ y: ['network', 'compute', 'storage'],
1078
+ color: [
1079
+ { light: '#0d9488', dark: '#5eead4' },
1080
+ { light: '#6366f1', dark: '#a5b4fc' },
1081
+ { light: '#d97706', dark: '#fcd34d' },
1082
+ ],
1083
+ inset: 8,
1084
+ tooltip: ({ label, y }) => document.createTextNode(label + ': ' + y + ' ms'),
1085
+ })],
1086
+ height: 260,
1087
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1088
+ axis: { x: { label: '' }, y: { min: 0, max: 100, label: '' } },
1089
+ background: { light: '#ffffff', dark: '#151b28' },
1090
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1091
+ grid: { x: false, y: true },
1092
+ border: false,
1093
+ zoom_pan: false,
1094
+ }
1095
+
1096
+ document.body.append(plot)
1097
+ ```
1098
+
1099
+ Horizontal stacks
1100
+
1101
+ Pass multiple fields to `x` and categories to `y` to compare contributions across services.
1102
+
1103
+ ```ts
1104
+ import { bar, type PlotArgs } from '@antadesign/plot'
1105
+ import '@antadesign/plot/elements/a-plot'
1106
+ import type { APlotElement } from '@antadesign/plot/browser'
1107
+
1108
+ function stackedHorizontal(): PlotArgs<Node> {
1109
+ const data = [
1110
+ { service: 'Search', network: 18, compute: 46, storage: 14 },
1111
+ { service: 'Feed', network: 26, compute: 28, storage: 22 },
1112
+ { service: 'Upload', network: 42, compute: 16, storage: 32 },
1113
+ { service: 'Export', network: 12, compute: 36, storage: 18 },
1114
+ ]
1115
+ return {
1116
+ series: [bar<Node>({ data, x: ['network', 'compute', 'storage'], y: 'service',
1117
+ color: [
1118
+ { light: '#0d9488', dark: '#5eead4' },
1119
+ { light: '#6366f1', dark: '#a5b4fc' },
1120
+ { light: '#d97706', dark: '#fcd34d' },
1121
+ ],
1122
+ inset: 8, border_radius: 3,
1123
+ tooltip: ({ label, x }) => document.createTextNode(label + ': ' + x + ' ms'),
1124
+ })],
1125
+ height: 260,
1126
+ margin: { top: 5, right: 8, bottom: 20, left: 64 },
1127
+ axis: { x: { min: 0, max: 100, label: '' }, y: { label: '' } },
1128
+ background: { light: '#ffffff', dark: '#151b28' },
1129
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1130
+ grid: { x: true, y: false },
1131
+ border: false,
1132
+ zoom_pan: false,
1133
+ }
1134
+ }
1135
+
1136
+ const plot = document.createElement('a-plot') as APlotElement
1137
+ plot.plotArgs = stackedHorizontal()
1138
+ document.body.append(plot)
1139
+ ```
1140
+
1141
+ Positive and negative stacks
1142
+
1143
+ Positive and negative values stack separately from zero. Each segment retains its sign and field label.
1144
+
1145
+ ```ts
1146
+ import { bar, type PlotArgs } from '@antadesign/plot'
1147
+ import '@antadesign/plot/elements/a-plot'
1148
+ import type { APlotElement } from '@antadesign/plot/browser'
1149
+
1150
+ function stackedDiverging(): PlotArgs<Node> {
1151
+ const data = [
1152
+ { month: 'Jan', new: 28, expansion: 12, churn: -14, contraction: -6 },
1153
+ { month: 'Feb', new: 18, expansion: 8, churn: -22, contraction: -10 },
1154
+ { month: 'Mar', new: 36, expansion: 16, churn: -8, contraction: -4 },
1155
+ { month: 'Apr', new: 24, expansion: 18, churn: -18, contraction: -12 },
1156
+ { month: 'May', new: 32, expansion: 10, churn: -12, contraction: -8 },
1157
+ ]
1158
+ return {
1159
+ series: [bar<Node>({ data, x: 'month', y: ['new', 'expansion', 'churn', 'contraction'],
1160
+ color: [
1161
+ { light: '#0d9488', dark: '#2dd4bf' },
1162
+ { light: '#5eead4', dark: '#99f6e4' },
1163
+ { light: '#ea580c', dark: '#fb923c' },
1164
+ { light: '#fdba74', dark: '#fed7aa' },
1165
+ ],
1166
+ inset: 7,
1167
+ tooltip: ({ label, y }) => document.createTextNode(label + ': ' + (Number(y) > 0 ? '+' : '') + y),
1168
+ })],
1169
+ height: 260,
1170
+ margin: { top: 5, right: 8, bottom: 20, left: 36 },
1171
+ axis: { x: { label: '' }, y: { min: -40, max: 60, label: '' } },
1172
+ background: { light: '#ffffff', dark: '#151b28' },
1173
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1174
+ grid: { x: false, y: true },
1175
+ border: false,
1176
+ zoom_pan: false,
1177
+ }
1178
+ }
1179
+
1180
+ const plot = document.createElement('a-plot') as APlotElement
1181
+ plot.plotArgs = stackedDiverging()
1182
+ document.body.append(plot)
1183
+ ```
1184
+
1185
+ Full-stack tooltips
1186
+
1187
+ Hover a segment to see every value in its stack. The callback reads the values from `row` and uses `label` to display the hovered segment’s name and value in bold.
1188
+
1189
+ ```ts
1190
+ import { bar, type PlotArgs } from '@antadesign/plot'
1191
+ import '@antadesign/plot/elements/a-plot'
1192
+ import type { APlotElement } from '@antadesign/plot/browser'
1193
+
1194
+ function stackedTooltip(): PlotArgs<Node> {
1195
+ const data = [
1196
+ { day: 'Mon', network: 18, compute: 42, storage: 24 },
1197
+ { day: 'Tue', network: 24, compute: 32, storage: 18 },
1198
+ { day: 'Wed', network: 12, compute: 56, storage: 28 },
1199
+ { day: 'Thu', network: 28, compute: 24, storage: 16 },
1200
+ { day: 'Fri', network: 16, compute: 48, storage: 22 },
1201
+ ]
1202
+
1203
+ return {
1204
+ series: [bar<Node>({
1205
+ data,
1206
+ x: 'day',
1207
+ y: ['network', 'compute', 'storage'],
1208
+ color: [
1209
+ { light: '#0d9488', dark: '#5eead4' },
1210
+ { light: '#6366f1', dark: '#a5b4fc' },
1211
+ { light: '#d97706', dark: '#fcd34d' },
1212
+ ],
1213
+ inset: 10,
1214
+ hover_span_x: false,
1215
+ hover_span_y: false,
1216
+ tooltip: ({ label, row }) => {
1217
+ const content = document.createElement('div')
1218
+ for (const field of ['network', 'compute', 'storage']) {
1219
+ const entry = document.createElement('div')
1220
+ const value = document.createElement(field === label ? 'strong' : 'span')
1221
+ value.textContent = field + ': ' + row[field] + ' ms'
1222
+ entry.append(value)
1223
+ content.append(entry)
1224
+ }
1225
+ return content
1226
+ },
1227
+ })],
1228
+ height: 260,
1229
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1230
+ axis: { x: { label: '' }, y: { min: 0, max: 100, label: '' } },
1231
+ background: { light: '#ffffff', dark: '#151b28' },
1232
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1233
+ grid: { x: false, y: true },
1234
+ border: false,
1235
+ zoom_pan: false,
1236
+ }
1237
+ }
1238
+
1239
+ const plot = document.createElement('a-plot') as APlotElement
1240
+ plot.plotArgs = stackedTooltip()
1241
+ document.body.append(plot)
1242
+ ```
1243
+
1244
+ ```ts
1245
+ import { bar } from '@antadesign/plot'
1246
+
1247
+ const series = [bar({
1248
+ data: [
1249
+ { group: 'A', passed: 8, failed: 2 },
1250
+ { group: 'B', passed: 12, failed: 3 },
1251
+ ],
1252
+ x: 'group',
1253
+ y: ['passed', 'failed'],
1254
+ color: ['seagreen', 'coral'],
1255
+ border_radius: 3,
1256
+ inset: 4,
1257
+ min_size: 2,
1258
+ hover_span_x: true,
1259
+ hover_span_y: false,
1260
+ tooltip: ({ label, y }) => `${label}: ${y}`,
1261
+ on_select: ({ label, row }) => console.log('Selected segment:', label, row),
1262
+ hoverable: true,
1263
+ highlight: true,
1264
+ })]
1265
+ ```
1266
+
1267
+ #### Area
1268
+
1269
+ Fill the space between two boundaries. Set `y2` for a second vertical value
1270
+ or `x2` for a second horizontal value. The second boundary can be a field
1271
+ name or a constant, such as a zero baseline.
1272
+
1273
+ Fill to a baseline
1274
+
1275
+ Fill beneath a signal with a constant zero baseline. A stroke traces the area’s boundary.
1276
+
1277
+ ```ts
1278
+ import { area, type APlotElement } from '@antadesign/plot/browser'
1279
+ import '@antadesign/plot/elements/a-plot'
1280
+
1281
+ const plot = document.createElement('a-plot') as APlotElement
1282
+
1283
+ const data = Array.from({ length: 97 }, (_, i) => ({
1284
+ x: i / 4,
1285
+ y: 28 + 42 * Math.exp(-(((i - 55) / 24) ** 2))
1286
+ + 9 * Math.sin(i / 5) + 4 * Math.cos(i * 1.3),
1287
+ }))
1288
+
1289
+ plot.plotArgs = {
1290
+ series: [
1291
+ area<Node>({
1292
+ data,
1293
+ y2: 0,
1294
+ color: { light: '#99f6e4', dark: '#134e4a' },
1295
+ stroke: { color: { light: '#0d9488', dark: '#5eead4' }, width: 2 },
1296
+ tooltip: ({ y }) => document.createTextNode('Load: ' + Math.round(Number(y))),
1297
+ }),
1298
+ ],
1299
+ height: 260,
1300
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1301
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 100, label: '' } },
1302
+ background: { light: '#ffffff', dark: '#151b28' },
1303
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1304
+ grid: { x: false, y: true },
1305
+ border: false,
1306
+ zoom_pan: false,
1307
+ }
1308
+
1309
+ document.body.append(plot)
1310
+ ```
1311
+
1312
+ A range between fields
1313
+
1314
+ Use two value fields to draw a changing interval. A dashed outline distinguishes its boundaries.
1315
+
1316
+ ```ts
1317
+ import { area, type APlotElement } from '@antadesign/plot/browser'
1318
+ import '@antadesign/plot/elements/a-plot'
1319
+
1320
+ const plot = document.createElement('a-plot') as APlotElement
1321
+
1322
+ const data = Array.from({ length: 81 }, (_, i) => {
1323
+ const center = 48 + 16 * Math.sin(i / 12) + 5 * Math.cos(i / 3)
1324
+ const spread = 8 + 10 * Math.sin(i / 17) ** 2
1325
+ return { x: i * 0.3, low: center - spread, high: center + spread }
1326
+ })
1327
+
1328
+ plot.plotArgs = {
1329
+ series: [
1330
+ area<Node>({
1331
+ data,
1332
+ y: 'high',
1333
+ y2: 'low',
1334
+ color: { light: '#ddd6fe', dark: '#4c1d95' },
1335
+ stroke: { color: { light: '#7c3aed', dark: '#c4b5fd' }, width: 2 },
1336
+ dash: [5, 3],
1337
+ tooltip: ({ row }) => document.createTextNode(Math.round(Number(row.low)) + '–' + Math.round(Number(row.high))),
1338
+ }),
1339
+ ],
1340
+ height: 260,
1341
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1342
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 100, label: '' } },
1343
+ background: { light: '#ffffff', dark: '#151b28' },
1344
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1345
+ grid: { x: false, y: true },
1346
+ border: false,
1347
+ zoom_pan: false,
1348
+ }
1349
+
1350
+ document.body.append(plot)
1351
+ ```
1352
+
1353
+ Layered contributions
1354
+
1355
+ Compose adjacent areas with explicit cumulative bounds. Each band shows one contribution to the total.
1356
+
1357
+ ```ts
1358
+ import { area, type APlotElement } from '@antadesign/plot/browser'
1359
+ import '@antadesign/plot/elements/a-plot'
1360
+
1361
+ const plot = document.createElement('a-plot') as APlotElement
1362
+
1363
+ const data = Array.from({ length: 97 }, (_, i) => {
1364
+ const first = 18 + 7 * Math.sin(i / 13) + 3 * Math.cos(i / 4)
1365
+ const second = first + 22 + 9 * Math.sin(i / 18 + 1)
1366
+ const total = second + 16 + 6 * Math.cos(i / 9)
1367
+ return { x: i / 4, first, second, total }
1368
+ })
1369
+
1370
+ plot.plotArgs = {
1371
+ series: [
1372
+ area<Node>({
1373
+ data,
1374
+ y: 'first',
1375
+ y2: 0,
1376
+ color: { light: '#2dd4bf', dark: '#0d9488' },
1377
+ tooltip: ({ row }) => document.createTextNode('Network: ' + Math.round(Number(row.first))),
1378
+ }),
1379
+ area<Node>({
1380
+ data,
1381
+ y: 'second',
1382
+ y2: 'first',
1383
+ color: { light: '#a5b4fc', dark: '#6366f1' },
1384
+ tooltip: ({ row }) => document.createTextNode('Compute: ' + Math.round(Number(row.second) - Number(row.first))),
1385
+ }),
1386
+ area<Node>({
1387
+ data,
1388
+ y: 'total',
1389
+ y2: 'second',
1390
+ color: { light: '#fcd34d', dark: '#b45309' },
1391
+ tooltip: ({ row }) => document.createTextNode('Storage: ' + Math.round(Number(row.total) - Number(row.second))),
1392
+ }),
1393
+ ],
1394
+ height: 260,
1395
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1396
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 100, label: '' } },
1397
+ background: { light: '#ffffff', dark: '#151b28' },
1398
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1399
+ grid: { x: false, y: true },
1400
+ border: false,
1401
+ zoom_pan: false,
1402
+ }
1403
+
1404
+ document.body.append(plot)
1405
+ ```
1406
+
1407
+ Horizontal intervals
1408
+
1409
+ Use `x` and `x2` for horizontal bounds. The filled band shows how an interval shifts along the vertical axis.
1410
+
1411
+ ```ts
1412
+ import { area, type APlotElement } from '@antadesign/plot/browser'
1413
+ import '@antadesign/plot/elements/a-plot'
1414
+
1415
+ const plot = document.createElement('a-plot') as APlotElement
1416
+
1417
+ const data = Array.from({ length: 81 }, (_, i) => {
1418
+ const center = 46 + 18 * Math.sin(i / 16) + 4 * Math.cos(i / 4)
1419
+ const spread = 9 + 6 * Math.cos(i / 11) ** 2
1420
+ return { y: i * 0.3, low: center - spread, high: center + spread }
1421
+ })
1422
+
1423
+ plot.plotArgs = {
1424
+ series: [
1425
+ area<Node>({
1426
+ data,
1427
+ x: 'low',
1428
+ x2: 'high',
1429
+ color: { light: '#fed7aa', dark: '#7c2d12' },
1430
+ stroke: { color: { light: '#ea580c', dark: '#fdba74' }, width: 2 },
1431
+ tooltip: ({ row }) => document.createTextNode(Math.round(Number(row.low)) + '–' + Math.round(Number(row.high))),
1432
+ }),
1433
+ ],
1434
+ height: 260,
1435
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1436
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 24, label: '' } },
1437
+ background: { light: '#ffffff', dark: '#151b28' },
1438
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1439
+ grid: { x: true, y: false },
1440
+ border: false,
1441
+ zoom_pan: false,
1442
+ }
1443
+
1444
+ document.body.append(plot)
1445
+ ```
1446
+
1447
+ Area arguments
1448
+
1449
+ | Field | Type | Default | Description |
1450
+ |---|---|---|---|
1451
+ | `data` | `Record<string, unknown>[]` | Required | Rows used to build the series. |
1452
+ | `x?` | [`FieldArg`](#data-fields) | `'x'` | Horizontal field name or accessor. |
1453
+ | `x2?` | [`FieldArg \| number`](#data-fields) | None | Second horizontal boundary, as a field, accessor, or constant. Use either `x2` or `y2`. |
1454
+ | `y?` | [`FieldArg`](#data-fields) | `'y'` | Vertical field name or accessor. |
1455
+ | `y2?` | [`FieldArg \| number`](#data-fields) | Zero baseline if neither bound is set | Second vertical boundary, as a field, accessor, or constant. |
1456
+ | `color?` | [`ThemeColor`](#theme-colors) | Black | Series color; theme pairs provide separate light and dark colors. |
1457
+ | [`stroke?`](#strokes) | `StrokeArg` | None | Outline color and optional width. |
1458
+ | `dash?` | `number[]` | Solid | Alternating dash and gap lengths in pixels. |
1459
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
1460
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
1461
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
1462
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
1463
+
1464
+ ```ts
1465
+ import { area } from '@antadesign/plot'
1466
+
1467
+ const series = [area({
1468
+ data: [
1469
+ { time: 1, low: 2, high: 4 },
1470
+ { time: 2, low: 3, high: 6 },
1471
+ { time: 3, low: 2, high: 5 },
1472
+ ],
1473
+ x: 'time',
1474
+ y: 'high',
1475
+ y2: 'low',
1476
+ color: { light: 'lightsteelblue', dark: 'midnightblue' },
1477
+ stroke: { color: 'steelblue', width: 2 },
1478
+ dash: [6, 3],
1479
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
1480
+ on_select: ({ row }) => console.log('Selected row:', row),
1481
+ hoverable: true,
1482
+ highlight: true,
1483
+ })]
1484
+ ```
1485
+ For a horizontal area, use `x2` instead of `y2`:
1486
+
1487
+ ```ts
1488
+ import { area } from '@antadesign/plot'
1489
+
1490
+ const series = [area({
1491
+ data: [{ low: 2, high: 4, time: 1 }, { low: 3, high: 6, time: 2 }],
1492
+ x: 'high',
1493
+ x2: 'low',
1494
+ y: 'time',
1495
+ color: 'lightsteelblue',
1496
+ })]
1497
+ ```
1498
+
1499
+ #### Rect
1500
+
1501
+ Draw rectangles with explicit bounds. Use `x`, `x2`, `y`, and `y2` to select
1502
+ the fields defining each rectangle's corners. Rectangles can represent
1503
+ intervals, regions, or cells in a grid.
1504
+
1505
+ Categorical cells
1506
+
1507
+ Combine two categorical axes to form a heatmap. Color each cell from its value and use `inset` to separate neighboring cells.
1508
+
1509
+ ```ts
1510
+ import { rect, type APlotElement } from '@antadesign/plot/browser'
1511
+ import '@antadesign/plot/elements/a-plot'
1512
+
1513
+ const plot = document.createElement('a-plot') as APlotElement
1514
+
1515
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
1516
+ const hours = ['09', '11', '13', '15', '17', '19']
1517
+ const data = days.flatMap((day, d) => hours.map((hour, h) => ({
1518
+ day, hour, activity: Math.round(20 + 75 * Math.sin(h * 0.8 + d * 0.5) ** 2),
1519
+ })))
1520
+
1521
+ plot.plotArgs = {
1522
+ series: [
1523
+ rect<Node>({
1524
+ data,
1525
+ x: 'hour',
1526
+ y: 'day',
1527
+ inset: 2,
1528
+ color: row => ({
1529
+ light: 'hsl(174, 65%, ' + (94 - Number(row.activity) * 0.6) + '%)',
1530
+ dark: 'hsl(174, 55%, ' + (16 + Number(row.activity) * 0.4) + '%)',
1531
+ }),
1532
+ tooltip: ({ row }) => document.createTextNode(row.day + ' ' + row.hour + ':00 · ' + row.activity + '%'),
1533
+ }),
1534
+ ],
1535
+ height: 260,
1536
+ margin: { top: 5, right: 8, bottom: 20, left: 38 },
1537
+ axis: { x: { categories: hours, label: '' }, y: { categories: days, label: '' } },
1538
+ background: { light: '#ffffff', dark: '#151b28' },
1539
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1540
+ grid: false,
1541
+ border: false,
1542
+ zoom_pan: false,
1543
+ }
1544
+
1545
+ document.body.append(plot)
1546
+ ```
1547
+
1548
+ Intervals with explicit bounds
1549
+
1550
+ Set `x` and `x2` to show durations within categorical rows. Each rectangle retains its start and end values as the plot resizes.
1551
+
1552
+ ```ts
1553
+ import { rect, type APlotElement } from '@antadesign/plot/browser'
1554
+ import '@antadesign/plot/elements/a-plot'
1555
+
1556
+ const plot = document.createElement('a-plot') as APlotElement
1557
+
1558
+ const data = [
1559
+ { task: 'Fetch', start: 0.5, end: 3.2 },
1560
+ { task: 'Parse', start: 2.8, end: 5.1 },
1561
+ { task: 'Build', start: 4.5, end: 9.4 },
1562
+ { task: 'Test', start: 7.8, end: 11.2 },
1563
+ { task: 'Ship', start: 10.8, end: 12.5 },
1564
+ ]
1565
+
1566
+ plot.plotArgs = {
1567
+ series: [
1568
+ rect<Node>({
1569
+ data,
1570
+ x: 'start',
1571
+ x2: 'end',
1572
+ y: 'task',
1573
+ inset: 9,
1574
+ color: { light: '#c4b5fd', dark: '#6d28d9' },
1575
+ stroke: { color: { light: '#7c3aed', dark: '#c4b5fd' }, width: 1 },
1576
+ hover_span_y: true,
1577
+ tooltip: ({ row }) => document.createTextNode(row.task + ': ' + row.start + '–' + row.end + ' s'),
1578
+ }),
1579
+ ],
1580
+ height: 260,
1581
+ margin: { top: 5, right: 8, bottom: 20, left: 44 },
1582
+ axis: { x: { min: 0, max: 14, label: '' }, y: { label: '' } },
1583
+ background: { light: '#ffffff', dark: '#151b28' },
1584
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1585
+ grid: false,
1586
+ border: false,
1587
+ zoom_pan: false,
1588
+ }
1589
+
1590
+ document.body.append(plot)
1591
+ ```
1592
+
1593
+ Fixed pixel dimensions
1594
+
1595
+ Set `size` to draw rectangles with a consistent width and height at numeric coordinates. An outline keeps nearby marks distinct.
1596
+
1597
+ ```ts
1598
+ import { rect, type APlotElement } from '@antadesign/plot/browser'
1599
+ import '@antadesign/plot/elements/a-plot'
1600
+
1601
+ const plot = document.createElement('a-plot') as APlotElement
1602
+
1603
+ const data = Array.from({ length: 32 }, (_, i) => ({
1604
+ x: 8 + (i * 23 % 84),
1605
+ y: 18 + (i * 13 % 65),
1606
+ value: 20 + (i * 17 % 80),
1607
+ }))
1608
+
1609
+ plot.plotArgs = {
1610
+ series: [
1611
+ rect<Node>({
1612
+ data,
1613
+ size: { width: 16, height: 9 },
1614
+ color: row => Number(row.value) > 65
1615
+ ? { light: '#c2410c', dark: '#fdba74' }
1616
+ : { light: '#0891b2', dark: '#67e8f9' },
1617
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 1 },
1618
+ tooltip: ({ row }) => document.createTextNode('Value: ' + row.value),
1619
+ }),
1620
+ ],
1621
+ height: 260,
1622
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1623
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
1624
+ background: { light: '#ffffff', dark: '#151b28' },
1625
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1626
+ grid: false,
1627
+ border: false,
1628
+ zoom_pan: false,
1629
+ }
1630
+
1631
+ document.body.append(plot)
1632
+ ```
1633
+
1634
+ Run history
1635
+
1636
+ Place fixed-size rectangles on a time axis in Passed and Failed lanes. Use `band_align` to anchor both sets of marks to the shared divider, with passing runs above and failures below.
1637
+
1638
+ ```ts
1639
+ import { rect, type APlotElement } from '@antadesign/plot/browser'
1640
+ import '@antadesign/plot/elements/a-plot'
1641
+
1642
+ const plot = document.createElement('a-plot') as APlotElement
1643
+
1644
+ const start = Date.UTC(2026, 0, 1, 9)
1645
+ const data = Array.from({ length: 36 }, (_, i) => ({
1646
+ run: i + 1,
1647
+ end_ms: start + (i * 3 + (i % 3)) * 60_000,
1648
+ lane: [5, 6, 14, 23, 24, 25, 32].includes(i) ? 'Failed' : 'Passed',
1649
+ }))
1650
+
1651
+ plot.plotArgs = {
1652
+ series: [
1653
+ rect<Node>({
1654
+ data: data.filter(row => row.lane === 'Passed'),
1655
+ x: 'end_ms',
1656
+ y: 'lane',
1657
+ size: { width: 10, height: 40 },
1658
+ band_align: 'end',
1659
+ inset: 0,
1660
+ color: { light: '#189e3e', dark: '#4ade80' },
1661
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 0.5 },
1662
+ tooltip: ({ row }) => document.createTextNode('Run ' + row.run + ': ' + row.lane),
1663
+ }),
1664
+ rect<Node>({
1665
+ data: data.filter(row => row.lane === 'Failed'),
1666
+ x: 'end_ms',
1667
+ y: 'lane',
1668
+ size: { width: 10, height: 40 },
1669
+ band_align: 'start',
1670
+ inset: 0,
1671
+ color: { light: '#c41e5e', dark: '#fb7185' },
1672
+ stroke: { color: { light: '#ffffff', dark: '#151b28' }, width: 0.5 },
1673
+ tooltip: ({ row }) => document.createTextNode('Run ' + row.run + ': ' + row.lane),
1674
+ }),
1675
+ ],
1676
+ height: 260,
1677
+ margin: { top: 5, right: 8, bottom: 24, left: 56 },
1678
+ axis: {
1679
+ x: {
1680
+ scale: 'utc',
1681
+ min: start - 5 * 60_000,
1682
+ max: start + 115 * 60_000,
1683
+ label: '',
1684
+ line: false,
1685
+ tick_label: { format: value => new Date(Number(value)).toISOString().slice(11, 16) },
1686
+ },
1687
+ y: { categories: ['Failed', 'Passed'], grid_align: 'edge', label: '', line: false },
1688
+ },
1689
+ background: { light: '#ffffff', dark: '#151b28' },
1690
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1691
+ grid: true,
1692
+ border: false,
1693
+ theme_invert: false,
1694
+ zoom_pan: false,
1695
+ }
1696
+
1697
+ document.body.append(plot)
1698
+ ```
1699
+
1700
+ Rect arguments
1701
+
1702
+ | Field | Type | Default | Description |
1703
+ |---|---|---|---|
1704
+ | `data` | `Record<string, unknown>[]` | Required | Rows used to build the series. |
1705
+ | `x?` | [`FieldArg`](#data-fields) | `'x'` | Horizontal field name or accessor. |
1706
+ | `x2?` | [`FieldArg`](#data-fields) | `'x2'` when present in data | Opposite horizontal corner. Numeric axes need a second corner or pixel size. |
1707
+ | `y?` | [`FieldArg`](#data-fields) | `'y'` | Vertical field name or accessor. |
1708
+ | `y2?` | [`FieldArg`](#data-fields) | `'y2'` when present in data | Opposite vertical corner. Numeric axes need a second corner or pixel size. |
1709
+ | `span?` | `'x' \| 'y'` | None | Stretch the rectangle across the full plot on this axis. |
1710
+ | `size?` | `number \| { width?: number; height?: number }` | Derived from bounds or band | Fixed pixel size, shared or per dimension. |
1711
+ | `band_align?` | `'center' \| 'start' \| 'end'` | `'center'` | Alignment of a fixed-size rectangle within a categorical band. |
1712
+ | `hover_span_x?` | `boolean` | `false` | Extend the horizontal hit region across the categorical band. |
1713
+ | `hover_span_y?` | `boolean` | `false` | Extend the vertical hit region across the categorical band. |
1714
+ | `inset?` | `number` | One sixth of band width | Inset on each side of a categorical band, in pixels. |
1715
+ | `min_size?` | `number` | `0` | Minimum pixel length for a rectangle defined by data bounds. |
1716
+ | `offset?` | `{ x?: number; y?: number }` | `{ x: 0, y: 0 }` | Pixel offsets applied to each rectangle. |
1717
+ | `color?` | [`ColorArg`](#series-colors) | Black | Series color; theme pairs provide separate light and dark colors. |
1718
+ | [`stroke?`](#strokes) | `StrokeArg` | None | Outline color and optional width. |
1719
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
1720
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
1721
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
1722
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
1723
+
1724
+ ```ts
1725
+ import { rect } from '@antadesign/plot'
1726
+
1727
+ const series = [rect({
1728
+ data: [
1729
+ { start: 1, end: 3, low: 2, high: 5 },
1730
+ { start: 4, end: 6, low: 1, high: 4 },
1731
+ ],
1732
+ x: 'start',
1733
+ x2: 'end',
1734
+ y: 'low',
1735
+ y2: 'high',
1736
+ min_size: 2,
1737
+ offset: { x: 0, y: 0 },
1738
+ color: 'lightsteelblue',
1739
+ stroke: { color: 'steelblue', width: 1 },
1740
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
1741
+ on_select: ({ row }) => console.log('Selected row:', row),
1742
+ hoverable: true,
1743
+ highlight: true,
1744
+ })]
1745
+ ```
1746
+ For fixed-size marks in categorical bands, use `size` and `band_align`.
1747
+ `inset` controls the space around a band when its size is not fixed:
1748
+
1749
+ ```ts
1750
+ import { rect } from '@antadesign/plot'
1751
+
1752
+ const series = [rect({
1753
+ data: [{ group: 'A', row: 'First' }, { group: 'B', row: 'Second' }],
1754
+ x: 'group',
1755
+ y: 'row',
1756
+ size: { width: 12 },
1757
+ band_align: 'start',
1758
+ inset: 4,
1759
+ offset: { x: 2, y: 0 },
1760
+ hover_span_x: true,
1761
+ hover_span_y: true,
1762
+ color: 'steelblue',
1763
+ tooltip: true,
1764
+ })]
1765
+ ```
1766
+
1767
+ Use `span` to fill one axis, leaving its coordinates unspecified. Add this
1768
+ series before the data series to draw a background region:
1769
+
1770
+ ```ts
1771
+ import { rect } from '@antadesign/plot'
1772
+
1773
+ const series = [rect({
1774
+ data: [{ low: 2, high: 5 }],
1775
+ span: 'x',
1776
+ y: 'low',
1777
+ y2: 'high',
1778
+ color: 'aliceblue',
1779
+ hoverable: false,
1780
+ highlight: false,
1781
+ })]
1782
+ ```
1783
+
1784
+ #### Rule
1785
+
1786
+ Draw a reference line across the plot. Supply `x` for a vertical line or
1787
+ `y` for a horizontal line. A single rule needs no data array; add it to the
1788
+ same series array as the data it annotates.
1789
+
1790
+ Horizontal reference values
1791
+
1792
+ Use numeric `y` values for reference lines without a data array. A thin dashed target and a thick solid upper limit show how width and dash patterns distinguish reference values.
1793
+
1794
+ ```ts
1795
+ import { rule, line, type APlotElement } from '@antadesign/plot/browser'
1796
+ import '@antadesign/plot/elements/a-plot'
1797
+
1798
+ const plot = document.createElement('a-plot') as APlotElement
1799
+
1800
+ const data = Array.from({ length: 97 }, (_, i) => ({
1801
+ x: i / 4,
1802
+ y: 44 + 15 * Math.sin(i / 9) + 5 * Math.cos(i * 0.8)
1803
+ + 38 * Math.exp(-(((i - 62) / 10) ** 2)),
1804
+ }))
1805
+
1806
+ plot.plotArgs = {
1807
+ series: [
1808
+ line<Node>({
1809
+ data,
1810
+ color: { light: '#0891b2', dark: '#67e8f9' },
1811
+ width: 2,
1812
+ }),
1813
+ rule<Node>({
1814
+ y: 60,
1815
+ color: { light: '#7c3aed', dark: '#c4b5fd' },
1816
+ width: 1,
1817
+ dash: [7, 4],
1818
+ tooltip: () => document.createTextNode('Target: 60 ms'),
1819
+ }),
1820
+ rule<Node>({
1821
+ y: 90,
1822
+ color: { light: '#c2410c', dark: '#fdba74' },
1823
+ width: 4,
1824
+ tooltip: () => document.createTextNode('Upper limit: 90 ms'),
1825
+ }),
1826
+ ],
1827
+ height: 260,
1828
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1829
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 110, label: '' } },
1830
+ background: { light: '#ffffff', dark: '#151b28' },
1831
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1832
+ grid: false,
1833
+ border: false,
1834
+ zoom_pan: false,
1835
+ }
1836
+
1837
+ document.body.append(plot)
1838
+ ```
1839
+
1840
+ Vertical event markers
1841
+
1842
+ Read `x` positions from data to mark events along a signal. A solid rule marks deployment; thinner dashed rules mark recovery events. Tooltips identify each event.
1843
+
1844
+ ```ts
1845
+ import { rule, line, type APlotElement } from '@antadesign/plot/browser'
1846
+ import '@antadesign/plot/elements/a-plot'
1847
+
1848
+ const plot = document.createElement('a-plot') as APlotElement
1849
+
1850
+ const data = Array.from({ length: 97 }, (_, i) => ({
1851
+ x: i / 4,
1852
+ y: 35 + 12 * Math.sin(i / 11) + 4 * Math.cos(i * 0.7)
1853
+ + (i >= 28 && i < 64 ? 30 : 0),
1854
+ }))
1855
+ const events = [
1856
+ { hour: 7, name: 'Deploy', recovery: false },
1857
+ { hour: 16, name: 'Rollback', recovery: true },
1858
+ { hour: 21, name: 'Verified', recovery: true },
1859
+ ]
1860
+
1861
+ plot.plotArgs = {
1862
+ series: [
1863
+ line<Node>({
1864
+ data,
1865
+ color: { light: '#6366f1', dark: '#a5b4fc' },
1866
+ width: 2,
1867
+ }),
1868
+ rule<Node>({
1869
+ data: events.filter(event => !event.recovery),
1870
+ x: 'hour',
1871
+ color: { light: '#c2410c', dark: '#fdba74' },
1872
+ width: 3,
1873
+ tooltip: ({ row }) => document.createTextNode(row?.name + ' at hour ' + row?.hour),
1874
+ }),
1875
+ rule<Node>({
1876
+ data: events.filter(event => event.recovery),
1877
+ x: 'hour',
1878
+ color: { light: '#0d9488', dark: '#5eead4' },
1879
+ width: 2,
1880
+ dash: [5, 4],
1881
+ tooltip: ({ row }) => document.createTextNode(row?.name + ' at hour ' + row?.hour),
1882
+ }),
1883
+ ],
1884
+ height: 260,
1885
+ margin: { top: 5, right: 8, bottom: 20, left: 30 },
1886
+ axis: { x: { min: 0, max: 24, label: '' }, y: { min: 0, max: 110, label: '' } },
1887
+ background: { light: '#ffffff', dark: '#151b28' },
1888
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
1889
+ grid: false,
1890
+ border: false,
1891
+ zoom_pan: false,
1892
+ }
1893
+
1894
+ document.body.append(plot)
1895
+ ```
1896
+
1897
+ Rule arguments
1898
+
1899
+ | Field | Type | Default | Description |
1900
+ |---|---|---|---|
1901
+ | `data?` | `Record<string, unknown>[]` | None | Optional rows for multiple reference lines. |
1902
+ | `x?` | [`number \| FieldArg`](#data-fields) | None | Vertical rule position, or a field/accessor when `data` is supplied. Set exactly one of `x` and `y`. |
1903
+ | `y?` | [`number \| FieldArg`](#data-fields) | None | Horizontal rule position, or a field/accessor when `data` is supplied. |
1904
+ | `color?` | [`ColorArg`](#series-colors) | Black | Series color; theme pairs provide separate light and dark colors. |
1905
+ | `width?` | `number` | `1` | Rule width in pixels. |
1906
+ | `dash?` | `number[]` | Solid | Alternating dash and gap lengths in pixels. |
1907
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
1908
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
1909
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
1910
+ | `highlight?` | `boolean` | `true` | Draw hover feedback for a hit point. |
1911
+
1912
+ ```ts
1913
+ import { rule } from '@antadesign/plot'
1914
+
1915
+ const series = [rule({
1916
+ data: [{ threshold: 5 }, { threshold: 8 }],
1917
+ y: 'threshold',
1918
+ color: (row) => Number(row.threshold) >= 8 ? 'tomato' : 'steelblue',
1919
+ width: 2,
1920
+ dash: [6, 4],
1921
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
1922
+ on_select: ({ row }) => console.log('Selected row:', row),
1923
+ hoverable: true,
1924
+ highlight: true,
1925
+ })]
1926
+ ```
1927
+ For a single vertical reference line, supply a numeric `x` without `data`:
1928
+
1929
+ ```ts
1930
+ import { rule } from '@antadesign/plot'
1931
+
1932
+ const series = [rule({
1933
+ x: 3,
1934
+ color: 'tomato',
1935
+ width: 2,
1936
+ hoverable: false,
1937
+ highlight: false,
1938
+ })]
1939
+ ```
1940
+
1941
+ #### Custom
1942
+
1943
+ Provide a [`renderer`](#custom-rendering) to draw directly on the canvas. Its second argument
1944
+ includes the canvas context, plot bounds, scales, and helpers that convert
1945
+ data positions into pixel coordinates.
1946
+
1947
+ Confidence ellipses
1948
+
1949
+ Overlay 95% contours for two specified Gaussian models on their sampled points. Draw the ellipse in data coordinates so its shape follows the axis scales.
1950
+
1951
+ ```ts
1952
+ import { custom, scatter, type APlotElement } from '@antadesign/plot/browser'
1953
+ import '@antadesign/plot/elements/a-plot'
1954
+
1955
+ const plot = document.createElement('a-plot') as APlotElement
1956
+
1957
+ const groups = [
1958
+ { x: 32, y: 40, sx: 10, sy: 5, angle: 0.6 },
1959
+ { x: 68, y: 62, sx: 8, sy: 5, angle: -0.8 },
1960
+ ]
1961
+ const points = groups.flatMap((group, g) => Array.from({ length: 80 }, (_, i) => {
1962
+ const radius = Math.sqrt(-2 * Math.log((i + 0.5) / 80))
1963
+ const theta = i * 2.399963
1964
+ const u = group.sx * radius * Math.cos(theta)
1965
+ const v = group.sy * radius * Math.sin(theta)
1966
+ return {
1967
+ x: group.x + u * Math.cos(group.angle) - v * Math.sin(group.angle),
1968
+ y: group.y + u * Math.sin(group.angle) + v * Math.cos(group.angle),
1969
+ group: g,
1970
+ }
1971
+ }))
1972
+
1973
+ plot.plotArgs = {
1974
+ series: [
1975
+ scatter<Node>({
1976
+ data: points,
1977
+ size: 3,
1978
+ color: row => Number(row.group) === 0
1979
+ ? { light: '#0d9488', dark: '#5eead4' }
1980
+ : { light: '#7c3aed', dark: '#c4b5fd' },
1981
+ tooltip: ({ x, y }) => document.createTextNode(Number(x).toFixed(1) + ', ' + Number(y).toFixed(1)),
1982
+ }),
1983
+ custom<Node>({
1984
+ data: groups,
1985
+ x: 'x', y: 'y',
1986
+ color: (_, i) => i === 0
1987
+ ? { light: '#0f766e', dark: '#5eead4' }
1988
+ : { light: '#6d28d9', dark: '#c4b5fd' },
1989
+ hoverable: false,
1990
+ renderer: (_, { ctx, x_scale, y_scale, color_at }) => {
1991
+ ctx.save()
1992
+ ctx.lineWidth = 2
1993
+ groups.forEach((group, i) => {
1994
+ ctx.strokeStyle = color_at(i)
1995
+ ctx.beginPath()
1996
+ // The 95% contour of the specified bivariate Gaussian model.
1997
+ for (let step = 0; step <= 100; step++) {
1998
+ const theta = step / 100 * Math.PI * 2
1999
+ const u = Math.sqrt(5.991) * group.sx * Math.cos(theta)
2000
+ const v = Math.sqrt(5.991) * group.sy * Math.sin(theta)
2001
+ const x = Number(x_scale(group.x + u * Math.cos(group.angle) - v * Math.sin(group.angle)))
2002
+ const y = Number(y_scale(group.y + u * Math.sin(group.angle) + v * Math.cos(group.angle)))
2003
+ if (step === 0) ctx.moveTo(x, y)
2004
+ else ctx.lineTo(x, y)
2005
+ }
2006
+ ctx.closePath()
2007
+ ctx.stroke()
2008
+ })
2009
+ ctx.restore()
2010
+ },
2011
+ }),
2012
+ ],
2013
+ height: 260,
2014
+ margin: { top: 8, right: 8, bottom: 24, left: 30 },
2015
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2016
+ background: { light: '#ffffff', dark: '#151b28' },
2017
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2018
+ grid: false,
2019
+ border: false,
2020
+ zoom_pan: false,
2021
+ }
2022
+
2023
+ document.body.append(plot)
2024
+ ```
2025
+
2026
+ Chord diagram
2027
+
2028
+ Connect five groups with ribbons whose widths represent shared volume. The outer arcs use the same weights, showing each group’s total connections. Hover an outer arc to inspect its total.
2029
+
2030
+ ```ts
2031
+ import { custom, type APlotElement } from '@antadesign/plot/browser'
2032
+ import '@antadesign/plot/elements/a-plot'
2033
+
2034
+ const plot = document.createElement('a-plot') as APlotElement
2035
+
2036
+ const names = ['A', 'B', 'C', 'D', 'E']
2037
+ const matrix = [
2038
+ [0, 12, 8, 4, 16],
2039
+ [12, 0, 18, 6, 3],
2040
+ [8, 18, 0, 14, 7],
2041
+ [4, 6, 14, 0, 11],
2042
+ [16, 3, 7, 11, 0],
2043
+ ]
2044
+ const gap = 0.08
2045
+ const total = matrix.flat().reduce((sum, value) => sum + value, 0)
2046
+ const unit = (Math.PI * 2 - gap * names.length) / total
2047
+ let angle = -Math.PI / 2
2048
+ const groups = matrix.map((row, i) => {
2049
+ const start = angle
2050
+ const segments = row.map(value => {
2051
+ const from = angle
2052
+ angle += value * unit
2053
+ return { start: from, end: angle }
2054
+ })
2055
+ const end = angle
2056
+ angle += gap
2057
+ return { name: names[i], total: row.reduce((sum, value) => sum + value, 0), start, end, segments }
2058
+ })
2059
+
2060
+ plot.plotArgs = {
2061
+ series: [custom<Node>({
2062
+ data: groups,
2063
+ color: (_, i) => [
2064
+ { light: '#0d9488', dark: '#5eead4' },
2065
+ { light: '#7c3aed', dark: '#c4b5fd' },
2066
+ { light: '#db2777', dark: '#f9a8d4' },
2067
+ { light: '#d97706', dark: '#fcd34d' },
2068
+ { light: '#2563eb', dark: '#93c5fd' },
2069
+ ][i],
2070
+ hit_test: (_, { cursor, inner }) => {
2071
+ const cx = (inner.left + inner.right) / 2
2072
+ const cy = (inner.top + inner.bottom) / 2
2073
+ const radius = Math.min(inner.right - inner.left, inner.bottom - inner.top) / 2 - 32
2074
+ const distance = Math.hypot(cursor.x - cx, cursor.y - cy)
2075
+ if (Math.abs(distance - (radius + 7)) > 8) return null
2076
+ let angle = Math.atan2(cursor.y - cy, cursor.x - cx)
2077
+ if (angle < -Math.PI / 2) angle += Math.PI * 2
2078
+ const index = groups.findIndex(group => angle >= group.start && angle <= group.end)
2079
+ return index < 0 ? null : index
2080
+ },
2081
+ tooltip: ({ row }) => document.createTextNode('Group ' + row?.name + ': ' + row?.total + ' connections'),
2082
+ renderer: (_, { ctx, inner, color_at }) => {
2083
+ const cx = (inner.left + inner.right) / 2
2084
+ const cy = (inner.top + inner.bottom) / 2
2085
+ const radius = Math.min(inner.right - inner.left, inner.bottom - inner.top) / 2 - 32
2086
+ const point = (a: number) => [cx + radius * Math.cos(a), cy + radius * Math.sin(a)]
2087
+ ctx.save()
2088
+ // Allocate ribbon endpoints from the same weights as the outer arcs.
2089
+ groups.forEach((group, i) => {
2090
+ for (let j = i + 1; j < groups.length; j++) {
2091
+ const source = group.segments[j]
2092
+ const target = groups[j].segments[i]
2093
+ const [sx, sy] = point(source.start)
2094
+ const [tx, ty] = point(target.start)
2095
+ ctx.fillStyle = color_at(i)
2096
+ ctx.globalAlpha = 0.45
2097
+ ctx.beginPath()
2098
+ ctx.moveTo(sx, sy)
2099
+ ctx.arc(cx, cy, radius, source.start, source.end)
2100
+ ctx.quadraticCurveTo(cx, cy, tx, ty)
2101
+ ctx.arc(cx, cy, radius, target.start, target.end)
2102
+ ctx.quadraticCurveTo(cx, cy, sx, sy)
2103
+ ctx.closePath()
2104
+ ctx.fill()
2105
+ }
2106
+ })
2107
+ ctx.globalAlpha = 1
2108
+ ctx.font = '12px sans-serif'
2109
+ ctx.textAlign = 'center'
2110
+ ctx.textBaseline = 'middle'
2111
+ groups.forEach((group, i) => {
2112
+ ctx.strokeStyle = color_at(i)
2113
+ ctx.fillStyle = color_at(i)
2114
+ ctx.lineWidth = 9
2115
+ ctx.beginPath()
2116
+ ctx.arc(cx, cy, radius + 7, group.start, group.end)
2117
+ ctx.stroke()
2118
+ const mid = (group.start + group.end) / 2
2119
+ ctx.fillText(group.name, cx + (radius + 22) * Math.cos(mid), cy + (radius + 22) * Math.sin(mid))
2120
+ })
2121
+ ctx.restore()
2122
+ },
2123
+ })],
2124
+ height: 260,
2125
+ margin: 0,
2126
+ axis: { x: { hidden: true, min: 0, max: 1 }, y: { hidden: true, min: 0, max: 1 } },
2127
+ background: { light: '#ffffff', dark: '#151b28' },
2128
+ grid: false,
2129
+ border: false,
2130
+ zoom_pan: false,
2131
+ }
2132
+
2133
+ document.body.append(plot)
2134
+ ```
2135
+
2136
+ Ridgeline distributions
2137
+
2138
+ Compare six weekly distributions with overlapping density curves. Each ridge uses the same horizontal scale and density normalization, revealing shifts in location and spread. Hover a ridge to see its sample count and mean.
2139
+
2140
+ ```ts
2141
+ import { custom, type APlotElement } from '@antadesign/plot/browser'
2142
+ import '@antadesign/plot/elements/a-plot'
2143
+
2144
+ const plot = document.createElement('a-plot') as APlotElement
2145
+
2146
+ // Test the same closed polygon used for drawing, including the spaces between sampled points.
2147
+ function containsPoint(points: number[][], x: number, y: number): boolean {
2148
+ let inside = false
2149
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
2150
+ const [xi, yi] = points[i]
2151
+ const [xj, yj] = points[j]
2152
+ if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside
2153
+ }
2154
+ return inside
2155
+ }
2156
+
2157
+ const data = Array.from({ length: 6 }, (_, i) => {
2158
+ const values = Array.from({ length: 80 }, (_, j) => {
2159
+ const z = Math.sqrt(-2 * Math.log((j + 0.5) / 80)) * Math.cos(j * 2.399963)
2160
+ return 25 + i * 8 + z * [8, 5, 11, 4, 9, 6][i] + (j % 3 === 0 ? 14 : 0)
2161
+ })
2162
+ return {
2163
+ group: 'Week ' + (i + 1),
2164
+ mean: values.reduce((sum, value) => sum + value, 0) / values.length,
2165
+ density: Array.from({ length: 101 }, (_, value) => ({
2166
+ value,
2167
+ density: values.reduce((sum, sample) => sum + Math.exp(-0.5 * ((value - sample) / 3) ** 2), 0)
2168
+ / (values.length * 3 * Math.sqrt(2 * Math.PI)),
2169
+ })),
2170
+ }
2171
+ })
2172
+ const peak = Math.max(...data.flatMap(row => row.density.map(point => point.density)))
2173
+
2174
+ plot.plotArgs = {
2175
+ series: [custom<Node>({
2176
+ data,
2177
+ y: 'group',
2178
+ color: (_, i) => ({ light: 'hsl(' + (175 + i * 15) + ', 55%, 55%)', dark: 'hsl(' + (175 + i * 15) + ', 50%, 45%)' }),
2179
+ hit_test: (_, { cursor, x_scale, resolve_y, inner }) => {
2180
+ const step = (inner.bottom - inner.top) / data.length
2181
+ // Reverse paint order selects the foreground ridge where shapes overlap.
2182
+ for (let i = data.length - 1; i >= 0; i--) {
2183
+ const center = resolve_y(i)
2184
+ if (center === undefined) continue
2185
+ const baseline = center + step * 0.45
2186
+ const points = [
2187
+ [Number(x_scale(0)), baseline],
2188
+ ...data[i].density.map(point => [
2189
+ Number(x_scale(point.value)), baseline - point.density / peak * step * 1.9,
2190
+ ]),
2191
+ [Number(x_scale(100)), baseline],
2192
+ ]
2193
+ if (containsPoint(points, cursor.x, cursor.y)) return i
2194
+ }
2195
+ return null
2196
+ },
2197
+ tooltip: ({ row }) => document.createTextNode(row?.group + ' · 80 samples · mean ' + Number(row?.mean).toFixed(1)),
2198
+ renderer: (_, { ctx, x_scale, resolve_y, color_at, inner }) => {
2199
+ const step = (inner.bottom - inner.top) / data.length
2200
+ ctx.save()
2201
+ // Paint from the top row down, so foreground ridges cover the preceding tails.
2202
+ data.forEach((row, i) => {
2203
+ const center = resolve_y(i)
2204
+ if (center === undefined) return
2205
+ const baseline = center + step * 0.45
2206
+ ctx.fillStyle = color_at(i)
2207
+ ctx.strokeStyle = '#334155'
2208
+ ctx.lineWidth = 1
2209
+ ctx.beginPath()
2210
+ ctx.moveTo(Number(x_scale(0)), baseline)
2211
+ row.density.forEach(point => {
2212
+ ctx.lineTo(Number(x_scale(point.value)), baseline - point.density / peak * step * 1.9)
2213
+ })
2214
+ ctx.lineTo(Number(x_scale(100)), baseline)
2215
+ ctx.closePath()
2216
+ ctx.globalAlpha = [1, 0.7, 0.85, 0.65, 0.8, 0.7][i]
2217
+ ctx.fill()
2218
+ ctx.globalAlpha = 1
2219
+ ctx.stroke()
2220
+ })
2221
+ ctx.restore()
2222
+ },
2223
+ })],
2224
+ height: 260,
2225
+ margin: { top: 24, right: 8, bottom: 24, left: 52 },
2226
+ axis: {
2227
+ x: { min: 0, max: 100, label: '' },
2228
+ y: { categories: data.map(row => row.group).reverse(), padding_top: 28, label: '' },
2229
+ },
2230
+ background: { light: '#ffffff', dark: '#151b28' },
2231
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2232
+ grid: false,
2233
+ border: false,
2234
+ zoom_pan: false,
2235
+ }
2236
+
2237
+ document.body.append(plot)
2238
+ ```
2239
+
2240
+ Violin distributions
2241
+
2242
+ Mirror a kernel density estimate around each group’s center. Wider sections contain more observations; the central segment shows the interquartile range and the white dot marks the median. Hover a violin to inspect its median and quartiles.
2243
+
2244
+ ```ts
2245
+ import { custom, type APlotElement } from '@antadesign/plot/browser'
2246
+ import '@antadesign/plot/elements/a-plot'
2247
+
2248
+ const plot = document.createElement('a-plot') as APlotElement
2249
+
2250
+ // Deterministic samples and a Gaussian kernel density estimate, used by the violin example.
2251
+ function distributions() {
2252
+ return ['A', 'B', 'C'].map((group, g) => {
2253
+ const values = Array.from({ length: 60 }, (_, i) => {
2254
+ const z = Math.sqrt(-2 * Math.log((i + 0.5) / 60)) * Math.cos(i * 2.399963)
2255
+ return 36 + g * 10 + z * (6 + g * 2) + (g === 2 && i % 2 === 0 ? 18 : 0)
2256
+ }).sort((a, b) => a - b)
2257
+ const density = Array.from({ length: 101 }, (_, value) => ({
2258
+ value,
2259
+ density: values.reduce((sum, sample) => sum + Math.exp(-0.5 * ((value - sample) / 4) ** 2), 0)
2260
+ / (values.length * 4 * Math.sqrt(2 * Math.PI)),
2261
+ }))
2262
+ return { group, values, density, q1: values[15], median: (values[29] + values[30]) / 2, q3: values[44] }
2263
+ })
2264
+ }
2265
+
2266
+ // Test the same closed polygon used for drawing, including the spaces between sampled points.
2267
+ function containsPoint(points: number[][], x: number, y: number): boolean {
2268
+ let inside = false
2269
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
2270
+ const [xi, yi] = points[i]
2271
+ const [xj, yj] = points[j]
2272
+ if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside
2273
+ }
2274
+ return inside
2275
+ }
2276
+
2277
+ const data = distributions()
2278
+ const peak = Math.max(...data.flatMap(row => row.density.map(point => point.density)))
2279
+
2280
+ plot.plotArgs = {
2281
+ series: [custom<Node>({
2282
+ data,
2283
+ x: 'group',
2284
+ color: (_, i) => [
2285
+ { light: '#0d9488', dark: '#5eead4' },
2286
+ { light: '#7c3aed', dark: '#c4b5fd' },
2287
+ { light: '#c2410c', dark: '#fdba74' },
2288
+ ][i],
2289
+ hit_test: (_, { cursor, y_scale, resolve_x, inner }) => {
2290
+ const width = Math.min(34, (inner.right - inner.left) / 9)
2291
+ for (let i = data.length - 1; i >= 0; i--) {
2292
+ const x = resolve_x(i)
2293
+ if (x === undefined) continue
2294
+ const points = [
2295
+ ...data[i].density.map(point => [x + point.density / peak * width, Number(y_scale(point.value))]),
2296
+ ...data[i].density.slice().reverse().map(point => [x - point.density / peak * width, Number(y_scale(point.value))]),
2297
+ ]
2298
+ if (containsPoint(points, cursor.x, cursor.y)) return i
2299
+ }
2300
+ return null
2301
+ },
2302
+ tooltip: ({ row }) => document.createTextNode('Group ' + row?.group
2303
+ + ' · median ' + Number(row?.median).toFixed(1)
2304
+ + ' · middle 50%: ' + Number(row?.q1).toFixed(1) + '–' + Number(row?.q3).toFixed(1)),
2305
+ renderer: (_, { ctx, y_scale, resolve_x, color_at, inner }) => {
2306
+ const width = Math.min(34, (inner.right - inner.left) / 9)
2307
+ ctx.save()
2308
+ data.forEach((row, i) => {
2309
+ const x = resolve_x(i)
2310
+ if (x === undefined) return
2311
+ ctx.fillStyle = color_at(i)
2312
+ ctx.beginPath()
2313
+ row.density.forEach((point, j) => {
2314
+ const px = x + point.density / peak * width
2315
+ const py = Number(y_scale(point.value))
2316
+ if (j === 0) ctx.moveTo(px, py)
2317
+ else ctx.lineTo(px, py)
2318
+ })
2319
+ row.density.slice().reverse().forEach(point => {
2320
+ ctx.lineTo(x - point.density / peak * width, Number(y_scale(point.value)))
2321
+ })
2322
+ ctx.closePath()
2323
+ ctx.fill()
2324
+ ctx.strokeStyle = '#151b28'
2325
+ ctx.lineWidth = 4
2326
+ ctx.beginPath()
2327
+ ctx.moveTo(x, Number(y_scale(row.q1)))
2328
+ ctx.lineTo(x, Number(y_scale(row.q3)))
2329
+ ctx.stroke()
2330
+ ctx.fillStyle = '#ffffff'
2331
+ ctx.beginPath()
2332
+ ctx.arc(x, Number(y_scale(row.median)), 3, 0, Math.PI * 2)
2333
+ ctx.fill()
2334
+ })
2335
+ ctx.restore()
2336
+ },
2337
+ })],
2338
+ height: 260,
2339
+ margin: { top: 8, right: 8, bottom: 24, left: 30 },
2340
+ axis: { x: { categories: ['A', 'B', 'C'], label: '' }, y: { min: 0, max: 100, label: '' } },
2341
+ background: { light: '#ffffff', dark: '#151b28' },
2342
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2343
+ grid: false,
2344
+ border: false,
2345
+ zoom_pan: false,
2346
+ }
2347
+
2348
+ document.body.append(plot)
2349
+ ```
2350
+
2351
+ Custom arguments
2352
+
2353
+ | Field | Type | Default | Description |
2354
+ |---|---|---|---|
2355
+ | `data?` | `Record<string, unknown>[]` | None | Optional rows; required when using field selectors or hit testing. |
2356
+ | `x?` | [`FieldArg`](#data-fields) | Unclaimed | Horizontal field or accessor; omitted axes do not contribute data bounds. |
2357
+ | `y?` | [`FieldArg`](#data-fields) | Unclaimed | Vertical field or accessor; omitted axes do not contribute data bounds. |
2358
+ | [`renderer`](#custom-rendering) | `CustomRendererFn` | Required | Draw the series using its data and the supplied canvas context and pixel resolvers. |
2359
+ | [`hit_test?`](#custom-hit-testing) | `CustomHitTestFn` | None | Return a row index for a hit, or `null`. Required for custom tooltips and selection. |
2360
+ | `color?` | [`ColorArg`](#series-colors) | Black | Color made available to the renderer, including optional per-row colors. |
2361
+ | `axis_range?` | `{ x?: number[]; y?: number[] }` | Data extent | Explicit `[low, high]` bounds for either axis. |
2362
+ | [`tooltip?`](#tooltips) | `TooltipArg` | None | Use `true` for the default tooltip, or a callback for custom content. |
2363
+ | [`on_select?`](#selection-callbacks) | `SelectFn` | None | Callback receiving the selected point and its source row. |
2364
+ | `hoverable?` | `boolean` | `true` | Allow the series to participate in hit testing. |
2365
+
2366
+ This example draws a cross at each point and finds the nearest cross within
2367
+ eight pixels for tooltips and selection:
2368
+
2369
+ ```ts
2370
+ import { custom } from '@antadesign/plot'
2371
+
2372
+ const series = [custom({
2373
+ data: [{ x: 1, y: 3 }, { x: 2, y: 5 }, { x: 3, y: 4 }],
2374
+ x: 'x',
2375
+ y: 'y',
2376
+ color: { light: 'steelblue', dark: 'lightskyblue' },
2377
+ axis_range: { x: [0, 4], y: [0, 6] },
2378
+ renderer: (series, { ctx, resolve_x, resolve_y, color_at }) => {
2379
+ ctx.save()
2380
+ ctx.lineWidth = 2
2381
+ for (let i = 0; i < series.x.length; i++) {
2382
+ const x = resolve_x(i)
2383
+ const y = resolve_y(i)
2384
+ if (x === undefined || y === undefined) continue
2385
+ ctx.strokeStyle = color_at(i)
2386
+ ctx.beginPath()
2387
+ ctx.moveTo(x - 4, y)
2388
+ ctx.lineTo(x + 4, y)
2389
+ ctx.moveTo(x, y - 4)
2390
+ ctx.lineTo(x, y + 4)
2391
+ ctx.stroke()
2392
+ }
2393
+ ctx.restore()
2394
+ },
2395
+ hit_test: (series, { cursor, resolve_x, resolve_y }) => {
2396
+ let nearest: number | null = null
2397
+ let distance = 8
2398
+ for (let i = 0; i < series.x.length; i++) {
2399
+ const x = resolve_x(i)
2400
+ const y = resolve_y(i)
2401
+ if (x === undefined || y === undefined) continue
2402
+ const candidate = Math.hypot(cursor.x - x, cursor.y - y)
2403
+ if (candidate <= distance) {
2404
+ nearest = i
2405
+ distance = candidate
2406
+ }
2407
+ }
2408
+ return nearest
2409
+ },
2410
+ tooltip: ({ x, y }) => `(${x}, ${y})`,
2411
+ on_select: ({ row }) => console.log('Selected row:', row),
2412
+ hoverable: true,
2413
+ })]
2414
+ ```
2415
+
2416
+ For tooltips or selection, also provide `data` and a [`hit_test`](#custom-hit-testing) that returns
2417
+ the matching row index, or `null` when there is no hit. Use `axis_range`
2418
+ to declare bounds when they cannot be inferred from the data.
2419
+
2420
+ ## Interactions
2421
+
2422
+ ### Hover highlights
2423
+
2424
+ Hover bars A through D to see their color intensify. Bar E sets `highlight: false`, so its appearance stays unchanged. Bar F uses `hoverable: false`, so it does not participate in hit testing.
2425
+
2426
+ Hover feedback is temporary. For custom geometry, supply a [hit-testing callback](#custom-hit-testing).
2427
+
2428
+ ```ts
2429
+ import { bar, type APlotElement } from '@antadesign/plot/browser'
2430
+ import '@antadesign/plot/elements/a-plot'
2431
+
2432
+ const plot = document.createElement('a-plot') as APlotElement
2433
+
2434
+ const data = [
2435
+ { x: 'A', y: 64 }, { x: 'B', y: 82 },
2436
+ { x: 'C', y: 53 }, { x: 'D', y: 71 },
2437
+ { x: 'E', y: 44 }, { x: 'F', y: 61 },
2438
+ ]
2439
+
2440
+ plot.plotArgs = {
2441
+ series: [
2442
+ bar<Node>({
2443
+ data: data.slice(0, 4), inset: 7, border_radius: 4,
2444
+ color: { light: 'rgba(13, 148, 136, 0.35)', dark: 'rgba(94, 234, 212, 0.35)' },
2445
+ }),
2446
+ bar<Node>({
2447
+ data: data.slice(4, 5), inset: 7, border_radius: 4,
2448
+ color: { light: '#7c3aed', dark: '#c4b5fd' },
2449
+ highlight: false,
2450
+ }),
2451
+ bar<Node>({
2452
+ data: data.slice(5), inset: 7, border_radius: 4,
2453
+ color: { light: '#94a3b8', dark: '#64748b' },
2454
+ hoverable: false,
2455
+ }),
2456
+ ],
2457
+ height: 260,
2458
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2459
+ axis: { x: { categories: data.map(row => row.x), label: '' }, y: { min: 0, max: 100, label: '' } },
2460
+ background: { light: '#ffffff', dark: '#151b28' },
2461
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2462
+ grid: false,
2463
+ border: false,
2464
+ zoom_pan: false,
2465
+ }
2466
+
2467
+ document.body.append(plot)
2468
+ ```
2469
+
2470
+ #### Custom hit testing
2471
+
2472
+ `CustomHitTestFn<TooltipContent>` receives the composed custom series and a
2473
+ `HitContext`. Return the matching row index or `null` when there is no hit.
2474
+ Custom series need this callback for tooltips and selection.
2475
+
2476
+ ```ts
2477
+ import type { ComposedCustom, HitContext } from '@antadesign/plot'
2478
+
2479
+ type HitTest = (series: ComposedCustom, context: HitContext) => number | null
2480
+ ```
2481
+
2482
+ | Context field | Type | Description |
2483
+ |---|---|---|
2484
+ | `cursor` | `{ x: number; y: number }` | Pointer position in the same pixel coordinates as the resolvers. |
2485
+ | `inner` | [`Rect`](#custom-rendering) | Plot bounds in pixels. |
2486
+ | `x_scale` | [`Scale`](#custom-rendering) | Composed horizontal scale. |
2487
+ | `y_scale` | [`Scale`](#custom-rendering) | Composed vertical scale. |
2488
+ | `resolve_x` | [`PixelResolver`](#custom-rendering) | Resolve a row index to its x position. |
2489
+ | `resolve_y` | [`PixelResolver`](#custom-rendering) | Resolve a row index to its y position. |
2490
+
2491
+ ### Tooltips
2492
+
2493
+ Hover a rectangle to inspect its data. The series with `tooltip: true` shows the default tooltip. The series with a tooltip callback shows custom content describing its bounds. Where the two series overlap, both tooltip entries appear together, separated by a divider. The tooltip follows the pointer.
2494
+
2495
+ Use `tooltip: true` for default content, or return a DOM node from the standalone host’s callback. React callbacks can return JSX. Both receive the [tooltip and selection callback parameters](#tooltip-and-selection-callback-parameters). Omit `tooltip` to keep hover feedback without a tooltip. See [Tooltip content](#tooltip-content) for the callback types.
2496
+
2497
+ ```ts
2498
+ import { rect, type APlotElement } from '@antadesign/plot/browser'
2499
+ import '@antadesign/plot/elements/a-plot'
2500
+
2501
+ const plot = document.createElement('a-plot') as APlotElement
2502
+
2503
+ const data = [
2504
+ { x: 8, x2: 38, y: 15, y2: 48, name: 'Window A' },
2505
+ { x: 55, x2: 86, y: 48, y2: 82, name: 'Window B' },
2506
+ ]
2507
+
2508
+ plot.plotArgs = {
2509
+ series: [
2510
+ rect<Node>({
2511
+ data,
2512
+ color: { light: 'rgba(13, 148, 136, 0.45)', dark: 'rgba(94, 234, 212, 0.45)' },
2513
+ stroke: { color: { light: '#0d9488', dark: '#5eead4' }, width: 2 },
2514
+ tooltip: true,
2515
+ }),
2516
+ rect<Node>({
2517
+ data: data.map(row => ({
2518
+ ...row, x: row.x + 12, x2: row.x2 + 8, y: row.y + 12, y2: row.y2 + 10,
2519
+ })),
2520
+ color: { light: 'rgba(124, 58, 237, 0.45)', dark: 'rgba(196, 181, 253, 0.45)' },
2521
+ stroke: { color: { light: '#7c3aed', dark: '#c4b5fd' }, width: 2 },
2522
+ tooltip: ({ row }) => {
2523
+ const content = document.createElement('div')
2524
+ const title = document.createElement('strong')
2525
+ title.textContent = String(row.name)
2526
+ const value = document.createElement('div')
2527
+ value.textContent = 'Bounds: x ' + row.x + '–' + row.x2 + ', y ' + row.y + '–' + row.y2
2528
+ content.append(title, value)
2529
+ return content
2530
+ },
2531
+ }),
2532
+ ],
2533
+ height: 260,
2534
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2535
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2536
+ background: { light: '#ffffff', dark: '#151b28' },
2537
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2538
+ grid: false,
2539
+ border: false,
2540
+ zoom_pan: false,
2541
+ }
2542
+
2543
+ document.body.append(plot)
2544
+ ```
2545
+
2546
+ #### Tooltip content
2547
+
2548
+ `TooltipArg<Row, TooltipContent>` accepts `true` or a `TooltipFn` callback.
2549
+ Omit `tooltip` to show no tooltip for that series.
2550
+
2551
+ | Option | Type | Description |
2552
+ |---|---|---|
2553
+ | Default tooltip | `true` | Enable the host's default presentation of the point's axis values. Omit `tooltip` to disable it; `false` is not accepted. |
2554
+ | Custom tooltip | [`(data: TooltipData<Row>) => TooltipContent`](#tooltip-and-selection-callback-parameters) | Return content for the hovered point. |
2555
+
2556
+ With the React component, `TooltipContent` is a `ReactNode`, including strings
2557
+ and JSX. With the standalone browser host, return a DOM `Node`. The core
2558
+ leaves the content type to the host.
2559
+
2560
+ ### Selection
2561
+
2562
+ Select a point to display its source data in the preview’s text readout. `on_select` receives the selected point and its original row, so you can update a detail panel or application state.
2563
+
2564
+ The callback does not create persistent selection styling. This preview keeps the last selected value in a text readout. See [Selection callbacks](#selection-callbacks) for the payload.
2565
+
2566
+ ```ts
2567
+ import { scatter, type APlotElement } from '@antadesign/plot/browser'
2568
+ import '@antadesign/plot/elements/a-plot'
2569
+
2570
+ const plot = document.createElement('a-plot') as APlotElement
2571
+ const output = document.createElement('output')
2572
+ output.setAttribute('aria-live', 'polite')
2573
+ output.style.display = 'block'
2574
+ output.textContent = 'Select a point to inspect its data.'
2575
+
2576
+ const data = Array.from({ length: 24 }, (_, i) => ({
2577
+ x: 8 + (i * 19 % 84),
2578
+ y: 12 + (i * 29 % 76),
2579
+ name: 'Sample ' + (i + 1),
2580
+ }))
2581
+
2582
+ plot.plotArgs = {
2583
+ series: [
2584
+ scatter<Node>({
2585
+ data, size: 13,
2586
+ color: { light: '#0891b2', dark: '#67e8f9' },
2587
+ tooltip: true,
2588
+ on_select: ({ row, x, y }) => {
2589
+ output.textContent = row.name + ' · x: ' + x + ' · y: ' + y
2590
+ },
2591
+ }),
2592
+ ],
2593
+ height: 260,
2594
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2595
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2596
+ background: { light: '#ffffff', dark: '#151b28' },
2597
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2598
+ grid: false,
2599
+ border: false,
2600
+ zoom_pan: false,
2601
+ }
2602
+
2603
+ document.body.append(plot, output)
2604
+ ```
2605
+
2606
+ #### Selection callbacks
2607
+
2608
+ `SelectFn<Row>` is [`(data: TooltipData<Row>) => unknown`](#tooltip-and-selection-callback-parameters). Assign it to
2609
+ `on_select` to respond when a point is selected. Its return value is ignored.
2610
+
2611
+ ```ts
2612
+ import type { SelectFn } from '@antadesign/plot'
2613
+
2614
+ const on_select: SelectFn = ({ x, y, row }) => {
2615
+ console.log('Selected point:', x, y, row)
2616
+ }
2617
+ ```
2618
+
2619
+ ##### Tooltip and selection callback parameters
2620
+
2621
+ Tooltip and selection callbacks receive the same `TooltipData<Row>` shape:
2622
+
2623
+ | Field | Type | Description |
2624
+ |---|---|---|
2625
+ | `x` | `number \| string` | Point's x value, or category label. |
2626
+ | `y` | `number \| string` | Point's y value, or category label. |
2627
+ | `row` | `Row` | Source data row. May be `undefined` for rule or custom series without data. |
2628
+ | `label?` | `string` | Point label when available, such as the field name of a stacked bar segment. |
2629
+
2630
+ `Row` defaults to `Record<string, unknown>`. The related `PointData` type has
2631
+ the same fields with an optional `row`.
2632
+
2633
+ ### Zoom and pan
2634
+
2635
+ Zoom continuous axes with the scroll wheel and pan by dragging. The reset control restores the full extent after the view changes. Categorical axes do not zoom. See [Zoom and pan options](#zoom-and-pan-options).
2636
+
2637
+ Zoom with a modifier
2638
+
2639
+ Hold Ctrl and scroll to zoom around the pointer, or hold Ctrl and drag to pan. The modifier leaves ordinary scrolling available for navigating the page.
2640
+
2641
+ ```ts
2642
+ import { line, type APlotElement } from '@antadesign/plot/browser'
2643
+ import '@antadesign/plot/elements/a-plot'
2644
+
2645
+ const plot = document.createElement('a-plot') as APlotElement
2646
+ const output = document.createElement('output')
2647
+ output.setAttribute('aria-live', 'polite')
2648
+ output.style.display = 'block'
2649
+ output.textContent = 'Hold Ctrl and scroll to zoom, or Ctrl-drag to pan.'
2650
+
2651
+ const data = Array.from({ length: 201 }, (_, i) => ({
2652
+ x: i / 2,
2653
+ y: 45 + 18 * Math.sin(i / 18) + 9 * Math.cos(i / 5) + 4 * Math.sin(i * 1.3),
2654
+ }))
2655
+
2656
+ plot.plotArgs = {
2657
+ series: [
2658
+ line<Node>({
2659
+ data, width: 2,
2660
+ color: { light: '#6366f1', dark: '#a5b4fc' },
2661
+ tooltip: true,
2662
+ }),
2663
+ ],
2664
+ height: 260,
2665
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2666
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2667
+ background: { light: '#ffffff', dark: '#151b28' },
2668
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2669
+ grid: false,
2670
+ border: false,
2671
+ zoom_pan: { x: true, y: true, modifier: true },
2672
+ on_viewport_change: ({ x, y }) => {
2673
+ const format = (axis: { window: number[] } | null) => axis
2674
+ ? axis.window.map(value => value.toFixed(1)).join('–') : 'full extent'
2675
+ output.textContent = 'Visible x: ' + format(x) + ' · y: ' + format(y)
2676
+ },
2677
+ }
2678
+
2679
+ document.body.append(plot, output)
2680
+ ```
2681
+
2682
+ Zoom without a modifier
2683
+
2684
+ Scroll over the plot to zoom, or drag to pan. Set `modifier: false` when the plot should own these gestures without requiring Ctrl.
2685
+
2686
+ ```ts
2687
+ import { line, type APlotElement } from '@antadesign/plot/browser'
2688
+ import '@antadesign/plot/elements/a-plot'
2689
+
2690
+ const plot = document.createElement('a-plot') as APlotElement
2691
+ const output = document.createElement('output')
2692
+ output.setAttribute('aria-live', 'polite')
2693
+ output.style.display = 'block'
2694
+ output.textContent = 'Scroll to zoom, or drag to pan.'
2695
+
2696
+ const data = Array.from({ length: 201 }, (_, i) => ({
2697
+ x: i / 2,
2698
+ y: 45 + 18 * Math.sin(i / 18) + 9 * Math.cos(i / 5) + 4 * Math.sin(i * 1.3),
2699
+ }))
2700
+
2701
+ plot.plotArgs = {
2702
+ series: [
2703
+ line<Node>({
2704
+ data, width: 2,
2705
+ color: { light: '#6366f1', dark: '#a5b4fc' },
2706
+ tooltip: true,
2707
+ }),
2708
+ ],
2709
+ height: 260,
2710
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2711
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2712
+ background: { light: '#ffffff', dark: '#151b28' },
2713
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2714
+ grid: false,
2715
+ border: false,
2716
+ zoom_pan: { x: true, y: true, modifier: false },
2717
+ on_viewport_change: ({ x, y }) => {
2718
+ const format = (axis: { window: number[] } | null) => axis
2719
+ ? axis.window.map(value => value.toFixed(1)).join('–') : 'full extent'
2720
+ output.textContent = 'Visible x: ' + format(x) + ' · y: ' + format(y)
2721
+ },
2722
+ }
2723
+
2724
+ document.body.append(plot, output)
2725
+ ```
2726
+
2727
+ Zoom on one axis
2728
+
2729
+ Hold Ctrl and scroll or drag. Only the horizontal window changes; the vertical range stays fixed. Set `x: false, y: true` for vertical-only navigation.
2730
+
2731
+ ```ts
2732
+ import { line, type APlotElement } from '@antadesign/plot/browser'
2733
+ import '@antadesign/plot/elements/a-plot'
2734
+
2735
+ const plot = document.createElement('a-plot') as APlotElement
2736
+ const output = document.createElement('output')
2737
+ output.setAttribute('aria-live', 'polite')
2738
+ output.style.display = 'block'
2739
+ output.textContent = 'Hold Ctrl and scroll to zoom, or Ctrl-drag to pan.'
2740
+
2741
+ const data = Array.from({ length: 201 }, (_, i) => ({
2742
+ x: i / 2,
2743
+ y: 45 + 18 * Math.sin(i / 18) + 9 * Math.cos(i / 5) + 4 * Math.sin(i * 1.3),
2744
+ }))
2745
+
2746
+ plot.plotArgs = {
2747
+ series: [
2748
+ line<Node>({
2749
+ data, width: 2,
2750
+ color: { light: '#6366f1', dark: '#a5b4fc' },
2751
+ tooltip: true,
2752
+ }),
2753
+ ],
2754
+ height: 260,
2755
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2756
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2757
+ background: { light: '#ffffff', dark: '#151b28' },
2758
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2759
+ grid: false,
2760
+ border: false,
2761
+ zoom_pan: { x: true, y: false, modifier: true },
2762
+ on_viewport_change: ({ x, y }) => {
2763
+ const format = (axis: { window: number[] } | null) => axis
2764
+ ? axis.window.map(value => value.toFixed(1)).join('–') : 'full extent'
2765
+ output.textContent = 'Visible x: ' + format(x) + ' · y: ' + format(y)
2766
+ },
2767
+ }
2768
+
2769
+ document.body.append(plot, output)
2770
+ ```
2771
+
2772
+ #### Zoom and pan options
2773
+
2774
+ `zoom_pan` accepts a boolean for both axes or this `ZoomPanArg` object:
2775
+
2776
+ | Field | Type | Default | Description |
2777
+ |---|---|---|---|
2778
+ | `x?` | `boolean` | `true` | Enable horizontal zoom and pan on a continuous axis. |
2779
+ | `y?` | `boolean` | `true` | Enable vertical zoom and pan on a continuous axis. |
2780
+ | `modifier?` | `boolean` | `true` | Require Ctrl for gestures. Set `false` to allow gestures without a modifier. |
2781
+
2782
+ Setting both axes to `false` disables zoom and pan. Categorical axes do not zoom.
2783
+
2784
+ ### Controlling the viewport
2785
+
2786
+ Use the buttons to focus on a range or restore the full extent. You can still zoom and pan horizontally with Ctrl after a button sets the view.
2787
+
2788
+ Set `viewport` to control the visible range. Increment its `key` to apply a new request after the user zooms or pans. See [Viewport](#viewport) for initialization and reset behavior.
2789
+
2790
+ `on_viewport_change` reports the visible and full ranges. Keep this notification separate from viewport requests to avoid reapplying the view on every gesture. See [Viewport changes](#viewport-changes) for the callback parameters.
2791
+
2792
+ ```ts
2793
+ import { line, type APlotElement } from '@antadesign/plot/browser'
2794
+ import '@antadesign/plot/elements/a-plot'
2795
+ import '@antadesign/anta/elements/a-button'
2796
+
2797
+ const plot = document.createElement('a-plot') as APlotElement
2798
+ const output = document.createElement('output')
2799
+ output.setAttribute('aria-live', 'polite')
2800
+ output.style.display = 'block'
2801
+ output.textContent = 'Hold Ctrl and scroll to zoom, or Ctrl-drag to pan.'
2802
+
2803
+ const data = Array.from({ length: 201 }, (_, i) => ({
2804
+ x: i / 2,
2805
+ y: 45 + 18 * Math.sin(i / 18) + 9 * Math.cos(i / 5) + 4 * Math.sin(i * 1.3),
2806
+ }))
2807
+
2808
+ plot.plotArgs = {
2809
+ series: [
2810
+ line<Node>({
2811
+ data, width: 2,
2812
+ color: { light: '#6366f1', dark: '#a5b4fc' },
2813
+ tooltip: true,
2814
+ }),
2815
+ ],
2816
+ height: 260,
2817
+ margin: { top: 8, right: 12, bottom: 24, left: 32 },
2818
+ axis: { x: { min: 0, max: 100, label: '' }, y: { min: 0, max: 100, label: '' } },
2819
+ background: { light: '#ffffff', dark: '#151b28' },
2820
+ chrome_color: { light: '#e2e8f0', dark: '#334155' },
2821
+ grid: false,
2822
+ border: false,
2823
+ zoom_pan: { x: true, y: false, modifier: true },
2824
+ viewport: { x: [20, 45], y: null, key: 0 },
2825
+ on_viewport_change: ({ x, y }) => {
2826
+ const format = (axis: { window: number[] } | null) => axis
2827
+ ? axis.window.map(value => value.toFixed(1)).join('–') : 'full extent'
2828
+ output.textContent = 'Visible x: ' + format(x) + ' · y: ' + format(y)
2829
+ },
2830
+ }
2831
+
2832
+ const controls = document.createElement('div')
2833
+ let key = 0
2834
+ const windows: [string, number[] | null][] = [
2835
+ ['Focus 20–45', [20, 45]], ['Focus 55–80', [55, 80]], ['Full extent', null],
2836
+ ]
2837
+ for (const [label, x] of windows) {
2838
+ const button = document.createElement('a-button')
2839
+ button.textContent = label
2840
+ button.addEventListener('click', () => {
2841
+ const args = plot.plotArgs
2842
+ if (!args) return
2843
+ plot.plotArgs = { ...args, viewport: { x, y: null, key: ++key } }
2844
+ })
2845
+ controls.append(button)
2846
+ }
2847
+ document.body.append(plot, controls, output)
2848
+ ```
2849
+
2850
+ #### Viewport
2851
+
2852
+ `viewport` requests a visible window within the full data domain. It applies
2853
+ at mount and when `key` changes; user gestures can then move away from it.
2854
+ Changing only `x` or `y` with the same key does not reapply the request.
2855
+
2856
+ | Field | Type | Default | Description |
2857
+ |---|---|---|---|
2858
+ | `x?` | `number[] \| null` | Leave unchanged | Requested `[min, max]` on x, clamped to the full domain. `null` restores its full extent. |
2859
+ | `y?` | `number[] \| null` | Leave unchanged | Requested `[min, max]` on y, clamped to the full domain. `null` restores its full extent. |
2860
+ | `key?` | `string \| number` | None | Change this value to apply a new viewport request. |
2861
+
2862
+ At mount, an omitted axis starts at its full extent. Viewport windows apply
2863
+ to continuous axes.
2864
+
2865
+ ##### Viewport changes
2866
+
2867
+ `on_viewport_change` receives a `ViewportChange` after a gesture or reset:
2868
+
2869
+ | Field | Type | Description |
2870
+ |---|---|---|
2871
+ | `x` | `AxisViewport \| null` | Current horizontal viewport, or `null` for a categorical axis. |
2872
+ | `y` | `AxisViewport \| null` | Current vertical viewport, or `null` for a categorical axis. |
2873
+
2874
+ Each `AxisViewport` contains:
2875
+
2876
+ | Field | Type | Description |
2877
+ |---|---|---|
2878
+ | `window` | `number[]` | Current visible `[min, max]`. |
2879
+ | `full` | `number[]` | Full unzoomed `[min, max]`. |
2880
+
2881
+ ## Configuration details
2882
+
2883
+ ### Title
2884
+
2885
+ Set the title text, size, and purple theme colors.
2886
+
2887
+ ```ts
2888
+ import { scatter, type PlotArgs } from '@antadesign/plot'
2889
+ import '@antadesign/plot/elements/a-plot'
2890
+ import type { APlotElement } from '@antadesign/plot/browser'
2891
+
2892
+ function base(): PlotArgs<Node> {
2893
+ return {
2894
+ series: [scatter<Node>({ data: [
2895
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
2896
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
2897
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
2898
+ height: 260,
2899
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
2900
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
2901
+ background: { light: '#ffffff', dark: '#202124' },
2902
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
2903
+ border: true,
2904
+ grid: false,
2905
+ zoom_pan: false,
2906
+ }
2907
+ }
2908
+
2909
+ function title(): PlotArgs<Node> {
2910
+ return { ...base(), margin: { top: 44, right: 24, bottom: 48, left: 76 },
2911
+ title: { text: 'Plot title', size: 20, color: { light: '#713fff', dark: '#c4b5fd' } } }
2912
+ }
2913
+
2914
+ const plot = document.createElement('a-plot') as APlotElement
2915
+ plot.plotArgs = title()
2916
+ document.body.append(plot)
2917
+ ```
2918
+
2919
+ `title` accepts a string or a `TitleArg` object:
2920
+
2921
+ | Field | Type | Default | Description |
2922
+ |---|---|---|---|
2923
+ | `text` | `string` | Required | Plot title text. |
2924
+ | `size?` | `number` | `14` | Font size in pixels. |
2925
+ | `color?` | [`ThemeColor`](#theme-colors) | Theme default | Title color. |
2926
+
2927
+ ### Margins
2928
+
2929
+ The canvas uses a gray CSS background. The plot background fills only the area inside the axes, making the margins visible.
2930
+
2931
+ ```ts
2932
+ import { scatter, type PlotArgs } from '@antadesign/plot'
2933
+ import '@antadesign/plot/elements/a-plot'
2934
+ import type { APlotElement } from '@antadesign/plot/browser'
2935
+
2936
+ function base(): PlotArgs<Node> {
2937
+ return {
2938
+ series: [scatter<Node>({ data: [
2939
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
2940
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
2941
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
2942
+ height: 260,
2943
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
2944
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
2945
+ background: { light: '#ffffff', dark: '#202124' },
2946
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
2947
+ border: true,
2948
+ grid: false,
2949
+ zoom_pan: false,
2950
+ }
2951
+ }
2952
+
2953
+ function margins(): PlotArgs<Node> {
2954
+ return { ...base(), margin: { top: 40, right: 40, bottom: 64, left: 88 },
2955
+ background: { light: 'white', dark: '#202124' } }
2956
+ }
2957
+
2958
+ const plot = document.createElement('a-plot') as APlotElement
2959
+ plot.plotArgs = margins()
2960
+ plot.style.background = 'color-mix(in srgb, var(--text-1, #111827) 18%, var(--bg-1, #ffffff))'
2961
+ document.body.append(plot)
2962
+ ```
2963
+
2964
+ `margin` accepts one number for all sides or a `SideMargins` object.
2965
+ Margins reserve space outside the plot area for labels and the title;
2966
+ axis padding adds space inside it.
2967
+
2968
+ | Field | Type | Default | Description |
2969
+ |---|---|---|---|
2970
+ | `top?` | `number` | `60` | Top margin in pixels. |
2971
+ | `right?` | `number` | `60` | Right margin in pixels. |
2972
+ | `bottom?` | `number` | `60` | Bottom margin in pixels. |
2973
+ | `left?` | `number` | `60` | Left margin in pixels. |
2974
+
2975
+ When both axes are hidden and there is no title, omitted margins use `2`
2976
+ pixels instead. Explicit margins still apply.
2977
+
2978
+ ### Grid
2979
+
2980
+ Compare both grid directions, each direction separately, and no grid. Seven categories set the vertical grid positions; tick marks and labels are hidden.
2981
+
2982
+ Both grid directions
2983
+
2984
+ ```ts
2985
+ import { scatter, type PlotArgs } from '@antadesign/plot'
2986
+ import '@antadesign/plot/elements/a-plot'
2987
+ import type { APlotElement } from '@antadesign/plot/browser'
2988
+
2989
+ function base(): PlotArgs<Node> {
2990
+ return {
2991
+ series: [scatter<Node>({ data: [
2992
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
2993
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
2994
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
2995
+ height: 260,
2996
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
2997
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
2998
+ background: { light: '#ffffff', dark: '#202124' },
2999
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3000
+ border: true,
3001
+ grid: false,
3002
+ zoom_pan: false,
3003
+ }
3004
+ }
3005
+
3006
+ function grid(): PlotArgs<Node> {
3007
+ const categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
3008
+ return { ...base(), height: 160, margin: { top: 10, right: 10, bottom: 10, left: 10 },
3009
+ series: [scatter<Node>({ data: categories.map((x, i) => ({ x, y: 20 + i * 9 })),
3010
+ size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3011
+ axis: { x: { categories, label: '', tick_mark: false, tick_label: { format: () => '' } },
3012
+ y: { min: 0, max: 100, label: '', tick_mark: false, tick_label: { format: () => '' } } },
3013
+ grid: { x: true, y: true } }
3014
+ }
3015
+
3016
+ const plot = document.createElement('a-plot') as APlotElement
3017
+ plot.plotArgs = grid()
3018
+ document.body.append(plot)
3019
+ ```
3020
+
3021
+ Vertical grid lines
3022
+
3023
+ ```ts
3024
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3025
+ import '@antadesign/plot/elements/a-plot'
3026
+ import type { APlotElement } from '@antadesign/plot/browser'
3027
+
3028
+ function base(): PlotArgs<Node> {
3029
+ return {
3030
+ series: [scatter<Node>({ data: [
3031
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3032
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3033
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3034
+ height: 260,
3035
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3036
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3037
+ background: { light: '#ffffff', dark: '#202124' },
3038
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3039
+ border: true,
3040
+ grid: false,
3041
+ zoom_pan: false,
3042
+ }
3043
+ }
3044
+
3045
+ function grid(): PlotArgs<Node> {
3046
+ const categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
3047
+ return { ...base(), height: 160, margin: { top: 10, right: 10, bottom: 10, left: 10 },
3048
+ series: [scatter<Node>({ data: categories.map((x, i) => ({ x, y: 20 + i * 9 })),
3049
+ size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3050
+ axis: { x: { categories, label: '', tick_mark: false, tick_label: { format: () => '' } },
3051
+ y: { min: 0, max: 100, label: '', tick_mark: false, tick_label: { format: () => '' } } },
3052
+ grid: { x: true, y: true } }
3053
+ }
3054
+
3055
+ function gridX(): PlotArgs<Node> {
3056
+ return { ...grid(), grid: { x: true, y: false } }
3057
+ }
3058
+
3059
+ const plot = document.createElement('a-plot') as APlotElement
3060
+ plot.plotArgs = gridX()
3061
+ document.body.append(plot)
3062
+ ```
3063
+
3064
+ Horizontal grid lines
3065
+
3066
+ ```ts
3067
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3068
+ import '@antadesign/plot/elements/a-plot'
3069
+ import type { APlotElement } from '@antadesign/plot/browser'
3070
+
3071
+ function base(): PlotArgs<Node> {
3072
+ return {
3073
+ series: [scatter<Node>({ data: [
3074
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3075
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3076
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3077
+ height: 260,
3078
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3079
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3080
+ background: { light: '#ffffff', dark: '#202124' },
3081
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3082
+ border: true,
3083
+ grid: false,
3084
+ zoom_pan: false,
3085
+ }
3086
+ }
3087
+
3088
+ function grid(): PlotArgs<Node> {
3089
+ const categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
3090
+ return { ...base(), height: 160, margin: { top: 10, right: 10, bottom: 10, left: 10 },
3091
+ series: [scatter<Node>({ data: categories.map((x, i) => ({ x, y: 20 + i * 9 })),
3092
+ size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3093
+ axis: { x: { categories, label: '', tick_mark: false, tick_label: { format: () => '' } },
3094
+ y: { min: 0, max: 100, label: '', tick_mark: false, tick_label: { format: () => '' } } },
3095
+ grid: { x: true, y: true } }
3096
+ }
3097
+
3098
+ function gridY(): PlotArgs<Node> {
3099
+ return { ...grid(), grid: { x: false, y: true } }
3100
+ }
3101
+
3102
+ const plot = document.createElement('a-plot') as APlotElement
3103
+ plot.plotArgs = gridY()
3104
+ document.body.append(plot)
3105
+ ```
3106
+
3107
+ No grid lines
3108
+
3109
+ ```ts
3110
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3111
+ import '@antadesign/plot/elements/a-plot'
3112
+ import type { APlotElement } from '@antadesign/plot/browser'
3113
+
3114
+ function base(): PlotArgs<Node> {
3115
+ return {
3116
+ series: [scatter<Node>({ data: [
3117
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3118
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3119
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3120
+ height: 260,
3121
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3122
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3123
+ background: { light: '#ffffff', dark: '#202124' },
3124
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3125
+ border: true,
3126
+ grid: false,
3127
+ zoom_pan: false,
3128
+ }
3129
+ }
3130
+
3131
+ function grid(): PlotArgs<Node> {
3132
+ const categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
3133
+ return { ...base(), height: 160, margin: { top: 10, right: 10, bottom: 10, left: 10 },
3134
+ series: [scatter<Node>({ data: categories.map((x, i) => ({ x, y: 20 + i * 9 })),
3135
+ size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3136
+ axis: { x: { categories, label: '', tick_mark: false, tick_label: { format: () => '' } },
3137
+ y: { min: 0, max: 100, label: '', tick_mark: false, tick_label: { format: () => '' } } },
3138
+ grid: { x: true, y: true } }
3139
+ }
3140
+
3141
+ function gridNone(): PlotArgs<Node> {
3142
+ return { ...grid(), grid: false }
3143
+ }
3144
+
3145
+ const plot = document.createElement('a-plot') as APlotElement
3146
+ plot.plotArgs = gridNone()
3147
+ document.body.append(plot)
3148
+ ```
3149
+
3150
+ `grid` accepts a boolean for both axes or this `GridSpec` object:
3151
+
3152
+ | Field | Type | Default | Description |
3153
+ |---|---|---|---|
3154
+ | `x?` | `boolean` | `false` | Draw vertical grid lines at x-axis ticks. |
3155
+ | `y?` | `boolean` | `false` | Draw horizontal grid lines at y-axis ticks. |
3156
+
3157
+ ### Theme colors
3158
+
3159
+ The scatter marks use `tomato` in light mode and `lightskyblue` in dark mode, with `size: 18`. Switch the site theme to compare them.
3160
+
3161
+ ```ts
3162
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3163
+ import '@antadesign/plot/elements/a-plot'
3164
+ import type { APlotElement } from '@antadesign/plot/browser'
3165
+
3166
+ function base(): PlotArgs<Node> {
3167
+ return {
3168
+ series: [scatter<Node>({ data: [
3169
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3170
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3171
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3172
+ height: 260,
3173
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3174
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3175
+ background: { light: '#ffffff', dark: '#202124' },
3176
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3177
+ border: true,
3178
+ grid: false,
3179
+ zoom_pan: false,
3180
+ }
3181
+ }
3182
+
3183
+ function themeColors(): PlotArgs<Node> {
3184
+ return { ...base(), series: [scatter<Node>({
3185
+ data: [{ x: 20, y: 30 }, { x: 50, y: 65 }, { x: 80, y: 45 }],
3186
+ size: 18, color: { light: 'tomato', dark: 'lightskyblue' }, hoverable: false,
3187
+ })] }
3188
+ }
3189
+
3190
+ const plot = document.createElement('a-plot') as APlotElement
3191
+ plot.plotArgs = themeColors()
3192
+ document.body.append(plot)
3193
+ ```
3194
+
3195
+ `ThemeColor` accepts a CSS color string or a `ColorPair`. Use a pair to choose
3196
+ separate colors for light and dark themes.
3197
+
3198
+ | Form | Type | Example |
3199
+ |---|---|---|
3200
+ | One color | `string` | `'steelblue'`, `'#4682b4'`, `'rgb(70, 130, 180)'`, `'hsl(207, 44%, 49%)'` |
3201
+ | Theme pair | `{ light: string; dark: string }` | `{ light: 'steelblue', dark: 'lightskyblue' }` |
3202
+
3203
+ Both fields in a `ColorPair` are required. `ColorTheme` is `'light'` or `'dark'`.
3204
+ Plain colors remain subject to the plot's `theme_invert` setting.
3205
+
3206
+ ### Scales
3207
+
3208
+ Compare linear and logarithmic axes, then time and category axes. Each axis label names its scale.
3209
+
3210
+ Linear and logarithmic scales
3211
+
3212
+ ```ts
3213
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3214
+ import '@antadesign/plot/elements/a-plot'
3215
+ import type { APlotElement } from '@antadesign/plot/browser'
3216
+
3217
+ function base(): PlotArgs<Node> {
3218
+ return {
3219
+ series: [scatter<Node>({ data: [
3220
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3221
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3222
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3223
+ height: 260,
3224
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3225
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3226
+ background: { light: '#ffffff', dark: '#202124' },
3227
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3228
+ border: true,
3229
+ grid: false,
3230
+ zoom_pan: false,
3231
+ }
3232
+ }
3233
+
3234
+ function scales(): PlotArgs<Node> {
3235
+ return { ...base(),
3236
+ series: [scatter<Node>({ data: [1, 10, 100, 1000, 10000].map((y, i) => ({ x: 10 + i * 20, y })),
3237
+ size: 4, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3238
+ axis: { x: { scale: 'linear', min: 0, max: 100, label: 'Linear' },
3239
+ y: { scale: 'log', min: 1, max: 10000, label: 'Logarithmic',
3240
+ tick_label: { format: value => Number(value) >= 1000 ? Number(value) / 1000 + 'k' : String(value) } } },
3241
+ grid: { y: true },
3242
+ }
3243
+ }
3244
+
3245
+ const plot = document.createElement('a-plot') as APlotElement
3246
+ plot.plotArgs = scales()
3247
+ document.body.append(plot)
3248
+ ```
3249
+
3250
+ Time and category scales
3251
+
3252
+ ```ts
3253
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3254
+ import '@antadesign/plot/elements/a-plot'
3255
+ import type { APlotElement } from '@antadesign/plot/browser'
3256
+
3257
+ function base(): PlotArgs<Node> {
3258
+ return {
3259
+ series: [scatter<Node>({ data: [
3260
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3261
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3262
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3263
+ height: 260,
3264
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3265
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3266
+ background: { light: '#ffffff', dark: '#202124' },
3267
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3268
+ border: true,
3269
+ grid: false,
3270
+ zoom_pan: false,
3271
+ }
3272
+ }
3273
+
3274
+ function timeCategory(): PlotArgs<Node> {
3275
+ const start = new Date(2026, 0, 12).getTime()
3276
+ return { ...base(),
3277
+ series: [scatter<Node>({ data: [
3278
+ { x: start, y: 'Alpha' }, { x: start + 3600000, y: 'Beta' },
3279
+ { x: start + 7200000, y: 'Alpha' }, { x: start + 10800000, y: 'Gamma' },
3280
+ { x: start + 14400000, y: 'Beta' },
3281
+ ], size: 4, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3282
+ margin: { top: 20, right: 24, bottom: 48, left: 94 },
3283
+ axis: { x: { scale: 'time', min: start, max: start + 14400000, label: 'Time', padding: 8,
3284
+ tick_label: { format: value => new Date(Number(value)).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }) } },
3285
+ y: { scale: 'category', categories: ['Alpha', 'Beta', 'Gamma'], label: 'Category' } },
3286
+ }
3287
+ }
3288
+
3289
+ const plot = document.createElement('a-plot') as APlotElement
3290
+ plot.plotArgs = timeCategory()
3291
+ document.body.append(plot)
3292
+ ```
3293
+
3294
+ Set `axis.x.scale` or `axis.y.scale` to choose how data maps to positions.
3295
+ Plot uses [D3 scales](https://d3js.org/d3-scale) internally.
3296
+
3297
+ | Value | D3 scale | Description |
3298
+ |---|---|---|
3299
+ | `'linear'` | `scaleLinear` | Equal differences in value produce equal distances. The default for numeric data. |
3300
+ | `'log'` | `scaleLog` | Equal ratios produce equal distances. Use for positive values spanning several orders of magnitude. |
3301
+ | `'category'` | `scaleBand` | Place discrete categories in evenly spaced bands. Inferred from string data. |
3302
+ | `'time'` | `scaleTime` | Position timestamps on a continuous timeline, with ticks in local time. |
3303
+ | `'utc'` | `scaleUtc` | Position timestamps on a continuous timeline, with ticks in UTC. |
3304
+
3305
+ Both time scales accept Unix timestamps in milliseconds. Use `categories`
3306
+ to set a category order, or `min` and `max` to set continuous-domain bounds.
3307
+
3308
+ ### Axis labels
3309
+
3310
+ Compare label positions, sizes, and colors: 18px and 12px labels aligned right and top, then 12px and 20px labels aligned left and bottom.
3311
+
3312
+ Right and top labels
3313
+
3314
+ ```ts
3315
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3316
+ import '@antadesign/plot/elements/a-plot'
3317
+ import type { APlotElement } from '@antadesign/plot/browser'
3318
+
3319
+ function base(): PlotArgs<Node> {
3320
+ return {
3321
+ series: [scatter<Node>({ data: [
3322
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3323
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3324
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3325
+ height: 260,
3326
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3327
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3328
+ background: { light: '#ffffff', dark: '#202124' },
3329
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3330
+ border: true,
3331
+ grid: false,
3332
+ zoom_pan: false,
3333
+ }
3334
+ }
3335
+
3336
+ function axisLabels(): PlotArgs<Node> {
3337
+ return { ...base(), margin: { top: 44, right: 28, bottom: 62, left: 94 }, axis: {
3338
+ x: { min: 0, max: 100, label: { text: 'Time', position: 'right', size: 18, color: { light: '#713fff', dark: '#c4b5fd' } } },
3339
+ y: { min: 0, max: 100, label: { text: 'Value', position: 'top', size: 12, color: { light: '#c2410c', dark: '#fdba74' } } },
3340
+ } }
3341
+ }
3342
+
3343
+ const plot = document.createElement('a-plot') as APlotElement
3344
+ plot.plotArgs = axisLabels()
3345
+ document.body.append(plot)
3346
+ ```
3347
+
3348
+ Left and bottom labels
3349
+
3350
+ ```ts
3351
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3352
+ import '@antadesign/plot/elements/a-plot'
3353
+ import type { APlotElement } from '@antadesign/plot/browser'
3354
+
3355
+ function base(): PlotArgs<Node> {
3356
+ return {
3357
+ series: [scatter<Node>({ data: [
3358
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3359
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3360
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3361
+ height: 260,
3362
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3363
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3364
+ background: { light: '#ffffff', dark: '#202124' },
3365
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3366
+ border: true,
3367
+ grid: false,
3368
+ zoom_pan: false,
3369
+ }
3370
+ }
3371
+
3372
+ function axisLabelsOpposite(): PlotArgs<Node> {
3373
+ return { ...base(), margin: { top: 44, right: 28, bottom: 62, left: 94 }, axis: {
3374
+ x: { min: 0, max: 100, label: { text: 'Time', position: 'left', size: 12,
3375
+ color: { light: '#c2410c', dark: '#fdba74' } } },
3376
+ y: { min: 0, max: 100, label: { text: 'Value', position: 'bottom', size: 20,
3377
+ color: { light: '#713fff', dark: '#c4b5fd' } } },
3378
+ } }
3379
+ }
3380
+
3381
+ const plot = document.createElement('a-plot') as APlotElement
3382
+ plot.plotArgs = axisLabelsOpposite()
3383
+ document.body.append(plot)
3384
+ ```
3385
+
3386
+ `LabelArg` accepts a string or this object at `axis.x.label` or `axis.y.label`:
3387
+
3388
+ | Field | Type | Default | Description |
3389
+ |---|---|---|---|
3390
+ | `text` | `string` | Required | Axis label text. |
3391
+ | `size?` | `number` | `12` | Font size in pixels. |
3392
+ | `color?` | [`ThemeColor`](#theme-colors) | Theme default | Label color. |
3393
+ | `position?` | `'center' \| 'left' \| 'right' \| 'top' \| 'bottom'` | `'center'` | Use `left` or `right` on x; `top` or `bottom` on y. |
3394
+
3395
+ ### Tick labels
3396
+
3397
+ Add units to tick labels with formatting callbacks. The horizontal ticks use 10px purple text; the vertical ticks use 15px orange text.
3398
+
3399
+ ```ts
3400
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3401
+ import '@antadesign/plot/elements/a-plot'
3402
+ import type { APlotElement } from '@antadesign/plot/browser'
3403
+
3404
+ function base(): PlotArgs<Node> {
3405
+ return {
3406
+ series: [scatter<Node>({ data: [
3407
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3408
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3409
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3410
+ height: 260,
3411
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3412
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3413
+ background: { light: '#ffffff', dark: '#202124' },
3414
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3415
+ border: true,
3416
+ grid: false,
3417
+ zoom_pan: false,
3418
+ }
3419
+ }
3420
+
3421
+ function tickLabels(): PlotArgs<Node> {
3422
+ return { ...base(), margin: { top: 44, right: 28, bottom: 62, left: 94 }, axis: {
3423
+ x: { min: 0, max: 100, label: 'Elapsed time', tick_label: { format: value => value + ' s', size: 10, color: { light: '#713fff', dark: '#c4b5fd' } } },
3424
+ y: { min: 0, max: 100, label: 'Utilization', tick_label: { format: value => value + '%', size: 15, color: { light: '#c2410c', dark: '#fdba74' } } },
3425
+ } }
3426
+ }
3427
+
3428
+ const plot = document.createElement('a-plot') as APlotElement
3429
+ plot.plotArgs = tickLabels()
3430
+ document.body.append(plot)
3431
+ ```
3432
+
3433
+ Use `TickLabelArg` at `axis.x.tick_label` or `axis.y.tick_label`:
3434
+
3435
+ | Field | Type | Default | Description |
3436
+ |---|---|---|---|
3437
+ | `format?` | `(value: number \| string, index: number) => string` | Scale-dependent | Format each tick value. Return `''` for an empty label. |
3438
+ | `size?` | `number` | `10` | Font size in pixels. |
3439
+ | `color?` | [`ThemeColor`](#theme-colors) | Theme default | Tick label color. |
3440
+
3441
+ ### Series colors
3442
+
3443
+ Use a color accessor to distinguish values at or above 60.
3444
+
3445
+ ```ts
3446
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3447
+ import '@antadesign/plot/elements/a-plot'
3448
+ import type { APlotElement } from '@antadesign/plot/browser'
3449
+
3450
+ function base(): PlotArgs<Node> {
3451
+ return {
3452
+ series: [scatter<Node>({ data: [
3453
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3454
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3455
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3456
+ height: 260,
3457
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3458
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3459
+ background: { light: '#ffffff', dark: '#202124' },
3460
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3461
+ border: true,
3462
+ grid: false,
3463
+ zoom_pan: false,
3464
+ }
3465
+ }
3466
+
3467
+ function seriesColors(): PlotArgs<Node> {
3468
+ return { ...base(), series: [scatter<Node>({
3469
+ data: [{ x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 }, { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 }],
3470
+ size: 8, color: row => Number(row.y) >= 60
3471
+ ? { light: '#c2410c', dark: '#fdba74' } : { light: '#64748b', dark: '#94a3b8' },
3472
+ hoverable: false,
3473
+ })] }
3474
+ }
3475
+
3476
+ const plot = document.createElement('a-plot') as APlotElement
3477
+ plot.plotArgs = seriesColors()
3478
+ document.body.append(plot)
3479
+ ```
3480
+
3481
+ `ColorArg` adds a per-row accessor to [`ThemeColor`](#theme-colors). Scatter, bar, rect, rule,
3482
+ and custom series accept it; line and area use one `ThemeColor` per series.
3483
+
3484
+ | Form | Type | Description |
3485
+ |---|---|---|
3486
+ | Shared color | [`ThemeColor`](#theme-colors) | Apply one color or theme pair to the series. |
3487
+ | Accessor | [`(row: Record<string, unknown>, index: number) => ThemeColor`](#theme-colors) | Choose a color or theme pair for each row. |
3488
+
3489
+ ```ts
3490
+ import type { ColorArg } from '@antadesign/plot'
3491
+
3492
+ const color: ColorArg = (row) => Number(row.value) >= 10 ? 'coral' : 'steelblue'
3493
+ ```
3494
+
3495
+ For these series, a valid `color` on a data row takes precedence over the
3496
+ series color or accessor. Stacked bars also accept a [`ThemeColor[]`](#theme-colors), with
3497
+ one entry per stacked field.
3498
+
3499
+ ### Mark shapes
3500
+
3501
+ Set a different mark shape for each category.
3502
+
3503
+ ```ts
3504
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3505
+ import '@antadesign/plot/elements/a-plot'
3506
+ import type { APlotElement } from '@antadesign/plot/browser'
3507
+
3508
+ function base(): PlotArgs<Node> {
3509
+ return {
3510
+ series: [scatter<Node>({ data: [
3511
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3512
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3513
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3514
+ height: 260,
3515
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3516
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3517
+ background: { light: '#ffffff', dark: '#202124' },
3518
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3519
+ border: true,
3520
+ grid: false,
3521
+ zoom_pan: false,
3522
+ }
3523
+ }
3524
+
3525
+ function markShapes(): PlotArgs<Node> {
3526
+ const marks = ['circle', 'square', 'diamond', 'triangle'] as const
3527
+ return { ...base(), series: marks.map((mark, i) => scatter<Node>({
3528
+ data: [{ x: mark, y: 40 + i * 10 }], mark, size: 12,
3529
+ color: [
3530
+ { light: '#7c3aed', dark: '#c4b5fd' }, { light: '#db2777', dark: '#f9a8d4' },
3531
+ { light: '#0891b2', dark: '#67e8f9' }, { light: '#ea580c', dark: '#fdba74' },
3532
+ ][i], hoverable: false,
3533
+ })), axis: { x: { categories: [...marks], label: '' }, y: { min: 0, max: 100, label: '' } },
3534
+ margin: { top: 20, right: 12, bottom: 32, left: 32 },
3535
+ }
3536
+ }
3537
+
3538
+ const plot = document.createElement('a-plot') as APlotElement
3539
+ plot.plotArgs = markShapes()
3540
+ document.body.append(plot)
3541
+ ```
3542
+
3543
+ `MarkShape` controls [`scatter.mark`](#scatter) and [`line.mark`](#line):
3544
+
3545
+ | Value | Description |
3546
+ |---|---|
3547
+ | `'circle'` | Circular mark. |
3548
+ | `'square'` | Square mark. |
3549
+ | `'diamond'` | Diamond mark. |
3550
+ | `'triangle'` | Triangular mark. |
3551
+
3552
+ Scatter defaults to `'circle'`; line draws no marks unless `mark` is set.
3553
+
3554
+ ### Strokes
3555
+
3556
+ Compare outlines with widths of 1, 3, and 5 pixels on equal-sized circles.
3557
+
3558
+ ```ts
3559
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3560
+ import '@antadesign/plot/elements/a-plot'
3561
+ import type { APlotElement } from '@antadesign/plot/browser'
3562
+
3563
+ function base(): PlotArgs<Node> {
3564
+ return {
3565
+ series: [scatter<Node>({ data: [
3566
+ { x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3567
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 },
3568
+ ], size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false })],
3569
+ height: 260,
3570
+ margin: { top: 20, right: 24, bottom: 48, left: 76 },
3571
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3572
+ background: { light: '#ffffff', dark: '#202124' },
3573
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3574
+ border: true,
3575
+ grid: false,
3576
+ zoom_pan: false,
3577
+ }
3578
+ }
3579
+
3580
+ function strokes(): PlotArgs<Node> {
3581
+ return { ...base(), series: [1, 3, 5].map((width, i) => scatter<Node>({
3582
+ data: [{ x: 25 + i * 25, y: 50 }], size: 18,
3583
+ color: { light: '#f0abfc', dark: '#c026d3' },
3584
+ stroke: { color: { light: '#7e22ce', dark: '#f5d0fe' }, width }, hoverable: false,
3585
+ })) }
3586
+ }
3587
+
3588
+ const plot = document.createElement('a-plot') as APlotElement
3589
+ plot.plotArgs = strokes()
3590
+ document.body.append(plot)
3591
+ ```
3592
+
3593
+ `StrokeArg` accepts a [`ThemeColor`](#theme-colors) or a `Stroke` object. Use it for `stroke`
3594
+ on scatter, area, and rect series, or `mark_stroke` on a line series.
3595
+
3596
+ | Field | Type | Default | Description |
3597
+ |---|---|---|---|
3598
+ | `color` | [`ThemeColor`](#theme-colors) | Required | Outline color or theme pair. |
3599
+ | `width?` | `number` | Series-dependent | Outline width in pixels. Mark outlines scale with mark size when omitted. |
3600
+
3601
+ ```ts
3602
+ import type { StrokeArg } from '@antadesign/plot'
3603
+
3604
+ const stroke: StrokeArg = {
3605
+ color: { light: 'navy', dark: 'lightsteelblue' },
3606
+ width: 1,
3607
+ }
3608
+ ```
3609
+
3610
+ ### Data fields
3611
+
3612
+ `FieldArg` selects a value from each data row. Use it for series coordinates
3613
+ such as `x` and `y`, or for a second boundary where the series supports one.
3614
+
3615
+ | Form | Type | Description |
3616
+ |---|---|---|
3617
+ | Field name | `string` | Read the named property from each row. |
3618
+ | Accessor | `(row: Record<string, unknown>, index: number) => number \| string` | Compute a value from the row and its zero-based index. Return numbers for continuous axes or strings for categories. |
3619
+
3620
+ ```ts
3621
+ import type { FieldArg } from '@antadesign/plot'
3622
+
3623
+ const x: FieldArg = 'elapsed_ms'
3624
+ const y: FieldArg = (row) => Number(row.duration_ms) / 1000
3625
+ ```
3626
+
3627
+ ### Custom rendering
3628
+
3629
+ `CustomRendererFn<TooltipContent>` receives the composed custom series and
3630
+ its drawing context:
3631
+
3632
+ ```ts
3633
+ import type { ComposedCustom, CustomRenderContext } from '@antadesign/plot'
3634
+
3635
+ type Renderer = (series: ComposedCustom, context: CustomRenderContext) => void
3636
+ ```
3637
+
3638
+ The composed series contains resolved `x` and `y` columns, source `rows` when
3639
+ provided, and theme-resolved colors. Category coordinates are stored as
3640
+ indices; use the pixel resolvers to place them on the canvas.
3641
+
3642
+ | Context field | Type | Description |
3643
+ |---|---|---|
3644
+ | `ctx` | `CanvasContext` | Main-thread or offscreen 2D canvas context. |
3645
+ | `inner` | `Rect` | Plot bounds in pixels: `left`, `right`, `top`, and `bottom`. |
3646
+ | `x_scale` | `Scale` | Composed horizontal D3 scale. |
3647
+ | `y_scale` | `Scale` | Composed vertical D3 scale. |
3648
+ | `color` | `string` | Resolved fallback series color. |
3649
+ | `x_categories?` | `string[]` | Horizontal category labels, when categorical. |
3650
+ | `y_categories?` | `string[]` | Vertical category labels, when categorical. |
3651
+ | `resolve_x` | `PixelResolver` | Resolve a row index to its x position in pixels. |
3652
+ | `resolve_y` | `PixelResolver` | Resolve a row index to its y position in pixels. |
3653
+ | `color_at` | `ColorResolver` | Resolve a row index to its color, including the series fallback. |
3654
+
3655
+ `PixelResolver` is `(index: number) => number | undefined`; skip points whose
3656
+ position is `undefined`. `ColorResolver` is `(index: number) => string`.
3657
+ `CanvasContext` is `CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D`.
3658
+ `Scale` is a D3 linear, logarithmic, band, or time scale.
3659
+
3660
+ ## Web Component
3661
+
3662
+ `<a-plot>` is a light-DOM custom element that takes the same arguments
3663
+ as a property. Use it directly from plain JavaScript or TypeScript; it owns
3664
+ controller setup, drawing, and cleanup in the browser.
3665
+
3666
+ ```ts
3667
+ import { scatter, type APlotElement } from '@antadesign/plot/browser'
3668
+ import '@antadesign/plot/elements/a-plot'
3669
+
3670
+ const plot = document.createElement('a-plot') as APlotElement
3671
+ plot.plotArgs = { series: [scatter({ data: [{ x: 1, y: 2 }, { x: 2, y: 3 }] })] }
3672
+ document.body.append(plot)
3673
+ ```
3674
+
3675
+ [`plotArgs`](#plot-arguments) is a property, not an attribute — assign the object rather
3676
+ than serializing it. Import `@antadesign/plot/elements/a-plot` to register the standalone plot and its surface dependency.
3677
+ Registration skips existing definitions and does nothing when `customElements`
3678
+ is unavailable, including during server rendering.
3679
+
3680
+ ## Styling
3681
+
3682
+ The JSX `Plot` component fills its parent with `width: 100%` and `height: 100%`.
3683
+ Give its container a height, or set `width` and `height` in `plotArgs`. Removing
3684
+ those arguments restores parent-relative sizing. Use `className` and `style` to
3685
+ style the wrapper; inline `style` props can override its default dimensions.
3686
+
3687
+ The standalone `<a-plot>` element has a 300px fallback height. The preview below
3688
+ uses that element. Configure canvas colors, lines, and text through `plotArgs`.
3689
+
3690
+ This preview uses CSS for the rounded container, padding, and responsive aspect
3691
+ ratio. It omits `width` and `height` from `plotArgs` so CSS controls the size.
3692
+ Set `background: false` to let the container background show through the canvas.
3693
+
3694
+ ```ts
3695
+ import { scatter, type PlotArgs } from '@antadesign/plot'
3696
+ import '@antadesign/plot/elements/a-plot'
3697
+ import type { APlotElement } from '@antadesign/plot/browser'
3698
+
3699
+ function stylingExample(): PlotArgs<Node> {
3700
+ return {
3701
+ series: [scatter<Node>({
3702
+ data: [{ x: 12, y: 18 }, { x: 30, y: 42 }, { x: 48, y: 35 },
3703
+ { x: 58, y: 64 }, { x: 76, y: 58 }, { x: 88, y: 82 }],
3704
+ size: 3, color: { light: '#9ca3af', dark: '#6b7280' }, hoverable: false,
3705
+ })],
3706
+ margin: { top: 16, right: 16, bottom: 40, left: 60 },
3707
+ axis: { x: { min: 0, max: 100, label: 'Time' }, y: { min: 0, max: 100, label: 'Value' } },
3708
+ background: false,
3709
+ chrome_color: { light: '#9ca3af', dark: '#d1d5db' },
3710
+ border: true,
3711
+ grid: false,
3712
+ zoom_pan: false,
3713
+ }
3714
+ }
3715
+
3716
+ const container = document.createElement('div')
3717
+ container.className = 'plot-card'
3718
+ const plot = document.createElement('a-plot') as APlotElement
3719
+ plot.className = 'styled-plot'
3720
+ plot.setAttribute('aria-label', 'Scatter plot in a rounded container')
3721
+ plot.plotArgs = stylingExample()
3722
+ container.append(plot)
3723
+ document.body.append(container)
3724
+ ```
3725
+
3726
+ ```css
3727
+ .plot-card {
3728
+ box-sizing: border-box;
3729
+ width: 100%;
3730
+ max-width: 460px;
3731
+ margin-inline: auto;
3732
+ padding: 16px;
3733
+ border: 1px solid #9ca3af;
3734
+ border-radius: 16px;
3735
+ background: rgb(156 163 175 / 12%);
3736
+ }
3737
+
3738
+ .styled-plot {
3739
+ display: block;
3740
+ width: 100%;
3741
+ height: auto;
3742
+ aspect-ratio: 4 / 3;
3743
+ }
3744
+ ```
3745
+
3746
+ The `a-plot-surface` element installs its structural styles automatically,
3747
+ including canvas stacking and overlay positioning. It does not require a
3748
+ separate CSS import.
3749
+
3750
+ The complete `a-plot` host installs its base styles automatically too, giving it
3751
+ a width of `100%` and a fallback height of `300px`. No `plot.css` import is needed.
3752
+ Its sizing rule uses `:where()` with zero specificity, so your CSS can override
3753
+ it. Set `height: auto` when using `aspect-ratio` to replace the fixed fallback
3754
+ height, as this example does.
3755
+
3756
+ Alternatively, pass `width` and `height` to set inline styles on the host.
3757
+ These override stylesheet rules. Removing an argument restores the previous
3758
+ inline value, allowing stylesheet rules to apply when no inline value remains.
3759
+
3760
+ Inside the canvas, use [`chrome_color`](#theme-colors) for axis lines, tick marks,
3761
+ grid lines, and the plot border. Use `background` for the plot fill and each
3762
+ series’ `color` for its marks. These colors accept `{ light, dark }` pairs.
3763
+ Plain color strings remain subject to the plot’s `theme_invert` setting.
3764
+
3765
+ Set text colors separately with `title.color`, `axis.x.label.color`,
3766
+ `axis.y.label.color`, `axis.x.tick_label.color`, and `axis.y.tick_label.color`.
3767
+
3768
+ With the JSX `Plot` component, return content supported by your renderer from a
3769
+ series’ [`tooltip`](#tooltips) callback and style it like other markup. Anta’s
3770
+ `Tooltip` renders that content in the component tree. The reset control is an Anta `Button` and
3771
+ follows the application’s theme.