apexcharts 8.2.0 → 8.3.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.
Files changed (222) hide show
  1. package/README.md +7 -7
  2. package/dist/apexcharts.common.js +2 -2
  3. package/dist/apexcharts.core.js +271 -173
  4. package/dist/apexcharts.core.min.js +2 -2
  5. package/dist/apexcharts.css +35 -0
  6. package/dist/apexcharts.esm.js +1722 -1527
  7. package/dist/apexcharts.full.js +1459 -1075
  8. package/dist/apexcharts.full.min.js +2 -2
  9. package/dist/apexcharts.js +994 -715
  10. package/dist/apexcharts.min.js +2 -2
  11. package/dist/apexcharts.ssr.common.js +2 -2
  12. package/dist/apexcharts.ssr.esm.js +994 -715
  13. package/dist/area.common.js +2 -2
  14. package/dist/area.esm.js +91 -40
  15. package/dist/bar.common.js +2 -2
  16. package/dist/bar.esm.js +433 -394
  17. package/dist/bar.js +2 -2
  18. package/dist/boxPlot.common.js +2 -2
  19. package/dist/boxPlot.esm.js +364 -322
  20. package/dist/bubble.common.js +2 -2
  21. package/dist/bubble.esm.js +91 -40
  22. package/dist/candlestick.common.js +2 -2
  23. package/dist/candlestick.esm.js +364 -322
  24. package/dist/candlestick.js +2 -2
  25. package/dist/column.common.js +2 -2
  26. package/dist/column.esm.js +433 -394
  27. package/dist/core.common.js +2 -2
  28. package/dist/core.esm.js +271 -173
  29. package/dist/donut.common.js +1 -1
  30. package/dist/donut.esm.js +1 -1
  31. package/dist/dumbbell.common.js +2 -2
  32. package/dist/dumbbell.esm.js +433 -394
  33. package/dist/features/all.common.js +2 -2
  34. package/dist/features/all.esm.js +250 -187
  35. package/dist/features/annotations.common.js +1 -1
  36. package/dist/features/annotations.esm.js +1 -1
  37. package/dist/features/annotations.js +1 -1
  38. package/dist/features/context-menu.common.js +1 -1
  39. package/dist/features/context-menu.esm.js +1 -1
  40. package/dist/features/context-menu.js +1 -1
  41. package/dist/features/drilldown.common.js +2 -2
  42. package/dist/features/drilldown.esm.js +4 -2
  43. package/dist/features/drilldown.js +2 -2
  44. package/dist/features/dumbbell.common.js +1 -1
  45. package/dist/features/dumbbell.esm.js +1 -1
  46. package/dist/features/dumbbell.js +1 -1
  47. package/dist/features/exports.common.js +1 -1
  48. package/dist/features/exports.esm.js +1 -1
  49. package/dist/features/exports.js +1 -1
  50. package/dist/features/facet.common.js +1 -1
  51. package/dist/features/facet.esm.js +1 -1
  52. package/dist/features/facet.js +1 -1
  53. package/dist/features/highlight-filter.common.js +2 -2
  54. package/dist/features/highlight-filter.esm.js +14 -9
  55. package/dist/features/highlight-filter.js +2 -2
  56. package/dist/features/history.common.js +1 -1
  57. package/dist/features/history.esm.js +1 -1
  58. package/dist/features/history.js +1 -1
  59. package/dist/features/ink.common.js +1 -1
  60. package/dist/features/ink.esm.js +1 -1
  61. package/dist/features/ink.js +1 -1
  62. package/dist/features/keyboard.common.js +2 -2
  63. package/dist/features/keyboard.esm.js +97 -145
  64. package/dist/features/keyboard.js +2 -2
  65. package/dist/features/legend.common.js +2 -2
  66. package/dist/features/legend.esm.js +63 -43
  67. package/dist/features/legend.js +2 -2
  68. package/dist/features/link.common.js +1 -1
  69. package/dist/features/link.esm.js +1 -1
  70. package/dist/features/link.js +1 -1
  71. package/dist/features/marks.common.js +1 -1
  72. package/dist/features/marks.esm.js +1 -1
  73. package/dist/features/marks.js +1 -1
  74. package/dist/features/measure.common.js +1 -1
  75. package/dist/features/measure.esm.js +1 -1
  76. package/dist/features/measure.js +1 -1
  77. package/dist/features/morph.common.js +1 -1
  78. package/dist/features/morph.esm.js +1 -1
  79. package/dist/features/morph.js +1 -1
  80. package/dist/features/perspectives.common.js +1 -1
  81. package/dist/features/perspectives.esm.js +1 -1
  82. package/dist/features/perspectives.js +1 -1
  83. package/dist/features/raincloud.common.js +1 -1
  84. package/dist/features/raincloud.esm.js +1 -1
  85. package/dist/features/raincloud.js +1 -1
  86. package/dist/features/renderer-canvas.common.js +2 -2
  87. package/dist/features/renderer-canvas.esm.js +2 -1
  88. package/dist/features/renderer-canvas.js +2 -2
  89. package/dist/features/stats.common.js +1 -1
  90. package/dist/features/stats.esm.js +1 -1
  91. package/dist/features/stats.js +1 -1
  92. package/dist/features/storyboard.common.js +1 -1
  93. package/dist/features/storyboard.esm.js +1 -1
  94. package/dist/features/storyboard.js +1 -1
  95. package/dist/features/streamgraph.common.js +2 -2
  96. package/dist/features/streamgraph.esm.js +3 -2
  97. package/dist/features/streamgraph.js +2 -2
  98. package/dist/features/toolbar.common.js +2 -2
  99. package/dist/features/toolbar.esm.js +92 -2
  100. package/dist/features/toolbar.js +2 -2
  101. package/dist/features/trellis.common.js +2 -2
  102. package/dist/features/trellis.esm.js +83 -47
  103. package/dist/features/trellis.js +2 -2
  104. package/dist/features/waterfall.common.js +1 -1
  105. package/dist/features/waterfall.esm.js +1 -1
  106. package/dist/features/waterfall.js +1 -1
  107. package/dist/features/weave.common.js +1 -1
  108. package/dist/features/weave.esm.js +1 -1
  109. package/dist/features/weave.js +1 -1
  110. package/dist/full.common.js +2 -2
  111. package/dist/full.esm.js +950 -795
  112. package/dist/heatmap.common.js +2 -2
  113. package/dist/heatmap.esm.js +3 -17
  114. package/dist/heatmap.js +2 -2
  115. package/dist/histogram.common.js +2 -2
  116. package/dist/histogram.esm.js +433 -394
  117. package/dist/icicle.common.js +2 -2
  118. package/dist/icicle.esm.js +54 -38
  119. package/dist/icicle.js +2 -2
  120. package/dist/line.common.js +2 -2
  121. package/dist/line.esm.js +91 -40
  122. package/dist/line.js +2 -2
  123. package/dist/locales/en.json +2 -1
  124. package/dist/locales/ro.json +3 -2
  125. package/dist/pictograms.common.js +1 -1
  126. package/dist/pictograms.esm.js +1 -1
  127. package/dist/pictograms.js +1 -1
  128. package/dist/pie.common.js +1 -1
  129. package/dist/pie.esm.js +1 -1
  130. package/dist/pie.js +1 -1
  131. package/dist/polarArea.common.js +1 -1
  132. package/dist/polarArea.esm.js +1 -1
  133. package/dist/radar.common.js +2 -2
  134. package/dist/radar.esm.js +5 -4
  135. package/dist/radar.js +2 -2
  136. package/dist/radialBar.common.js +1 -1
  137. package/dist/radialBar.esm.js +1 -1
  138. package/dist/radialBar.js +1 -1
  139. package/dist/raincloud.common.js +2 -2
  140. package/dist/raincloud.esm.js +72 -26
  141. package/dist/rangeArea.common.js +2 -2
  142. package/dist/rangeArea.esm.js +91 -40
  143. package/dist/rangeBar.common.js +2 -2
  144. package/dist/rangeBar.esm.js +433 -394
  145. package/dist/scatter.common.js +2 -2
  146. package/dist/scatter.esm.js +91 -40
  147. package/dist/streamgraph.common.js +2 -2
  148. package/dist/streamgraph.esm.js +93 -41
  149. package/dist/sunburst.common.js +2 -2
  150. package/dist/sunburst.esm.js +103 -93
  151. package/dist/sunburst.js +2 -2
  152. package/dist/treemap.common.js +2 -2
  153. package/dist/treemap.esm.js +105 -86
  154. package/dist/treemap.js +2 -2
  155. package/dist/unit-shapes.common.js +1 -1
  156. package/dist/unit-shapes.esm.js +1 -1
  157. package/dist/unit-shapes.js +1 -1
  158. package/dist/unit.common.js +2 -2
  159. package/dist/unit.esm.js +280 -202
  160. package/dist/unit.js +2 -2
  161. package/dist/violin.common.js +2 -2
  162. package/dist/violin.esm.js +72 -26
  163. package/dist/violin.js +2 -2
  164. package/dist/waterfall.common.js +2 -2
  165. package/dist/waterfall.esm.js +432 -393
  166. package/package.json +1 -1
  167. package/src/apexcharts.js +13 -6
  168. package/src/assets/apexcharts.css +35 -0
  169. package/src/charts/Bar.js +62 -55
  170. package/src/charts/BarStacked.js +80 -88
  171. package/src/charts/BoxCandleStick.js +28 -31
  172. package/src/charts/Icicle.js +37 -37
  173. package/src/charts/Line.js +118 -42
  174. package/src/charts/Radar.js +5 -7
  175. package/src/charts/RangeBar.js +47 -43
  176. package/src/charts/Sunburst.js +101 -98
  177. package/src/charts/Treemap.js +108 -66
  178. package/src/charts/Unit.js +368 -250
  179. package/src/charts/Violin.js +48 -30
  180. package/src/charts/common/Breadcrumb.js +2 -2
  181. package/src/charts/common/Hierarchy.js +32 -11
  182. package/src/charts/common/Jitter.js +32 -7
  183. package/src/charts/common/bar/DataLabels.js +76 -81
  184. package/src/charts/common/bar/Helpers.js +43 -7
  185. package/src/charts/common/line/Helpers.js +12 -15
  186. package/src/charts/common/partition/NodeTooltip.js +8 -1
  187. package/src/charts/common/partition/Partition.js +33 -1
  188. package/src/charts/common/treemap/Helpers.js +3 -11
  189. package/src/locales/en.json +2 -1
  190. package/src/locales/ro.json +3 -2
  191. package/src/modules/Core.js +4 -0
  192. package/src/modules/CoreUtils.js +18 -42
  193. package/src/modules/Data.js +23 -24
  194. package/src/modules/DataLabels.js +6 -6
  195. package/src/modules/Graphics.js +62 -49
  196. package/src/modules/Markers.js +3 -1
  197. package/src/modules/Range.js +44 -29
  198. package/src/modules/RendererController.js +17 -3
  199. package/src/modules/Scales.js +32 -9
  200. package/src/modules/Series.js +39 -18
  201. package/src/modules/Theme.js +1 -1
  202. package/src/modules/ZoomPanSelection.js +119 -2
  203. package/src/modules/accessibility/KeyboardNavigation.js +114 -186
  204. package/src/modules/animations/LengthTransition.js +22 -2
  205. package/src/modules/drilldown/Breadcrumb.js +5 -2
  206. package/src/modules/helpers/Destroy.js +1 -1
  207. package/src/modules/highlightFilter/HighlightFilter.js +8 -9
  208. package/src/modules/highlightFilter/TreemapPart.js +2 -2
  209. package/src/modules/legend/HeatmapGradientLegend.js +76 -45
  210. package/src/modules/legend/Helpers.js +2 -0
  211. package/src/modules/legend/Legend.js +11 -12
  212. package/src/modules/settings/Options.js +7 -0
  213. package/src/modules/streamgraph/StreamLabels.js +1 -0
  214. package/src/modules/tooltip/Tooltip.js +20 -4
  215. package/src/modules/tooltip/Utils.js +8 -0
  216. package/src/modules/trellis/Trellis.js +6 -0
  217. package/src/modules/trellis/TrellisChrome.js +18 -7
  218. package/src/modules/trellis/TrellisSync.js +2 -0
  219. package/src/renderers/canvas/CanvasGraphics.js +1 -0
  220. package/src/ssr/DOMShim.js +10 -0
  221. package/src/types/internal.d.ts +9 -3
  222. package/types/apexcharts.d.ts +31 -12
