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.
- package/README.md +7 -7
- package/dist/apexcharts.common.js +2 -2
- package/dist/apexcharts.core.js +271 -173
- package/dist/apexcharts.core.min.js +2 -2
- package/dist/apexcharts.css +35 -0
- package/dist/apexcharts.esm.js +1722 -1527
- package/dist/apexcharts.full.js +1459 -1075
- package/dist/apexcharts.full.min.js +2 -2
- package/dist/apexcharts.js +994 -715
- package/dist/apexcharts.min.js +2 -2
- package/dist/apexcharts.ssr.common.js +2 -2
- package/dist/apexcharts.ssr.esm.js +994 -715
- package/dist/area.common.js +2 -2
- package/dist/area.esm.js +91 -40
- package/dist/bar.common.js +2 -2
- package/dist/bar.esm.js +433 -394
- package/dist/bar.js +2 -2
- package/dist/boxPlot.common.js +2 -2
- package/dist/boxPlot.esm.js +364 -322
- package/dist/bubble.common.js +2 -2
- package/dist/bubble.esm.js +91 -40
- package/dist/candlestick.common.js +2 -2
- package/dist/candlestick.esm.js +364 -322
- package/dist/candlestick.js +2 -2
- package/dist/column.common.js +2 -2
- package/dist/column.esm.js +433 -394
- package/dist/core.common.js +2 -2
- package/dist/core.esm.js +271 -173
- package/dist/donut.common.js +1 -1
- package/dist/donut.esm.js +1 -1
- package/dist/dumbbell.common.js +2 -2
- package/dist/dumbbell.esm.js +433 -394
- package/dist/features/all.common.js +2 -2
- package/dist/features/all.esm.js +250 -187
- package/dist/features/annotations.common.js +1 -1
- package/dist/features/annotations.esm.js +1 -1
- package/dist/features/annotations.js +1 -1
- package/dist/features/context-menu.common.js +1 -1
- package/dist/features/context-menu.esm.js +1 -1
- package/dist/features/context-menu.js +1 -1
- package/dist/features/drilldown.common.js +2 -2
- package/dist/features/drilldown.esm.js +4 -2
- package/dist/features/drilldown.js +2 -2
- package/dist/features/dumbbell.common.js +1 -1
- package/dist/features/dumbbell.esm.js +1 -1
- package/dist/features/dumbbell.js +1 -1
- package/dist/features/exports.common.js +1 -1
- package/dist/features/exports.esm.js +1 -1
- package/dist/features/exports.js +1 -1
- package/dist/features/facet.common.js +1 -1
- package/dist/features/facet.esm.js +1 -1
- package/dist/features/facet.js +1 -1
- package/dist/features/highlight-filter.common.js +2 -2
- package/dist/features/highlight-filter.esm.js +14 -9
- package/dist/features/highlight-filter.js +2 -2
- package/dist/features/history.common.js +1 -1
- package/dist/features/history.esm.js +1 -1
- package/dist/features/history.js +1 -1
- package/dist/features/ink.common.js +1 -1
- package/dist/features/ink.esm.js +1 -1
- package/dist/features/ink.js +1 -1
- package/dist/features/keyboard.common.js +2 -2
- package/dist/features/keyboard.esm.js +97 -145
- package/dist/features/keyboard.js +2 -2
- package/dist/features/legend.common.js +2 -2
- package/dist/features/legend.esm.js +63 -43
- package/dist/features/legend.js +2 -2
- package/dist/features/link.common.js +1 -1
- package/dist/features/link.esm.js +1 -1
- package/dist/features/link.js +1 -1
- package/dist/features/marks.common.js +1 -1
- package/dist/features/marks.esm.js +1 -1
- package/dist/features/marks.js +1 -1
- package/dist/features/measure.common.js +1 -1
- package/dist/features/measure.esm.js +1 -1
- package/dist/features/measure.js +1 -1
- package/dist/features/morph.common.js +1 -1
- package/dist/features/morph.esm.js +1 -1
- package/dist/features/morph.js +1 -1
- package/dist/features/perspectives.common.js +1 -1
- package/dist/features/perspectives.esm.js +1 -1
- package/dist/features/perspectives.js +1 -1
- package/dist/features/raincloud.common.js +1 -1
- package/dist/features/raincloud.esm.js +1 -1
- package/dist/features/raincloud.js +1 -1
- package/dist/features/renderer-canvas.common.js +2 -2
- package/dist/features/renderer-canvas.esm.js +2 -1
- package/dist/features/renderer-canvas.js +2 -2
- package/dist/features/stats.common.js +1 -1
- package/dist/features/stats.esm.js +1 -1
- package/dist/features/stats.js +1 -1
- package/dist/features/storyboard.common.js +1 -1
- package/dist/features/storyboard.esm.js +1 -1
- package/dist/features/storyboard.js +1 -1
- package/dist/features/streamgraph.common.js +2 -2
- package/dist/features/streamgraph.esm.js +3 -2
- package/dist/features/streamgraph.js +2 -2
- package/dist/features/toolbar.common.js +2 -2
- package/dist/features/toolbar.esm.js +92 -2
- package/dist/features/toolbar.js +2 -2
- package/dist/features/trellis.common.js +2 -2
- package/dist/features/trellis.esm.js +83 -47
- package/dist/features/trellis.js +2 -2
- package/dist/features/waterfall.common.js +1 -1
- package/dist/features/waterfall.esm.js +1 -1
- package/dist/features/waterfall.js +1 -1
- package/dist/features/weave.common.js +1 -1
- package/dist/features/weave.esm.js +1 -1
- package/dist/features/weave.js +1 -1
- package/dist/full.common.js +2 -2
- package/dist/full.esm.js +950 -795
- package/dist/heatmap.common.js +2 -2
- package/dist/heatmap.esm.js +3 -17
- package/dist/heatmap.js +2 -2
- package/dist/histogram.common.js +2 -2
- package/dist/histogram.esm.js +433 -394
- package/dist/icicle.common.js +2 -2
- package/dist/icicle.esm.js +54 -38
- package/dist/icicle.js +2 -2
- package/dist/line.common.js +2 -2
- package/dist/line.esm.js +91 -40
- package/dist/line.js +2 -2
- package/dist/locales/en.json +2 -1
- package/dist/locales/ro.json +3 -2
- package/dist/pictograms.common.js +1 -1
- package/dist/pictograms.esm.js +1 -1
- package/dist/pictograms.js +1 -1
- package/dist/pie.common.js +1 -1
- package/dist/pie.esm.js +1 -1
- package/dist/pie.js +1 -1
- package/dist/polarArea.common.js +1 -1
- package/dist/polarArea.esm.js +1 -1
- package/dist/radar.common.js +2 -2
- package/dist/radar.esm.js +5 -4
- package/dist/radar.js +2 -2
- package/dist/radialBar.common.js +1 -1
- package/dist/radialBar.esm.js +1 -1
- package/dist/radialBar.js +1 -1
- package/dist/raincloud.common.js +2 -2
- package/dist/raincloud.esm.js +72 -26
- package/dist/rangeArea.common.js +2 -2
- package/dist/rangeArea.esm.js +91 -40
- package/dist/rangeBar.common.js +2 -2
- package/dist/rangeBar.esm.js +433 -394
- package/dist/scatter.common.js +2 -2
- package/dist/scatter.esm.js +91 -40
- package/dist/streamgraph.common.js +2 -2
- package/dist/streamgraph.esm.js +93 -41
- package/dist/sunburst.common.js +2 -2
- package/dist/sunburst.esm.js +103 -93
- package/dist/sunburst.js +2 -2
- package/dist/treemap.common.js +2 -2
- package/dist/treemap.esm.js +105 -86
- package/dist/treemap.js +2 -2
- package/dist/unit-shapes.common.js +1 -1
- package/dist/unit-shapes.esm.js +1 -1
- package/dist/unit-shapes.js +1 -1
- package/dist/unit.common.js +2 -2
- package/dist/unit.esm.js +280 -202
- package/dist/unit.js +2 -2
- package/dist/violin.common.js +2 -2
- package/dist/violin.esm.js +72 -26
- package/dist/violin.js +2 -2
- package/dist/waterfall.common.js +2 -2
- package/dist/waterfall.esm.js +432 -393
- package/package.json +1 -1
- package/src/apexcharts.js +13 -6
- package/src/assets/apexcharts.css +35 -0
- package/src/charts/Bar.js +62 -55
- package/src/charts/BarStacked.js +80 -88
- package/src/charts/BoxCandleStick.js +28 -31
- package/src/charts/Icicle.js +37 -37
- package/src/charts/Line.js +118 -42
- package/src/charts/Radar.js +5 -7
- package/src/charts/RangeBar.js +47 -43
- package/src/charts/Sunburst.js +101 -98
- package/src/charts/Treemap.js +108 -66
- package/src/charts/Unit.js +368 -250
- package/src/charts/Violin.js +48 -30
- package/src/charts/common/Breadcrumb.js +2 -2
- package/src/charts/common/Hierarchy.js +32 -11
- package/src/charts/common/Jitter.js +32 -7
- package/src/charts/common/bar/DataLabels.js +76 -81
- package/src/charts/common/bar/Helpers.js +43 -7
- package/src/charts/common/line/Helpers.js +12 -15
- package/src/charts/common/partition/NodeTooltip.js +8 -1
- package/src/charts/common/partition/Partition.js +33 -1
- package/src/charts/common/treemap/Helpers.js +3 -11
- package/src/locales/en.json +2 -1
- package/src/locales/ro.json +3 -2
- package/src/modules/Core.js +4 -0
- package/src/modules/CoreUtils.js +18 -42
- package/src/modules/Data.js +23 -24
- package/src/modules/DataLabels.js +6 -6
- package/src/modules/Graphics.js +62 -49
- package/src/modules/Markers.js +3 -1
- package/src/modules/Range.js +44 -29
- package/src/modules/RendererController.js +17 -3
- package/src/modules/Scales.js +32 -9
- package/src/modules/Series.js +39 -18
- package/src/modules/Theme.js +1 -1
- package/src/modules/ZoomPanSelection.js +119 -2
- package/src/modules/accessibility/KeyboardNavigation.js +114 -186
- package/src/modules/animations/LengthTransition.js +22 -2
- package/src/modules/drilldown/Breadcrumb.js +5 -2
- package/src/modules/helpers/Destroy.js +1 -1
- package/src/modules/highlightFilter/HighlightFilter.js +8 -9
- package/src/modules/highlightFilter/TreemapPart.js +2 -2
- package/src/modules/legend/HeatmapGradientLegend.js +76 -45
- package/src/modules/legend/Helpers.js +2 -0
- package/src/modules/legend/Legend.js +11 -12
- package/src/modules/settings/Options.js +7 -0
- package/src/modules/streamgraph/StreamLabels.js +1 -0
- package/src/modules/tooltip/Tooltip.js +20 -4
- package/src/modules/tooltip/Utils.js +8 -0
- package/src/modules/trellis/Trellis.js +6 -0
- package/src/modules/trellis/TrellisChrome.js +18 -7
- package/src/modules/trellis/TrellisSync.js +2 -0
- package/src/renderers/canvas/CanvasGraphics.js +1 -0
- package/src/ssr/DOMShim.js +10 -0
- package/src/types/internal.d.ts +9 -3
- package/types/apexcharts.d.ts +31 -12
package/src/charts/Unit.js
CHANGED
|
@@ -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}
|
|
132
|
-
* @property {number}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
|
931
|
-
|
|
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,
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
-
|
|
1557
|
-
|
|
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
|
|
1560
|
-
//
|
|
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
|
-
|
|
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(
|
|
1682
|
+
const p = { j, px: plotX(v), y: cy }
|
|
1567
1683
|
if (sizeStats) p.r = this._scatterRadius(d, sizeStats, r)
|
|
1568
|
-
|
|
1684
|
+
pts.push(p)
|
|
1569
1685
|
})
|
|
1570
1686
|
if (jitter) {
|
|
1571
|
-
// Deterministic pseudo-jitter
|
|
1572
|
-
//
|
|
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
|
|
1575
|
-
|
|
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
|
-
|
|
1709
|
-
const pts =
|
|
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(
|
|
1828
|
+
const p = { j, py: plotY(v), x: cx }
|
|
1713
1829
|
if (sizeStats) p.r = this._scatterRadius(d, sizeStats, r)
|
|
1714
|
-
|
|
1830
|
+
pts.push(p)
|
|
1715
1831
|
})
|
|
1716
1832
|
if (jitter) {
|
|
1717
1833
|
const halfLane = Math.max(maxR, laneW / 2 - maxR)
|
|
1718
|
-
pts.forEach((p
|
|
1719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1808
|
-
|
|
1809
|
-
const
|
|
1810
|
-
scfg.xMin
|
|
1811
|
-
scfg.xMax
|
|
1812
|
-
|
|
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
|
|
1815
|
-
scfg.yMin
|
|
1816
|
-
scfg.yMax
|
|
1817
|
-
|
|
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 =
|
|
1820
|
-
const xMax =
|
|
1821
|
-
const yMin =
|
|
1822
|
-
const yMax =
|
|
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
|
-
|
|
1848
|
-
const dots =
|
|
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
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
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:
|
|
1896
|
-
|
|
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
|
|
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 `
|
|
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 {
|
|
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(
|
|
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
|
|
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 (
|
|
2043
|
-
let min =
|
|
2044
|
-
let max =
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
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
|
|
2302
|
-
*
|
|
2303
|
-
* @param {number} v @
|
|
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
|
-
|
|
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
|
|
2394
|
-
* object's `value` / `y`. Null when there is
|
|
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
|
-
|
|
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(' +
|
|
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
|
-
|
|
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
|
-
*
|
|
2703
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
-
|
|
2782
|
-
|
|
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 =
|
|
2964
|
-
dots[k].delay =
|
|
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
|
|
2983
|
-
// previous size instead of flashing at the new one for a frame.
|
|
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.
|
|
2990
|
-
d.node.
|
|
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
|
-
//
|
|
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
|
|
3030
|
-
//
|
|
3031
|
-
//
|
|
3032
|
-
if (this.
|
|
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
|
-
|
|
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 +
|
|
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
|
|
3095
|
-
d.
|
|
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
|
-
|
|
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,
|
|
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
|
|
3196
|
-
if (this.
|
|
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
|
}
|