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.
Files changed (172) hide show
  1. package/LICENSE +30 -11
  2. package/dist/apexcharts.common.js +2 -2
  3. package/dist/apexcharts.core.js +545 -463
  4. package/dist/apexcharts.core.min.js +2 -2
  5. package/dist/apexcharts.esm.js +1149 -801
  6. package/dist/apexcharts.js +684 -254
  7. package/dist/apexcharts.min.js +2 -2
  8. package/dist/apexcharts.ssr.common.js +2 -2
  9. package/dist/apexcharts.ssr.esm.js +684 -254
  10. package/dist/area.common.js +1 -1
  11. package/dist/area.esm.js +1 -1
  12. package/dist/bar.common.js +2 -2
  13. package/dist/bar.esm.js +20 -10
  14. package/dist/bar.js +2 -2
  15. package/dist/boxPlot.common.js +2 -2
  16. package/dist/boxPlot.esm.js +20 -10
  17. package/dist/bubble.common.js +1 -1
  18. package/dist/bubble.esm.js +1 -1
  19. package/dist/candlestick.common.js +2 -2
  20. package/dist/candlestick.esm.js +20 -10
  21. package/dist/candlestick.js +2 -2
  22. package/dist/column.common.js +2 -2
  23. package/dist/column.esm.js +20 -10
  24. package/dist/core.common.js +2 -2
  25. package/dist/core.esm.js +545 -463
  26. package/dist/donut.common.js +1 -1
  27. package/dist/donut.esm.js +1 -1
  28. package/dist/dumbbell.common.js +2 -2
  29. package/dist/dumbbell.esm.js +20 -10
  30. package/dist/features/all.common.js +2 -2
  31. package/dist/features/all.esm.js +364 -26
  32. package/dist/features/annotations.common.js +1 -1
  33. package/dist/features/annotations.esm.js +1 -1
  34. package/dist/features/annotations.js +1 -1
  35. package/dist/features/context-menu.common.js +1 -1
  36. package/dist/features/context-menu.esm.js +1 -1
  37. package/dist/features/context-menu.js +1 -1
  38. package/dist/features/drilldown.common.js +1 -1
  39. package/dist/features/drilldown.esm.js +1 -1
  40. package/dist/features/drilldown.js +1 -1
  41. package/dist/features/dumbbell.common.js +1 -1
  42. package/dist/features/dumbbell.esm.js +1 -1
  43. package/dist/features/dumbbell.js +1 -1
  44. package/dist/features/exports.common.js +2 -2
  45. package/dist/features/exports.esm.js +12 -14
  46. package/dist/features/exports.js +2 -2
  47. package/dist/features/facet.common.js +1 -1
  48. package/dist/features/facet.esm.js +1 -1
  49. package/dist/features/facet.js +1 -1
  50. package/dist/features/history.common.js +1 -1
  51. package/dist/features/history.esm.js +1 -1
  52. package/dist/features/history.js +1 -1
  53. package/dist/features/ink.common.js +1 -1
  54. package/dist/features/ink.esm.js +1 -1
  55. package/dist/features/ink.js +1 -1
  56. package/dist/features/keyboard.common.js +2 -2
  57. package/dist/features/keyboard.esm.js +14 -1
  58. package/dist/features/keyboard.js +2 -2
  59. package/dist/features/legend.common.js +1 -1
  60. package/dist/features/legend.esm.js +1 -1
  61. package/dist/features/legend.js +1 -1
  62. package/dist/features/link.common.js +1 -1
  63. package/dist/features/link.esm.js +38 -38
  64. package/dist/features/link.js +1 -1
  65. package/dist/features/marks.common.js +1 -1
  66. package/dist/features/marks.esm.js +1 -1
  67. package/dist/features/marks.js +1 -1
  68. package/dist/features/measure.common.js +1 -1
  69. package/dist/features/measure.esm.js +1 -1
  70. package/dist/features/measure.js +1 -1
  71. package/dist/features/morph.common.js +1 -1
  72. package/dist/features/morph.esm.js +1 -1
  73. package/dist/features/morph.js +1 -1
  74. package/dist/features/perspectives.common.js +1 -1
  75. package/dist/features/perspectives.esm.js +11 -11
  76. package/dist/features/perspectives.js +1 -1
  77. package/dist/features/raincloud.common.js +1 -1
  78. package/dist/features/raincloud.esm.js +1 -1
  79. package/dist/features/raincloud.js +1 -1
  80. package/dist/features/renderer-canvas.common.js +1 -1
  81. package/dist/features/renderer-canvas.esm.js +1 -1
  82. package/dist/features/renderer-canvas.js +1 -1
  83. package/dist/features/stats.common.js +1 -1
  84. package/dist/features/stats.esm.js +1 -1
  85. package/dist/features/stats.js +1 -1
  86. package/dist/features/storyboard.common.js +1 -1
  87. package/dist/features/storyboard.esm.js +11 -11
  88. package/dist/features/storyboard.js +1 -1
  89. package/dist/features/streamgraph.common.js +1 -1
  90. package/dist/features/streamgraph.esm.js +1 -1
  91. package/dist/features/streamgraph.js +1 -1
  92. package/dist/features/toolbar.common.js +2 -2
  93. package/dist/features/toolbar.esm.js +172 -25
  94. package/dist/features/toolbar.js +2 -2
  95. package/dist/features/trellis.common.js +1 -1
  96. package/dist/features/trellis.esm.js +1 -1
  97. package/dist/features/trellis.js +1 -1
  98. package/dist/features/waterfall.common.js +1 -1
  99. package/dist/features/waterfall.esm.js +1 -1
  100. package/dist/features/waterfall.js +1 -1
  101. package/dist/features/weave.common.js +2 -2
  102. package/dist/features/weave.esm.js +180 -2
  103. package/dist/features/weave.js +2 -2
  104. package/dist/heatmap.common.js +1 -1
  105. package/dist/heatmap.esm.js +1 -1
  106. package/dist/heatmap.js +1 -1
  107. package/dist/histogram.common.js +2 -2
  108. package/dist/histogram.esm.js +20 -10
  109. package/dist/line.common.js +1 -1
  110. package/dist/line.esm.js +1 -1
  111. package/dist/line.js +1 -1
  112. package/dist/pictograms.common.js +1 -1
  113. package/dist/pictograms.esm.js +1 -1
  114. package/dist/pictograms.js +1 -1
  115. package/dist/pie.common.js +1 -1
  116. package/dist/pie.esm.js +1 -1
  117. package/dist/pie.js +1 -1
  118. package/dist/polarArea.common.js +1 -1
  119. package/dist/polarArea.esm.js +1 -1
  120. package/dist/radar.common.js +1 -1
  121. package/dist/radar.esm.js +1 -1
  122. package/dist/radar.js +1 -1
  123. package/dist/radialBar.common.js +1 -1
  124. package/dist/radialBar.esm.js +1 -1
  125. package/dist/radialBar.js +1 -1
  126. package/dist/raincloud.common.js +2 -2
  127. package/dist/raincloud.esm.js +20 -10
  128. package/dist/rangeArea.common.js +1 -1
  129. package/dist/rangeArea.esm.js +1 -1
  130. package/dist/rangeBar.common.js +2 -2
  131. package/dist/rangeBar.esm.js +20 -10
  132. package/dist/scatter.common.js +1 -1
  133. package/dist/scatter.esm.js +1 -1
  134. package/dist/streamgraph.common.js +1 -1
  135. package/dist/streamgraph.esm.js +1 -1
  136. package/dist/sunburst.common.js +1 -1
  137. package/dist/sunburst.esm.js +1 -1
  138. package/dist/sunburst.js +1 -1
  139. package/dist/treemap.common.js +1 -1
  140. package/dist/treemap.esm.js +1 -1
  141. package/dist/treemap.js +1 -1
  142. package/dist/unit-shapes.common.js +1 -1
  143. package/dist/unit-shapes.esm.js +1 -1
  144. package/dist/unit-shapes.js +1 -1
  145. package/dist/unit.common.js +1 -1
  146. package/dist/unit.esm.js +79 -79
  147. package/dist/unit.js +1 -1
  148. package/dist/violin.common.js +2 -2
  149. package/dist/violin.esm.js +20 -10
  150. package/dist/violin.js +2 -2
  151. package/dist/waterfall.common.js +2 -2
  152. package/dist/waterfall.esm.js +20 -10
  153. package/package.json +2 -2
  154. package/src/apexcharts.js +39 -15
  155. package/src/charts/common/bar/Helpers.js +31 -12
  156. package/src/modules/Data.js +8 -2
  157. package/src/modules/Exports.js +14 -12
  158. package/src/modules/Formatters.js +24 -1
  159. package/src/modules/Range.js +28 -1
  160. package/src/modules/Toolbar.js +112 -2
  161. package/src/modules/ZoomPanSelection.js +85 -7
  162. package/src/modules/accessibility/KeyboardNavigation.js +14 -0
  163. package/src/modules/axes/YAxis.js +12 -15
  164. package/src/modules/helpers/UpdateHelpers.js +25 -8
  165. package/src/modules/settings/Globals.js +5 -10
  166. package/src/modules/settings/Options.js +15 -0
  167. package/src/modules/weave/PluginAPI.js +57 -1
  168. package/src/modules/weave/WeaveHost.js +148 -0
  169. package/src/ssr/DOMShim.js +50 -0
  170. package/src/types/internal.d.ts +1 -1
  171. package/src/utils/Utils.js +6 -9
  172. package/types/apexcharts.d.ts +17 -0
