apexcharts-skill 2.1.0 → 3.0.0

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.
@@ -7,6 +7,8 @@
7
7
  - **Range Bar** (`'rangeBar'`) — Bars with start/end values (used for timelines, Gantt charts)
8
8
  - **Funnel** (`'funnel'`, **new in v6**): Stage-by-stage drop-off chart
9
9
  - **Pyramid** (`'pyramid'`, **new in v6**): A funnel with the wide base at the bottom
10
+ - **Waterfall** (`'waterfall'`, **new in v7.1**): Deltas that accumulate into a running total
11
+ - **Dumbbell** (`'dumbbell'`, **new in v7.1**): Two or more measures per category, joined by a connector
10
12
 
11
13
  ## Tree-Shakeable Import
12
14
 
@@ -14,10 +16,18 @@
14
16
  import ApexCharts from 'apexcharts/bar'
15
17
  // Registers: bar, column, rangeBar
16
18
  // Aliases: apexcharts/column, apexcharts/rangeBar
17
- // Also covers funnel + pyramid (they normalize to the bar engine in v6)
19
+ // Also covers funnel + pyramid (they normalize to the bar engine)
20
+
21
+ // waterfall and dumbbell (v7.1) each add a feature module on top of the bar engine:
22
+ import ApexCharts from 'apexcharts/waterfall' // == apexcharts/bar + features/waterfall
23
+ import ApexCharts from 'apexcharts/dumbbell' // == apexcharts/bar + features/dumbbell
18
24
  ```
19
25
 
20
- **v6 note:** `funnel` and `pyramid` are first-class `chart.type` aliases that render through the bar engine. Use them directly as `chart.type`; you do **not** need `plotOptions.bar.isFunnel`. `plotOptions.funnel` carries the funnel-specific shape options.
26
+ Both are **Tier 1**, so the default `apexcharts` bundle already has them and no import is needed there.
27
+
28
+ **Alias note:** `funnel` and `pyramid` are first-class `chart.type` aliases that render through the bar engine. Use them directly as `chart.type`; you do **not** need `plotOptions.bar.isFunnel`. `plotOptions.funnel` carries the funnel-specific shape options.
29
+
30
+ **Histogram (v6.9):** `chart.type: 'histogram'` also renders through the bar pathway, but its series carry raw observations that the chart bins itself, and the binning needs the stats feature (`apexcharts/histogram` entry, or `apexcharts/features/stats` alongside `apexcharts/bar`). It is documented with the other statistical charts in `references/financial-charts.md`.
21
31
 
22
32
  ---
23
33
 
@@ -182,6 +192,105 @@ Same shape as funnel, but order values **smallest → largest** so the wide base
182
192
  }
183
193
  ```
184
194
 
