apexcharts 7.2.0 → 7.4.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/LICENSE +30 -11
- package/dist/apexcharts.common.js +2 -2
- package/dist/apexcharts.core.js +545 -463
- package/dist/apexcharts.core.min.js +2 -2
- package/dist/apexcharts.esm.js +1149 -801
- package/dist/apexcharts.js +684 -254
- package/dist/apexcharts.min.js +2 -2
- package/dist/apexcharts.ssr.common.js +2 -2
- package/dist/apexcharts.ssr.esm.js +684 -254
- package/dist/area.common.js +1 -1
- package/dist/area.esm.js +1 -1
- package/dist/bar.common.js +2 -2
- package/dist/bar.esm.js +20 -10
- package/dist/bar.js +2 -2
- package/dist/boxPlot.common.js +2 -2
- package/dist/boxPlot.esm.js +20 -10
- package/dist/bubble.common.js +1 -1
- package/dist/bubble.esm.js +1 -1
- package/dist/candlestick.common.js +2 -2
- package/dist/candlestick.esm.js +20 -10
- package/dist/candlestick.js +2 -2
- package/dist/column.common.js +2 -2
- package/dist/column.esm.js +20 -10
- package/dist/core.common.js +2 -2
- package/dist/core.esm.js +545 -463
- 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 +20 -10
- package/dist/features/all.common.js +2 -2
- package/dist/features/all.esm.js +364 -26
- 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 +1 -1
- package/dist/features/drilldown.esm.js +1 -1
- package/dist/features/drilldown.js +1 -1
- 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 +2 -2
- package/dist/features/exports.esm.js +12 -14
- package/dist/features/exports.js +2 -2
- 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/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 +14 -1
- package/dist/features/keyboard.js +2 -2
- package/dist/features/legend.common.js +1 -1
- package/dist/features/legend.esm.js +1 -1
- package/dist/features/legend.js +1 -1
- package/dist/features/link.common.js +1 -1
- package/dist/features/link.esm.js +38 -38
- 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 +11 -11
- 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 +1 -1
- package/dist/features/renderer-canvas.esm.js +1 -1
- package/dist/features/renderer-canvas.js +1 -1
- 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 +11 -11
- package/dist/features/storyboard.js +1 -1
- package/dist/features/streamgraph.common.js +1 -1
- package/dist/features/streamgraph.esm.js +1 -1
- package/dist/features/streamgraph.js +1 -1
- package/dist/features/toolbar.common.js +2 -2
- package/dist/features/toolbar.esm.js +172 -25
- package/dist/features/toolbar.js +2 -2
- package/dist/features/trellis.common.js +1 -1
- package/dist/features/trellis.esm.js +1 -1
- package/dist/features/trellis.js +1 -1
- 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 +2 -2
- package/dist/features/weave.esm.js +180 -2
- package/dist/features/weave.js +2 -2
- package/dist/heatmap.common.js +1 -1
- package/dist/heatmap.esm.js +1 -1
- package/dist/heatmap.js +1 -1
- package/dist/histogram.common.js +2 -2
- package/dist/histogram.esm.js +20 -10
- package/dist/line.common.js +1 -1
- package/dist/line.esm.js +1 -1
- package/dist/line.js +1 -1
- 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 +1 -1
- package/dist/radar.esm.js +1 -1
- package/dist/radar.js +1 -1
- 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 +20 -10
- package/dist/rangeArea.common.js +1 -1
- package/dist/rangeArea.esm.js +1 -1
- package/dist/rangeBar.common.js +2 -2
- package/dist/rangeBar.esm.js +20 -10
- package/dist/scatter.common.js +1 -1
- package/dist/scatter.esm.js +1 -1
- package/dist/streamgraph.common.js +1 -1
- package/dist/streamgraph.esm.js +1 -1
- package/dist/sunburst.common.js +1 -1
- package/dist/sunburst.esm.js +1 -1
- package/dist/sunburst.js +1 -1
- package/dist/treemap.common.js +1 -1
- package/dist/treemap.esm.js +1 -1
- package/dist/treemap.js +1 -1
- 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 +1 -1
- package/dist/unit.esm.js +79 -79
- package/dist/unit.js +1 -1
- package/dist/violin.common.js +2 -2
- package/dist/violin.esm.js +20 -10
- package/dist/violin.js +2 -2
- package/dist/waterfall.common.js +2 -2
- package/dist/waterfall.esm.js +20 -10
- package/package.json +2 -2
- package/src/apexcharts.js +39 -15
- package/src/charts/common/bar/Helpers.js +31 -12
- package/src/modules/Data.js +8 -2
- package/src/modules/Exports.js +14 -12
- package/src/modules/Formatters.js +24 -1
- package/src/modules/Range.js +28 -1
- package/src/modules/Toolbar.js +112 -2
- package/src/modules/ZoomPanSelection.js +85 -7
- package/src/modules/accessibility/KeyboardNavigation.js +14 -0
- package/src/modules/axes/YAxis.js +12 -15
- package/src/modules/helpers/UpdateHelpers.js +25 -8
- package/src/modules/settings/Globals.js +5 -10
- package/src/modules/settings/Options.js +15 -0
- package/src/modules/weave/PluginAPI.js +57 -1
- package/src/modules/weave/WeaveHost.js +148 -0
- package/src/ssr/DOMShim.js +50 -0
- package/src/types/internal.d.ts +1 -1
- package/src/utils/Utils.js +6 -9
- package/types/apexcharts.d.ts +17 -0
package/src/modules/Data.js
CHANGED
|
@@ -1919,8 +1919,11 @@ export default class Data {
|
|
|
1919
1919
|
// Segregate user provided data into appropriate vars
|
|
1920
1920
|
/**
|
|
1921
1921
|
* @param {any[]} ser
|
|
1922
|
+
* @param {boolean} [overwriteInitialSeries=true] false when the caller hands
|
|
1923
|
+
* back the series the chart already has (an internal re-render), so the
|
|
1924
|
+
* baseline resetSeries() restores must not move.
|
|
1922
1925
|
*/
|
|
1923
|
-
parseData(ser) {
|
|
1926
|
+
parseData(ser, overwriteInitialSeries = true) {
|
|
1924
1927
|
const w = this.w
|
|
1925
1928
|
const cnf = w.config
|
|
1926
1929
|
const gl = w.globals
|
|
@@ -1993,7 +1996,10 @@ export default class Data {
|
|
|
1993
1996
|
// Re-cloning from cnf.series each parse would corrupt initialSeries and
|
|
1994
1997
|
// break resetZoom (it would only restore one zoom step). Instead, snapshot
|
|
1995
1998
|
// from the raw stash so initialSeries always represents the true input.
|
|
1996
|
-
if (
|
|
1999
|
+
if (!overwriteInitialSeries) {
|
|
2000
|
+
// An internal re-render replays the series the chart already has, so the
|
|
2001
|
+
// baseline it was given stays put (#5283).
|
|
2002
|
+
} else if (gl.dataReducerRawSeries && cnf.chart.dataReducer?.enabled) {
|
|
1997
2003
|
const stash = gl.dataReducerRawSeries
|
|
1998
2004
|
gl.initialSeries = ser.map((s, i) => ({
|
|
1999
2005
|
...s,
|
package/src/modules/Exports.js
CHANGED
|
@@ -998,8 +998,9 @@ class Exports {
|
|
|
998
998
|
}
|
|
999
999
|
|
|
1000
1000
|
const handleUnequalXValues = () => {
|
|
1001
|
-
|
|
1002
|
-
|
|
1001
|
+
// Map stops a plain object category named `__proto__` from writing
|
|
1002
|
+
/** @type {Map<string, {cat: any, values: string[]}>} */
|
|
1003
|
+
const byCategory = new Map()
|
|
1003
1004
|
|
|
1004
1005
|
/**
|
|
1005
1006
|
* @param {Record<string, any>} s
|
|
@@ -1020,14 +1021,13 @@ class Exports {
|
|
|
1020
1021
|
} else {
|
|
1021
1022
|
return
|
|
1022
1023
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
).fill('')
|
|
1024
|
+
const key = String(cat)
|
|
1025
|
+
let row = byCategory.get(key)
|
|
1026
|
+
if (!row) {
|
|
1027
|
+
row = { cat, values: Array(series.length).fill('') }
|
|
1028
|
+
byCategory.set(key, row)
|
|
1027
1029
|
}
|
|
1028
|
-
|
|
1029
|
-
getFormattedValue(value)
|
|
1030
|
-
categories.add(cat)
|
|
1030
|
+
row.values[sI] = getFormattedValue(value)
|
|
1031
1031
|
})
|
|
1032
1032
|
})
|
|
1033
1033
|
|
|
@@ -1035,12 +1035,14 @@ class Exports {
|
|
|
1035
1035
|
rows.push(columns.join(columnDelimiter))
|
|
1036
1036
|
}
|
|
1037
1037
|
|
|
1038
|
-
Array.from(
|
|
1038
|
+
Array.from(byCategory.keys())
|
|
1039
1039
|
.sort()
|
|
1040
|
-
.forEach((
|
|
1040
|
+
.forEach((key) => {
|
|
1041
|
+
const { cat, values } = /** @type {{cat: any, values: string[]}} */ (
|
|
1042
|
+
byCategory.get(key)
|
|
1043
|
+
)
|
|
1041
1044
|
// Join here: pushing the array would leave rows.join() to stringify
|
|
1042
1045
|
// it, which always uses a comma between category and values.
|
|
1043
|
-
const values = /** @type {Record<string,any>} */ (data)[cat]
|
|
1044
1046
|
rows.push([getFormattedCategory(cat), ...values].join(columnDelimiter))
|
|
1045
1047
|
})
|
|
1046
1048
|
}
|
|
@@ -84,6 +84,12 @@ class Formatters {
|
|
|
84
84
|
setLabelFormatters() {
|
|
85
85
|
const w = this.w
|
|
86
86
|
const fmt = w.formatters
|
|
87
|
+
const inferredNumericX =
|
|
88
|
+
w.axisFlags.isXNumeric &&
|
|
89
|
+
w.axisFlags.dataFormatXNumeric &&
|
|
90
|
+
w.config.xaxis.type !== 'datetime' &&
|
|
91
|
+
!w.config.xaxis.convertedCatToNumeric &&
|
|
92
|
+
!w.globals.isBarHorizontal
|
|
87
93
|
|
|
88
94
|
fmt.xaxisTooltipFormatter = (/** @type {any} */ val) => {
|
|
89
95
|
return this.defaultGeneralFormatter(val)
|
|
@@ -109,8 +115,16 @@ class Formatters {
|
|
|
109
115
|
if (Utils.isNumber(val)) {
|
|
110
116
|
if (
|
|
111
117
|
!w.config.xaxis.convertedCatToNumeric &&
|
|
112
|
-
w.config.xaxis.type === 'numeric'
|
|
118
|
+
(w.config.xaxis.type === 'numeric' || inferredNumericX)
|
|
113
119
|
) {
|
|
120
|
+
const ticks = w.globals.xAxisScale?.result
|
|
121
|
+
if (
|
|
122
|
+
inferredNumericX &&
|
|
123
|
+
Array.isArray(ticks) &&
|
|
124
|
+
ticks.every(Number.isInteger)
|
|
125
|
+
) {
|
|
126
|
+
return val.toFixed(0)
|
|
127
|
+
}
|
|
114
128
|
if (Utils.isNumber(w.config.xaxis.decimalsInFloat)) {
|
|
115
129
|
return val.toFixed(w.config.xaxis.decimalsInFloat)
|
|
116
130
|
} else {
|
|
@@ -145,6 +159,15 @@ class Formatters {
|
|
|
145
159
|
|
|
146
160
|
if (typeof w.config.tooltip.x.formatter === 'function') {
|
|
147
161
|
fmt.ttKeyFormatter = w.config.tooltip.x.formatter
|
|
162
|
+
} else if (
|
|
163
|
+
w.config.xaxis.labels.formatter === undefined &&
|
|
164
|
+
inferredNumericX
|
|
165
|
+
) {
|
|
166
|
+
const xLabelFormatter = /** @type {(val: any) => any} */ (
|
|
167
|
+
fmt.xLabelFormatter
|
|
168
|
+
)
|
|
169
|
+
fmt.ttKeyFormatter = (/** @type {any} */ val) =>
|
|
170
|
+
Number.isInteger(val) ? val.toFixed(0) : xLabelFormatter(val)
|
|
148
171
|
} else {
|
|
149
172
|
fmt.ttKeyFormatter = fmt.xLabelFormatter
|
|
150
173
|
}
|
package/src/modules/Range.js
CHANGED
|
@@ -740,7 +740,34 @@ class Range {
|
|
|
740
740
|
ticks = Math.round(gl.svgWidth / 150)
|
|
741
741
|
|
|
742
742
|
// no labels provided and total number of dataPoints is less than 30
|
|
743
|
-
|
|
743
|
+
const inferredNumericX =
|
|
744
|
+
this.w.axisFlags.isXNumeric &&
|
|
745
|
+
this.w.axisFlags.dataFormatXNumeric &&
|
|
746
|
+
cnf.xaxis.type !== 'datetime' &&
|
|
747
|
+
!cnf.xaxis.convertedCatToNumeric &&
|
|
748
|
+
!gl.isBarHorizontal
|
|
749
|
+
const fontSize = parseFloat(cnf.xaxis.labels.style.fontSize) || 12
|
|
750
|
+
const widestLabelLength = this.w.labelData.labels.reduce(
|
|
751
|
+
(widest, seriesLabels) =>
|
|
752
|
+
Array.isArray(seriesLabels)
|
|
753
|
+
? seriesLabels.reduce(
|
|
754
|
+
(seriesWidest, label) =>
|
|
755
|
+
Math.max(seriesWidest, String(label).length),
|
|
756
|
+
widest,
|
|
757
|
+
)
|
|
758
|
+
: widest,
|
|
759
|
+
0,
|
|
760
|
+
)
|
|
761
|
+
// Range runs before axis text is laid out, so use the conventional
|
|
762
|
+
// 0.6em average glyph width to reject one-tick-per-point when the
|
|
763
|
+
// labels cannot plausibly fit in the chart's available width.
|
|
764
|
+
const allPointLabelsFit =
|
|
765
|
+
gl.dataPoints * widestLabelLength * fontSize * 0.6 <= gl.svgWidth
|
|
766
|
+
if (
|
|
767
|
+
(cnf.xaxis.type === 'numeric' ||
|
|
768
|
+
(inferredNumericX && allPointLabelsFit)) &&
|
|
769
|
+
gl.dataPoints < 30
|
|
770
|
+
) {
|
|
744
771
|
ticks = gl.dataPoints - 1
|
|
745
772
|
}
|
|
746
773
|
|
package/src/modules/Toolbar.js
CHANGED
|
@@ -45,10 +45,60 @@ export default class Toolbar {
|
|
|
45
45
|
/** @type {HTMLElement | null} */ this.elMenu = null
|
|
46
46
|
/** @type {HTMLElement[]} */ this.elMenuItems = []
|
|
47
47
|
/** @type {any} */ this.t = null
|
|
48
|
+
/**
|
|
49
|
+
* What the last render drew purely because the chart was zoomed: the whole
|
|
50
|
+
* toolbar ('wrap'), just the reset control ('control'), or neither. Read by
|
|
51
|
+
* {@link Toolbar#clearZoomAffordance}, which has to take it down by hand.
|
|
52
|
+
*
|
|
53
|
+
* @type {'wrap' | 'control' | null}
|
|
54
|
+
*/
|
|
55
|
+
this._drawnForZoom = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether this chart's built-in ways back out of a zoom are switched on.
|
|
60
|
+
*
|
|
61
|
+
* The gate `chart.zoom.resetControl` opens, and the one the Escape shortcut
|
|
62
|
+
* reads too, so a page that says it supplies its own reset gets neither.
|
|
63
|
+
* 'auto' means "supply one when nothing else on screen can": a toolbar
|
|
64
|
+
* showing its reset tool is a way back, and anything else is not.
|
|
65
|
+
*
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
resetControlAllowed() {
|
|
69
|
+
const c = this.w.config.chart
|
|
70
|
+
if (!c.zoom || !c.zoom.enabled) return false
|
|
71
|
+
const setting =
|
|
72
|
+
c.zoom.resetControl === undefined ? 'auto' : c.zoom.resetControl
|
|
73
|
+
if (setting !== 'auto') return !!setting
|
|
74
|
+
const onScreen =
|
|
75
|
+
c.toolbar && c.toolbar.show && c.toolbar.tools && c.toolbar.tools.reset
|
|
76
|
+
return !onScreen
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Whether a reset control has to be drawn for the state the chart is in now.
|
|
81
|
+
*
|
|
82
|
+
* Only while the chart is actually zoomed, which is the whole idea: the
|
|
83
|
+
* control appears at the moment the viewer changed the view, where they are
|
|
84
|
+
* already looking, and goes again when the range does. A page that never
|
|
85
|
+
* zooms never sees it, so `toolbar: { show: false }` still means an empty
|
|
86
|
+
* chart for everyone who does not zoom.
|
|
87
|
+
*
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
resetControlDue() {
|
|
91
|
+
return !!this.w.interact.zoomed && this.resetControlAllowed()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {{ resetOnly?: boolean }} [opts] `resetOnly` draws the on-demand
|
|
96
|
+
* reset control and nothing else, for a chart whose page asked for no
|
|
97
|
+
* toolbar at all. See {@link Toolbar#resetControlDue}.
|
|
98
|
+
*/
|
|
99
|
+
createToolbar(opts = {}) {
|
|
51
100
|
const w = this.w
|
|
101
|
+
const resetOnly = !!opts.resetOnly
|
|
52
102
|
|
|
53
103
|
const createDiv = () => {
|
|
54
104
|
return BrowserAPIs.createElementNS('http://www.w3.org/1999/xhtml', 'div')
|
|
@@ -84,6 +134,32 @@ export default class Toolbar {
|
|
|
84
134
|
|
|
85
135
|
this.t = w.config.chart.toolbar.tools
|
|
86
136
|
|
|
137
|
+
this._drawnForZoom = null
|
|
138
|
+
|
|
139
|
+
if (resetOnly) {
|
|
140
|
+
// Every control off but the one that undoes the zoom. A copy, because
|
|
141
|
+
// this is the state of one render and not a change to the page's config.
|
|
142
|
+
this.t = {
|
|
143
|
+
zoom: false,
|
|
144
|
+
zoomin: false,
|
|
145
|
+
zoomout: false,
|
|
146
|
+
selection: false,
|
|
147
|
+
pan: false,
|
|
148
|
+
measure: false,
|
|
149
|
+
download: false,
|
|
150
|
+
customIcons: [],
|
|
151
|
+
reset: true,
|
|
152
|
+
}
|
|
153
|
+
this._drawnForZoom = 'wrap'
|
|
154
|
+
} else if (!this.t.reset && this.resetControlDue()) {
|
|
155
|
+
// A toolbar that is shown with its reset tool switched off, on a chart
|
|
156
|
+
// that is now zoomed: the same dead end, so the same answer. It comes and
|
|
157
|
+
// goes with the zoom, which does shift the icons beside it; that is the
|
|
158
|
+
// cost of a config which otherwise offers no way back at all.
|
|
159
|
+
this.t = { ...this.t, reset: true }
|
|
160
|
+
this._drawnForZoom = 'control'
|
|
161
|
+
}
|
|
162
|
+
|
|
87
163
|
if (Array.isArray(this.t.customIcons)) {
|
|
88
164
|
for (let i = 0; i < this.t.customIcons.length; i++) {
|
|
89
165
|
this.elCustomIcons.push(createBtn())
|
|
@@ -221,7 +297,19 @@ export default class Toolbar {
|
|
|
221
297
|
this.elMenuIcon.setAttribute('aria-expanded', 'false')
|
|
222
298
|
}
|
|
223
299
|
|
|
224
|
-
|
|
300
|
+
// Nothing opens it in a reset-only toolbar, and an export menu is not what
|
|
301
|
+
// a page that asked for no toolbar is being handed a control for.
|
|
302
|
+
if (!resetOnly) this._createHamburgerMenu(elToolbarWrap)
|
|
303
|
+
|
|
304
|
+
if (resetOnly) {
|
|
305
|
+
// Just the one binding, rather than the whole set: there is no menu to
|
|
306
|
+
// open, no mode to toggle, and nothing here reaches for the document.
|
|
307
|
+
this.elZoomReset?.addEventListener(
|
|
308
|
+
'click',
|
|
309
|
+
this.handleZoomReset.bind(this),
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
225
313
|
|
|
226
314
|
if (w.interact.zoomEnabled) {
|
|
227
315
|
this.elZoom.classList.add(this.selectedClass)
|
|
@@ -736,6 +824,27 @@ export default class Toolbar {
|
|
|
736
824
|
}
|
|
737
825
|
}
|
|
738
826
|
|
|
827
|
+
/**
|
|
828
|
+
* Take down whatever the zoom alone put on screen.
|
|
829
|
+
*
|
|
830
|
+
* By hand, and not left to the next render, because of the order in
|
|
831
|
+
* {@link Toolbar#handleZoomReset}: the re-render happens while
|
|
832
|
+
* `interact.zoomed` is still true, so the control is drawn once more and then
|
|
833
|
+
* the flag clears with no further pass to notice. Reversing that order would
|
|
834
|
+
* change what every listener downstream of the update sees, which is a much
|
|
835
|
+
* larger promise than this control is worth.
|
|
836
|
+
*/
|
|
837
|
+
clearZoomAffordance() {
|
|
838
|
+
const drawn = this._drawnForZoom
|
|
839
|
+
this._drawnForZoom = null
|
|
840
|
+
if (!drawn || !this.elZoomReset) return
|
|
841
|
+
const parent = this.elZoomReset.parentNode
|
|
842
|
+
// 'wrap' means the whole toolbar existed only to carry this one control, so
|
|
843
|
+
// the page goes back to the bare canvas it asked for.
|
|
844
|
+
const gone = drawn === 'wrap' ? parent : this.elZoomReset
|
|
845
|
+
if (gone && gone.parentNode) gone.parentNode.removeChild(gone)
|
|
846
|
+
}
|
|
847
|
+
|
|
739
848
|
handleZoomReset() {
|
|
740
849
|
const charts = this.ctx.getSyncedCharts()
|
|
741
850
|
|
|
@@ -784,6 +893,7 @@ export default class Toolbar {
|
|
|
784
893
|
)
|
|
785
894
|
|
|
786
895
|
w.interact.zoomed = false
|
|
896
|
+
ch.ctx.toolbar?.clearZoomAffordance()
|
|
787
897
|
})
|
|
788
898
|
}
|
|
789
899
|
|
|
@@ -138,6 +138,16 @@ export default class ZoomPanSelection extends Toolbar {
|
|
|
138
138
|
})
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// Escape: the way back for the viewer who never goes looking for a button.
|
|
142
|
+
// Bound on the node the gestures are on, which the chart focuses on any
|
|
143
|
+
// pointer down inside it, so the drag that zoomed also left Escape live.
|
|
144
|
+
// Bound unconditionally and resolved per event, because the gate reads
|
|
145
|
+
// config a page can change under a live chart.
|
|
146
|
+
this.hoverArea.addEventListener('keydown', me.escapeResetEvent.bind(me), {
|
|
147
|
+
capture: false,
|
|
148
|
+
passive: true,
|
|
149
|
+
})
|
|
150
|
+
|
|
141
151
|
// Momentum: passive:false touch listeners (following the wheel-listener
|
|
142
152
|
// template above) so two-finger pinch and horizontal pan can preventDefault
|
|
143
153
|
// the browser's native page zoom/scroll. Every _updateOptions destroys and
|
|
@@ -348,12 +358,16 @@ export default class ZoomPanSelection extends Toolbar {
|
|
|
348
358
|
/**
|
|
349
359
|
* A wheel or pinch zoom is an incidental gesture: the viewer can land in a
|
|
350
360
|
* zoomed window without meaning to (a page scroll over the chart, a two-finger
|
|
351
|
-
* swipe), so it is only offered when there is a way back out of it.
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
361
|
+
* swipe), so it is only offered when there is a way back out of it. 'auto'
|
|
362
|
+
* (the default for both allowMouseWheelZoom and pinch) resolves against a
|
|
363
|
+
* reset button that is already on screen. A page that builds its own reset
|
|
364
|
+
* control sets the option to true and gets the gesture with no toolbar.
|
|
365
|
+
* Drag-to-zoom is deliberate, so it is not gated this way.
|
|
366
|
+
*
|
|
367
|
+
* `chart.zoom.resetControl` supplies a reset of its own once a chart IS
|
|
368
|
+
* zoomed, and deliberately does NOT open this gate. It arrives after the
|
|
369
|
+
* fact, and what an incidental wheel zoom takes from the viewer first is the
|
|
370
|
+
* page scroll it swallowed, which no button hands back.
|
|
357
371
|
*
|
|
358
372
|
* @param {boolean|'auto'} setting
|
|
359
373
|
*/
|
|
@@ -369,6 +383,61 @@ export default class ZoomPanSelection extends Toolbar {
|
|
|
369
383
|
return this._incidentalZoomEnabled(zoom && zoom.allowMouseWheelZoom)
|
|
370
384
|
}
|
|
371
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Put keyboard focus on the chart, where the chart is focusable at all.
|
|
388
|
+
*
|
|
389
|
+
* A drag is swallowed by the zoom handlers before the browser can move focus,
|
|
390
|
+
* so a viewer who has just zoomed by hand leaves nothing focused, and every
|
|
391
|
+
* key the chart offers is out of reach: Escape to reset (see
|
|
392
|
+
* {@link ZoomPanSelection#escapeResetEvent}) and the +, - and 0 the keyboard
|
|
393
|
+
* module already binds. Focusing what they just acted on puts those in reach.
|
|
394
|
+
*
|
|
395
|
+
* Pointer-driven focus, which keyboard navigation expects and does not read
|
|
396
|
+
* as a request to start navigating, and which the stylesheet draws no ring
|
|
397
|
+
* around (`svg:focus:not(:focus-visible)`).
|
|
398
|
+
*
|
|
399
|
+
* Only where the accessibility module has made the SVG focusable, which is
|
|
400
|
+
* the default: a page that turned keyboard support off is not handed a tab
|
|
401
|
+
* stop it never asked for, and still has the reset control as its way back.
|
|
402
|
+
*/
|
|
403
|
+
_focusForKeyboard() {
|
|
404
|
+
const node = this.w.dom.Paper && this.w.dom.Paper.node
|
|
405
|
+
if (!node || typeof node.focus !== 'function') return
|
|
406
|
+
if (node.getAttribute('tabindex') === null) return
|
|
407
|
+
// Said before the focus lands, or the accessibility module reads it as a
|
|
408
|
+
// request to start navigating and flashes a tooltip after every zoom.
|
|
409
|
+
this.ctx.keyboardNavigation?.notePointerFocus?.()
|
|
410
|
+
try {
|
|
411
|
+
node.focus({ preventScroll: true })
|
|
412
|
+
} catch {
|
|
413
|
+
node.focus()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Escape, on a zoomed chart, puts the range back.
|
|
419
|
+
*
|
|
420
|
+
* The quiet half of the same answer the on-demand reset control gives, and
|
|
421
|
+
* gated on it, so a page that says it supplies its own way back gets neither.
|
|
422
|
+
*
|
|
423
|
+
* It defers to keyboard navigation, whose Escape dismisses the tooltip and
|
|
424
|
+
* which offers `0` for this, so the key means one thing at a time. It does
|
|
425
|
+
* not stop the event either, so a page listening for Escape still hears it.
|
|
426
|
+
*
|
|
427
|
+
* Reachable because a completed drag-zoom puts focus on the chart; see
|
|
428
|
+
* {@link ZoomPanSelection#_focusForKeyboard}.
|
|
429
|
+
*
|
|
430
|
+
* @param {any} e
|
|
431
|
+
*/
|
|
432
|
+
escapeResetEvent(e) {
|
|
433
|
+
if (e.key !== 'Escape' && e.key !== 'Esc') return
|
|
434
|
+
if (!this.w.interact.zoomed) return
|
|
435
|
+
const nav = this.ctx.keyboardNavigation
|
|
436
|
+
if (nav && nav.active) return
|
|
437
|
+
if (!this.resetControlAllowed()) return
|
|
438
|
+
this.handleZoomReset()
|
|
439
|
+
}
|
|
440
|
+
|
|
372
441
|
/** Lazily-created, re-render-surviving wheel-gesture state. */
|
|
373
442
|
_wheel() {
|
|
374
443
|
const it = this.w.interact
|
|
@@ -1071,12 +1140,21 @@ export default class ZoomPanSelection extends Toolbar {
|
|
|
1071
1140
|
// fix issue #650
|
|
1072
1141
|
options.yaxis = yaxis
|
|
1073
1142
|
}
|
|
1074
|
-
me.ctx.updateHelpers._updateOptions(
|
|
1143
|
+
const applied = me.ctx.updateHelpers._updateOptions(
|
|
1075
1144
|
options,
|
|
1076
1145
|
false,
|
|
1077
1146
|
me.w.config.chart.animations.dynamicAnimation.enabled,
|
|
1078
1147
|
)
|
|
1079
1148
|
|
|
1149
|
+
// After the render, not before it: the update replaces the SVG, and
|
|
1150
|
+
// focus set on the node that is about to be thrown away goes back to
|
|
1151
|
+
// the document body with it. Measured, not theorised.
|
|
1152
|
+
if (applied && typeof applied.then === 'function') {
|
|
1153
|
+
applied.then(() => me._focusForKeyboard())
|
|
1154
|
+
} else {
|
|
1155
|
+
me._focusForKeyboard()
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1080
1158
|
if (typeof w.config.chart.events.zoomed === 'function') {
|
|
1081
1159
|
toolbar.zoomCallback(xaxis, yaxis)
|
|
1082
1160
|
}
|
|
@@ -147,6 +147,20 @@ export default class KeyboardNavigation {
|
|
|
147
147
|
this._lastPointerDownAt = Date.now()
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Note that a pointer gesture is about to move focus into the chart.
|
|
152
|
+
*
|
|
153
|
+
* The 100 ms window above catches the browser's own click-to-focus, which
|
|
154
|
+
* lands immediately. A drag-zoom moves focus deliberately, and only once its
|
|
155
|
+
* re-render is done (ZoomPanSelection#_focusForKeyboard), which is far
|
|
156
|
+
* outside that window. Without this it reads as a viewer asking to navigate
|
|
157
|
+
* by keyboard, which activates nav and flashes a tooltip at the first visible
|
|
158
|
+
* point after every zoom.
|
|
159
|
+
*/
|
|
160
|
+
notePointerFocus() {
|
|
161
|
+
this._lastPointerDownAt = Date.now()
|
|
162
|
+
}
|
|
163
|
+
|
|
150
164
|
/**
|
|
151
165
|
* Called from Events.js keydown handler. Navigation keys are already handled
|
|
152
166
|
* by the direct SVG listener (which can call preventDefault). This entry
|
|
@@ -551,42 +551,39 @@ export default class YAxis {
|
|
|
551
551
|
|
|
552
552
|
setYAxisTextAlignments() {
|
|
553
553
|
const w = this.w
|
|
554
|
-
const yaxis = Array.from(
|
|
555
|
-
w.dom.baseEl.getElementsByClassName('apexcharts-yaxis'),
|
|
556
|
-
)
|
|
557
554
|
|
|
558
|
-
|
|
559
|
-
|
|
555
|
+
// Iterate config, not the DOM collection: Axes.drawAxis skips
|
|
556
|
+
// ignoreYAxisIndexes entirely, so a collapsed axis makes the DOM
|
|
557
|
+
// shorter than config.yaxis and a later visible axis would be missed.
|
|
558
|
+
w.config.yaxis.forEach((yaxe, index) => {
|
|
560
559
|
if (yaxe && !yaxe.floating && yaxe.labels.align !== undefined) {
|
|
561
560
|
const yAxisInner = w.dom.baseEl.querySelector(
|
|
562
561
|
`.apexcharts-yaxis[rel='${index}'] .apexcharts-yaxis-texts-g`,
|
|
563
562
|
)
|
|
563
|
+
// drawYaxis returns before adding .apexcharts-yaxis-texts-g when the
|
|
564
|
+
// axis is hidden, so there is nothing to align.
|
|
565
|
+
if (!yAxisInner) return
|
|
566
|
+
|
|
564
567
|
const yAxisTexts = Array.from(
|
|
565
568
|
w.dom.baseEl.querySelectorAll(
|
|
566
569
|
`.apexcharts-yaxis[rel='${index}'] .apexcharts-yaxis-label`,
|
|
567
570
|
),
|
|
568
571
|
)
|
|
569
|
-
const rect =
|
|
572
|
+
const rect = yAxisInner.getBoundingClientRect()
|
|
570
573
|
|
|
571
574
|
yAxisTexts.forEach((label) => {
|
|
572
575
|
label.setAttribute('text-anchor', yaxe.labels.align)
|
|
573
576
|
})
|
|
574
577
|
|
|
575
578
|
if (yaxe.labels.align === 'left' && !yaxe.opposite) {
|
|
576
|
-
|
|
577
|
-
'transform',
|
|
578
|
-
`translate(-${rect.width}, 0)`,
|
|
579
|
-
)
|
|
579
|
+
yAxisInner.setAttribute('transform', `translate(-${rect.width}, 0)`)
|
|
580
580
|
} else if (yaxe.labels.align === 'center') {
|
|
581
|
-
|
|
581
|
+
yAxisInner.setAttribute(
|
|
582
582
|
'transform',
|
|
583
583
|
`translate(${(rect.width / 2) * (!yaxe.opposite ? -1 : 1)}, 0)`,
|
|
584
584
|
)
|
|
585
585
|
} else if (yaxe.labels.align === 'right' && yaxe.opposite) {
|
|
586
|
-
|
|
587
|
-
'transform',
|
|
588
|
-
`translate(${rect.width}, 0)`,
|
|
589
|
-
)
|
|
586
|
+
yAxisInner.setAttribute('transform', `translate(${rect.width}, 0)`)
|
|
590
587
|
}
|
|
591
588
|
}
|
|
592
589
|
})
|
|
@@ -180,9 +180,10 @@ export default class UpdateHelpers {
|
|
|
180
180
|
? prevInitialSeries
|
|
181
181
|
: Utils.copySeriesShallow(w.config.series)
|
|
182
182
|
w.globals.initialConfig = initialConfig
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
if (options.series) {
|
|
184
|
+
// lazy snapshot: deep clone deferred to first read
|
|
185
|
+
w.globals.initialSeries = w.config.series
|
|
186
|
+
}
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// Keep legend-hidden (collapsed) series hidden across ANY series
|
|
@@ -277,6 +278,9 @@ export default class UpdateHelpers {
|
|
|
277
278
|
w.globals.treemapRawSeries = null
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
const definedSeries = newSeries
|
|
282
|
+
let reconciledCollapses = false
|
|
283
|
+
|
|
280
284
|
// Keep legend-hidden (collapsed) series hidden across a data update,
|
|
281
285
|
// reconciled BY CATEGORY NAME by the same code the options path uses: a
|
|
282
286
|
// name still present stays hidden at its possibly-new index, one the
|
|
@@ -299,11 +303,15 @@ export default class UpdateHelpers {
|
|
|
299
303
|
s && typeof s === 'object' ? { ...s } : s,
|
|
300
304
|
)
|
|
301
305
|
this.ctx.series.reconcileCollapsedByName(newSeries)
|
|
306
|
+
reconciledCollapses = true
|
|
302
307
|
}
|
|
303
308
|
|
|
304
309
|
this.ctx.data.resetParsingFlags()
|
|
305
310
|
// Phase 1: return value captured; writer stubs are no-ops.
|
|
306
|
-
const parsedState = this.ctx.data.parseData(
|
|
311
|
+
const parsedState = this.ctx.data.parseData(
|
|
312
|
+
newSeries,
|
|
313
|
+
overwriteInitialSeries,
|
|
314
|
+
)
|
|
307
315
|
this.ctx._writeParsedSeriesData(parsedState.seriesData)
|
|
308
316
|
this.ctx._writeParsedRangeData(parsedState.rangeData)
|
|
309
317
|
this.ctx._writeParsedCandleData(parsedState.candleData)
|
|
@@ -315,14 +323,23 @@ export default class UpdateHelpers {
|
|
|
315
323
|
// defined. A copy, never the live array: this site used to assign
|
|
316
324
|
// w.config.series itself, restoring the very alias #5118 came from, so
|
|
317
325
|
// one updateSeries() would undo the capture Globals.init now makes.
|
|
318
|
-
//
|
|
319
|
-
//
|
|
326
|
+
// Both snapshots come off the array the caller handed in once the
|
|
327
|
+
// reconcile above has run, since that emptied the collapsed rows and
|
|
328
|
+
// parseData baselines from what it parses: the baseline would forget
|
|
329
|
+
// the data of exactly the series the viewer switched off.
|
|
330
|
+
// _updateOptions gets this for free, it reconciles after capturing.
|
|
320
331
|
if (w.globals.initialConfig) {
|
|
321
332
|
w.globals.initialConfig.series = Utils.copySeriesShallow(
|
|
322
|
-
w.config.series,
|
|
333
|
+
reconciledCollapses ? definedSeries : w.config.series,
|
|
323
334
|
)
|
|
324
335
|
}
|
|
325
|
-
|
|
336
|
+
// With no reconcile, initialSeries stays as parseData left it: on the
|
|
337
|
+
// raw-stash and dataReducer types config.series holds rows this library
|
|
338
|
+
// derived, and reassigning it here made the baseline the bin counts,
|
|
339
|
+
// the accumulated pairs or the downsampled window (#5283).
|
|
340
|
+
if (reconciledCollapses) {
|
|
341
|
+
w.globals.initialSeries = definedSeries
|
|
342
|
+
}
|
|
326
343
|
}
|
|
327
344
|
|
|
328
345
|
// Use the fast path when the series structure is compatible:
|
|
@@ -554,16 +554,11 @@ export default class Globals {
|
|
|
554
554
|
* which cannot reach the captured copies because each series object was
|
|
555
555
|
* copied at capture time.
|
|
556
556
|
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* case is not honoured for a separate, older reason: Data.parseData()
|
|
563
|
-
* re-captures initialSeries unconditionally on the re-render appendData
|
|
564
|
-
* triggers. Detaching would mean copying the data arrays, which is exactly
|
|
565
|
-
* the per-point cost this snapshot exists to avoid. The same exception
|
|
566
|
-
* applies to `initialConfig.series`, which is captured with the same shape.
|
|
557
|
+
* appendData() used to be the exception, growing the shared data array in
|
|
558
|
+
* place. It now replaces the array instead (`data = data.concat(newData)`):
|
|
559
|
+
* one array copy per call, against the per-point cost this snapshot exists
|
|
560
|
+
* to avoid. With that, no internal edit can reach a captured copy. The same
|
|
561
|
+
* holds for `initialConfig.series`, which is captured with the same shape.
|
|
567
562
|
*
|
|
568
563
|
* @param {Record<string, any>} globals
|
|
569
564
|
*/
|
|
@@ -769,6 +769,17 @@ export default class Options {
|
|
|
769
769
|
// around the pinch centroid (matching the x-only wheel/toolbar zoom),
|
|
770
770
|
// frame-by-frame rather than the 400ms wheel throttle.
|
|
771
771
|
pinch: 'auto',
|
|
772
|
+
// Drag-to-zoom is deliberate, so it is not gated the way those two
|
|
773
|
+
// are: it stays on with the toolbar hidden. The reset button that
|
|
774
|
+
// undoes it does not, which leaves the viewer in a window with no
|
|
775
|
+
// exit. So while the chart IS zoomed and nothing else on screen can
|
|
776
|
+
// reset it, one reset control is drawn where the toolbar would have
|
|
777
|
+
// been, and it goes when the range does. A chart nobody zooms is
|
|
778
|
+
// untouched. Set false for a page that supplies its own reset (which
|
|
779
|
+
// also turns off the Escape shortcut), or true to force the control
|
|
780
|
+
// on whenever the chart is zoomed, even where `toolbar.tools.reset`
|
|
781
|
+
// is off.
|
|
782
|
+
resetControl: 'auto',
|
|
772
783
|
zoomedArea: {
|
|
773
784
|
fill: {
|
|
774
785
|
color: '#90CAF9',
|
|
@@ -832,6 +843,10 @@ export default class Options {
|
|
|
832
843
|
distributed: false,
|
|
833
844
|
borderRadius: 0,
|
|
834
845
|
borderRadiusApplication: 'around', // [around, end]
|
|
846
|
+
// Which segments of a stack round. 'all' caps both ends of the
|
|
847
|
+
// stack (baseline and far end); 'last' caps only the far end, so
|
|
848
|
+
// the stack sits square on the baseline. Stacked charts only.
|
|
849
|
+
borderRadiusWhenStacked: 'all', // [all, last]
|
|
835
850
|
rangeBarOverlap: true,
|
|
836
851
|
rangeBarGroupRows: false,
|
|
837
852
|
hideZeroBarsWhenGrouped: false,
|