@@ -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 (gl.dataReducerRawSeries && cnf.chart.dataReducer?.enabled) {
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,
@@ -998,8 +998,9 @@ class Exports {
998
998
  }
999
999
 
1000
1000
  const handleUnequalXValues = () => {
1001
- const categories = new Set()
1002
- const data = {}
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
- if (!(/** @type {Record<string,any>} */ (data)[cat])) {
1024
- ;/** @type {Record<string,any>} */ (data)[cat] = Array(
1025
- series.length,
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
- ;/** @type {Record<string,any>} */ (data)[cat][sI] =
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(categories)
1038
+ Array.from(byCategory.keys())
1039
1039
  .sort()
1040
- .forEach((cat) => {
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
  }
@@ -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
- if (cnf.xaxis.type === 'numeric' && gl.dataPoints < 30) {
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
 
@@ -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
- createToolbar() {
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
- this._createHamburgerMenu(elToolbarWrap)
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. The only
352
- * built-in way back is the toolbar's reset button, hence 'auto' (the default
353
- * for both allowMouseWheelZoom and pinch) resolves against that button being
354
- * present. A page that builds its own reset control sets the option to true
355
- * and gets the gesture with no toolbar. Drag-to-zoom is deliberate, so it is
356
- * not gated this way.
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
- yaxis.forEach((y, index) => {
559
- const yaxe = w.config.yaxis[index]
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 = /** @type {Element} */ (yAxisInner).getBoundingClientRect()
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
- ;/** @type {Element} */ (yAxisInner).setAttribute(
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
- ;/** @type {Element} */ (yAxisInner).setAttribute(
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
- ;/** @type {Element} */ (yAxisInner).setAttribute(
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
- // lazy snapshot: deep clone deferred to first read
184
- w.globals.initialSeries = w.config.series
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(newSeries)
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
- // initialSeries was already captured by parseData above through the
319
- // lazy-snapshot setter, so no deep clone happens here either.
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
- w.globals.initialSeries = w.config.series
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
- * The one in-place mutator is appendData(), whose push loop grows the shared
558
- * data array. Re-capturing after it does not undo that: the pushed points
559
- * are already in the array both snapshots point at, so a snapshot taken
560
- * before the append reads back as appended. That is the documented
561
- * behaviour of appendData(overwriteInitialSeries = true), and the `false`
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,