@carto/ps-react-ui 4.3.5 → 4.3.7

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 (125) hide show
  1. package/dist/components.js +123 -123
  2. package/dist/components.js.map +1 -1
  3. package/dist/error-CEkRPccv.js +39 -0
  4. package/dist/error-CEkRPccv.js.map +1 -0
  5. package/dist/{lasso-tool-wFqOD6wk.js → lasso-tool-jl4YK02H.js} +184 -159
  6. package/dist/lasso-tool-jl4YK02H.js.map +1 -0
  7. package/dist/no-data-hR3KcJ-_.js +60 -0
  8. package/dist/no-data-hR3KcJ-_.js.map +1 -0
  9. package/dist/{row-DrHwXNvF.js → row-BKmVAUN5.js} +2 -2
  10. package/dist/{row-DrHwXNvF.js.map → row-BKmVAUN5.js.map} +1 -1
  11. package/dist/{series-D3Pc-kYX.js → series-D1pynfeh.js} +3 -3
  12. package/dist/{series-D3Pc-kYX.js.map → series-D1pynfeh.js.map} +1 -1
  13. package/dist/{styles-CCZnY17y.js → styles-DrPyd0y5.js} +28 -22
  14. package/dist/styles-DrPyd0y5.js.map +1 -0
  15. package/dist/types/components/lasso-tool/types.d.ts +1 -1
  16. package/dist/types/widgets/_shared/chart-config/index.d.ts +1 -1
  17. package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +7 -0
  18. package/dist/types/widgets/_shared/chart-config/option-builders.test.d.ts +1 -0
  19. package/dist/types/widgets/actions/index.d.ts +4 -4
  20. package/dist/types/widgets/actions/lock-selection/types.d.ts +0 -13
  21. package/dist/types/widgets/actions/relative-data/types.d.ts +0 -4
  22. package/dist/types/widgets/actions/searcher/types.d.ts +0 -2
  23. package/dist/types/widgets/actions/stack-toggle/stack-toggle.d.ts +3 -2
  24. package/dist/types/widgets/actions/stack-toggle/types.d.ts +0 -4
  25. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  26. package/dist/types/widgets/echart/types.d.ts +0 -4
  27. package/dist/types/widgets/echart/utils.d.ts +2 -1
  28. package/dist/types/widgets/error/error.d.ts +1 -1
  29. package/dist/types/widgets/error/types.d.ts +8 -0
  30. package/dist/types/widgets/loader/loader.d.ts +1 -1
  31. package/dist/types/widgets/loader/types.d.ts +1 -1
  32. package/dist/types/widgets/stores/index.d.ts +1 -1
  33. package/dist/types/widgets/stores/types.d.ts +15 -0
  34. package/dist/{use-widget-ref-B0aNCANx.js → use-widget-ref-P-2i0MJG.js} +2 -2
  35. package/dist/{use-widget-ref-B0aNCANx.js.map → use-widget-ref-P-2i0MJG.js.map} +1 -1
  36. package/dist/{utils-D3-eQyDR.js → utils-idmvq0Oa.js} +17 -16
  37. package/dist/utils-idmvq0Oa.js.map +1 -0
  38. package/dist/widget-store-CzDt8oSK.js +163 -0
  39. package/dist/widget-store-CzDt8oSK.js.map +1 -0
  40. package/dist/widgets/actions.js +714 -659
  41. package/dist/widgets/actions.js.map +1 -1
  42. package/dist/widgets/bar.js +67 -63
  43. package/dist/widgets/bar.js.map +1 -1
  44. package/dist/widgets/category.js +250 -241
  45. package/dist/widgets/category.js.map +1 -1
  46. package/dist/widgets/echart.js +93 -100
  47. package/dist/widgets/echart.js.map +1 -1
  48. package/dist/widgets/error.js +1 -1
  49. package/dist/widgets/formula.js +64 -72
  50. package/dist/widgets/formula.js.map +1 -1
  51. package/dist/widgets/histogram.js +75 -73
  52. package/dist/widgets/histogram.js.map +1 -1
  53. package/dist/widgets/loader.js +41 -40
  54. package/dist/widgets/loader.js.map +1 -1
  55. package/dist/widgets/markdown.js +2 -2
  56. package/dist/widgets/no-data.js +1 -1
  57. package/dist/widgets/pie.js +4 -4
  58. package/dist/widgets/range.js +97 -105
  59. package/dist/widgets/range.js.map +1 -1
  60. package/dist/widgets/scatterplot.js +8 -8
  61. package/dist/widgets/skeleton-loader.js +1 -1
  62. package/dist/widgets/spread.js +84 -100
  63. package/dist/widgets/spread.js.map +1 -1
  64. package/dist/widgets/stores.js +1 -1
  65. package/dist/widgets/table.js +493 -485
  66. package/dist/widgets/table.js.map +1 -1
  67. package/dist/widgets/timeseries.js +4 -4
  68. package/dist/widgets/wrapper.js +156 -156
  69. package/dist/widgets/wrapper.js.map +1 -1
  70. package/dist/widgets.js +4 -4
  71. package/package.json +3 -3
  72. package/src/components/lasso-tool/lasso-tool-inline.tsx +19 -17
  73. package/src/components/lasso-tool/lasso-tool.tsx +27 -22
  74. package/src/components/lasso-tool/types.ts +4 -3
  75. package/src/widgets/_shared/chart-config/index.ts +1 -0
  76. package/src/widgets/_shared/chart-config/option-builders.test.ts +40 -0
  77. package/src/widgets/_shared/chart-config/option-builders.ts +12 -0
  78. package/src/widgets/actions/fullscreen/fullscreen.tsx +5 -8
  79. package/src/widgets/actions/index.ts +4 -7
  80. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +28 -30
  81. package/src/widgets/actions/lock-selection/lock-selection.tsx +25 -26
  82. package/src/widgets/actions/lock-selection/types.ts +0 -17
  83. package/src/widgets/actions/relative-data/relative-data.test.tsx +13 -13
  84. package/src/widgets/actions/relative-data/relative-data.tsx +18 -21
  85. package/src/widgets/actions/relative-data/types.ts +0 -7
  86. package/src/widgets/actions/searcher/searcher.tsx +40 -22
  87. package/src/widgets/actions/searcher/types.ts +0 -2
  88. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +160 -16
  89. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +79 -78
  90. package/src/widgets/actions/stack-toggle/types.ts +0 -8
  91. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +137 -87
  92. package/src/widgets/bar/config.ts +37 -28
  93. package/src/widgets/category/category-ui.tsx +25 -22
  94. package/src/widgets/echart/echart-ui.test.tsx +3 -18
  95. package/src/widgets/echart/echart-ui.tsx +4 -22
  96. package/src/widgets/echart/echart.test.tsx +9 -25
  97. package/src/widgets/echart/echart.tsx +36 -29
  98. package/src/widgets/echart/types.ts +0 -4
  99. package/src/widgets/echart/utils.ts +3 -1
  100. package/src/widgets/error/error.tsx +17 -14
  101. package/src/widgets/error/types.ts +10 -0
  102. package/src/widgets/formula/components/value.tsx +13 -13
  103. package/src/widgets/histogram/config.ts +36 -29
  104. package/src/widgets/loader/loader.tsx +20 -8
  105. package/src/widgets/loader/types.ts +3 -1
  106. package/src/widgets/no-data/no-data.tsx +8 -11
  107. package/src/widgets/range/components/range-item.tsx +9 -13
  108. package/src/widgets/spread/components/max-value.tsx +13 -13
  109. package/src/widgets/spread/components/min-value.tsx +13 -13
  110. package/src/widgets/stores/index.ts +1 -0
  111. package/src/widgets/stores/types.ts +17 -0
  112. package/src/widgets/stores/widget-store.test.ts +141 -0
  113. package/src/widgets/stores/widget-store.ts +73 -2
  114. package/src/widgets/table/hooks/use-pagination.ts +44 -35
  115. package/src/widgets/table/hooks/use-sort.ts +25 -23
  116. package/src/widgets/wrapper/wrapper-ui.tsx +16 -17
  117. package/dist/error-B2IJ9d2h.js +0 -38
  118. package/dist/error-B2IJ9d2h.js.map +0 -1
  119. package/dist/lasso-tool-wFqOD6wk.js.map +0 -1
  120. package/dist/no-data-C54XJt13.js +0 -61
  121. package/dist/no-data-C54XJt13.js.map +0 -1
  122. package/dist/styles-CCZnY17y.js.map +0 -1
  123. package/dist/utils-D3-eQyDR.js.map +0 -1
  124. package/dist/widget-store-CB6Trp_0.js +0 -131
  125. package/dist/widget-store-CB6Trp_0.js.map +0 -1