@@ -12,6 +12,7 @@ import {
12
12
  measureLabelWidth,
13
13
  spaceOutLabels,
14
14
  } from './common/OuterLabels'
15
+ import { hash01 } from './common/Jitter'
15
16
 
16
17
  /**
17
18
  * ApexCharts Unit Class - dot-cluster / pictogram ("unit") chart.
@@ -40,6 +41,28 @@ import {
40
41
 
41
42
  const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5))
42
43
 
44
+ /**
45
+ * Suffix a repeated id ("a", "a#2", ...) so every unit sharing it keeps a key of
46
+ * its own.
47
+ * @param {Map<string, number>} seen
48
+ * @param {string} id
49
+ */
50
+ const uniqueId = (seen, id) => {
51
+ const n = (seen.get(id) || 0) + 1
52
+ seen.set(id, n)
53
+ return n > 1 ? id + '#' + n : id
54
+ }
55
+
56
+ /**
57
+ * A finite number from a number or numeric string, else null.
58
+ * @param {any} v
59
+ * @returns {number|null}
60
+ */
61
+ const toNum = (v) => {
62
+ if (typeof v === 'string') v = parseFloat(v)
63
+ return typeof v === 'number' && isFinite(v) ? v : null
64
+ }
65
+
43
66
  /** @param {number} t */
