@carto/ps-react-ui 4.3.4 → 4.3.6

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 (67) hide show
  1. package/dist/components.js +2 -2
  2. package/dist/{error-B2IJ9d2h.js → error-piB8FwYO.js} +2 -2
  3. package/dist/{error-B2IJ9d2h.js.map → error-piB8FwYO.js.map} +1 -1
  4. package/dist/{lasso-tool-wFqOD6wk.js → lasso-tool-BctzdzBu.js} +185 -160
  5. package/dist/lasso-tool-BctzdzBu.js.map +1 -0
  6. package/dist/{no-data-C54XJt13.js → no-data-jdlbMef0.js} +2 -2
  7. package/dist/{no-data-C54XJt13.js.map → no-data-jdlbMef0.js.map} +1 -1
  8. package/dist/{row-DrHwXNvF.js → row-D3uVFImu.js} +2 -2
  9. package/dist/{row-DrHwXNvF.js.map → row-D3uVFImu.js.map} +1 -1
  10. package/dist/{series-D3Pc-kYX.js → series-BAImrSBo.js} +3 -3
  11. package/dist/{series-D3Pc-kYX.js.map → series-BAImrSBo.js.map} +1 -1
  12. package/dist/types/widgets/actions/index.d.ts +2 -2
  13. package/dist/types/widgets/actions/stack-toggle/stack-toggle.d.ts +3 -2
  14. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  15. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  16. package/dist/types/widgets/echart/types.d.ts +4 -0
  17. package/dist/types/widgets/loader/loader.d.ts +1 -1
  18. package/dist/types/widgets/loader/types.d.ts +1 -1
  19. package/dist/types/widgets/stores/index.d.ts +1 -1
  20. package/dist/types/widgets/stores/types.d.ts +15 -0
  21. package/dist/{use-widget-ref-B0aNCANx.js → use-widget-ref-B8x4sHIj.js} +2 -2
  22. package/dist/{use-widget-ref-B0aNCANx.js.map → use-widget-ref-B8x4sHIj.js.map} +1 -1
  23. package/dist/widget-store-Dn0Bnc4h.js +178 -0
  24. package/dist/widget-store-Dn0Bnc4h.js.map +1 -0
  25. package/dist/widgets/actions.js +698 -617
  26. package/dist/widgets/actions.js.map +1 -1
  27. package/dist/widgets/bar.js +2 -2
  28. package/dist/widgets/category.js +2 -2
  29. package/dist/widgets/echart.js +96 -85
  30. package/dist/widgets/echart.js.map +1 -1
  31. package/dist/widgets/error.js +1 -1
  32. package/dist/widgets/formula.js +5 -5
  33. package/dist/widgets/histogram.js +2 -2
  34. package/dist/widgets/loader.js +41 -40
  35. package/dist/widgets/loader.js.map +1 -1
  36. package/dist/widgets/markdown.js +2 -2
  37. package/dist/widgets/no-data.js +1 -1
  38. package/dist/widgets/pie.js +2 -2
  39. package/dist/widgets/range.js +2 -2
  40. package/dist/widgets/scatterplot.js +2 -2
  41. package/dist/widgets/skeleton-loader.js +1 -1
  42. package/dist/widgets/spread.js +5 -5
  43. package/dist/widgets/stores.js +1 -1
  44. package/dist/widgets/table.js +3 -3
  45. package/dist/widgets/timeseries.js +2 -2
  46. package/dist/widgets/wrapper.js +2 -2
  47. package/dist/widgets.js +4 -4
  48. package/package.json +1 -1
  49. package/src/components/lasso-tool/lasso-tool.tsx +5 -2
  50. package/src/widgets/actions/index.ts +2 -2
  51. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +143 -9
  52. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +61 -70
  53. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +85 -53
  54. package/src/widgets/echart/echart-ui.test.tsx +18 -0
  55. package/src/widgets/echart/echart-ui.tsx +25 -13
  56. package/src/widgets/echart/echart.test.tsx +25 -0
  57. package/src/widgets/echart/echart.tsx +9 -1
  58. package/src/widgets/echart/types.ts +4 -0
  59. package/src/widgets/loader/loader.tsx +18 -8
  60. package/src/widgets/loader/types.ts +1 -1
  61. package/src/widgets/stores/index.ts +1 -0
  62. package/src/widgets/stores/types.ts +20 -0
  63. package/src/widgets/stores/widget-store.test.ts +141 -0
  64. package/src/widgets/stores/widget-store.ts +99 -2
  65. package/dist/lasso-tool-wFqOD6wk.js.map +0 -1
  66. package/dist/widget-store-CB6Trp_0.js +0 -131
  67. package/dist/widget-store-CB6Trp_0.js.map +0 -1