195
+ ### Waterfall (v7.1)
196
+
197
+ `chart.type: 'waterfall'` **accumulates the running total for you**. The series holds the **deltas**; a row flagged `isSubtotal` or `isTotal` draws the running total from zero at that position and carries **no `y` of its own**. Connectors bridge each bar's finish to the next one's start, and rising, falling and total bars take their own colors.
198
+
199
+ ```js
200
+ {
201
+ chart: { type: 'waterfall', height: 350 },
202
+ series: [{
203
+ name: 'Operating income',
204
+ data: [
205
+ { x: 'Net revenue', y: 8786000 },
206
+ { x: 'Cost of sales', y: -2786000 },
207
+ { x: 'Gross profit', isSubtotal: true }, // no y: measured for you
208
+ { x: 'Operating expenses', y: -1786000 },
209
+ { x: 'Operating income', isTotal: true }, // no y: sum from zero
210
+ ],
211
+ }],
212
+ }
213
+ ```
214
+
215
+ **Row shape** (`ApexWaterfallPoint`, every field optional so a series can mix the two kinds freely):
216
+
217
+ | Field | Meaning |
218
+ |---|---|
219
+ | `x` | Category label, timestamp, or `Date` |
220
+ | `y` | The **step**: the signed amount it moves the running total by. Omit on a subtotal / total row. |
221
+ | `isSubtotal` | Sum of the steps since the previous subtotal / total bar |
222
+ | `isTotal` | Sum of every step from zero |
223
+ | `fillColor` | Overrides the semantic fill for this row |
224
+
225
+ **`plotOptions.waterfall`:**
226
+
227
+ ```js
228
+ plotOptions: {
229
+ waterfall: {
230
+ colors: {
231
+ positive: '#00A86F', // a step that raises the running total
232
+ negative: '#FF4560', // a step that lowers it
233
+ subtotal: undefined, // defaults to the series color from the active palette
234
+ total: undefined, // same default, so running totals stay distinct from steps
235
+ },
236
+ connectors: {
237
+ show: true, // without them the floating columns read as unrelated bars
238
+ color: undefined, // defaults to grid.borderColor, so it is theme-aware
239
+ strokeWidth: 1,
240
+ strokeDashArray: 3,
241
+ },
242
+ },
243
+ }
244
+ ```
245
+
246
+ A datum's own `fillColor` always wins over `plotOptions.waterfall.colors`.
247
+
248
+ ### Dumbbell (v7.1)
249
+
250
+ `chart.type: 'dumbbell'` compares two or more measures per category. **Each series is one measure**, and the chart joins them with a connector per category. You do not zip the values into `[low, high]` pairs.
251
+
252
+ ```js
253
+ {
254
+ chart: { type: 'dumbbell', height: 350 },
255
+ series: [
256
+ { name: '2020', data: [{ x: 'Backend', y: 92 }, { x: 'Frontend', y: 78 }] },
257
+ { name: '2025', data: [{ x: 'Backend', y: 118 }, { x: 'Frontend', y: 96 }] },
258
+ ],
259
+ plotOptions: { bar: { horizontal: true } }, // rows; omit for columns
260
+ }
261
+ ```
262
+
263
+ The connector's thickness is `plotOptions.bar.barHeight` (rows) or `columnWidth` (columns), and the size of the marked ends is `markers.size`: they are the bar and its markers, so they are configured as such.
264
+
265
+ **`plotOptions.bar.dumbbell`:**
266
+
267
+ ```js
268
+ plotOptions: {
269
+ bar: {
270
+ dumbbell: {
271
+ connector: {
272
+ color: undefined, // undefined = a gradient between the two endpoint colours,
273
+ // resolved per row so a row where the measures cross runs the right way
274
+ opacity: 0.55, // the join is context for the marked ends, not a third mark
275
+ },
276
+ dataLabels: {
277
+ enabled: true, // default on for chart.type 'dumbbell', off for the bare isDumbbell flag
278
+ offset: 6, // px clear of the marked end, outward from the connector
279
+ colorFromMarker: true,
280
+ // formatter(value, { seriesIndex, dataPointIndex, endpointIndex, w })
281
+ // NOTE: defaults to the value axis' label formatter, NOT dataLabels.formatter
282
+ // (on a range bar that one reads out end - start, which is the gap, not the endpoint)
283
+ },
284
+ tooltip: { differenceLabel: 'Difference' },
285
+ },
286
+ },
287
+ }
288
+ ```
289
+
290
+ With three or more measures only the two extremes are labelled: anything between them sits on the connector, where a label has nowhere to go that is not over the line or over its neighbour.
291
+
292
+ **Versus the older range-bar form.** `plotOptions.bar.isDumbbell` on a `rangeBar` with `y: [lo, hi]` pairs still works and is a different chart: one series, endpoint colours from `plotOptions.bar.dumbbellColors` (`[[startColor, endColor]]`). `chart.type: 'dumbbell'` takes one series per measure and colours each dot after the series it belongs to, from `colors`. Prefer the type unless you already have paired data.
293
+
185
294
  ---
186
295
 
187
296
  ## Key plotOptions.bar Options