44
67
  function easeOutCubic(t) {
45
68
  return 1 - Math.pow(1 - t, 3)
@@ -123,13 +146,15 @@ function springParams(preset, speed) {
123
146
  * @property {number} cy0 start y
124
147
  * @property {number} delay stagger delay (ms) before this dot starts moving
125
148
  * @property {boolean} isEnter true when the dot has no previous slot
149
+ * @property {boolean} [pair] launches with the next dot (a waffle track cell
150
+ * and the cell on it)
126
151
  * @property {any} spec this mark's placement/draw spec (see Unit#_place). The
127
152
  * frame loop switches on `spec.pk` rather than on a chart-wide shape, which
128
153
  * is what lets one population mix circles and pictograms.
129
154
  * @property {string} [key] identity key, used to carry live spring state
130
155
  * across a gather that was interrupted by a re-render
131
- * @property {number} [r0]
132
- * @property {number} [r1]
156
+ * @property {number} r0 size (radius) it starts at
157
+ * @property {number} r1 size (radius) it lands at
133
158
  * @property {string} [fill0]
134
159
  * @property {string} [fill1]
135
160
  * @property {number[]|null} [_c0]
@@ -170,6 +195,11 @@ export default class Unit {
170
195
  const graphics = new Graphics(w, this.ctx)
171
196
 
172
197
  const ret = graphics.group({ class: 'apexcharts-unit' })
198
+ // The previous render's loops animate nodes this render just replaced,
199
+ // whether or not this one animates.
200
+ BrowserAPIs.cancelAnimationFrame(w.globals.unitGatherRAF || 0)
201
+ BrowserAPIs.cancelAnimationFrame(w.globals.unitExitRAF || 0)
202
+ w.globals.unitGatherRAF = w.globals.unitExitRAF = null
173
203
  if (w.globals.noData || !Array.isArray(series) || series.length === 0) {
174
204
  return ret
175
205
  }
@@ -207,14 +237,20 @@ export default class Unit {
207
237
  const identity = transition === 'identity'
208
238
  const unitValue = opts.unitValue > 0 ? opts.unitValue : 1
209
239
 
240
+ // Small-multiple waffles fill a fixed cell budget per tile from the raw
241
+ // value, so they are neither rounded nor capped.
242
+ const gridSplit = layout === 'grid' && !!(opts.grid && opts.grid.split)
210
243
  // value -> dot count. A non-zero value always yields at least one dot so a
211
244
  // tiny category (e.g. 3 of 545) never vanishes entirely.
212
245
  let counts = series.map((v) => {
213
246
  const n = Math.abs(Utils.parseNumber(v)) / unitValue
214
- return n > 0 ? Math.max(1, Math.round(n)) : 0
247
+ return gridSplit ? n : n > 0 ? Math.max(1, Math.round(n)) : 0
215
248
  })
216
249
 
217
- counts = this._applyMaxUnits(counts, opts.maxUnits)
250
+ // A scatter caps its own datums (see _layoutScatter).
251
+ if (!gridSplit && layout !== 'scatter') {
252
+ counts = this._applyMaxUnits(counts, opts.maxUnits)
253
+ }
218
254
 
219
255
  const total = counts.reduce((a, b) => a + b, 0)
220
256
  /** @type {{ i: number, cx: number, cy: number, outerR: number, dots: {x:number,y:number,slot?:number,r?:number,j?:number}[] }[]} */
@@ -233,10 +269,6 @@ export default class Unit {
233
269
  ? this._layoutCustom(counts, opts)
234
270
  : this._layoutGrouped(counts, opts)
235
271
 
236
- // Small-multiple waffles paint a faint track backdrop (the "of N" cells)
237
- // behind every tile's filled dots, and carry a per-tile label.
238
- const gridSplit = layout === 'grid' && !!(opts.grid && opts.grid.split)
239
- if (gridSplit) this._drawGridTrack(ret, graphics, opts)
240
272
  // Scatter (beeswarm) paints its value X axis + category lanes behind the dots.
241
273
  if (layout === 'scatter') this._drawScatterAxes(ret, graphics)
242
274
 
@@ -311,7 +343,68 @@ export default class Unit {
311
343
  // Running index across ALL dots of ALL clusters, used as the key in 'flow'
312
344
  // mode so a dot is identified by its global order, not its category slot.
313
345
  let gIndex = 0
314
- clusters.forEach((cluster) => {
346
+ const seen = new Map()
347
+
348
+ // Small-multiple waffles paint a faint track backdrop (the "of N" cells)
349
+ // behind every tile's filled dots, and carry a per-tile label. Each track
350
+ // cell is keyed by its physical slot like the filled cell on it and rides
351
+ // the same transition, so a re-tiled waffle moves its backdrop with its
352
+ // cells; it is queued just before that cell and launches with it.
353
+ const track = (gridSplit && this._gridTrack && this._gridTrack.cells) || []
354
+ const trackG = graphics.group({ class: 'apexcharts-unit-track' })
355
+ if (track.length) ret.add(trackG)
356
+ const trackFill = (opts.grid && opts.grid.trackColor) || 'rgba(128,128,128,0.14)'
357
+ const trackSpec = this._baseSpec({ shape: opts.shape === 'square' && 'square' }, dotR)
358
+ const perTile = track.length / Math.max(1, clusters.length)
359
+ const live = this.ctx && this.ctx._unitSprings
360
+ // Glide a track cell or tile label with its tile's cells, from where it was
361
+ // (or fading in), queued at `at`. One resting on its slot (no move, no
362
+ // spring still in flight) stays put.
363
+ /**
364
+ * @param {any} node @param {any} spec @param {number} x @param {number} y
365
+ * @param {string} key @param {number} r @param {number} at @param {boolean} pair
366
+ */
367
+ const glide = (node, spec, x, y, key, r, at, pair) => {
368
+ const from = prev && prev.get(key)
369
+ const s = live && live.get(key)
370
+ const still =
371
+ from &&
372
+ from.x === x &&
373
+ from.y === y &&
374
+ from.r === r &&
375
+ (!s || (s.x.value === x && s.y.value === y))
376
+ nextPrev.set(key, { x, y, fill: trackFill, r, spec })
377
+ if (prev && animate && !pieceTakeover && !still) {
378
+ const cx0 = from ? from.x : x
379
+ const cy0 = from ? from.y : y
380
+ node.style.opacity = from ? '1' : '0'
381
+ this._place(node, spec, cx0, cy0)
382
+ animDots.splice(at, 0, {
383
+ node,
384
+ spec,
385
+ x,
386
+ y,
387
+ cx0,
388
+ cy0,
389
+ key,
390
+ r0: from && from.r != null ? from.r : r,
391
+ r1: r,
392
+ delay: 0,
393
+ isEnter: !from,
394
+ pair,
395
+ })
396
+ } else {
397
+ this._place(node, spec, x, y)
398
+ }
399
+ }
400
+ const ride = (/** @type {number} */ k, /** @type {boolean} */ pair) => {
401
+ const el = this._drawDot(graphics, opts, dotR, trackFill, -1, 0, trackSpec)
402
+ el.node.classList.add('apexcharts-unit-track-cell')
403
+ trackG.add(el)
404
+ glide(el.node, trackSpec, track[k].x, track[k].y, 'trk:' + k, dotR, animDots.length, pair)
405
+ }
406
+
407
+ clusters.forEach((cluster, ci) => {
315
408
  const color = w.globals.colors[cluster.i] || w.globals.colors[0] || '#008FFB'
316
409
  const elSeries = graphics.group({
317
410
  class: 'apexcharts-series',
@@ -325,7 +418,9 @@ export default class Unit {
325
418
  const burstCount = cluster.dots.length
326
419
  const catData = unitData[cluster.i]
327
420
 
421
+ const at = animDots.length
328
422
  cluster.dots.forEach((d, jj) => {
423
+ if (track.length) ride(ci * perTile + jj, true)
329
424
  // The mark's own dataPointIndex where the layout carried one (a custom
330
425
  // provider may omit marks, which would otherwise shift every later
331
426
  // datum by one), else its position in the cluster.
@@ -357,14 +452,15 @@ export default class Unit {
357
452
  let key
358
453
  if (identity) {
359
454
  // Needs an explicit id/name; a bare primitive is not a stable identity
360
- // (duplicates would collide), so those fall back to global order.
455
+ // (duplicates would collide), so those fall back to global order. A
456
+ // repeated id/name is suffixed so its holders never share one slot.
361
457
  const id =
362
458
  datum && typeof datum === 'object'
363
459
  ? datum.id != null
364
460
  ? datum.id
365
461
  : datum.name
366
462
  : undefined
367
- key = id != null ? `id:${id}` : `g:${gIndex}`
463
+ key = id != null ? `id:${uniqueId(seen, String(id))}` : `g:${gIndex}`
368
464
  } else if (flow) {
369
465
  key = String(gIndex)
370
466
  } else if (d.slot != null) {
@@ -406,9 +502,12 @@ export default class Unit {
406
502
  // from a centre).
407
503
  const enter = (opts.gather && opts.gather.enter) || 'burst'
408
504
  const inPlace = gridSplit || enter === 'fade' || enter === 'rise'
409
- const cx0 = anchor ? anchor.x : inPlace ? d.x : cluster.cx
410
- const cy0 = anchor
411
- ? anchor.y
505
+ // A waffle cell filling in starts on its track cell, wherever that
506
+ // was, so the two travel together.
507
+ const start = anchor || (gridSplit && prev && prev.get('trk:' + d.slot))
508
+ const cx0 = start ? start.x : inPlace ? d.x : cluster.cx
509
+ const cy0 = start
510
+ ? start.y
412
511
  : enter === 'rise' && !gridSplit
413
512
  ? d.y + 14
414
513
  : inPlace
@@ -442,6 +541,8 @@ export default class Unit {
442
541
  this._place(el.node, spec, d.x, d.y)
443
542
  }
444
543
  })
544
+ // The tile's unfilled track cells.
545
+ for (let k = cluster.dots.length; k < perTile; k++) ride(ci * perTile + k, false)
445
546
 
446
547
  // Per-cluster label: a curved arc over a 'grouped' blob, a straight label
447
548
  // by a 'columns' bar, or a straight per-tile label on a small-multiple
@@ -457,7 +558,14 @@ export default class Unit {
457
558
  counts[cluster.i] > 0
458
559
  ) {
459
560
  const labelTotal = gridSplit ? this._gridDenom : total
460
- this._drawClusterLabel(elSeries, cluster, counts[cluster.i], labelTotal, opts, color)
561
+ const t = this._drawClusterLabel(elSeries, cluster, counts[cluster.i], labelTotal, opts, color)
562
+ // A tile label rides with its cells (kept out of the exit ghosts).
563
+ if (t && gridSplit) {
564
+ const spec = { pk: PK_CORNER, hx: 0, hy: 0, r: 0 }
565
+ const lx = +(t.getAttribute('x') || 0)
566
+ const ly = +(t.getAttribute('y') || 0)
567
+ glide(t, spec, lx, ly, 'lbl:' + cluster.i, 0, at, animDots.length > at)
568
+ }
461
569
  }
462
570
 
463
571
  ret.add(elSeries)
@@ -524,11 +632,16 @@ export default class Unit {
524
632
  const total = counts.reduce((a, b) => a + b, 0)
525
633
  if (!maxUnits || maxUnits <= 0 || total <= maxUnits) return counts
526
634
  const scale = maxUnits / total
527
- console.warn(
528
- `[ApexCharts] unit chart: ${total} dots exceeds maxUnits (${maxUnits}); ` +
529
- `counts were scaled down proportionally. Raise plotOptions.unit.maxUnits ` +
530
- `or use plotOptions.unit.unitValue to represent more units per dot.`,
531
- )
635
+ // Once per chart, not on every update of a live chart.
636
+ const ctx = this.ctx || {}
637
+ if (!ctx._unitCapWarned) {
638
+ ctx._unitCapWarned = true
639
+ console.warn(
640
+ `[ApexCharts] unit chart: ${total} dots exceeds maxUnits (${maxUnits}); ` +
641
+ `counts were scaled down proportionally. Raise plotOptions.unit.maxUnits ` +
642
+ `or use plotOptions.unit.unitValue to represent more units per dot.`,
643
+ )
644
+ }
532
645
  return counts.map((c) => (c > 0 ? Math.max(1, Math.round(c * scale)) : 0))
533
646
  }
534
647
 
@@ -675,7 +788,8 @@ export default class Unit {
675
788
  *
676
789
  * `id` is the datum's own id/name where the per-unit object form supplies
677
790
  * one, so a provider can address a specific unit ("Texas", "employee 41")
678
- * rather than a positional slot. It falls back to `"<category>:<index>"`.
791
+ * rather than a positional slot. It falls back to `"<category>:<index>"`, and
792
+ * a repeated one is suffixed (`"Alex#2"`).
679
793
  *
680
794
  * @param {number[]} counts
681
795
  * @param {number} dotR the radius the engine would use, so a provider that
@@ -688,15 +802,19 @@ export default class Unit {
688
802
  const names = w.seriesData.seriesNames || []
689
803
  /** @type {{id:string,index:number,seriesIndex:number,dataPointIndex:number,label:string,value:number|undefined,datum:any,r:number}[]} */
690
804
  const objects = []
805
+ const seen = new Map()
691
806
  let index = 0
692
807
  counts.forEach((n, i) => {
693
808
  const catData = unitData[i]
694
809
  for (let j = 0; j < n; j++) {
695
810
  const datum = catData ? catData[j] : undefined
696
- const id =
811
+ // A repeated id/name is suffixed ("Alex#2") so each mark is placed.
812
+ const id = uniqueId(
813
+ seen,
697
814
  datum && typeof datum === 'object' && (datum.id != null || datum.name != null)
698
815
  ? String(datum.id != null ? datum.id : datum.name)
699
- : `${i}:${j}`
816
+ : `${i}:${j}`,
817
+ )
700
818
  objects.push({
701
819
  id,
702
820
  index,
@@ -927,12 +1045,23 @@ export default class Unit {
927
1045
  const pad = Math.min(gw, gh) * 0.04
928
1046
  const boxW = Math.max(1e-6, b.maxX - b.minX)
929
1047
  const boxH = Math.max(1e-6, b.maxY - b.minY)
930
- const r1 = Math.max(4, Math.min((gw - 2 * pad) / boxW, (gh - 2 * pad) / boxH))
931
- const r0 = r1 * innerRatio
1048
+ const fit = (/** @type {number} */ inset) =>
1049
+ Math.max(4, Math.min((gw - 2 * pad - inset) / boxW, (gh - 2 * pad - inset) / boxH))
1050
+ // The box bounds seat CENTRES, so inset the fit by the seat size. Seats
1051
+ // shrink with the radius: bisect for the largest radius its own seats fit.
1052
+ const seat = (/** @type {number} */ r) =>
1053
+ fit(2 * this._arcAllocate(total, r * innerRatio, r, span, opts).dotR)
1054
+ let hi = fit(0)
1055
+ let r1 = seat(hi)
1056
+ for (let k = 0; k < 6; k++) {
1057
+ const m = (r1 + hi) / 2
1058
+ if (m <= seat(m)) r1 = m
1059
+ else hi = m
1060
+ }
932
1061
  const cx = gw / 2 - ((b.minX + b.maxX) / 2) * r1
933
1062
  const cy = gh / 2 - ((b.minY + b.maxY) / 2) * r1
934
1063
 
935
- const alloc = this._arcAllocate(total, r0, r1, span, opts)
1064
+ const alloc = this._arcAllocate(total, r1 * innerRatio, r1, span, opts)
936
1065
  this._lastDotR = alloc.dotR
937
1066
 
938
1067
  // Every seat position, then sorted by angle so category-ordered assignment
@@ -1294,7 +1423,7 @@ export default class Unit {
1294
1423
  * value over a denominator (`grid.max`, else the largest count so the leader
1295
1424
  * fills its tile and every other tile stays proportionally full - no empty
1296
1425
  * tiles for arbitrary data). The unfilled cells are drawn as a faint TRACK
1297
- * backdrop (see _drawGridTrack) so each tile reads as a part-to-whole "of N".
1426
+ * backdrop (drawn by draw()) so each tile reads as a part-to-whole "of N".
1298
1427
  * Only VISIBLE (non-zero) categories claim a tile, so a legend hide drops the
1299
1428
  * tile and the rest re-flow. Each filled cell is keyed by a physical
1300
1429
  * `tile*cells + localCell` slot, so a value change grows/shrinks a tile's fill
@@ -1319,7 +1448,7 @@ export default class Unit {
1319
1448
  // Value -> filled-cell denominator. Default = the largest count (leader
1320
1449
  // fills its tile, the rest proportional); `grid.max` pins an explicit whole
1321
1450
  // (e.g. 100 so a value of 35 fills 35 of 100 cells - a percentage waffle).
1322
- const denom = gcfg.max > 0 ? gcfg.max : Math.max(1, ...counts)
1451
+ const denom = gcfg.max > 0 ? gcfg.max : Math.max(...counts) || 1
1323
1452
 
1324
1453
  // Arrange the tiles in a near-square grid unless grid.tileColumns pins it.
1325
1454
  const tileCols = Math.max(
@@ -1410,41 +1539,6 @@ export default class Unit {
1410
1539
  return clusters
1411
1540
  }
1412
1541
 
1413
- /**
1414
- * Draw the faint "track" backdrop for the small-multiple grid: every cell of
1415
- * every tile's full lattice, so the filled (coloured) cells drawn on top read
1416
- * as a fraction of the whole. Static (redrawn each render, never animated);
1417
- * painted BEHIND the series groups. `grid.trackColor` overrides the default
1418
- * theme-neutral grey.
1419
- * @param {any} ret @param {Graphics} graphics @param {any} opts
1420
- */
1421
- _drawGridTrack(ret, graphics, opts) {
1422
- const track = this._gridTrack
1423
- if (!track || !track.cells || !track.cells.length) return
1424
- const r = this._lastDotR
1425
- const gcfg = opts.grid || {}
1426
- const trackColor = gcfg.trackColor || 'rgba(128,128,128,0.14)'
1427
- const g = graphics.group({ class: 'apexcharts-unit-track' })
1428
- track.cells.forEach((c) => {
1429
- let el
1430
- if (opts.shape === 'square') {
1431
- const side = r * 2
1432
- el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, trackColor, 1, 0, 'none')
1433
- el.node.setAttribute('fill', trackColor)
1434
- el.node.setAttribute('x', String(c.x - r))
1435
- el.node.setAttribute('y', String(c.y - r))
1436
- } else {
1437
- el = graphics.drawCircle(r, { fill: trackColor, 'stroke-width': 0, stroke: 'none' })
1438
- el.node.setAttribute('fill', trackColor)
1439
- el.node.setAttribute('cx', String(c.x))
1440
- el.node.setAttribute('cy', String(c.y))
1441
- }
1442
- el.node.classList.add('apexcharts-unit-track-cell')
1443
- g.add(el)
1444
- })
1445
- ret.add(g)
1446
- }
1447
-
1448
1542
  /**
1449
1543
  * Scatter / beeswarm layout: position every unit on a real numeric X value
1450
1544
  * axis by its own value (`_unitValueOf`), laned by category on Y. Within a
@@ -1459,13 +1553,19 @@ export default class Unit {
1459
1553
  _layoutScatter(opts) {
1460
1554
  const w = this.w
1461
1555
  const scfg = opts.scatter || {}
1556
+ const unitData = w.seriesData.unitData || []
1557
+ // maxUnits keeps an evenly spaced sample of each category, one dot per datum.
1558
+ const lens = unitData.map((c) => (c ? c.length : 0))
1559
+ const caps = this._applyMaxUnits(lens, opts.maxUnits)
1560
+ /** @param {number} ci @param {number} j */
1561
+ const keep = (ci, j) =>
1562
+ Math.floor(((j + 1) * caps[ci]) / lens[ci]) > Math.floor((j * caps[ci]) / lens[ci])
1462
1563
  // 2D value-value scatter: each datum's own x AND y on two numeric axes.
1463
- if (scfg.y === 'value') return this._layoutScatter2D(opts)
1564
+ if (scfg.y === 'value') return this._layoutScatter2D(opts, keep)
1464
1565
  // Vertical beeswarm: value on the Y axis, category lanes as X columns.
1465
- if (scfg.orientation === 'vertical') return this._layoutScatterVertical(opts)
1566
+ if (scfg.orientation === 'vertical') return this._layoutScatterVertical(opts, keep)
1466
1567
  const gw = w.layout.gridWidth
1467
1568
  const gh = w.layout.gridHeight
1468
- const unitData = w.seriesData.unitData || []
1469
1569
  const names = w.seriesData.seriesNames || []
1470
1570
  const valueOf = (/** @type {any} */ d) => this._unitValueOf(d)
1471
1571
  // Optional bubble sizing (by a separate `sizeField`): a bubble beeswarm.
@@ -1500,14 +1600,22 @@ export default class Unit {
1500
1600
  vmax = 1
1501
1601
  }
1502
1602
  const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5))
1503
- const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount)
1603
+ const domain = this._scatterValueDomain(scfg.xMin, scfg.xMax, vmin, vmax, tickAmount)
1504
1604
  const xMin = domain.min
1505
1605
  const xMax = domain.max
1506
1606
  const xSpan = xMax - xMin || 1
1507
1607
 
1508
- // Plot box: left gutter for lane labels, bottom gutter for the axis.
1608
+ // Plot box: left gutter for lane labels, bottom gutter for the axis. The
1609
+ // gutter fits the widest lane label (drawn 12px / 600), up to a third of
1610
+ // the plot; a label past it is shortened with an ellipsis.
1611
+ const font = { fontFamily: w.config.chart.fontFamily, fontWeight: 600 }
1612
+ const laneName = (/** @type {number} */ i) => names[i] || `series-${i + 1}`
1509
1613
  const laneW =
1510
- scfg.laneLabelWidth != null ? Math.max(0, scfg.laneLabelWidth) : Kv > 1 ? 92 : 8
1614
+ scfg.laneLabelWidth != null
1615
+ ? Math.max(0, scfg.laneLabelWidth)
1616
+ : Kv > 1
1617
+ ? Math.min(gw / 3, measureLabelWidth(w, visible.map(laneName), font) + 14)
1618
+ : 8
1511
1619
  const bottomGutter = 30 + (scfg.xTitle ? 20 : 0)
1512
1620
  const plotL = laneW
1513
1621
  const plotR = gw - 8
@@ -1553,38 +1661,44 @@ export default class Unit {
1553
1661
 
1554
1662
  visible.forEach((ci, slot) => {
1555
1663
  const cy = laneCy(slot)
1556
- lanes.push({ i: ci, cy, name: names[ci] || `series-${ci + 1}` })
1557
- const cat = unitData[ci] || []
1664
+ let name = laneName(ci)
1665
+ // Only a gutter wide enough to draw labels in (see _drawScatterAxes).
1666
+ const wid = laneW > 12 ? measureLabelWidth(w, [name], font) : 0
1667
+ if (wid > laneW - 14 && wid) {
1668
+ name = name.slice(0, Math.max(1, Math.floor((name.length * (laneW - 14)) / wid) - 1)) + '\u2026'
1669
+ }
1670
+ lanes.push({ i: ci, cy, name })
1558
1671
  // Points keep their original datum index j so draw() can still resolve
1559
- // unitData[i][j] for per-unit colour + tooltip. A non-numeric datum is
1560
- // pinned to the axis start (defensive; scatter expects numeric values).
1672
+ // unitData[i][j] for per-unit colour + tooltip. A datum with no numeric
1673
+ // value has no place on the axis and is not drawn.
1561
1674
  // With bubble sizing each point carries its own radius `r` (used by the
1562
1675
  // pack + read back by draw()); otherwise the shared radius applies.
1563
- const pts = cat.map((d, j) => {
1676
+ /** @type {{j:number,px:number,y:number,r?:number}[]} */
1677
+ const pts = []
1678
+ ;(unitData[ci] || []).forEach((/** @type {any} */ d, /** @type {number} */ j) => {
1564
1679
  const v = valueOf(d)
1680
+ if (!isNum(v) || !keep(ci, j)) return
1565
1681
  /** @type {{j:number,px:number,y:number,r?:number}} */
1566
- const p = { j, px: plotX(isNum(v) ? v : xMin), y: cy }
1682
+ const p = { j, px: plotX(v), y: cy }
1567
1683
  if (sizeStats) p.r = this._scatterRadius(d, sizeStats, r)
1568
- return p
1684
+ pts.push(p)
1569
1685
  })
1570
1686
  if (jitter) {
1571
- // Deterministic pseudo-jitter by index (no Math.random - keeps SSR +
1572
- // re-render stable): spread within the lane band.
1687
+ // Deterministic pseudo-jitter (no Math.random - keeps SSR + re-render
1688
+ // stable): spread within the lane band.
1573
1689
  const halfLane = Math.max(maxR, laneH / 2 - maxR)
1574
- pts.forEach((p, k) => {
1575
- const t = ((k * 9301 + 49297) % 233280) / 233280 // LCG in [0,1)
1576
- p.y = cy + (t * 2 - 1) * halfLane
1690
+ pts.forEach((p) => {
1691
+ p.y = cy + (hash01(ci * 7919 + p.j) * 2 - 1) * halfLane
1577
1692
  })
1578
1693
  } else {
1579
1694
  this._beeswarm(pts, cy, r, step, maxR)
1580
1695
  }
1581
- // draw() indexes cluster.dots[j] against unitData[i][j], so keep j order.
1582
1696
  clusters.push({
1583
1697
  i: ci,
1584
1698
  cx: (plotL + plotR) / 2,
1585
1699
  cy,
1586
1700
  outerR: laneH / 2,
1587
- dots: pts.map((p) => ({ x: p.px, y: p.y, r: p.r })),
1701
+ dots: pts.map((p) => ({ x: p.px, y: p.y, r: p.r, j: p.j })),
1588
1702
  })
1589
1703
  })
1590
1704
 
@@ -1615,9 +1729,9 @@ export default class Unit {
1615
1729
  * dots horizontally off each column's centre line. The value-axis config keys
1616
1730
  * (`xMin`/`xMax`/`xTitle`/`xFormatter`/`tickAmount`) still describe the value
1617
1731
  * axis (now Y), so flipping `orientation` keeps the same value settings.
1618
- * @param {any} opts
1732
+ * @param {any} opts @param {(ci:number, j:number) => boolean} keep
1619
1733
  */
1620
- _layoutScatterVertical(opts) {
1734
+ _layoutScatterVertical(opts, keep) {
1621
1735
  const w = this.w
1622
1736
  const scfg = opts.scatter || {}
1623
1737
  const gw = w.layout.gridWidth
@@ -1653,7 +1767,7 @@ export default class Unit {
1653
1767
  vmax = 1
1654
1768
  }
1655
1769
  const tickAmount = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5))
1656
- const domain = this._scatterValueDomain(scfg, vmin, vmax, tickAmount)
1770
+ const domain = this._scatterValueDomain(scfg.xMin, scfg.xMax, vmin, vmax, tickAmount)
1657
1771
  const vMin = domain.min
1658
1772
  const vMax = domain.max
1659
1773
  const vSpan = vMax - vMin || 1
@@ -1705,19 +1819,20 @@ export default class Unit {
1705
1819
  visible.forEach((ci, slot) => {
1706
1820
  const cx = laneCx(slot)
1707
1821
  lanes.push({ i: ci, cx, name: names[ci] || `series-${ci + 1}` })
1708
- const cat = unitData[ci] || []
1709
- const pts = cat.map((d, j) => {
1822
+ /** @type {{j:number,py:number,x:number,r?:number}[]} */
1823
+ const pts = []
1824
+ ;(unitData[ci] || []).forEach((/** @type {any} */ d, /** @type {number} */ j) => {
1710
1825
  const v = valueOf(d)
1826
+ if (!isNum(v) || !keep(ci, j)) return
1711
1827
  /** @type {{j:number,py:number,x:number,r?:number}} */
1712
- const p = { j, py: plotY(isNum(v) ? v : vMin), x: cx }
1828
+ const p = { j, py: plotY(v), x: cx }
1713
1829
  if (sizeStats) p.r = this._scatterRadius(d, sizeStats, r)
1714
- return p
1830
+ pts.push(p)
1715
1831
  })
1716
1832
  if (jitter) {
1717
1833
  const halfLane = Math.max(maxR, laneW / 2 - maxR)
1718
- pts.forEach((p, k) => {
1719
- const t = ((k * 9301 + 49297) % 233280) / 233280 // LCG in [0,1)
1720
- p.x = cx + (t * 2 - 1) * halfLane
1834
+ pts.forEach((p) => {
1835
+ p.x = cx + (hash01(ci * 7919 + p.j) * 2 - 1) * halfLane
1721
1836
  })
1722
1837
  } else {
1723
1838
  this._beeswarm(pts, cx, r, step, maxR, true)
@@ -1727,7 +1842,7 @@ export default class Unit {
1727
1842
  cx,
1728
1843
  cy: (plotT + plotB) / 2,
1729
1844
  outerR: laneW / 2,
1730
- dots: pts.map((p) => ({ x: p.x, y: p.py, r: p.r })),
1845
+ dots: pts.map((p) => ({ x: p.x, y: p.py, r: p.r, j: p.j })),
1731
1846
  })
1732
1847
  })
1733
1848
 
@@ -1760,19 +1875,20 @@ export default class Unit {
1760
1875
  * per category). With `scatter.sizeRange` set, each dot is a BUBBLE scaled (by
1761
1876
  * area) from its `sizeField` (default 'z'). Needs the object form with numeric
1762
1877
  * `x` + `y`.
1763
- * @param {any} opts
1878
+ * @param {any} opts @param {(ci:number, j:number) => boolean} keep
1764
1879
  */
1765
- _layoutScatter2D(opts) {
1880
+ _layoutScatter2D(opts, keep) {
1766
1881
  const w = this.w
1767
1882
  const gw = w.layout.gridWidth
1768
1883
  const gh = w.layout.gridHeight
1769
1884
  const scfg = opts.scatter || {}
1770
1885
  const unitData = w.seriesData.unitData || []
1771
- const isNum = (/** @type {any} */ v) => typeof v === 'number' && isFinite(v)
1886
+ /** @param {any} v @returns {v is number} */
1887
+ const isNum = (v) => v != null
1772
1888
  const xOf = (/** @type {any} */ d) =>
1773
- d && typeof d === 'object' ? d.x : null
1889
+ d && typeof d === 'object' ? toNum(d.x) : null
1774
1890
  const yOf = (/** @type {any} */ d) =>
1775
- d && typeof d === 'object' ? (d.y != null ? d.y : d.value) : null
1891
+ d && typeof d === 'object' ? toNum(d.y != null ? d.y : d.value) : null
1776
1892
 
1777
1893
  // A category is visible if it has at least one point with numeric x AND y.
1778
1894
  const visible = unitData
@@ -1804,22 +1920,26 @@ export default class Unit {
1804
1920
  ymn = 0
1805
1921
  ymx = 1
1806
1922
  }
1807
- const xTicksN = Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5))
1808
- const yTicksN = Math.max(2, Math.round(scfg.yTickAmount > 0 ? scfg.yTickAmount : 5))
1809
- const nx = this._niceScale(
1810
- scfg.xMin != null ? scfg.xMin : xmn,
1811
- scfg.xMax != null ? scfg.xMax : xmx,
1812
- xTicksN,
1923
+ // Pinned bounds frame each axis and extend to keep every point inside the
1924
+ // plot, as the 1D beeswarm's value axis does.
1925
+ const dx = this._scatterValueDomain(
1926
+ scfg.xMin,
1927
+ scfg.xMax,
1928
+ xmn,
1929
+ xmx,
1930
+ Math.max(2, Math.round(scfg.tickAmount > 0 ? scfg.tickAmount : 5)),
1813
1931
  )
1814
- const ny = this._niceScale(
1815
- scfg.yMin != null ? scfg.yMin : ymn,
1816
- scfg.yMax != null ? scfg.yMax : ymx,
1817
- yTicksN,
1932
+ const dy = this._scatterValueDomain(
1933
+ scfg.yMin,
1934
+ scfg.yMax,
1935
+ ymn,
1936
+ ymx,
1937
+ Math.max(2, Math.round(scfg.yTickAmount > 0 ? scfg.yTickAmount : 5)),
1818
1938
  )
1819
- const xMin = scfg.xMin != null ? scfg.xMin : nx.min
1820
- const xMax = scfg.xMax != null ? scfg.xMax : nx.max
1821
- const yMin = scfg.yMin != null ? scfg.yMin : ny.min
1822
- const yMax = scfg.yMax != null ? scfg.yMax : ny.max
1939
+ const xMin = dx.min
1940
+ const xMax = dx.max
1941
+ const yMin = dy.min
1942
+ const yMax = dy.max
1823
1943
  const xSpan = xMax - xMin || 1
1824
1944
  const ySpan = yMax - yMin || 1
1825
1945
 
@@ -1844,15 +1964,19 @@ export default class Unit {
1844
1964
  /** @type {{ i:number, cx:number, cy:number, outerR:number, dots:{x:number,y:number,r?:number}[] }[]} */
1845
1965
  const clusters = []
1846
1966
  visible.forEach((ci) => {
1847
- const cat = unitData[ci] || []
1848
- const dots = cat.map((d) => {
1967
+ /** @type {{x:number,y:number,r?:number,j:number}[]} */
1968
+ const dots = []
1969
+ ;(unitData[ci] || []).forEach((/** @type {any} */ d, /** @type {number} */ j) => {
1849
1970
  const x = xOf(d)
1850
1971
  const y = yOf(d)
1851
- return {
1852
- x: plotX(isNum(x) ? x : xMin),
1853
- y: plotY(isNum(y) ? y : yMin),
1972
+ // A point with no numeric x or y has no place on the plane.
1973
+ if (x == null || y == null || !keep(ci, j)) return
1974
+ dots.push({
1975
+ x: plotX(x),
1976
+ y: plotY(y),
1854
1977
  r: sizeStats ? this._scatterRadius(d, sizeStats, baseR) : undefined,
1855
- }
1978
+ j,
1979
+ })
1856
1980
  })
1857
1981
  clusters.push({
1858
1982
  i: ci,
@@ -1863,27 +1987,6 @@ export default class Unit {
1863
1987
  })
1864
1988
  })
1865
1989
 
1866
- const mkTicks = (
1867
- /** @type {number} */ lo,
1868
- /** @type {number} */ hi,
1869
- /** @type {number} */ span,
1870
- /** @type {number} */ spacing,
1871
- /** @type {boolean} */ pinned,
1872
- /** @type {number} */ n,
1873
- ) => {
1874
- /** @type {number[]} */
1875
- const out = []
1876
- if (pinned) {
1877
- for (let k = 0; k < n; k++) out.push(lo + (span * k) / (n - 1))
1878
- } else {
1879
- const sp = spacing || span / Math.max(1, n - 1)
1880
- for (let v = lo; v <= hi + sp * 0.5; v += sp) {
1881
- out.push(Math.abs(v) < sp * 1e-9 ? 0 : v)
1882
- }
1883
- }
1884
- return out
1885
- }
1886
-
1887
1990
  this._scatterAxis = {
1888
1991
  mode: '2d',
1889
1992
  plotL,
@@ -1892,22 +1995,8 @@ export default class Unit {
1892
1995
  plotB,
1893
1996
  plotX,
1894
1997
  plotY,
1895
- xTicks: mkTicks(
1896
- xMin,
1897
- xMax,
1898
- xSpan,
1899
- nx.spacing,
1900
- scfg.xMin != null || scfg.xMax != null,
1901
- xTicksN,
1902
- ),
1903
- yTicks: mkTicks(
1904
- yMin,
1905
- yMax,
1906
- ySpan,
1907
- ny.spacing,
1908
- scfg.yMin != null || scfg.yMax != null,
1909
- yTicksN,
1910
- ),
1998
+ xTicks: dx.ticks,
1999
+ yTicks: dy.ticks,
1911
2000
  xTitle: scfg.xTitle,
1912
2001
  yTitle: scfg.yTitle,
1913
2002
  xFormatter: typeof scfg.xFormatter === 'function' ? scfg.xFormatter : null,
@@ -2019,36 +2108,47 @@ export default class Unit {
2019
2108
  }
2020
2109
 
2021
2110
  /**
2022
- * Value-axis domain + ticks for a 1D beeswarm. The domain ALWAYS contains
2111
+ * Value-axis domain + ticks for a scatter axis. The domain ALWAYS contains
2023
2112
  * every datum: a swarm that clips a dot outside the plot box is a bug, so an
2024
- * explicit `xMin`/`xMax` only FRAMES the axis and is extended by whole
2113
+ * explicit `lo`/`hi` only FRAMES the axis and is extended by whole
2025
2114
  * tick-steps whenever the data would otherwise overflow. Orientation-agnostic:
2026
2115
  * the same value axis is X for a horizontal swarm and Y for a vertical one.
2027
- * @param {any} scfg scatter config
2116
+ * @param {number|undefined} lo pinned minimum @param {number|undefined} hi
2117
+ * pinned maximum
2028
2118
  * @param {number} vmin data minimum @param {number} vmax data maximum
2029
2119
  * @param {number} tickAmount desired tick count
2030
2120
  * @returns {{ min:number, max:number, ticks:number[] }}
2031
2121
  */
2032
- _scatterValueDomain(scfg, vmin, vmax, tickAmount) {
2122
+ _scatterValueDomain(lo, hi, vmin, vmax, tickAmount) {
2033
2123
  /** @param {number} min @param {number} max @param {number} spacing */
2034
2124
  const buildTicks = (min, max, spacing) => {
2035
2125
  /** @type {number[]} */
2036
2126
  const ticks = []
2037
- for (let v = min; v <= max + spacing * 0.5; v += spacing) {
2127
+ for (let k = 0, v = min; v <= max + spacing * 0.5; v = min + ++k * spacing) {
2038
2128
  ticks.push(Math.abs(v) < spacing * 1e-9 ? 0 : v)
2039
2129
  }
2040
2130
  return ticks
2041
2131
  }
2042
- if (scfg.xMin != null || scfg.xMax != null) {
2043
- let min = scfg.xMin != null ? scfg.xMin : vmin
2044
- let max = scfg.xMax != null ? scfg.xMax : vmax
2045
- if (!(max > min)) max = min + 1
2046
- const spacing = (max - min) / Math.max(1, tickAmount - 1)
2132
+ if (lo != null || hi != null) {
2133
+ let min = /** @type {number} */ (lo != null ? lo : hi)
2134
+ let max = /** @type {number} */ (hi != null ? hi : lo)
2135
+ // Both ends pinned: split the span. One end pinned: step from it by a
2136
+ // nice spacing, so the free end lands on a whole step.
2137
+ const spacing =
2138
+ max > min
2139
+ ? (max - min) / Math.max(1, tickAmount - 1)
2140
+ : this._niceScale(Math.min(min, vmin), Math.max(max, vmax), tickAmount).spacing
2047
2141
  // Extend the framed bounds by whole steps so no dot falls outside the
2048
2142
  // plot (keeps the user's tick origin + spacing, just adds ticks).
2049
2143
  if (vmin < min) min -= Math.ceil((min - vmin) / spacing) * spacing
2050
2144
  if (vmax > max) max += Math.ceil((vmax - max) / spacing) * spacing
2051
- return { min, max, ticks: buildTicks(min, max, spacing) }
2145
+ if (!(max > min)) max = min + spacing
2146
+ if ((max - min) / spacing <= 2 * tickAmount) {
2147
+ return { min, max, ticks: buildTicks(min, max, spacing) }
2148
+ }
2149
+ // A far outlier would add a tick per step: nice-number the whole span.
2150
+ vmin = min
2151
+ vmax = max
2052
2152
  }
2053
2153
  const nice = this._niceScale(vmin, vmax, tickAmount)
2054
2154
  const spacing =
@@ -2168,14 +2268,14 @@ export default class Unit {
2168
2268
  ax.yTicks.forEach((/** @type {number} */ v) => {
2169
2269
  const y = ax.plotY(v)
2170
2270
  if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor)
2171
- const label = ax.yFormatter ? String(ax.yFormatter(v)) : this._formatTick(v)
2271
+ const label = ax.yFormatter ? String(ax.yFormatter(v)) : this._formatTick(v, ax.yTicks)
2172
2272
  text(label, ax.plotL - 8, y, 'end', labelColor, 11, 400, 'apexcharts-unit-tick')
2173
2273
  })
2174
2274
  // X gridlines + tick labels.
2175
2275
  ax.xTicks.forEach((/** @type {number} */ v) => {
2176
2276
  const x = ax.plotX(v)
2177
2277
  if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor)
2178
- const label = ax.xFormatter ? String(ax.xFormatter(v)) : this._formatTick(v)
2278
+ const label = ax.xFormatter ? String(ax.xFormatter(v)) : this._formatTick(v, ax.xTicks)
2179
2279
  text(label, x, ax.plotB + 14, 'middle', labelColor, 11, 400, 'apexcharts-unit-tick')
2180
2280
  })
2181
2281
  // Axis baselines (X along the bottom, Y up the left).
@@ -2220,7 +2320,7 @@ export default class Unit {
2220
2320
  ax.ticks.forEach((/** @type {number} */ v) => {
2221
2321
  const y = ax.plotY(v)
2222
2322
  if (ax.gridlines) line(ax.plotL, y, ax.plotR, y, gridColor)
2223
- const label = ax.formatter ? String(ax.formatter(v)) : this._formatTick(v)
2323
+ const label = ax.formatter ? String(ax.formatter(v)) : this._formatTick(v, ax.ticks)
2224
2324
  text(label, ax.plotL - 8, y, 'end', labelColor, 11, 400, 'apexcharts-unit-tick')
2225
2325
  })
2226
2326
  // Y axis baseline up the left.
@@ -2261,7 +2361,7 @@ export default class Unit {
2261
2361
  ax.ticks.forEach((/** @type {number} */ v, /** @type {number} */ idx) => {
2262
2362
  const x = ax.plotX(v)
2263
2363
  if (ax.gridlines) line(x, ax.plotT, x, ax.plotB, gridColor)
2264
- const label = ax.formatter ? String(ax.formatter(v)) : this._formatTick(v)
2364
+ const label = ax.formatter ? String(ax.formatter(v)) : this._formatTick(v, ax.ticks)
2265
2365
  const anchor =
2266
2366
  idx === 0 ? 'start' : idx === ax.ticks.length - 1 ? 'end' : 'middle'
2267
2367
  text(label, x, ax.plotB + 14, anchor, labelColor, 11, 400, 'apexcharts-unit-tick')
@@ -2298,17 +2398,19 @@ export default class Unit {
2298
2398
  }
2299
2399
 
2300
2400
  /**
2301
- * Compact tick-value formatting: integers as-is, otherwise trimmed to a short
2302
- * decimal; large magnitudes get a k/M suffix.
2303
- * @param {number} v @returns {string}
2401
+ * Compact tick-value formatting: integers as-is, otherwise trimmed to enough
2402
+ * decimals to tell neighbouring ticks apart; large magnitudes get a k/M suffix.
2403
+ * @param {number} v @param {number[]} ticks the axis ticks (their step sets
2404
+ * the decimals) @returns {string}
2304
2405
  */
2305
- _formatTick(v) {
2406
+ _formatTick(v, ticks) {
2306
2407
  if (!isFinite(v)) return ''
2307
2408
  const a = Math.abs(v)
2308
2409
  if (a >= 1e6) return `${+(v / 1e6).toFixed(1)}M`
2309
2410
  if (a >= 1e4) return `${+(v / 1e3).toFixed(1)}k`
2310
2411
  if (Number.isInteger(v)) return String(v)
2311
- return String(+v.toFixed(2))
2412
+ const step = Math.abs(ticks[1] - ticks[0]) || 1
2413
+ return String(+v.toFixed(Math.min(20, Math.max(2, 1 - Math.floor(Math.log10(step))))))
2312
2414
  }
2313
2415
 
2314
2416
  /**
@@ -2390,17 +2492,12 @@ export default class Unit {
2390
2492
  }
2391
2493
 
2392
2494
  /**
2393
- * This datum's numeric value for sizing / tooltip: the number itself, or an
2394
- * object's `value` / `y`. Null when there is no usable number.
2495
+ * This datum's numeric value for sizing / tooltip: the number (or numeric
2496
+ * string) itself, or an object's `value` / `y`. Null when there is none.
2395
2497
  * @param {any} d @returns {number | null}
2396
2498
  */
2397
2499
  _unitValueOf(d) {
2398
- if (typeof d === 'number') return d
2399
- if (d && typeof d === 'object') {
2400
- const v = d.value != null ? d.value : d.y
2401
- return typeof v === 'number' ? v : null
2402
- }
2403
- return null
2500
+ return toNum(d && typeof d === 'object' ? (d.value != null ? d.value : d.y) : d)
2404
2501
  }
2405
2502
 
2406
2503
  /**
@@ -2552,25 +2649,43 @@ export default class Unit {
2552
2649
  *
2553
2650
  * @param {SVGElement} node @param {UnitMarkSpec} spec
2554
2651
  * @param {number} x @param {number} y
2652
+ * @param {number} [k] scale against the spec's size, while a size tweens
2555
2653
  */
2556
- _place(node, spec, x, y) {
2654
+ _place(node, spec, x, y, k = 1) {
2557
2655
  // One typedef covers all three kinds, so every per-kind field is optional
2558
2656
  // on it. The `pk` switch is what makes them present.
2559
2657
  const s = /** @type {any} */ (spec)
2560
2658
  if (s.pk === PK_GLYPH) {
2561
2659
  node.setAttribute(
2562
2660
  'transform',
2563
- 'translate(' + (x - s.ox) + ',' + (y - s.oy) + s.tail,
2661
+ 'translate(' +
2662
+ (x - s.ox * k) +
2663
+ ',' +
2664
+ (y - s.oy * k) +
2665
+ (k === 1 ? s.tail : ') scale(' + s.s * k + ')'),
2564
2666
  )
2565
2667
  } else if (s.pk === PK_CORNER) {
2566
- node.setAttribute('x', String(x - s.hx))
2567
- node.setAttribute('y', String(y - s.hy))
2668
+ node.setAttribute('x', String(x - s.hx * k))
2669
+ node.setAttribute('y', String(y - s.hy * k))
2568
2670
  } else {
2569
2671
  node.setAttribute('cx', String(x))
2570
2672
  node.setAttribute('cy', String(y))
2571
2673
  }
2572
2674
  }
2573
2675
 
2676
+ /**
2677
+ * Size one mark at `k` times its spec's size (a glyph scales in `_place`).
2678
+ * @param {SVGElement} node @param {UnitMarkSpec} spec @param {number} k
2679
+ */
2680
+ _size(node, spec, k) {
2681
+ const s = /** @type {any} */ (spec)
2682
+ if (s.pk === PK_CIRCLE) node.setAttribute('r', String(s.r * k))
2683
+ else if (s.pk === PK_CORNER) {
2684
+ node.setAttribute('width', String(2 * s.hx * k))
2685
+ node.setAttribute('height', String(2 * s.hy * k))
2686
+ }
2687
+ }
2688
+
2574
2689
  /**
2575
2690
  * The spec for the chart-wide shape (no pictogram, no per-mark radius).
2576
2691
  * @param {any} opts @param {number} [r]
@@ -2637,7 +2752,8 @@ export default class Unit {
2637
2752
  // grow the cache to one entry per dot; sub-tenth-pixel scale differences
2638
2753
  // are not observable anyway.
2639
2754
  const qr = Math.round(dotR * 10) / 10
2640
- const key = mark.name + '|' + mark.path.length + '|' + qr
2755
+ // Inline marks are all named 'anonymous', so the outline itself keys.
2756
+ const key = mark.name + mark.path + mark.viewBox + mark.fillRule + qr
2641
2757
  const hit = this._specCache.get(key)
2642
2758
  if (hit) return hit
2643
2759
 
@@ -2698,9 +2814,9 @@ export default class Unit {
2698
2814
  * Draw one dot (circle, square, or image icon) with the category fill +
2699
2815
  * stroke, tagged so the shared non-axis tooltip and hover reuse work.
2700
2816
  * @param {Graphics} graphics @param {any} opts @param {number} dotR
2701
- * @param {string} color @param {number} i @param {number} j
2702
- * @param {UnitMarkSpec} [spec] this mark's resolved spec; defaults to the
2703
- * chart-wide shape
2817
+ * @param {string} color @param {number} i @param {number} j (i < 0: an exit
2818
+ * ghost, left untagged so tooltips and morph capture skip it)
2819
+ * @param {UnitMarkSpec} spec this mark's resolved spec
2704
2820
  * @returns {any}
2705
2821
  */
2706
2822
  _drawDot(graphics, opts, dotR, color, i, j, spec) {
@@ -2718,7 +2834,7 @@ export default class Unit {
2718
2834
  typeof w.config.fill.opacity === 'number' ? w.config.fill.opacity : 1
2719
2835
 
2720
2836
  let el
2721
- if (spec && spec.pk === PK_GLYPH) {
2837
+ if (spec.pk === PK_GLYPH) {
2722
2838
  // A monochrome vector glyph: ONE <path>, filled directly in the mark's
2723
2839
  // colour. No fetch, no decode, and crucially no per-colour <filter> - an
2724
2840
  // feFlood/feComposite pair (what `shape:'image'` needs to match the
@@ -2733,7 +2849,7 @@ export default class Unit {
2733
2849
  // Geometry the morph capture cannot read back off a transform. Written
2734
2850
  // once at creation; never touched in a frame.
2735
2851
  el.node.setAttribute('data:r', String(spec.r))
2736
- } else if (opts.shape === 'image' && opts.image && opts.image.src) {
2852
+ } else if (spec.pk === PK_CORNER && opts.shape === 'image' && opts.image && opts.image.src) {
2737
2853
  const iw = opts.image.width || 20
2738
2854
  const ih = opts.image.height || 20
2739
2855
  el = w.dom.Paper.image(opts.image.src)
@@ -2747,7 +2863,8 @@ export default class Unit {
2747
2863
  if (opts.image.tint) {
2748
2864
  el.node.setAttribute('filter', `url(#${this._tintFilter(color)})`)
2749
2865
  }
2750
- } else if (opts.shape === 'square') {
2866
+ } else if (spec.pk === PK_CORNER) {
2867
+ // A square, or the square fallback of an unresolved pictogram.
2751
2868
  const side = dotR * 2
2752
2869
  el = graphics.drawRect(0, 0, side, side, opts.borderRadius || 0, color, 1, strokeW, strokeColor)
2753
2870
  el.node.setAttribute('fill', color)
@@ -2761,9 +2878,11 @@ export default class Unit {
2761
2878
  el.node.setAttribute('fill', color)
2762
2879
  if (fillOpacity < 1) el.node.setAttribute('fill-opacity', String(fillOpacity))
2763
2880
  }
2764
- el.node.classList.add('apexcharts-unit-area')
2765
- el.node.setAttribute('i', String(i))
2766
- el.node.setAttribute('j', String(j))
2881
+ if (i >= 0) {
2882
+ el.node.classList.add('apexcharts-unit-area')
2883
+ el.node.setAttribute('i', String(i))
2884
+ el.node.setAttribute('j', String(j))
2885
+ }
2767
2886
  return el
2768
2887
  }
2769
2888
 
@@ -2778,10 +2897,14 @@ export default class Unit {
2778
2897
  _tintFilter(color) {
2779
2898
  const w = this.w
2780
2899
  const NS = 'http://www.w3.org/2000/svg'
2781
- const safe = String(color).replace(/[^a-zA-Z0-9]/g, '')
2782
- const id = `apexcharts-unit-tint-${w.globals.chartID}-${safe}`
2900
+ // Keyed by the raw colour: a sanitised one let rgb(1,23,4) and
2901
+ // rgb(12,3,4) share a filter.
2902
+ const tints = this._tints || (this._tints = new Map())
2903
+ let id = tints.get(color)
2904
+ if (id) return id
2905
+ id = `apexcharts-unit-tint-${w.globals.chartID}-${tints.size}`
2906
+ tints.set(color, id)
2783
2907
  const svg = w.dom.Paper.node
2784
- if (svg.querySelector(`#${id}`)) return id
2785
2908
 
2786
2909
  let defs = svg.querySelector('defs')
2787
2910
  if (!defs) {
@@ -2929,6 +3052,15 @@ export default class Unit {
2929
3052
  if (this.ctx) this.ctx._unitSprings = springs
2930
3053
  }
2931
3054
 
3055
+ /**
3056
+ * True once a loop's node no longer belongs to this chart's live SVG.
3057
+ * @param {Element} node @returns {boolean}
3058
+ */
3059
+ _stale(node) {
3060
+ const P = this.w.dom.Paper
3061
+ return !!this.w.globals.isDestroyed || !P || !P.node.contains(node)
3062
+ }
3063
+
2932
3064
  /**
2933
3065
  * One rAF loop that tweens every dot from its start (cx0/cy0 - either the
2934
3066
  * cluster centre on first mount / for entering dots, or its previous slot on
@@ -2960,8 +3092,12 @@ export default class Unit {
2960
3092
  // quickly. Dots animate outward roughly centre-first.
2961
3093
  const maxDelay = Math.min(speed * 0.6, 450)
2962
3094
  const n = dots.length
2963
- for (let k = 0; k < n; k++) {
2964
- dots[k].delay = n > 1 ? (k / (n - 1)) * maxDelay : 0
3095
+ for (let k = n; k--; ) {
3096
+ dots[k].delay = dots[k].pair
3097
+ ? dots[k + 1].delay
3098
+ : n > 1
3099
+ ? (k / (n - 1)) * maxDelay
3100
+ : 0
2965
3101
  }
2966
3102
 
2967
3103
  const gcfg = opts.gather || {}
@@ -2979,15 +3115,13 @@ export default class Unit {
2979
3115
  // the positions, so switching motion at runtime cannot resurrect them.
2980
3116
  else if (this.ctx) this.ctx._unitSprings = null
2981
3117
 
2982
- // Seed the starting radius for size-changing circles so they grow from the
2983
- // previous size instead of flashing at the new one for a frame. (The old
2984
- // "seed initial corner" pass that stood here is gone: it rewrote exactly
2985
- // what draw()'s _placeDot had already written, and what _seedSprings
2986
- // rewrites again for a carried spring.)
3118
+ // Seed the starting size of size-changing marks so they grow from the
3119
+ // previous size instead of flashing at the new one for a frame.
2987
3120
  for (let k = 0; k < n; k++) {
2988
3121
  const d = dots[k]
2989
- if (d.spec.pk === PK_CIRCLE && d.r0 != null && d.r1 != null && d.r0 !== d.r1) {
2990
- d.node.setAttribute('r', String(d.r0))
3122
+ if (d.r0 !== d.r1) {
3123
+ this._size(d.node, d.spec, d.r0 / d.r1)
3124
+ this._place(d.node, d.spec, d.cx0, d.cy0, d.r0 / d.r1)
2991
3125
  }
2992
3126
  }
2993
3127
 
@@ -3015,23 +3149,16 @@ export default class Unit {
3015
3149
  ? easeInOutCubic
3016
3150
  : easeOutCubic
3017
3151
 
3018
- // Cancel a gather loop still running from a previous render before starting
3019
- // a new one, so we don't keep animating the old (detached) dots on a rapid
3020
- // update.
3021
- if (this.w.globals.unitGatherRAF != null) {
3022
- BrowserAPIs.cancelAnimationFrame(this.w.globals.unitGatherRAF)
3023
- this.w.globals.unitGatherRAF = null
3024
- }
3152
+ // draw() already cancelled the previous render's loop.
3025
3153
  const start = performance.now()
3026
3154
  let last = start
3027
3155
  /** @param {number} now */
3028
3156
  const stepFn = (now) => {
3029
- // Bail if the chart was destroyed mid-gather (route change / re-render):
3030
- // the dots are detached and setAttribute on them is wasted work. Matches
3031
- // Animations.animatePop.
3032
- if (this.w.globals.isDestroyed) {
3157
+ // Bail once the chart is destroyed or another chart type rendered in its
3158
+ // place (no unit draw cancelled this loop). A detached container is not
3159
+ // a bail: the loop must still settle every dot.
3160
+ if (this._stale(dots[0].node)) {
3033
3161
  this.w.globals.unitGatherRAF = null
3034
- this.w.globals.animationEnded = true
3035
3162
  return
3036
3163
  }
3037
3164
  const dt = Math.min(MAX_FRAME_STEP, Math.max(0, (now - last) / 1000))
@@ -3063,7 +3190,13 @@ export default class Unit {
3063
3190
  cx = d.cx0 + (d.x - d.cx0) * e
3064
3191
  cy = d.cy0 + (d.y - d.cy0) * e
3065
3192
  }
3066
- this._place(d.node, d.spec, cx, cy)
3193
+ // Grow/shrink marks whose size changed (bubble sizing, a re-fit).
3194
+ let sk = 1
3195
+ if (d.r0 !== d.r1) {
3196
+ sk = (d.r0 + (d.r1 - d.r0) * ec) / d.r1
3197
+ this._size(d.node, d.spec, sk)
3198
+ }
3199
+ this._place(d.node, d.spec, cx, cy, sk)
3067
3200
  // Entering dots fade in over the first stretch; moving dots stay solid.
3068
3201
  if (d.isEnter) d.node.style.opacity = String(Math.min(1, t * 2.5))
3069
3202
  // Cross-fade the fill for dots that changed group colour.
@@ -3073,30 +3206,17 @@ export default class Unit {
3073
3206
  const cb = Math.round(d._c0[2] + (d._c1[2] - d._c0[2]) * ec)
3074
3207
  d.node.setAttribute('fill', `rgb(${cr},${cg},${cb})`)
3075
3208
  }
3076
- // Grow/shrink circles whose radius changed (bubble sizing).
3077
- if (
3078
- d.spec.pk === PK_CIRCLE &&
3079
- d.r0 != null &&
3080
- d.r1 != null &&
3081
- d.r0 !== d.r1
3082
- ) {
3083
- d.node.setAttribute('r', String(d.r0 + (d.r1 - d.r0) * ec))
3084
- }
3085
3209
  if (t < 1) done = false
3086
3210
  }
3087
3211
  if (done) {
3088
3212
  for (let k = 0; k < n; k++) {
3089
3213
  const d = dots[k]
3090
3214
  d.node.style.opacity = ''
3091
- // Settle on the exact target colour + radius (undo rounding drift).
3215
+ // Settle on the exact target colour + size (undo rounding drift).
3092
3216
  if (d._c1 && d.fill1) d.node.setAttribute('fill', d.fill1)
3093
- if (
3094
- d.spec.pk === PK_CIRCLE &&
3095
- d.r0 != null &&
3096
- d.r1 != null &&
3097
- d.r0 !== d.r1
3098
- ) {
3099
- d.node.setAttribute('r', String(d.r1))
3217
+ if (d.r0 !== d.r1) {
3218
+ this._size(d.node, d.spec, 1)
3219
+ this._place(d.node, d.spec, d.x, d.y)
3100
3220
  }
3101
3221
  }
3102
3222
  this.w.globals.unitGatherRAF = null
@@ -3126,7 +3246,8 @@ export default class Unit {
3126
3246
  /** @type {{x:number,y:number,fill:string,r?:number,spec?:any}[]} */
3127
3247
  const exits = []
3128
3248
  for (const [key, slot] of prev) {
3129
- if (!nextPrev.has(key)) {
3249
+ // A tile label ('lbl:') has no mark to leave as a ghost.
3250
+ if (!nextPrev.has(key) && key[0] !== 'l') {
3130
3251
  exits.push(slot)
3131
3252
  if (exits.length >= cap) break
3132
3253
  }
@@ -3168,7 +3289,7 @@ export default class Unit {
3168
3289
  // slot; without it every pictogram would ghost out as a plain circle.
3169
3290
  const r = slot.r != null ? slot.r : dotR
3170
3291
  const spec = slot.spec || this._baseSpec(opts, r)
3171
- const el = this._drawDot(graphics, opts, r, slot.fill, 0, 0, spec)
3292
+ const el = this._drawDot(graphics, opts, r, slot.fill, -1, 0, spec)
3172
3293
  el.node.classList.add('apexcharts-unit-exit')
3173
3294
  this._place(el.node, spec, slot.x, slot.y)
3174
3295
  group.add(el)
@@ -3182,18 +3303,12 @@ export default class Unit {
3182
3303
  }
3183
3304
 
3184
3305
  const speed = Math.max(1, w.config.chart.animations.speed || 800)
3185
- // Cancel an exit loop still running from a previous render before starting a
3186
- // new one, so stale ghosts don't keep fading on a rapid update.
3187
- if (this.w.globals.unitExitRAF != null) {
3188
- BrowserAPIs.cancelAnimationFrame(this.w.globals.unitExitRAF)
3189
- this.w.globals.unitExitRAF = null
3190
- }
3191
3306
  const start = performance.now()
3192
3307
 
3193
3308
  /** @param {number} now */
3194
3309
  const stepFn = (now) => {
3195
- // Bail if the chart was destroyed mid-exit (the ghost group is detached).
3196
- if (this.w.globals.isDestroyed) {
3310
+ // Bail once the chart is destroyed or replaced (see _runGather).
3311
+ if (this._stale(group.node)) {
3197
3312
  this.w.globals.unitExitRAF = null
3198
3313
  return
3199
3314
  }
@@ -3281,6 +3396,7 @@ export default class Unit {
3281
3396
  const width = measureLabelWidth(w, lines, {
3282
3397
  fontSize: cfg.fontSize,
3283
3398
  fontFamily: cfg.fontFamily || w.config.chart.fontFamily,
3399
+ fontWeight: cfg.fontWeight,
3284
3400
  })
3285
3401
  const gap = conn.gap != null ? conn.gap : 8
3286
3402
  const length = conn.length != null ? conn.length : 22
@@ -3456,6 +3572,7 @@ export default class Unit {
3456
3572
  * the blob in either direction.
3457
3573
  * @param {any} elSeries @param {{ i:number, cx:number, cy:number, outerR:number, flat?:boolean }} cluster
3458
3574
  * @param {number} value @param {number} total @param {any} opts @param {string} color
3575
+ * @returns {SVGElement|undefined}
3459
3576
  */
3460
3577
  _drawClusterLabel(elSeries, cluster, value, total, opts, color) {
3461
3578
  const w = this.w
@@ -3541,5 +3658,6 @@ export default class Unit {
3541
3658
  }
3542
3659
 
3543
3660
  elSeries.node.appendChild(textEl)
3661
+ return /** @type {any} */ (textEl)
3544
3662
  }
3545
3663
  }