@@ -2,7 +2,7 @@ import { jsxs as c, jsx as n } from "react/jsx-runtime";
2
2
  import { c as b } from "react/compiler-runtime";
3
3
  import "react";
4
4
  import "echarts";
5
- import "../widget-store-CB6Trp_0.js";
5
+ import "../widget-store-Dn0Bnc4h.js";
6
6
  import "zustand/shallow";
7
7
  import { g as k } from "../options-D9wflre6.js";
8
8
  import { m as w } from "../utils-D3-eQyDR.js";
@@ -11,7 +11,7 @@ import { Box as s, Skeleton as o } from "@mui/material";
11
11
  import "@mui/icons-material";
12
12
  import "react-markdown";
13
13
  import { d as g, a as d } from "../exports-Cr43OCul.js";
14
- import "../lasso-tool-wFqOD6wk.js";
14
+ import "../lasso-tool-BctzdzBu.js";
15
15
  import "../cjs-D4KH3azB.js";
16
16
  import "@dnd-kit/core";
17
17
  import "@dnd-kit/sortable";
@@ -3,10 +3,10 @@ import { c as _ } from "react/compiler-runtime";
3
3
  import { Box as O, IconButton as L, MenuItem as P, ListItemIcon as j, ListItemText as z, Menu as D, Typography as H, LinearProgress as N, AccordionSummary as G, AccordionDetails as U, Accordion as V } from "@mui/material";
4
4
  import { MoreVert as q } from "@mui/icons-material";
5
5
  import { useState as J, useEffect as K } from "react";
6
- import "../lasso-tool-wFqOD6wk.js";
6
+ import "../lasso-tool-BctzdzBu.js";
7
7
  import "../cjs-D4KH3azB.js";
8
8
  import { S as Q } from "../smart-tooltip-BEtBaIdz.js";
9
- import { u as T } from "../widget-store-CB6Trp_0.js";
9
+ import { u as T } from "../widget-store-Dn0Bnc4h.js";
10
10
  import { useShallow as R } from "zustand/shallow";