@@ -195,7 +304,7 @@ plotOptions: {
195
304
  distributed: false, // true = different color per data point
196
305
  borderRadius: 4, // rounded corners (number or object)
197
306
  borderRadiusApplication: 'end', // 'end' | 'around'
198
- borderRadiusWhenStacked: 'last', // 'all' | 'last'
307
+ // borderRadiusWhenStacked was REMOVED in v7.0, see the note below
199
308
 
200
309
  dataLabels: {
201
310
  position: 'top', // 'top' | 'center' | 'bottom'
@@ -213,6 +322,14 @@ plotOptions: {
213
322
  }
214
323
  ```
215
324
 
325
+ **⚠️ `borderRadiusWhenStacked` was removed in v7.0.** Rounded corners on a stacked bar are no longer a setting: corner ownership follows the **outer edge** of the stack, which is what `'last'` approximated and what `'all'` got wrong on any stack whose last series was empty. An unknown option is ignored, so leaving it in a config is harmless but does nothing. Delete it. Related v7.0 fixes: a stacked bar's bottom cap is now the top-rounded path mirrored (caps no longer invert or pop when a series collapses and rises again), and grouped stacked charts resolve caps per group rather than chart-wide, so every group gets its own bottom and top radius.
326
+
327
+ **⚠️ `dataLabels.animate.enabled` now defaults to `true` (v7.0), bar/column only.** Labels ride to their new position on a data-change update instead of snapping there, so they reflow on the same clock as the bars, markers and axis ticks. A label that has not moved is a per-label no-op; speed and easing follow `chart.animations.dynamicAnimation`. For the pre-v7 behavior:
328
+
329
+ ```js
330
+ dataLabels: { animate: { enabled: false } }
331
+ ```
332
+
216
333
  ### Bar Chart Race (v6.4)
217
334
 
218
335
  A reorder update animates into a bar chart race: when you re-sort the data and update the chart, the bars slide to their new ranks and their category labels ride along automatically (whenever `dynamicAnimation` is on). Two opt-in `dataLabels` flags complete the effect. Both are off by default and apply to **bar / column only**; their speed and easing follow `chart.animations.dynamicAnimation`.
@@ -307,4 +424,9 @@ series: [{
307
424
  4. **Timeline without `xaxis.type: 'datetime'`** — date-based range bars need `xaxis: { type: 'datetime' }` to render correctly.
308
425
  5. **`distributed: true` with multiple series** — distributed coloring applies to each data point independently. With multiple series, each point gets a unique color which is usually not desired. Use `distributed: true` only with single-series charts.
309
426
  6. **Funnel/pyramid value ordering**: funnel expects values ordered largest-to-smallest and pyramid smallest-to-largest. The renderer does not sort for you; unsorted data produces a jagged shape. Stage labels come from `xaxis.categories`, and per-stage labels in `dataLabels.formatter` are read via `opt.w.globals.labels[opt.dataPointIndex]`.
310
- 7. **Reaching for `plotOptions.bar.isFunnel` in v6**: unnecessary. Set `chart.type: 'funnel'` (or `'pyramid'`) and use `plotOptions.funnel` for shape options.
427
+ 7. **Reaching for `plotOptions.bar.isFunnel`**: unnecessary. Set `chart.type: 'funnel'` (or `'pyramid'`) and use `plotOptions.funnel` for shape options.
428
+ 8. **Pre-computing the running total for a waterfall** *(v7.1)*: the series carries the **deltas**; the chart accumulates. Feeding it cumulative values draws each bar from zero to the total, which is a column chart with extra steps.
429
+ 9. **Giving a waterfall subtotal row a `y`** *(v7.1)*: `isSubtotal` / `isTotal` rows are measured for you. Supply `x` and the flag, nothing else.
430
+ 10. **Zipping dumbbell values into `y: [lo, hi]`** *(v7.1)*: that is the older `plotOptions.bar.isDumbbell` range-bar form. `chart.type: 'dumbbell'` wants one series per measure, each with plain `{ x, y }` points.
431
+ 11. **Setting `borderRadiusWhenStacked`**: removed in v7.0. The option is ignored; stacked corner ownership follows the stack's outer edge automatically.
432
+ 12. **Expecting data labels to snap on update** *(v7.0)*: `dataLabels.animate.enabled` now defaults to `true` on bar/column. Set it to `false` for the old behavior.
@@ -7,6 +7,7 @@
7
7
  - **Scatter** (`'scatter'`) — Individual data points plotted by X/Y coordinates
8
8
  - **Bubble** (`'bubble'`) — Scatter with variable-size bubbles (requires z value)
9
9
  - **Range Area** (`'rangeArea'`) — Area chart showing a range between two values
10
+ - **Streamgraph** (`'streamgraph'`, **new in v7.1**): Series stacked as flowing bands around a chosen baseline
10
11
 
11
12
  ## Tree-Shakeable Import
12
13
 
@@ -14,8 +15,13 @@
14
15
  import ApexCharts from 'apexcharts/line'
15
16
  // Registers: line, area, scatter, bubble, rangeArea
16
17
  // Aliases: apexcharts/area, apexcharts/scatter, apexcharts/bubble, apexcharts/rangeArea
18
+
19
+ // streamgraph (v7.1) adds a feature module on top of the rangeArea engine:
20
+ import ApexCharts from 'apexcharts/streamgraph' // == apexcharts/rangeArea + features/streamgraph
17
21
  ```
18
22
 
23
+ Streamgraph is **Tier 1**, so the default `apexcharts` bundle already has it and no import is needed there.
24
+
19
25
  ---
20
26
 
21
27
  ## Data Formats (Detailed)
@@ -124,6 +130,62 @@ Always use XY format:
124
130
  }
125
131
  ```
126
132
 
133
+ ### Streamgraph (v7.1)
134
+
135
+ `chart.type: 'streamgraph'` stacks the series as flowing bands around a baseline chosen to keep the whole picture readable. Series format is the same as area; **the stacking is the chart's job, so do not set `chart.stacked`**.
136
+
137
+ ```js
138
+ {
139
+ chart: { type: 'streamgraph', height: 350 },
140
+ series: [
141
+ { name: 'Drama', data: [{ x: '2024-01-01', y: 32 }, { x: '2024-02-01', y: 41 }] },
142
+ { name: 'Comedy', data: [{ x: '2024-01-01', y: 18 }, { x: '2024-02-01', y: 26 }] },
143
+ ],
144
+ }
145
+ ```
146
+
147
+ Curves are `monotoneCubic` by default so a band never overshoots its data, each band's name is drawn inside it sized to the room that band actually has, and hovering a band fades the others. The legend and tooltip work as on any axis chart.
148
+
149
+ **`plotOptions.streamgraph`:**
150
+
151
+ ```js
152
+ plotOptions: {
153
+ streamgraph: {
154
+ offset: 'wiggle', // where the baseline goes:
155
+ // 'wiggle' (default) minimizes total weighted slope, so thick bands stay level
156
+ // 'silhouette' centres the stack on one horizontal line
157
+ // 'zero' an ordinary stacked area, on the zero line
158
+ // 'expand' normalizes each column to its own total (composition, not volume)
159
+
160
+ order: 'inside-out', // stacking order, bottom first:
161
+ // 'inside-out' (default) puts early-peaking series in the middle and fans
162
+ // later peaks outward, each to whichever side is thinner
163
+ // 'inverse' the series order, reversed
164
+ // 'none' the series order as given
165
+
166
+ hover: { show: true, opacity: 0.35 }, // what the OTHER bands drop to; much above ~0.5 and the
167
+ // hovered band stops reading as picked out
168
+
169
+ labels: {
170
+ show: true,
171
+ minWidth: 24, // a band narrower than this many px is left unlabelled rather than
172
+ // given a name truncated past the point of being a name
173
+ minFontSize: 9,
174
+ maxFontSize: 30,
175
+ style: {
176
+ fontSize: 'auto', // sizes each name to its band, bounded by min/maxFontSize.
177
+ // Give a literal ('12px') to draw every name at the same size.
178
+ fontWeight: 600,
179
+ colors: undefined, // per-series override; by default each label takes black or white,
180
+ // whichever reads on its own band
181
+ },
182
+ },
183
+ },
184
+ }
185
+ ```
186
+
187
+ `offset: 'wiggle'` with `order: 'inside-out'` is the classic streamgraph, and the pair is what keeps it readable: the middle of the stack moves least under a wiggle baseline. A faded band's name is recoloured to `chart.foreColor` rather than faded with it, because the name takes black or white by the contrast of the band at full strength.
188
+
127
189
  ---
128
190
 
129
191
  ## Key Options
@@ -242,6 +304,20 @@ xaxis: {
242
304
 
243
305
  1. **Bubble chart without z value** — renders with zero-size bubbles. Always provide `z` in data.
244
306
  2. **Using `xaxis.categories` with `{ x, y }` data** — categories are silently ignored when data contains x values. Choose one approach.
245
- 3. **Datetime axis with string dates** — pass timestamps (`new Date(...).getTime()`) or `Date` objects, not raw strings like `'January 2024'`.
307
+ 3. **Datetime axis with string dates**: pass timestamps (`new Date(...).getTime()`) or `Date` objects, not raw strings like `'January 2024'`. Since v6.10 a `Date` object `x` is accepted by the types and keeps millisecond resolution (previously milliseconds were truncated, collapsing points within the same second). **Since v7.1 a `Date` x also works on a non-datetime axis**; before that it only worked when the axis was `datetime` and stringified into garbage anywhere else, and an invalid `Date` slipped through unnoticed. Both are handled now.
246
308
  4. **`stroke.curve: 'smooth'` on sparse data** — can produce visual artifacts. Use `'monotoneCubic'` for mathematically smoother interpolation.
247
309
  5. **Range Area with single value instead of array** — `y` must be `[low, high]`, not a single number.
310
+ 6. **Setting `chart.stacked` on a streamgraph** *(v7.1)*: the chart owns its own stacking, baseline and band order through `plotOptions.streamgraph`. Reach for `offset: 'zero'` if what you actually want is a plain stacked area.
311
+ 7. **A series that starts with nulls used to mis-target the tooltip**: the hovered index resolved against the drawn points rather than the data, so the tooltip read the wrong point. Fixed in v7.1.
312
+ 8. **`autoScaleYaxis` scaling to points nobody can see**: before v7.1, zooming let one point just outside the window size the whole axis, and a series with nothing in the window still contributed. Fixed in v7.1; stacked charts now also scale to the stacked totals inside the window rather than to individual series.
313
+
314
+ ## Axis and layout fixes worth knowing (v7.0 / v7.1)
315
+
316
+ These need no config change; they matter because a workaround you carried from an earlier version may now be unnecessary.
317
+
318
+ - **Logarithmic axis geometry, and `tickAmount` on a log scale** *(v7.0)*. Log-scale positions were computed against the wrong domain, and `tickAmount` was ignored outright on a log axis. Both fixed, so `yaxis: { logarithmic: true, tickAmount: n }` now does what it says.
319
+ - **Bar width came from the smallest gap inside one series** *(fixed v7.1)*. A single series with one tight pair of x values shrank every bar on the chart. A bar's slot now comes from the axis, the union of gaps across all series.
320
+ - **Stacked baselines resolve by x, not by position in the array** *(fixed v7.0)*. Two series stacked on ragged data lined up by ordinal, so a series with a missing point stacked onto the wrong neighbour.
321
+ - **A container resize arriving mid-animation was lost for good** *(fixed v7.1)*. The ResizeObserver fires once per resize and the handler dropped that callback while an animation ran, so a chart in a collapsing sidebar kept its old size.
322
+ - **Annotations on a chart with no data** *(fixed v7.0)*. Placement was gated chart-wide on `dataPoints`, so an empty chart dropped every annotation including y-axis ones that are always placeable. Gating is now per annotation.
323
+ - **A tooltip inside Shadow DOM followed the wrong element** *(fixed v7.0)*. Reading `e.target` after a deferred hover gets the shadow host, not the hovered mark.
@@ -8,7 +8,7 @@
8
8
  - **Radial Bar** (`'radialBar'`): Circular progress chart (one or more concentric tracks)
9
9
  - **Gauge** (`'gauge'`, **new in v6**): Single-value gauge with arc/needle shapes, colored bands, and ticks
10
10
  - **Sunburst** (`'sunburst'`, **new in v6.7, free**): Hierarchical nested pie/donut; concentric rings, one per level
11
- - **Unit / Waffle** (`'unit'` / `'waffle'`, **new in v6.6, premium**): One mark per unit of value (dot clusters, pictograms, waffles, beeswarms, parliament)
11
+ - **Unit / Waffle** (`'unit'` / `'waffle'`, **new in v6.6, premium**): One mark per unit of value (dot clusters, pictograms, waffles, beeswarms, parliament, custom layouts and the v6.10 shape kit)
12
12
 
13
13
  ## Tree-Shakeable Import
14
14
 
@@ -26,8 +26,16 @@ import ApexCharts from 'apexcharts/sunburst'
26
26
 
27
27
  import ApexCharts from 'apexcharts/unit'
28
28
  // Registers: unit + waffle (v6.6). Premium: watermarked until a license is set.
29
+
30
+ import { heart, outlined, glyphs, preview } from 'apexcharts/unit-shapes'
31
+ // Unit-chart shape kit (v6.10), named exports tree-shaken per shape. See "Unit shapes" below.
32
+
33
+ import { person, registerMarks } from 'apexcharts/pictograms'
34
+ // Pictogram glyph collection (v7.0), one drawn mark per unit. See "Pictograms" below.
29
35
  ```
30
36
 
37
+ All of these are **Tier 1** (in the default `apexcharts` bundle). Nothing in the circular family is Tier 2.
38
+
31
39
  ---
32
40
 
33
41
  ## Data Format
@@ -149,7 +157,7 @@ Colours, `stroke`, `legend`, and `title` behave as they do on pie and donut. A s
149
157
 
150
158
  ### Unit / Waffle (v6.6, premium)
151
159
 
152
- A `unit` chart renders one discrete mark for every unit of value instead of a single bar or slice, so "37 of 200" reads as a countable quantity. It is a **non-axis** chart (dispatched like pie or treemap) and is **premium**: it renders fully in trial mode with an `APEXCHARTS` watermark until an entitled license is set (see `references/v6-features.md`). On every update each mark tweens from its old position to its new one, so re-grouping, filtering, or a changing count re-forms the marks.
160
+ A `unit` chart renders one discrete mark for every unit of value instead of a single bar or slice, so "37 of 200" reads as a countable quantity. It is a **non-axis** chart (dispatched like pie or treemap) and is **premium**: it renders fully in trial mode with an `APEXCHARTS` watermark until an entitled license is set (see `references/feature-platform.md`). On every update each mark tweens from its old position to its new one, so re-grouping, filtering, or a changing count re-forms the marks.
153
161
 
154
162
  Data is the same flat number array + `labels` shape as pie:
155
163
 
@@ -168,11 +176,103 @@ Data is the same flat number array + `labels` shape as pie:
168
176
  - `packed`: one shared blob, coloured by group (`sortByGroup: true` nests the minority in the centre).
169
177
  - `columns`: each category is a vertical bar built from stacked dots (a waffle column).
170
178
  - `grid`: one waffle lattice, a part-to-whole square "pie" (`grid.total: 100` rounds it to a fixed cell budget for a percentage waffle; `grid.split: true` makes small-multiple mini-waffles).
171
- - `scatter`: a beeswarm on real value axes (`scatter.y: 'lanes'` default, or `'value'` for a 2D value-value plot; `scatter.sizeRange` for area-scaled bubbles).
179
+ - `scatter`: a beeswarm on real value axes (`scatter.y: 'lanes'` default, or `'value'` for a 2D value-value plot; `scatter.sizeRange` for area-scaled bubbles). Since **v6.7.1** `scatter.orientation: 'horizontal' | 'vertical'` picks the beeswarm axis (lanes mode only): `'horizontal'` (default) puts the value on X with category lanes on Y; `'vertical'` puts the value on Y with lanes as columns. The value-axis keys (`xMin`/`xMax`/`xTitle`/`xFormatter`/`tickAmount`) describe the value axis in both orientations.
172
180
  - `arc` (**v6.7**): a parliament / hemicycle, seats in concentric arced rows across an annulus, filled in category order.
181
+ - `custom` (**v6.9**): positions come from you. `plotOptions.unit.positions` is either a function `(objects, rect) => [{ id, x, y, r? }]` or the name of a layout registered with `ApexCharts.registerUnitLayout(name, fn)`. `objects` carries identity and data per mark (`{ id, index, seriesIndex, dataPointIndex, label, value, datum, r }`), `rect` is the plot area in pixels. A layout is objects in, positions out; the engine already tweens position, radius, and colour and keeps mark identity across a relayout. Marks whose id the provider omits animate out; unknown ids are ignored.
173
182
 
174
183
  **Also available:** `shape` (`'circle'` | `'square'` | `'image'` pictogram with `image.tint`); per-mark object data `series: [{ name, data: [{ value, x, z, name, fillColor, id }] }]`; `transition` (`'group'` default | `'flow'` crowd migration | `'identity'` keyed by `id`/`name`); numeric or `'auto'` dot `size`; `sizeByValue` bubbles; `unitValue` (1 mark = N units); `maxUnits` cap; per-cluster `clusterLabels`; and per-mark `tooltip.formatter`.
175
184
 
185
+ **Motion (v6.9):** marks travel on a damped spring by default, so an update landing mid-flight retargets with the marks' velocity instead of restarting them (a dragged slider reads as continuous motion, not stutter). Configure via `plotOptions.unit.gather`: `motion: 'auto' | 'spring' | 'tween'`, `spring: 'crisp' | 'gentle' | 'snappy'`, `easing: 'outCubic' | 'inOutCubic' | 'outBack'` (setting `easing` implies tween), and `enter: 'burst' | 'fade' | 'rise'`.
186
+
187
+ ### Unit shapes (`apexcharts/unit-shapes`, v6.10)
188
+
189
+ A companion kit of **39 shapes** a count can take (a heart, a house, a globe, the figure 1,024 drawn in 1,024 dots). Each shape is a plain callable layout, so it plugs straight into `plotOptions.unit.positions` with `layout: 'custom'`; no registration step. The entry point is tree-shaken per shape (about 4 KB gzipped each). Dots are packed to fill the outline, so one shape serves 40 dots in a sparkline and 3,000 in a poster.
190
+
191
+ ```js
192
+ import ApexCharts from 'apexcharts'
193
+ import { heart } from 'apexcharts/unit-shapes'
194
+
195
+ new ApexCharts(el, {
196
+ chart: { type: 'unit' },
197
+ series: [57600, 16800, 4200, 3400],
198
+ labels: ['Repeat donors', 'First-time', 'Workplace drives', 'Emergency call-ups'],
199
+ plotOptions: {
200
+ unit: { layout: 'custom', positions: heart, unitValue: 100 },
201
+ },
202
+ }).render()
203
+ ```
204
+
205
+ Three kinds:
206
+
207
+ - **Silhouettes** (29, filled outlines): `heart`, `house`, `tree`, `leaf`, `flame`, `droplet`, `fish`, `sun`, `human`, `group`, `star`, `crown`, `trophy`, `moneybag`, `funnel`, `shield`, `gear`, `robot`, `bulb`, `flask`, `car`, `plane`, `rocket`, `battery`, `pin`, `mountain`, `cross`, `bolt`, `arrow`.
208
+ - **Strokes** (7, packed centrelines for things with no interior): `check`, `wifi`, `pulse`, `xmark`, `percent`, `question`, `spiral`.
209
+ - **Generated** (3, positions from maths, no outline): `globe`, `target`, `pyramid`.
210
+
211
+ Composition helpers (also exported from `apexcharts/unit-shapes`):
212
+
213
+ ```js
214
+ outlined(heart) // hollow twin: trace the outline instead of filling it (silhouettes only)
215
+ heart.with({ order: 'cols' }) // variant: where each series band lands ('rows' | 'rowsUp' | 'cols' |
216
+ // 'colsRev' | 'centerOut' | 'centerIn'); battery fills like a charge
217
+ // meter because its order is 'cols'
218
+ glyphs('1,024') // a number, drawn in that many dots (digits plus '-', '.', ',', ':')
219
+ preview(heart, { series }) // render to a standalone SVG string, no chart and no DOM (build-time galleries)
220
+ shapeFrom(path, opts?) // your own outline, packed like the catalog's
221
+ strokeFrom(path, opts?) // your own centreline
222
+ registerShapes([...]) // register so positions: '<name>' resolves by string
223
+ ```
224
+
225
+ Every shape carries metadata on `shape` (category, kind, and `minUnits`: the count below which it stops being recognisable; asking for fewer logs a console warning naming the shape). From a script tag, `dist/unit-shapes.js` exposes the kit as the global `ApexUnitShapes` with every shape pre-registered, so `positions: 'heart'` works by name.
226
+
227
+ ### Pictograms (`apexcharts/pictograms`, v7.0)
228
+
229
+ A **shape** is where the units go; a **pictogram** is what one unit looks like. They are independent, so `positions: heart` with `shape: 'pictogram'` arranges glyphs into a heart, and every other pairing is equally valid.
230
+
231
+ ```js
232
+ import ApexCharts from 'apexcharts'
233
+ import { person, registerMarks } from 'apexcharts/pictograms'
234
+
235
+ registerMarks([person]) // so pictogram.mark: 'person' resolves by name
236
+
237
+ const options = {
238
+ chart: { type: 'unit' },
239
+ series: [120, 80],
240
+ labels: ['Employed', 'Unemployed'],
241
+ plotOptions: {
242
+ unit: {
243
+ shape: 'pictogram',
244
+ pictogram: {
245
+ mark: 'person', // a registered name, a { path, viewBox?, fillRule? } object,
246
+ // raw path data, or an array (one per series).
247
+ // A datum's own `mark` overrides all of it, so one crowd can mix glyphs.
248
+ fit: 'contain', // which side of the glyph binds to the dot's box: 'contain' | 'width' | 'height'
249
+ scale: 1, // nudge for glyphs that read light
250
+ padding: 0, // 0..0.9 of the pitch, opening the lattice up
251
+ fallback: 'circle', // drawn when a mark cannot be resolved
252
+ },
253
+ },
254
+ },
255
+ }
256
+ ```
257
+
258
+ A glyph is drawn as one `<path>` per unit, filled in that unit's own colour: no request, no decode, no recolour filter, which is why `'pictogram'` is the shape that scales to thousands of units. There is deliberately **no size**: a glyph is fitted to the box the dot itself would have occupied, so `plotOptions.unit.size` and `spacing` size a pictogram exactly as they size a dot.
259
+
260
+ **Shipped glyphs** (each a named export, so import the one you use): `person`, `house`, `heart`, `tree`, `droplet`, `star`, `car`, `bag`, `book`, `cup`, `bulb`, `plane`. `catalog` is every glyph and ships all of them, so avoid it in an app bundle.
261
+
262
+ **Custom glyphs:** `definePictogram({ name, path, viewBox?, fillRule?, category?, source? })` builds one, `registerMarks(defs)` registers them by name, `unregisterMarks(names)` removes them, and `registeredMarkNames()` lists what resolves. Any glyph also has `.with(overrides)` for a variant with some metadata replaced.
263
+
264
+ **Versus `shape: 'image'`:** `'image'` fetches a raster or multi-colour icon (`plotOptions.unit.image.src`, with `tint: true` to recolour a monochrome icon to the category colour). Use it for multi-colour marks that should keep their own colours; use `'pictogram'` for anything that has to scale or follow the palette.
265
+
266
+ **Outer name labels (v6.10):** a shape packed with several categories can name them in the margin with a leader line to their own dots (the pie/donut outer-label mechanism), instead of needing a legend:
267
+
268
+ ```js
269
+ plotOptions: {
270
+ unit: { clusterLabels: { external: { show: true } } },
271
+ }
272
+ ```
273
+
274
+ `clusterLabels.external` also takes `connector: { show, width, color, gap, length }`, `offsetX`, `offsetY`. It applies to `layout: 'custom'` only and reads best on silhouettes whose category bands stack vertically. The gutter is reserved before the dot size is chosen, so the shape stays centred but renders slightly smaller.
275
+
176
276
  `waffle` is a thin alias of `unit` that presets the `grid` layout with square cells. With `grid.total: 100` the values are largest-remainder rounded to exactly 100 cells, so the grid reads as percentages. The original alias is preserved on the read-only `chart.requestedType`; an explicit `layout` or `shape` still wins.
177
277
 
178
278
  ```js
@@ -211,7 +311,17 @@ plotOptions: {
211
311
  pie: {
212
312
  startAngle: 0,
213
313
  endAngle: 360,
214
- expandOnClick: true, // expand slice on click
314
+ expandOnClick: true, // react to a slice click at all
315
+ expandOffset: 10, // (v6.9) how far the clicked slice slides out (px) along its mid-angle.
316
+ // The slice is translated, not redrawn bigger, so the quantity it
317
+ // encodes is unchanged. 0 keeps it in place. Ignored for polarArea.
318
+ hoverOutline: { // (v6.9) hover traces a translucent band outside the slice rim
319
+ show: true, // instead of lightening the fill
320
+ size: 8, // band thickness (px)
321
+ gap: 0, // clearance between slice rim and band (px)
322
+ opacity: 0.3,
323
+ color: undefined // defaults to the hovered slice's own color
324
+ },
215
325
  offsetX: 0,
216
326
  offsetY: 0,
217
327
 
@@ -377,3 +487,8 @@ plotOptions: {
377
487
  8. **Gauge value outside `min`/`max`**: unlike a plain radialBar (fixed 0-100), a gauge maps its value to the `min..max` domain you set. A value beyond that domain saturates at the arc ends.
378
488
  9. **Legend click now toggles the slice (v6.7 behavior change)**: on pie, donut, and polarArea a legend click hides and shows the slice (like other chart types). Previously it darkened and expanded the slice. If your app relied on the old darken-and-expand behavior, review this.
379
489
  10. **Expecting `unit` / `waffle` without a license to be watermark-free**: they are premium and render an `APEXCHARTS` watermark until an entitled `premium`/`enterprise` license is set. Sunburst, by contrast, is free.
490
+ 11. **Slice click behavior changed (v6.9)**: clicking a pie/donut slice now slides it out along its own mid-angle (`plotOptions.pie.expandOffset`, default 10 px) instead of darkening it and redrawing it at a larger radius, and hover traces an outline band (`plotOptions.pie.hoverOutline`) instead of lightening the fill. Set `expandOffset: 0` to keep slices in place; set `states.hover.filter.type: 'none'` to suppress the hover band.
491
+ 12. **Unit shapes below `minUnits`**: each `apexcharts/unit-shapes` shape declares the minimum dot count at which it still reads. Feeding fewer units logs a console warning naming the shape; pick a simpler shape or raise `unitValue` so the count lands above the threshold.
492
+ 13. **Confusing a shape with a pictogram** *(v7.0)*: `plotOptions.unit.positions` (from `apexcharts/unit-shapes`) is **where** the units go; `plotOptions.unit.shape: 'pictogram'` + `pictogram.mark` (from `apexcharts/pictograms`) is **what one unit looks like**. They compose freely.
493
+ 14. **Setting a size on a pictogram** *(v7.0)*: there is no `pictogram.size`. A glyph is fitted to the box the dot would have occupied, so use `plotOptions.unit.size` and `spacing`. Use `pictogram.scale` only to nudge a glyph that reads light.
494
+ 15. **Pie / radial charts pinned to the top of a tall container** *(fixed v7.1)*: the centre came from `min(width, height)`, so a tall, narrow container put the circle at the top with the leftover space below it. The circle now centres in the height it actually has. If you carried a manual `offsetY` to work around this, remove it.