apexcharts 7.3.0 → 7.5.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 +39 -11
- package/dist/apexcharts.common.js +2 -2
- package/dist/apexcharts.core.js +579 -467
- package/dist/apexcharts.core.min.js +2 -2
- package/dist/apexcharts.esm.js +1347 -810
- package/dist/apexcharts.js +896 -267
- package/dist/apexcharts.min.js +2 -2
- package/dist/apexcharts.ssr.common.js +2 -2
- package/dist/apexcharts.ssr.esm.js +896 -267
- 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 +579 -467
- 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 +536 -34
- 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 +352 -10
- 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 +2 -2
- package/dist/radialBar.esm.js +27 -2
- package/dist/radialBar.js +2 -2
- 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/Radial.js +12 -3
- package/src/charts/common/bar/Helpers.js +31 -12
- package/src/modules/Data.js +8 -2
- package/src/modules/DataLabels.js +14 -4
- package/src/modules/Exports.js +14 -12
- package/src/modules/Formatters.js +24 -1
- package/src/modules/Graphics.js +10 -0
- 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/Claims.js +231 -0
- package/src/modules/weave/Drawn.js +169 -0
- package/src/modules/weave/PluginAPI.js +195 -4
- package/src/modules/weave/WeaveHost.js +76 -3
- 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 +92 -0
|
@@ -560,6 +560,14 @@ export default class Helpers {
|
|
|
560
560
|
* second at the top, and nothing else touched. Stacked totals already resolve
|
|
561
561
|
* per group (see drawsStackedTotal, #4173); corners never got the same fix.
|
|
562
562
|
*
|
|
563
|
+
* plotOptions.bar.borderRadiusWhenStacked picks which ends round: 'all'
|
|
564
|
+
* caps both the baseline and the far end, 'last' only the far end (#4845).
|
|
565
|
+
*
|
|
566
|
+
* A single-data-point chart used to hand its baseline segment 'top' (and a
|
|
567
|
+
* solo segment 'top' instead of 'both'), which on a horizontal stack rounded
|
|
568
|
+
* the first segment on its INNER edge and left a pill in the middle of the
|
|
569
|
+
* bar (#4845). One data point is resolved like any other now.
|
|
570
|
+
*
|
|
563
571
|
* @param {any[]} series
|
|
564
572
|
* @returns {string[][]}
|
|
565
573
|
*/
|
|
@@ -577,12 +585,17 @@ export default class Helpers {
|
|
|
577
585
|
|
|
578
586
|
if (alwaysApplyRadius) return output
|
|
579
587
|
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
588
|
+
// 'last' rounds only the far end of a stack and leaves the baseline end
|
|
589
|
+
// square (#4845); 'all' caps both ends. Corner names are screen-side:
|
|
590
|
+
// for a positive segment the far end is 'top', for a negative one it is
|
|
591
|
+
// 'bottom', so a lone positive drops from 'both' to 'top' and a lone
|
|
592
|
+
// negative from 'both' to 'bottom'.
|
|
593
|
+
const lastOnly =
|
|
594
|
+
w.config.plotOptions.bar.borderRadiusWhenStacked === 'last'
|
|
595
|
+
const soloPositive = lastOnly ? 'top' : 'both'
|
|
596
|
+
const soloNegative = lastOnly ? 'bottom' : 'both'
|
|
597
|
+
const positiveBase = lastOnly ? 'none' : 'bottom'
|
|
598
|
+
const negativeBase = lastOnly ? 'none' : 'top'
|
|
586
599
|
|
|
587
600
|
for (const stack of this.getStackedSeriesIndices(numSeries)) {
|
|
588
601
|
for (let j = 0; j < numColumns; j++) {
|
|
@@ -599,24 +612,30 @@ export default class Helpers {
|
|
|
599
612
|
|
|
600
613
|
if (positiveIndices.length > 0 && negativeIndices.length === 0) {
|
|
601
614
|
if (positiveIndices.length === 1) {
|
|
602
|
-
output[positiveIndices[0]][j] =
|
|
615
|
+
output[positiveIndices[0]][j] = soloPositive
|
|
603
616
|
} else {
|
|
604
617
|
const first = positiveIndices[0]
|
|
605
618
|
const last = positiveIndices[positiveIndices.length - 1]
|
|
606
619
|
for (const i of positiveIndices) {
|
|
607
620
|
output[i][j] =
|
|
608
|
-
i === first ?
|
|
621
|
+
i === first ? positiveBase : i === last ? 'top' : 'none'
|
|
609
622
|
}
|
|
610
623
|
}
|
|
611
624
|
} else if (negativeIndices.length > 0 && positiveIndices.length === 0) {
|
|
612
625
|
if (negativeIndices.length === 1) {
|
|
613
|
-
output[negativeIndices[0]][j] =
|
|
626
|
+
output[negativeIndices[0]][j] = soloNegative
|
|
614
627
|
} else {
|
|
615
|
-
|
|
616
|
-
|
|
628
|
+
// Negatives stack away from the axis in series order, so the
|
|
629
|
+
// first is at the baseline and the last is the far end.
|
|
630
|
+
const nearest = Math.min(...negativeIndices)
|
|
631
|
+
const farthest = Math.max(...negativeIndices)
|
|
617
632
|
for (const i of negativeIndices) {
|
|
618
633
|
output[i][j] =
|
|
619
|
-
i ===
|
|
634
|
+
i === nearest
|
|
635
|
+
? negativeBase
|
|
636
|
+
: i === farthest
|
|
637
|
+
? 'bottom'
|
|
638
|
+
: 'none'
|
|
620
639
|
}
|
|
621
640
|
}
|
|
622
641
|
} else if (positiveIndices.length > 0 && negativeIndices.length > 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,
|
|
@@ -4,6 +4,7 @@ import Graphics from './Graphics'
|
|
|
4
4
|
import Filters from './Filters'
|
|
5
5
|
import { applyProgressiveReveal } from './Animations'
|
|
6
6
|
import { resolveDataLabelOffset } from './helpers/DataLabelOffset'
|
|
7
|
+
import { resolveClaimed } from './weave/Claims'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* ApexCharts DataLabels Class for drawing dataLabels on Axes based Charts.
|
|
@@ -247,10 +248,19 @@ class DataLabels {
|
|
|
247
248
|
} = opts
|
|
248
249
|
|
|
249
250
|
let dataLabelText = null
|
|
250
|
-
if
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
// The caller's answer for this series, then the plugin's if one claimed it.
|
|
252
|
+
// A claim is a boolean rather than a restatement of the caller's list
|
|
253
|
+
// shape, because "does THIS series print labels" is the question here.
|
|
254
|
+
const labelsOn = resolveClaimed(
|
|
255
|
+
w,
|
|
256
|
+
'dataLabels.enabledOnSeries',
|
|
257
|
+
i,
|
|
258
|
+
Array.isArray(w.config.dataLabels.enabledOnSeries)
|
|
259
|
+
? w.config.dataLabels.enabledOnSeries.indexOf(i) >= 0
|
|
260
|
+
: true,
|
|
261
|
+
)
|
|
262
|
+
if (!labelsOn) {
|
|
263
|
+
return dataLabelText
|
|
254
264
|
}
|
|
255
265
|
|
|
256
266
|
let correctedLabels = {
|
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/Graphics.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Animations from './Animations'
|
|
3
3
|
import Filters from './Filters'
|
|
4
4
|
import Utils from '../utils/Utils'
|
|
5
|
+
import { resolveClaimed } from './weave/Claims'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ApexCharts Graphics Class for all drawing operations.
|
|
@@ -609,6 +610,15 @@ class Graphics {
|
|
|
609
610
|
} else {
|
|
610
611
|
strokeDashArray = w.config.stroke.dashArray
|
|
611
612
|
}
|
|
613
|
+
// A plugin may have claimed this series' dash. Resolved here rather than
|
|
614
|
+
// written into the config above, so the caller's own dashing is untouched
|
|
615
|
+
// and there is nothing to restore when the claim goes. See weave/Claims.
|
|
616
|
+
strokeDashArray = resolveClaimed(
|
|
617
|
+
w,
|
|
618
|
+
'stroke.dashArray',
|
|
619
|
+
realIndex,
|
|
620
|
+
strokeDashArray,
|
|
621
|
+
)
|
|
612
622
|
|
|
613
623
|
const el = this.drawPath({
|
|
614
624
|
d,
|
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
|
})
|