11
11
  const A = {
12
12
  root: {
package/dist/widgets.js CHANGED
@@ -1,7 +1,7 @@
1
- import { u as r } from "./widget-store-CB6Trp_0.js";
2
- import { u as W } from "./use-widget-ref-B0aNCANx.js";
3
- import { W as s } from "./no-data-C54XJt13.js";
4
- import { W as d } from "./error-B2IJ9d2h.js";
1
+ import { u as r } from "./widget-store-Dn0Bnc4h.js";
2
+ import { u as W } from "./use-widget-ref-B8x4sHIj.js";
3
+ import { W as s } from "./no-data-jdlbMef0.js";
4
+ import { W as d } from "./error-piB8FwYO.js";
5
5
  import { W as i } from "./note-t51drNe0.js";
6
6
  export {
7
7
  d as WidgetError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "4.3.4",
3
+ "version": "4.3.6",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -131,7 +131,7 @@ function LassoToolsUIAction({
131
131
  }
132
132
 
133
133
  function Options({
134
- TriggerProps: { Icon = <ArrowDropDown />, sx } = {},
134
+ TriggerProps = {},
135
135
  MenuProps,
136
136
  children,
137
137
  }: {
@@ -142,6 +142,9 @@ function Options({
142
142
  MenuProps?: Partial<ComponentProps<typeof Menu>>
143
143
  children: (props: OptionsChildrenProps) => JSX.Element
144
144
  }) {
145
+ const { Icon, sx } = TriggerProps
146
+ const IconNode = Icon ?? <ArrowDropDown />
147
+
145
148
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
146
149
 
147
150
  const open = Boolean(anchorEl)
@@ -163,7 +166,7 @@ function Options({
163
166
  }}
164
167
  onClick={handleToggle}
165
168
  >
166
- {Icon}
169
+ {IconNode}
167
170
  </IconButton>
168
171
  <Menu
169
172
  id='lasso-menu'
@@ -15,7 +15,7 @@ export {
15
15
  export type { RelativeDataProps } from './relative-data/types'
16
16
 
17
17
  /* Zoom Toggle Widget */
18
- export { ZoomToggle } from './zoom-toggle/zoom-toggle'
18
+ export { ZoomToggle, ZOOM_TOGGLE_TOOL_ID } from './zoom-toggle/zoom-toggle'
19
19
  export type {
20
20
  ZoomToggleProps,
21
21
  ZoomState,
@@ -23,7 +23,7 @@ export type {
23
23
  } from './zoom-toggle/types'
24
24
 
25
25
  /* Stack Toggle Widget */
26
- export { StackToggle } from './stack-toggle/stack-toggle'
26
+ export { StackToggle, STACK_TOGGLE_TOOL_ID } from './stack-toggle/stack-toggle'
27
27
  export type { StackToggleProps, StackToggleState } from './stack-toggle/types'
28
28
 
29
29
  /* Searcher Toggle Widget */
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect, beforeEach } from 'vitest'
2
2
  import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
- import { StackToggle } from './stack-toggle'
3
+ import { StackToggle, STACK_TOGGLE_TOOL_ID } from './stack-toggle'
4
4
  import { useWidgetStore } from '../../stores/widget-store'
5
5
  import { DEFAULT_STACK_GROUP } from '../../echart/const'
6
6
 
@@ -113,19 +113,28 @@ describe('StackToggle', () => {
113
113
  expect((widget as { isStacked?: boolean })?.isStacked).toBe(true)
114
114
  })
115
115
 
116
- test('updates EChart series options when toggling to stacked', () => {
117
- // setupMultiSeriesWidget already ran in beforeEach
116
+ test('applies stack to EChart series via config pipeline when toggling to stacked', async () => {
118
117
  render(<StackToggle id={widgetId} />)
119
118
  const button = screen.getByRole('button')
120
119
  fireEvent.click(button)
121
120
 
121
+ // Run the config pipeline to apply the stack tool's transformation
122
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
123
+ option: {
124
+ series: [
125
+ { name: 'Series 1', type: 'bar' },
126
+ { name: 'Series 2', type: 'bar' },
127
+ ],
128
+ },
129
+ })
130
+
122
131
  const widget = useWidgetStore.getState().getWidget(widgetId)
123
132
  const series = (widget as { option?: { series?: { stack?: string }[] } })
124
133
  ?.option?.series?.[0]
125
134
  expect(series?.stack).toBe(DEFAULT_STACK_GROUP)
126
135
  })
127
136
 
128
- test('removes stack from EChart series when toggling to unstacked', () => {
137
+ test('removes stack from EChart series via config pipeline when toggling to unstacked', async () => {
129
138
  useWidgetStore.getState().setWidget(widgetId, {
130
139
  option: {
131
140
  series: [
@@ -141,13 +150,23 @@ describe('StackToggle', () => {
141
150
  // First click unstacks (since series has stack, it starts stacked)
142
151
  fireEvent.click(button)
143
152
 
153
+ // Run the config pipeline — tool is disabled when unstacked, so stack is removed
154
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
155
+ option: {
156
+ series: [
157
+ { name: 'Series 1', type: 'bar' },
158
+ { name: 'Series 2', type: 'bar' },
159
+ ],
160
+ },
161
+ })
162
+
144
163
  const widget = useWidgetStore.getState().getWidget(widgetId)
145
164
  const series = (widget as { option?: { series?: { stack?: string }[] } })
146
165
  ?.option?.series?.[0]
147
166
  expect(series?.stack).toBeUndefined()
148
167
  })
149
168
 
150
- test('updates multiple series when toggling stack', () => {
169
+ test('applies stack to multiple series via config pipeline', async () => {
151
170
  useWidgetStore.getState().setWidget(widgetId, {
152
171
  option: {
153
172
  series: [
@@ -162,6 +181,16 @@ describe('StackToggle', () => {
162
181
  const button = screen.getByRole('button')
163
182
  fireEvent.click(button)
164
183
 
184
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
185
+ option: {
186
+ series: [
187
+ { name: 'Series 1', type: 'bar' },
188
+ { name: 'Series 2', type: 'bar' },
189
+ { name: 'Series 3', type: 'bar' },
190
+ ],
191
+ },
192
+ })
193
+
165
194
  const widget = useWidgetStore.getState().getWidget(widgetId)
166
195
  const seriesArray = (
167
196
  widget as { option?: { series?: { stack?: string }[] } }
@@ -173,7 +202,7 @@ describe('StackToggle', () => {
173
202
  })
174
203
  })
175
204
 
176
- test('uses default stack group when re-stacking after unstacking', () => {
205
+ test('preserves custom stack group via config pipeline', async () => {
177
206
  const customStackGroup = 'custom-group'
178
207
  useWidgetStore.getState().setWidget(widgetId, {
179
208
  option: {
@@ -187,11 +216,20 @@ describe('StackToggle', () => {
187
216
  render(<StackToggle id={widgetId} />)
188
217
  const button = screen.getByRole('button')
189
218
 
190
- // Toggle off then on - re-stacking uses default stack group
219
+ // Toggle off then on
191
220
  fireEvent.click(button) // unstacked
192
221
  fireEvent.click(button) // stacked again
193
222
 
194
- void waitFor(() => {
223
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
224
+ option: {
225
+ series: [
226
+ { name: 'Series 1', type: 'bar', stack: customStackGroup },
227
+ { name: 'Series 2', type: 'bar', stack: customStackGroup },
228
+ ],
229
+ },
230
+ })
231
+
232
+ await waitFor(() => {
195
233
  const widget = useWidgetStore.getState().getWidget(widgetId)
196
234
  const series = (widget as { option?: { series?: { stack?: string }[] } })
197
235
  ?.option?.series?.[0]
@@ -238,7 +276,75 @@ describe('StackToggle', () => {
238
276
  expect(container.firstChild).toBeNull()
239
277
  })
240
278
 
241
- test('preserves other option properties when updating series', () => {
279
+ test('config tool re-applies stack when config pipeline re-runs', async () => {
280
+ // Start with stacked widget
281
+ useWidgetStore.getState().setWidget(widgetId, {
282
+ isStacked: true,
283
+ option: {
284
+ series: [
285
+ { name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP },
286
+ { name: 'Series 2', type: 'bar', stack: DEFAULT_STACK_GROUP },
287
+ ],
288
+ },
289
+ })
290
+
291
+ render(<StackToggle id={widgetId} defaultIsStacked />)
292
+
293
+ // Simulate loader running config pipeline with base config (no stack)
294
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
295
+ option: {
296
+ series: [
297
+ { name: 'Series 1', type: 'bar' },
298
+ { name: 'Series 2', type: 'bar' },
299
+ ],
300
+ },
301
+ })
302
+
303
+ // Stack should be re-applied by the config tool
304
+ await waitFor(() => {
305
+ const widget = useWidgetStore.getState().getWidget(widgetId)
306
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
307
+ ?.option?.series
308
+ expect(series?.[0]?.stack).toBe(DEFAULT_STACK_GROUP)
309
+ expect(series?.[1]?.stack).toBe(DEFAULT_STACK_GROUP)
310
+ })
311
+ })
312
+
313
+ test('config tool does not apply stack when unstacked and pipeline re-runs', async () => {
314
+ // Start with unstacked widget
315
+ useWidgetStore.getState().setWidget(widgetId, {
316
+ isStacked: false,
317
+ option: {
318
+ series: [
319
+ { name: 'Series 1', type: 'bar' },
320
+ { name: 'Series 2', type: 'bar' },
321
+ ],
322
+ },
323
+ })
324
+
325
+ render(<StackToggle id={widgetId} />)
326
+
327
+ // Run config pipeline with base config
328
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
329
+ option: {
330
+ series: [
331
+ { name: 'Series 1', type: 'bar' },
332
+ { name: 'Series 2', type: 'bar' },
333
+ ],
334
+ },
335
+ })
336
+
337
+ // Stack should NOT be applied since isStacked is false (tool disabled)
338
+ await waitFor(() => {
339
+ const widget = useWidgetStore.getState().getWidget(widgetId)
340
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
341
+ ?.option?.series
342
+ expect(series?.[0]?.stack).toBeUndefined()
343
+ expect(series?.[1]?.stack).toBeUndefined()
344
+ })
345
+ })
346
+
347
+ test('preserves other option properties via config pipeline', async () => {
242
348
  useWidgetStore.getState().setWidget(widgetId, {
243
349
  option: {
244
350
  title: { text: 'My Chart' },
@@ -254,6 +360,17 @@ describe('StackToggle', () => {
254
360
  const button = screen.getByRole('button')
255
361
  fireEvent.click(button)
256
362
 
363
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
364
+ option: {
365
+ title: { text: 'My Chart' },
366
+ xAxis: { type: 'category' },
367
+ series: [
368
+ { name: 'Series 1', type: 'bar' },
369
+ { name: 'Series 2', type: 'bar' },
370
+ ],
371
+ },
372
+ })
373
+
257
374
  const widget = useWidgetStore.getState().getWidget(widgetId)
258
375
  const option = (
259
376
  widget as {
@@ -267,4 +384,21 @@ describe('StackToggle', () => {
267
384
  expect(option?.title?.text).toBe('My Chart')
268
385
  expect(option?.xAxis?.type).toBe('category')
269
386
  })
387
+
388
+ test('registers config tool on mount and unregisters on unmount', () => {
389
+ const { unmount } = render(<StackToggle id={widgetId} />)
390
+
391
+ const widget = useWidgetStore.getState().getWidget(widgetId)
392
+ const tools = widget?.registeredTools ?? []
393
+ const stackTool = tools.find((t) => t.id === STACK_TOGGLE_TOOL_ID)
394
+ expect(stackTool).toBeDefined()
395
+ expect(stackTool?.type).toBe('config')
396
+
397
+ unmount()
398
+
399
+ const widgetAfter = useWidgetStore.getState().getWidget(widgetId)
400
+ const toolsAfter = widgetAfter?.registeredTools ?? []
401
+ const stackToolAfter = toolsAfter.find((t) => t.id === STACK_TOGGLE_TOOL_ID)
402
+ expect(stackToolAfter).toBeUndefined()
403
+ })
270
404
  })
@@ -8,13 +8,16 @@ import { GroupedBarChartIcon } from './grouped-bar-chart-icon'
8
8
  import { getEChartStackConfig } from '../../echart/utils'
9
9
  import { DEFAULT_STACK_GROUP } from '../../echart/const'
10
10
  import type { EchartWidgetState } from '../../echart/types'
11
+ import type { EchartOptionsProps } from '../../echart/types'
11
12
  import { useShallow } from 'zustand/shallow'
12
13
 
14
+ export const STACK_TOGGLE_TOOL_ID = 'stack-toggle'
15
+
13
16
  /**
14
17
  * Widget action to toggle stacking behavior in ECharts bar and histogram widgets.
15
18
  *
16
- * Stores the stack state in the widget store and updates the ECharts option
17
- * using getEChartStackConfig to apply stacking to all series.
19
+ * Registers as a config pipeline tool so that stack configuration is automatically
20
+ * re-applied when the base config is updated (e.g., by WidgetLoader).
18
21
  *
19
22
  * @example
20
23
  * ```tsx
@@ -32,93 +35,81 @@ export function StackToggle({
32
35
  IconButtonProps,
33
36
  }: StackToggleProps) {
34
37
  const setWidget = useWidgetStore((state) => state.setWidget)
38
+ const registerTool = useWidgetStore((state) => state.registerTool)
39
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
+ const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
41
+ const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
42
+
35
43
  const storeIsStacked = useWidgetStore(
36
44
  useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
37
45
  )
38
46
 
39
- const getWidget = useWidgetStore((state) => state.getWidget)
40
-
41
- /**
42
- * Checks if there are multiple series in the chart
43
- */
44
- const hasMultiSeries = useMemo(() => {
45
- const option = getWidget<EchartWidgetState>(id)?.option
46
- if (!option) return false
47
-
48
- const series = Array.isArray(option.series)
49
- ? option.series
50
- : [option.series]
51
-
52
- return series.length > 1
53
- }, [getWidget, id])
54
-
55
- /**
56
- * Checks if any series in the option has a stack property defined
57
- */
58
- const hasStackInSeries = useMemo(() => {
59
- const option = getWidget<EchartWidgetState>(id)?.option
60
- if (!option) return false
47
+ const option = useWidgetStore(
48
+ useShallow((state) => state.getWidget<EchartWidgetState>(id)?.option),
49
+ )
61
50
 
51
+ const { hasMultiSeries, hasStackInSeries } = useMemo(() => {
52
+ if (!option) return { hasMultiSeries: false, hasStackInSeries: false }
62
53
  const series = Array.isArray(option.series)
63
54
  ? option.series
64
55
  : [option.series]
65
-
66
- return series.some((s) => (s as { stack?: string })?.stack)
67
- }, [getWidget, id])
56
+ return {
57
+ hasMultiSeries: series.length > 1,
58
+ hasStackInSeries: series.some((s) => (s as { stack?: string })?.stack),
59
+ }
60
+ }, [option])
68
61
 
69
62
  // If series already has stack defined, default to stacked=true
70
63
  const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
71
64
  const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
72
65
 
73
- /**
74
- * Updates the ECharts option with the stack configuration
75
- * Preserves existing stack group names from series, falling back to DEFAULT_STACK_GROUP
76
- */
77
- const updateOptions = useCallback(
78
- (stacked: boolean) => {
79
- const option = getWidget<EchartWidgetState>(id)?.option
80
-
81
- if (!option) return
82
-
83
- const series = Array.isArray(option.series)
84
- ? option.series
85
- : [option.series]
86
-
87
- const updatedSeries = series.map((s) => {
88
- // Extract existing stack group from series, fallback to default
89
- const existingStack = (s as { stack?: string })?.stack
90
- const stackGroup =
91
- typeof existingStack === 'string'
92
- ? existingStack
93
- : DEFAULT_STACK_GROUP
94
-
95
- return {
96
- ...s,
97
- ...getEChartStackConfig(stacked, stackGroup),
98
- }
99
- })
100
-
101
- setWidget(id, {
102
- option: {
103
- ...option,
104
- series: updatedSeries,
105
- },
106
- })
107
- },
108
- [getWidget, id, setWidget],
109
- )
66
+ // Register config tool on mount
67
+ useEffect(() => {
68
+ registerTool(id, {
69
+ id: STACK_TOGGLE_TOOL_ID,
70
+ type: 'config',
71
+ order: 10,
72
+ enabled: isStacked && hasMultiSeries,
73
+ fn: (currentConfig, toolConfig) => {
74
+ const config = currentConfig as Record<string, unknown>
75
+ const option = config.option as EchartOptionsProps | undefined
76
+ if (!option) return currentConfig
77
+
78
+ const stacked = (toolConfig?.stacked as boolean) ?? false
79
+ const series = Array.isArray(option.series)
80
+ ? option.series
81
+ : [option.series]
82
+ const updatedSeries = series.map((s) => {
83
+ const existingStack = (s as { stack?: string })?.stack
84
+ const stackGroup =
85
+ typeof existingStack === 'string'
86
+ ? existingStack
87
+ : DEFAULT_STACK_GROUP
88
+ return { ...s, ...getEChartStackConfig(stacked, stackGroup) }
89
+ })
90
+
91
+ return { ...config, option: { ...option, series: updatedSeries } }
92
+ },
93
+ config: { stacked: isStacked },
94
+ })
95
+ return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
96
+ }, [id, registerTool, unregisterTool, isStacked, hasMultiSeries])
97
+
98
+ // Sync tool enabled/config when state changes
99
+ useEffect(() => {
100
+ setToolEnabled(id, STACK_TOGGLE_TOOL_ID, isStacked && hasMultiSeries)
101
+ updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: isStacked })
102
+ }, [id, isStacked, hasMultiSeries, setToolEnabled, updateToolConfig])
110
103
 
111
104
  // Initialize store with default value only if not already configured
112
105
  useEffect(() => {
106
+ if (storeIsStacked !== undefined) return
113
107
  setWidget(id, { isStacked: effectiveDefaultIsStacked })
114
- }, [effectiveDefaultIsStacked, id, setWidget])
108
+ }, [effectiveDefaultIsStacked, id, setWidget, storeIsStacked])
115
109
 
116
110
  const handleToggle = useCallback(() => {
117
- const newIsStacked = !isStacked
118
-
119
- setWidget(id, { isStacked: newIsStacked })
120
- updateOptions(newIsStacked)
121
- }, [isStacked, id, setWidget, updateOptions])
111
+ setWidget(id, { isStacked: !isStacked })
112
+ }, [isStacked, id, setWidget])
122
113
 
123
114
  const tooltipLabel = isStacked
124
115
  ? (labels?.unstacked ?? 'Disable stacking')