@@ -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
 
@@ -39,14 +39,17 @@ describe('StackToggle', () => {
39
39
  expect(button).toBeTruthy()
40
40
  })
41
41
 
42
- test('toggles to stacked mode and updates widget store', () => {
42
+ test('toggles to stacked mode and updates tool config', () => {
43
43
  render(<StackToggle id={widgetId} />)
44
44
 
45
45
  const button = screen.getByRole('button')
46
46
  fireEvent.click(button)
47
47
 
48
48
  const widget = useWidgetStore.getState().getWidget(widgetId)
49
- expect((widget as { isStacked?: boolean })?.isStacked).toBe(true)
49
+ const tool = widget?.registeredTools?.find(
50
+ (t) => t.id === STACK_TOGGLE_TOOL_ID,
51
+ )
52
+ expect(tool?.config?.stacked).toBe(true)
50
53
  })
51
54
 
52
55
  test('toggles back to unstacked mode', () => {
@@ -61,7 +64,10 @@ describe('StackToggle', () => {
61
64
  fireEvent.click(button)
62
65
 
63
66
  const widget = useWidgetStore.getState().getWidget(widgetId)
64
- expect((widget as { isStacked?: boolean })?.isStacked).toBe(false)
67
+ const tool = widget?.registeredTools?.find(
68
+ (t) => t.id === STACK_TOGGLE_TOOL_ID,
69
+ )
70
+ expect(tool?.config?.stacked).toBe(false)
65
71
  })
66
72
 
67
73
  test('has active state when in stacked mode', () => {
@@ -99,33 +105,48 @@ describe('StackToggle', () => {
99
105
  expect(button).toBeTruthy()
100
106
  })
101
107
 
102
- test('initializes store with default values on mount', () => {
108
+ test('initializes tool config with default values on mount', () => {
103
109
  render(<StackToggle id={widgetId} />)
104
110
 
105
111
  const widget = useWidgetStore.getState().getWidget(widgetId)
106
- expect((widget as { isStacked?: boolean })?.isStacked).toBe(false)
112
+ const tool = widget?.registeredTools?.find(
113
+ (t) => t.id === STACK_TOGGLE_TOOL_ID,
114
+ )
115
+ expect(tool?.config?.stacked).toBe(false)
107
116
  })
108
117
 
109
- test('initializes store with stacked values when defaultIsStacked is true', () => {
118
+ test('initializes tool config with stacked values when defaultIsStacked is true', () => {
110
119
  render(<StackToggle id={widgetId} defaultIsStacked />)
111
120
 
112
121
  const widget = useWidgetStore.getState().getWidget(widgetId)
113
- expect((widget as { isStacked?: boolean })?.isStacked).toBe(true)
122
+ const tool = widget?.registeredTools?.find(
123
+ (t) => t.id === STACK_TOGGLE_TOOL_ID,
124
+ )
125
+ expect(tool?.config?.stacked).toBe(true)
114
126
  })
115
127
 
116
- test('updates EChart series options when toggling to stacked', () => {
117
- // setupMultiSeriesWidget already ran in beforeEach
128
+ test('applies stack to EChart series via config pipeline when toggling to stacked', async () => {
118
129
  render(<StackToggle id={widgetId} />)
119
130
  const button = screen.getByRole('button')
120
131
  fireEvent.click(button)
121
132
 
133
+ // Run the config pipeline to apply the stack tool's transformation
134
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
135
+ option: {
136
+ series: [
137
+ { name: 'Series 1', type: 'bar' },
138
+ { name: 'Series 2', type: 'bar' },
139
+ ],
140
+ },
141
+ })
142
+
122
143
  const widget = useWidgetStore.getState().getWidget(widgetId)
123
144
  const series = (widget as { option?: { series?: { stack?: string }[] } })
124
145
  ?.option?.series?.[0]
125
146
  expect(series?.stack).toBe(DEFAULT_STACK_GROUP)
126
147
  })
127
148
 
128
- test('removes stack from EChart series when toggling to unstacked', () => {
149
+ test('removes stack from EChart series via config pipeline when toggling to unstacked', async () => {
129
150
  useWidgetStore.getState().setWidget(widgetId, {
130
151
  option: {
131
152
  series: [
@@ -141,13 +162,23 @@ describe('StackToggle', () => {
141
162
  // First click unstacks (since series has stack, it starts stacked)
142
163
  fireEvent.click(button)
143
164
 
165
+ // Run the config pipeline — tool is disabled when unstacked, so stack is removed
166
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
167
+ option: {
168
+ series: [
169
+ { name: 'Series 1', type: 'bar' },
170
+ { name: 'Series 2', type: 'bar' },
171
+ ],
172
+ },
173
+ })
174
+
144
175
  const widget = useWidgetStore.getState().getWidget(widgetId)
145
176
  const series = (widget as { option?: { series?: { stack?: string }[] } })
146
177
  ?.option?.series?.[0]
147
178
  expect(series?.stack).toBeUndefined()
148
179
  })
149
180
 
150
- test('updates multiple series when toggling stack', () => {
181
+ test('applies stack to multiple series via config pipeline', async () => {
151
182
  useWidgetStore.getState().setWidget(widgetId, {
152
183
  option: {
153
184
  series: [
@@ -162,6 +193,16 @@ describe('StackToggle', () => {
162
193
  const button = screen.getByRole('button')
163
194
  fireEvent.click(button)
164
195
 
196
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
197
+ option: {
198
+ series: [
199
+ { name: 'Series 1', type: 'bar' },
200
+ { name: 'Series 2', type: 'bar' },
201
+ { name: 'Series 3', type: 'bar' },
202
+ ],
203
+ },
204
+ })
205
+
165
206
  const widget = useWidgetStore.getState().getWidget(widgetId)
166
207
  const seriesArray = (
167
208
  widget as { option?: { series?: { stack?: string }[] } }
@@ -173,7 +214,7 @@ describe('StackToggle', () => {
173
214
  })
174
215
  })
175
216
 
176
- test('uses default stack group when re-stacking after unstacking', () => {
217
+ test('preserves custom stack group via config pipeline', async () => {
177
218
  const customStackGroup = 'custom-group'
178
219
  useWidgetStore.getState().setWidget(widgetId, {
179
220
  option: {
@@ -187,11 +228,20 @@ describe('StackToggle', () => {
187
228
  render(<StackToggle id={widgetId} />)
188
229
  const button = screen.getByRole('button')
189
230
 
190
- // Toggle off then on - re-stacking uses default stack group
231
+ // Toggle off then on
191
232
  fireEvent.click(button) // unstacked
192
233
  fireEvent.click(button) // stacked again
193
234
 
194
- void waitFor(() => {
235
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
236
+ option: {
237
+ series: [
238
+ { name: 'Series 1', type: 'bar', stack: customStackGroup },
239
+ { name: 'Series 2', type: 'bar', stack: customStackGroup },
240
+ ],
241
+ },
242
+ })
243
+
244
+ await waitFor(() => {
195
245
  const widget = useWidgetStore.getState().getWidget(widgetId)
196
246
  const series = (widget as { option?: { series?: { stack?: string }[] } })
197
247
  ?.option?.series?.[0]
@@ -238,7 +288,73 @@ describe('StackToggle', () => {
238
288
  expect(container.firstChild).toBeNull()
239
289
  })
240
290
 
241
- test('preserves other option properties when updating series', () => {
291
+ test('config tool re-applies stack when config pipeline re-runs', async () => {
292
+ // Start with stacked widget
293
+ useWidgetStore.getState().setWidget(widgetId, {
294
+ option: {
295
+ series: [
296
+ { name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP },
297
+ { name: 'Series 2', type: 'bar', stack: DEFAULT_STACK_GROUP },
298
+ ],
299
+ },
300
+ })
301
+
302
+ render(<StackToggle id={widgetId} defaultIsStacked />)
303
+
304
+ // Simulate loader running config pipeline with base config (no stack)
305
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
306
+ option: {
307
+ series: [
308
+ { name: 'Series 1', type: 'bar' },
309
+ { name: 'Series 2', type: 'bar' },
310
+ ],
311
+ },
312
+ })
313
+
314
+ // Stack should be re-applied by the config tool
315
+ await waitFor(() => {
316
+ const widget = useWidgetStore.getState().getWidget(widgetId)
317
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
318
+ ?.option?.series
319
+ expect(series?.[0]?.stack).toBe(DEFAULT_STACK_GROUP)
320
+ expect(series?.[1]?.stack).toBe(DEFAULT_STACK_GROUP)
321
+ })
322
+ })
323
+
324
+ test('config tool does not apply stack when unstacked and pipeline re-runs', async () => {
325
+ // Start with unstacked widget
326
+ useWidgetStore.getState().setWidget(widgetId, {
327
+ option: {
328
+ series: [
329
+ { name: 'Series 1', type: 'bar' },
330
+ { name: 'Series 2', type: 'bar' },
331
+ ],
332
+ },
333
+ })
334
+
335
+ render(<StackToggle id={widgetId} />)
336
+
337
+ // Run config pipeline with base config
338
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
339
+ option: {
340
+ series: [
341
+ { name: 'Series 1', type: 'bar' },
342
+ { name: 'Series 2', type: 'bar' },
343
+ ],
344
+ },
345
+ })
346
+
347
+ // Stack should NOT be applied since isStacked is false (tool disabled)
348
+ await waitFor(() => {
349
+ const widget = useWidgetStore.getState().getWidget(widgetId)
350
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
351
+ ?.option?.series
352
+ expect(series?.[0]?.stack).toBeUndefined()
353
+ expect(series?.[1]?.stack).toBeUndefined()
354
+ })
355
+ })
356
+
357
+ test('preserves other option properties via config pipeline', async () => {
242
358
  useWidgetStore.getState().setWidget(widgetId, {
243
359
  option: {
244
360
  title: { text: 'My Chart' },
@@ -254,6 +370,17 @@ describe('StackToggle', () => {
254
370
  const button = screen.getByRole('button')
255
371
  fireEvent.click(button)
256
372
 
373
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
374
+ option: {
375
+ title: { text: 'My Chart' },
376
+ xAxis: { type: 'category' },
377
+ series: [
378
+ { name: 'Series 1', type: 'bar' },
379
+ { name: 'Series 2', type: 'bar' },
380
+ ],
381
+ },
382
+ })
383
+
257
384
  const widget = useWidgetStore.getState().getWidget(widgetId)
258
385
  const option = (
259
386
  widget as {
@@ -267,4 +394,21 @@ describe('StackToggle', () => {
267
394
  expect(option?.title?.text).toBe('My Chart')
268
395
  expect(option?.xAxis?.type).toBe('category')
269
396
  })
397
+
398
+ test('registers config tool on mount and unregisters on unmount', () => {
399
+ const { unmount } = render(<StackToggle id={widgetId} />)
400
+
401
+ const widget = useWidgetStore.getState().getWidget(widgetId)
402
+ const tools = widget?.registeredTools ?? []
403
+ const stackTool = tools.find((t) => t.id === STACK_TOGGLE_TOOL_ID)
404
+ expect(stackTool).toBeDefined()
405
+ expect(stackTool?.type).toBe('config')
406
+
407
+ unmount()
408
+
409
+ const widgetAfter = useWidgetStore.getState().getWidget(widgetId)
410
+ const toolsAfter = widgetAfter?.registeredTools ?? []
411
+ const stackToolAfter = toolsAfter.find((t) => t.id === STACK_TOGGLE_TOOL_ID)
412
+ expect(stackToolAfter).toBeUndefined()
413
+ })
270
414
  })
@@ -1,20 +1,23 @@
1
1
  import { IconButton } from '@mui/material'
2
2
  import { useCallback, useEffect, useMemo } from 'react'
3
3
  import { useWidgetStore } from '../../stores/widget-store'
4
- import type { StackToggleProps, StackToggleState } from './types'
4
+ import type { StackToggleProps } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
7
7
  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
@@ -31,94 +34,92 @@ export function StackToggle({
31
34
  Icon,
32
35
  IconButtonProps,
33
36
  }: StackToggleProps) {
34
- const setWidget = useWidgetStore((state) => state.setWidget)
35
- const storeIsStacked = useWidgetStore(
36
- useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
37
- )
38
-
39
37
  const getWidget = useWidgetStore((state) => state.getWidget)
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
+
43
+ const stackTool = useWidgetStore(
44
+ useShallow((state) => {
45
+ const tools = state.getWidget(id)?.registeredTools ?? []
46
+ return tools.find((tool) => tool.id === STACK_TOGGLE_TOOL_ID)
47
+ }),
48
+ )
40
49
 
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
50
+ const option = useWidgetStore(
51
+ useShallow((state) => state.getWidget<EchartWidgetState>(id)?.option),
52
+ )
61
53
 
54
+ const { hasMultiSeries, hasStackInSeries } = useMemo(() => {
55
+ if (!option) return { hasMultiSeries: false, hasStackInSeries: false }
62
56
  const series = Array.isArray(option.series)
63
57
  ? option.series
64
58
  : [option.series]
65
-
66
- return series.some((s) => (s as { stack?: string })?.stack)
67
- }, [getWidget, id])
59
+ return {
60
+ hasMultiSeries: series.length > 1,
61
+ hasStackInSeries: series.some((s) => (s as { stack?: string })?.stack),
62
+ }
63
+ }, [option])
68
64
 
69
65
  // If series already has stack defined, default to stacked=true
70
66
  const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
71
- const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
72
-
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]
67
+ const isStacked =
68
+ (stackTool?.config?.stacked as boolean | undefined) ??
69
+ effectiveDefaultIsStacked
86
70
 
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
- )
110
-
111
- // Initialize store with default value only if not already configured
71
+ // Register config tool on mount
112
72
  useEffect(() => {
113
- setWidget(id, { isStacked: effectiveDefaultIsStacked })
114
- }, [effectiveDefaultIsStacked, id, setWidget])
73
+ const existingTool = getWidget(id)?.registeredTools?.find(
74
+ (tool) => tool.id === STACK_TOGGLE_TOOL_ID,
75
+ )
76
+
77
+ const initialStacked =
78
+ (existingTool?.config?.stacked as boolean | undefined) ??
79
+ effectiveDefaultIsStacked
80
+
81
+ registerTool(id, {
82
+ id: STACK_TOGGLE_TOOL_ID,
83
+ type: 'config',
84
+ order: 10,
85
+ enabled: initialStacked && hasMultiSeries,
86
+ fn: (currentConfig, toolConfig) => {
87
+ const config = currentConfig as Record<string, unknown>
88
+ const option = config.option as EchartOptionsProps | undefined
89
+ if (!option) return currentConfig
90
+
91
+ const stacked = (toolConfig?.stacked as boolean) ?? false
92
+ const series = Array.isArray(option.series)
93
+ ? option.series
94
+ : [option.series]
95
+ const updatedSeries = series.map((s) => {
96
+ const existingStack = (s as { stack?: string })?.stack
97
+ const stackGroup =
98
+ typeof existingStack === 'string'
99
+ ? existingStack
100
+ : DEFAULT_STACK_GROUP
101
+ return { ...s, ...getEChartStackConfig(stacked, stackGroup) }
102
+ })
103
+
104
+ return { ...config, option: { ...option, series: updatedSeries } }
105
+ },
106
+ config: { stacked: initialStacked },
107
+ })
108
+ return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
109
+ }, [
110
+ id,
111
+ registerTool,
112
+ unregisterTool,
113
+ effectiveDefaultIsStacked,
114
+ hasMultiSeries,
115
+ getWidget,
116
+ ])
115
117
 
116
118
  const handleToggle = useCallback(() => {
117
- const newIsStacked = !isStacked
118
-
119
- setWidget(id, { isStacked: newIsStacked })
120
- updateOptions(newIsStacked)
121
- }, [isStacked, id, setWidget, updateOptions])
119
+ const newStacked = !isStacked
120
+ setToolEnabled(id, STACK_TOGGLE_TOOL_ID, newStacked && hasMultiSeries)
121
+ updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: newStacked })
122
+ }, [isStacked, hasMultiSeries, id, setToolEnabled, updateToolConfig])
122
123
 
123
124
  const tooltipLabel = isStacked
124
125
  ? (labels?.unstacked ?? 'Disable stacking')
@@ -1,7 +1,5 @@
1
1
  import type { IconButtonProps } from '@mui/material'
2
2
  import type { ReactNode } from 'react'
3
- import type { BaseWidgetState } from '../../stores/types'
4
-
5
3
  export interface StackToggleProps {
6
4
  /** Widget ID to update stack configuration in the widget store */
7
5
  id: string
@@ -21,9 +19,3 @@ export interface StackToggleProps {
21
19
  /** Custom icon to display */
22
20
  Icon?: ReactNode
23
21
  }
24
-
25
- export type StackToggleState<T = unknown> = BaseWidgetState<
26
- T & {
27
- isStacked?: boolean
28
- }
29
- >