@carto/ps-react-ui 4.5.1 → 4.6.1

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 (94) hide show
  1. package/dist/{download-config-DemuQ3Jm.js → download-config-C3I0jWIL.js} +2 -2
  2. package/dist/{download-config-DemuQ3Jm.js.map → download-config-C3I0jWIL.js.map} +1 -1
  3. package/dist/{row-D4VOhcNI.js → row-DZSP99LW.js} +2 -2
  4. package/dist/{row-D4VOhcNI.js.map → row-DZSP99LW.js.map} +1 -1
  5. package/dist/{series-Bola3CmD.js → series-DLNHDWs0.js} +3 -3
  6. package/dist/{series-Bola3CmD.js.map → series-DLNHDWs0.js.map} +1 -1
  7. package/dist/types/hooks/index.d.ts +0 -1
  8. package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +3 -0
  9. package/dist/types/widgets/actions/index.d.ts +4 -4
  10. package/dist/types/widgets/actions/lock-selection/types.d.ts +2 -0
  11. package/dist/types/widgets/actions/relative-data/relative-data.d.ts +7 -2
  12. package/dist/types/widgets/actions/relative-data/types.d.ts +2 -0
  13. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  14. package/dist/types/widgets/category/index.d.ts +10 -2
  15. package/dist/types/widgets/no-data/no-data.d.ts +3 -2
  16. package/dist/types/widgets/no-data/types.d.ts +5 -1
  17. package/dist/types/widgets/stores/index.d.ts +1 -1
  18. package/dist/types/widgets/stores/types.d.ts +10 -10
  19. package/dist/types/widgets/stores/widget-store.d.ts +2 -3
  20. package/dist/types/widgets/table/index.d.ts +6 -2
  21. package/dist/{use-widget-ref-BFazQvJK.js → use-widget-ref-Ddr_SlJJ.js} +2 -2
  22. package/dist/{use-widget-ref-BFazQvJK.js.map → use-widget-ref-Ddr_SlJJ.js.map} +1 -1
  23. package/dist/{use-widget-selector-DqRmWQ1K.js → use-widget-selector-DFl2hW0R.js} +2 -2
  24. package/dist/{use-widget-selector-DqRmWQ1K.js.map → use-widget-selector-DFl2hW0R.js.map} +1 -1
  25. package/dist/{widget-store-CIrb9RKP.js → widget-store-Bw5zRUGg.js} +93 -95
  26. package/dist/widget-store-Bw5zRUGg.js.map +1 -0
  27. package/dist/widgets/actions.js +770 -755
  28. package/dist/widgets/actions.js.map +1 -1
  29. package/dist/widgets/bar.js +2 -2
  30. package/dist/widgets/category.js +3 -3
  31. package/dist/widgets/category.js.map +1 -1
  32. package/dist/widgets/echart.js +2 -2
  33. package/dist/widgets/error.js +37 -2
  34. package/dist/widgets/error.js.map +1 -1
  35. package/dist/widgets/formula.js +5 -5
  36. package/dist/widgets/histogram.js +1 -1
  37. package/dist/widgets/loader.js +1 -1
  38. package/dist/widgets/markdown.js +2 -2
  39. package/dist/widgets/no-data.js +58 -2
  40. package/dist/widgets/no-data.js.map +1 -1
  41. package/dist/widgets/note.js +121 -2
  42. package/dist/widgets/note.js.map +1 -1
  43. package/dist/widgets/pie.js +2 -2
  44. package/dist/widgets/range.js +3 -3
  45. package/dist/widgets/scatterplot.js +2 -2
  46. package/dist/widgets/skeleton-loader.js +1 -1
  47. package/dist/widgets/spread.js +5 -5
  48. package/dist/widgets/stores.js +2 -2
  49. package/dist/widgets/table.js +3 -3
  50. package/dist/widgets/timeseries.js +2 -2
  51. package/dist/widgets/utils.js +1 -1
  52. package/dist/widgets/wrapper.js +2 -2
  53. package/package.json +2 -6
  54. package/src/hooks/index.ts +0 -1
  55. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -22
  56. package/src/widgets/actions/change-column/change-column.test.tsx +1 -1
  57. package/src/widgets/actions/download/download.test.tsx +1 -1
  58. package/src/widgets/actions/index.ts +11 -2
  59. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +14 -0
  60. package/src/widgets/actions/lock-selection/lock-selection.tsx +18 -11
  61. package/src/widgets/actions/lock-selection/types.ts +2 -0
  62. package/src/widgets/actions/relative-data/relative-data.test.tsx +211 -20
  63. package/src/widgets/actions/relative-data/relative-data.tsx +65 -34
  64. package/src/widgets/actions/relative-data/types.ts +2 -0
  65. package/src/widgets/actions/searcher/searcher.tsx +28 -30
  66. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +11 -2
  67. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +53 -45
  68. package/src/widgets/category/category-ui.tsx +4 -6
  69. package/src/widgets/category/index.ts +13 -14
  70. package/src/widgets/no-data/no-data.test.tsx +90 -40
  71. package/src/widgets/no-data/no-data.tsx +7 -5
  72. package/src/widgets/no-data/types.ts +5 -1
  73. package/src/widgets/stores/index.ts +2 -0
  74. package/src/widgets/stores/types.ts +10 -18
  75. package/src/widgets/stores/widget-store.test.ts +132 -13
  76. package/src/widgets/stores/widget-store.ts +29 -35
  77. package/src/widgets/table/index.ts +6 -4
  78. package/dist/error-Cj8eUMrl.js +0 -40
  79. package/dist/error-Cj8eUMrl.js.map +0 -1
  80. package/dist/no-data-DkIt7Qt1.js +0 -61
  81. package/dist/no-data-DkIt7Qt1.js.map +0 -1
  82. package/dist/note-t51drNe0.js +0 -124
  83. package/dist/note-t51drNe0.js.map +0 -1
  84. package/dist/types/hooks/use-debounce.d.ts +0 -19
  85. package/dist/types/widgets/category/components/index.d.ts +0 -10
  86. package/dist/types/widgets/index.d.ts +0 -9
  87. package/dist/types/widgets/table/hooks/index.d.ts +0 -6
  88. package/dist/widget-store-CIrb9RKP.js.map +0 -1
  89. package/dist/widgets.js +0 -13
  90. package/dist/widgets.js.map +0 -1
  91. package/src/hooks/use-debounce.ts +0 -55
  92. package/src/widgets/category/components/index.ts +0 -14
  93. package/src/widgets/index.ts +0 -25
  94. package/src/widgets/table/hooks/index.ts +0 -7
@@ -3,7 +3,7 @@ import { PercentOutlined } from '@mui/icons-material'
3
3
  import { useCallback, useEffect, useRef } from 'react'
4
4
  import { widgetStoreActions } from '../../stores/widget-store'
5
5
  import { useWidgetSelector } from '../../stores/use-widget-selector'
6
- import type { RelativeDataProps } from './types'
6
+ import type { RelativeDataProps, RelativeDataState } from './types'
7
7
  import { actionButtonStyles } from '../shared/styles'
8
8
  import { Tooltip } from '../../../components'
9
9
  import { calculateTotal, toRelativeData } from './utils'
@@ -15,8 +15,13 @@ export const RELATIVE_DATA_CONFIG_TOOL_ID = 'relative-data-config'
15
15
  /**
16
16
  * Widget action to toggle between relative (percentage) and absolute data display.
17
17
  *
18
- * Registers a transformation tool in the widget pipeline when mounted.
19
- * When relative mode is active, transforms data to percentages via the pipeline.
18
+ * Registers two transformation tools in the widget pipeline when mounted:
19
+ * - A data tool that converts values to percentages (enabled/disabled via store)
20
+ * - A config tool that is **always enabled** and reads `isRelative` from the store
21
+ * to decide whether to apply the percentage formatter or restore the original one.
22
+ * The config tool must always participate because the original formatter may have
23
+ * been set via `setWidget` (not in the base config), so disabling the tool would
24
+ * leave the percentage formatter stuck on the widget.
20
25
  *
21
26
  * @example
22
27
  * ```tsx
@@ -39,15 +44,21 @@ export function RelativeData({
39
44
  undefined,
40
45
  )
41
46
 
42
- const { storeIsRelative } = useWidgetSelector(id, (w) => ({
43
- storeIsRelative: w?.registeredTools?.find(
44
- (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
45
- )?.config?.isRelative as boolean | undefined,
47
+ // Read isRelative from widget store root single source of truth
48
+ const { isRelative } = useWidgetSelector(id, (w) => ({
49
+ isRelative:
50
+ (w as RelativeDataState | undefined)?.isRelative ?? defaultIsRelative,
46
51
  }))
47
52
 
48
- const isRelative = storeIsRelative ?? defaultIsRelative
53
+ // Initialize store with default value on mount
54
+ useEffect(() => {
55
+ const current = widgetStoreActions.getWidget<RelativeDataState>(id)
56
+ if (current?.isRelative === undefined) {
57
+ widgetStoreActions.setWidget(id, { isRelative: defaultIsRelative })
58
+ }
59
+ }, [id, defaultIsRelative])
49
60
 
50
- // Register data tool with all reactive deps — store's no-op detection handles performance
61
+ // Register data tool once fn has no closure deps
51
62
  useEffect(() => {
52
63
  widgetStoreActions.registerTool(id, {
53
64
  id: RELATIVE_DATA_TOOL_ID,
@@ -63,18 +74,35 @@ export function RelativeData({
63
74
  return () => widgetStoreActions.unregisterTool(id, RELATIVE_DATA_TOOL_ID)
64
75
  }, [id, order, defaultIsRelative])
65
76
 
66
- // Register config tool with all reactive deps store's no-op detection handles performance
77
+ // Sync data tool enabledlightweight, no re-registration
78
+ useEffect(() => {
79
+ widgetStoreActions.setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative)
80
+ }, [id, isRelative])
81
+
82
+ // Register config tool — ALWAYS enabled.
83
+ // Reads isRelative, originalFormatter, originalMax from the store at execution time:
84
+ // - isRelative=true → applies percentage formatter and max=100
85
+ // - isRelative=false → restores original formatter/max from store
67
86
  useEffect(() => {
68
87
  widgetStoreActions.registerTool(id, {
69
88
  id: RELATIVE_DATA_CONFIG_TOOL_ID,
70
89
  type: 'config',
71
90
  order,
72
91
  enabled: true,
73
- fn: (currentConfig: unknown, toolConfig?: Record<string, unknown>) => {
92
+ fn: (currentConfig: unknown) => {
74
93
  const config = currentConfig as Record<string, unknown>
75
- if (toolConfig?.isRelative) {
94
+ const widget = widgetStoreActions.getWidget<RelativeDataState>(id)
95
+
96
+ const hasSourceData =
97
+ widget?.sourceData != null &&
98
+ !(Array.isArray(widget.sourceData) && widget.sourceData.length === 0)
99
+
100
+ if (widget?.isRelative) {
101
+ // Don't apply percentage formatter when there's no source data
102
+ if (!hasSourceData) return config
103
+
76
104
  if (!percentFormatterRef.current) {
77
- const locale = toolConfig?.locale as string | undefined
105
+ const locale = widget?.locale
78
106
  percentFormatterRef.current = (value: number) =>
79
107
  new Intl.NumberFormat(locale, {
80
108
  style: 'percent',
@@ -84,46 +112,49 @@ export function RelativeData({
84
112
  }
85
113
  return { ...config, formatter: percentFormatterRef.current, max: 100 }
86
114
  }
87
- // Switching back from relative mode
115
+
116
+ // Not in relative mode — restore originals from store if captured.
117
+ // Use `in` because originalFormatter may have been captured as undefined
118
+ // (widget had no formatter before toggling relative on).
88
119
  percentFormatterRef.current = undefined
89
- if (toolConfig && 'originalFormatter' in toolConfig) {
120
+ if (widget != null && 'originalFormatter' in widget) {
90
121
  return {
91
122
  ...config,
92
- formatter: toolConfig.originalFormatter,
93
- max: toolConfig.originalMax,
123
+ formatter: widget.originalFormatter,
124
+ max: widget.originalMax,
94
125
  }
95
126
  }
96
127
  return config
97
128
  },
98
- config: {
99
- isRelative: defaultIsRelative,
100
- },
101
129
  })
102
- return () =>
130
+ return () => {
131
+ // Restore original formatter/max if unmounting while in relative mode
132
+ const widget = widgetStoreActions.getWidget<RelativeDataState>(id)
133
+ if (widget?.isRelative && 'originalFormatter' in widget) {
134
+ widgetStoreActions.setWidget(id, {
135
+ formatter: widget.originalFormatter,
136
+ max: widget.originalMax,
137
+ })
138
+ }
139
+ percentFormatterRef.current = undefined
103
140
  widgetStoreActions.unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID)
141
+ }
104
142
  }, [id, order, defaultIsRelative])
105
143
 
106
144
  const handleToggle = useCallback(() => {
107
145
  const newIsRelative = !isRelative
108
- widgetStoreActions.setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
146
+ percentFormatterRef.current = undefined
109
147
 
110
148
  if (newIsRelative) {
111
- const widget = widgetStoreActions.getWidget(id) as {
112
- formatter?: (value: number) => string
113
- locale?: string
114
- max?: number
115
- }
116
- widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
149
+ // Capture current formatter/max in store before switching to relative
150
+ const widget = widgetStoreActions.getWidget(id)
151
+ widgetStoreActions.setWidget(id, {
117
152
  isRelative: true,
118
153
  originalFormatter: widget?.formatter,
119
- originalMax: widget?.max,
120
- locale: widget?.locale,
154
+ originalMax: (widget as unknown as Record<string, unknown>)?.max,
121
155
  })
122
156
  } else {
123
- percentFormatterRef.current = undefined
124
- widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
125
- isRelative: false,
126
- })
157
+ widgetStoreActions.setWidget(id, { isRelative: false })
127
158
  }
128
159
  }, [isRelative, id])
129
160
 
@@ -27,5 +27,7 @@ export interface RelativeDataProps {
27
27
  export type RelativeDataState<T = unknown> = BaseWidgetState<
28
28
  T & {
29
29
  isRelative?: boolean
30
+ originalFormatter?: (value: number) => string
31
+ originalMax?: number
30
32
  }
31
33
  >
@@ -57,42 +57,44 @@ export function Searcher({
57
57
  [id],
58
58
  )
59
59
 
60
- // Register tool with all reactive deps store's no-op detection handles performance
60
+ // Register tool once fn reads searchText from the store at execution time.
61
+ // Enabled is synced separately to avoid full re-registration on toggle.
61
62
  useEffect(() => {
62
63
  widgetStoreActions.registerTool(id, {
63
64
  id: SEARCHER_TOOL_ID,
64
65
  order,
65
- enabled,
66
- fn: async (data, config) => {
67
- const searchTextFromConfig = (config?.searchText as string) || ''
66
+ enabled: false,
67
+ fn: async (data) => {
68
+ const widget = widgetStoreActions.getWidget<SearcherState>(id)
69
+ const currentSearchText = widget?.searchText ?? ''
68
70
 
69
71
  // Execute filter (can be sync or async)
70
- const result = filter(data as EchartWidgetData, searchTextFromConfig)
72
+ const result = filter(data as EchartWidgetData, currentSearchText)
71
73
 
72
74
  // Return result directly (pipeline will handle Promise)
73
75
  return result
74
76
  },
75
- config: { searchText },
76
77
  disables: [LOCK_SELECTION_TOOL_ID],
77
78
  })
78
79
 
79
80
  return () => widgetStoreActions.unregisterTool(id, SEARCHER_TOOL_ID)
80
- }, [id, order, enabled, searchText, filter])
81
+ }, [id, order, filter])
81
82
 
82
- // Update config when search text changes (debounced)
83
- const debouncedUpdateConfig = useCallback(
84
- (text: string) => {
85
- if (debounceTimeoutRef.current) {
86
- clearTimeout(debounceTimeoutRef.current)
87
- }
88
- debounceTimeoutRef.current = setTimeout(() => {
89
- widgetStoreActions.updateToolConfig(id, SEARCHER_TOOL_ID, {
90
- searchText: text,
91
- })
92
- }, debounceDelay)
93
- },
94
- [id, debounceDelay],
95
- )
83
+ // Sync enabled from store lightweight, no re-registration
84
+ useEffect(() => {
85
+ widgetStoreActions.setToolEnabled(id, SEARCHER_TOOL_ID, enabled)
86
+ }, [id, enabled])
87
+
88
+ // Trigger pipeline re-execution when search text changes (debounced).
89
+ // The fn reads searchText from the store, so we just need to trigger the pipeline.
90
+ const debouncedTriggerPipeline = useCallback(() => {
91
+ if (debounceTimeoutRef.current) {
92
+ clearTimeout(debounceTimeoutRef.current)
93
+ }
94
+ debounceTimeoutRef.current = setTimeout(() => {
95
+ widgetStoreActions.triggerToolPipeline(id)
96
+ }, debounceDelay)
97
+ }, [id, debounceDelay])
96
98
 
97
99
  // Auto-focus when enabled becomes true
98
100
  useEffect(() => {
@@ -117,16 +119,14 @@ export function Searcher({
117
119
  (event: React.ChangeEvent<HTMLInputElement>) => {
118
120
  const newValue = event.target.value
119
121
  setSearchText(newValue)
120
- debouncedUpdateConfig(newValue)
122
+ debouncedTriggerPipeline()
121
123
  },
122
- [debouncedUpdateConfig, setSearchText],
124
+ [debouncedTriggerPipeline, setSearchText],
123
125
  )
124
126
 
125
127
  const handleClear = useCallback(() => {
126
128
  setSearchText('')
127
- widgetStoreActions.updateToolConfig(id, SEARCHER_TOOL_ID, {
128
- searchText: '',
129
- })
129
+ widgetStoreActions.triggerToolPipeline(id)
130
130
  if (inputRef.current) {
131
131
  inputRef.current.focus()
132
132
  }
@@ -187,10 +187,8 @@ const defaultFilterFn: SearcherFilterFn = (
187
187
  return Promise.resolve(
188
188
  data.map((series) =>
189
189
  series.filter((item) =>
190
- Object.values(item).some(
191
- (value) =>
192
- typeof value === 'string' &&
193
- value.toLowerCase().includes(lowerSearch),
190
+ Object.values(item).some((value) =>
191
+ String(value).toLowerCase().includes(lowerSearch),
194
192
  ),
195
193
  ),
196
194
  ),
@@ -54,13 +54,13 @@ export function StackToggle({
54
54
  const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
55
55
  const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
56
56
 
57
- // Register config tool with all reactive deps — store's no-op detection handles performance
57
+ // Register config tool once fn has no closure deps
58
58
  useEffect(() => {
59
59
  widgetStoreActions.registerTool(id, {
60
60
  id: STACK_TOGGLE_TOOL_ID,
61
61
  type: 'config',
62
62
  order: 10,
63
- enabled: isStacked && hasMultiSeries,
63
+ enabled: false,
64
64
  fn: (currentConfig: unknown) => {
65
65
  const config = currentConfig as Record<string, unknown>
66
66
  const option = config.option as EchartOptionsProps | undefined
@@ -82,6 +82,15 @@ export function StackToggle({
82
82
  },
83
83
  })
84
84
  return () => widgetStoreActions.unregisterTool(id, STACK_TOGGLE_TOOL_ID)
85
+ }, [id])
86
+
87
+ // Sync enabled from store — lightweight, no re-registration
88
+ useEffect(() => {
89
+ widgetStoreActions.setToolEnabled(
90
+ id,
91
+ STACK_TOGGLE_TOOL_ID,
92
+ isStacked && hasMultiSeries,
93
+ )
85
94
  }, [id, isStacked, hasMultiSeries])
86
95
 
87
96
  // Initialize store with default value only if not already configured
@@ -5,7 +5,7 @@ import {
5
5
  } from '@mui/icons-material'
6
6
  import { useEffect, useCallback } from 'react'
7
7
  import { widgetStoreActions } from '../../stores/widget-store'
8
- import type { ZoomToggleProps } from './types'
8
+ import type { ZoomToggleProps, ZoomState } from './types'
9
9
  import { styles } from './style'
10
10
  import { Tooltip } from '../../../components'
11
11
  import { getEChartZoomConfig } from '../../echart/utils'
@@ -20,6 +20,10 @@ export const ZOOM_TOGGLE_TOOL_ID = 'zoom-toggle'
20
20
  * Registers as a config pipeline tool so that zoom configuration is automatically
21
21
  * re-applied when the base config is updated (e.g., by WidgetLoader).
22
22
  *
23
+ * Zoom state (enabled, range) is stored in the widget store root, and the tool
24
+ * derives its `enabled` flag from the store. This keeps state accessible across
25
+ * component instances.
26
+ *
23
27
  * When zoom is active, displays an inline reset button to disable zoom.
24
28
  * Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
25
29
  *
@@ -45,19 +49,26 @@ export function ZoomToggle({
45
49
  }: ZoomToggleProps) {
46
50
  const theme = useTheme()
47
51
 
48
- const { zoom, zoomStart, zoomEnd } = useWidgetSelector(id, (w) => {
49
- const zoomTool = w?.registeredTools?.find(
50
- (tool) => tool.id === ZOOM_TOGGLE_TOOL_ID,
51
- )
52
- return {
53
- zoom: zoomTool?.enabled ?? defaultZoom,
54
- zoomStart:
55
- (zoomTool?.config?.start as number | undefined) ?? defaultZoomStart,
56
- zoomEnd: (zoomTool?.config?.end as number | undefined) ?? defaultZoomEnd,
52
+ // Read zoom state from widget store root single source of truth
53
+ const { zoom, zoomStart, zoomEnd } = useWidgetSelector(id, (w) => ({
54
+ zoom: (w as ZoomState | undefined)?.zoom ?? defaultZoom,
55
+ zoomStart: (w as ZoomState | undefined)?.zoomStart ?? defaultZoomStart,
56
+ zoomEnd: (w as ZoomState | undefined)?.zoomEnd ?? defaultZoomEnd,
57
+ }))
58
+
59
+ // Initialize store with default values on mount
60
+ useEffect(() => {
61
+ const current = widgetStoreActions.getWidget<ZoomState>(id)
62
+ if (current?.zoom === undefined) {
63
+ widgetStoreActions.setWidget(id, {
64
+ zoom: defaultZoom,
65
+ zoomStart: defaultZoomStart,
66
+ zoomEnd: defaultZoomEnd,
67
+ })
57
68
  }
58
- })
69
+ }, [id, defaultZoom, defaultZoomStart, defaultZoomEnd])
59
70
 
60
- // Handle dataZoom event to update zoom range in tool config
71
+ // Handle dataZoom event update store (source of truth)
61
72
  const handleDataZoom = useCallback(
62
73
  (event: unknown) => {
63
74
  const zoomEvent = event as {
@@ -73,41 +84,32 @@ export function ZoomToggle({
73
84
  const end = zoomEvent.end ?? zoomEvent.batch?.[0]?.end
74
85
 
75
86
  if (start !== undefined && end !== undefined) {
76
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
77
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
78
- start,
79
- end,
87
+ widgetStoreActions.setWidget(id, {
88
+ zoom: true,
89
+ zoomStart: start,
90
+ zoomEnd: end,
80
91
  })
81
92
  }
82
93
  },
83
94
  [id],
84
95
  )
85
96
 
86
- // Register config tool with all reactive deps store's no-op detection handles performance
97
+ // Register config tool once fn reads zoom range from the store at execution time.
87
98
  useEffect(() => {
88
- const existingTool = widgetStoreActions
89
- .getWidget(id)
90
- ?.registeredTools?.find((tool) => tool.id === ZOOM_TOGGLE_TOOL_ID)
91
-
92
- const initialEnabled = existingTool?.enabled ?? defaultZoom
93
- const initialStart =
94
- (existingTool?.config?.start as number | undefined) ?? defaultZoomStart
95
- const initialEnd =
96
- (existingTool?.config?.end as number | undefined) ?? defaultZoomEnd
97
-
98
99
  widgetStoreActions.registerTool(id, {
99
100
  id: ZOOM_TOGGLE_TOOL_ID,
100
101
  type: 'config',
101
102
  order: 20,
102
- enabled: initialEnabled,
103
- fn: (currentConfig, toolConfig) => {
103
+ enabled: defaultZoom,
104
+ fn: (currentConfig) => {
104
105
  const config = currentConfig as Record<string, unknown>
105
106
  const option = config.option as EchartOptionsProps | undefined
106
107
  const currentOnEvents =
107
108
  (config.onEvents as Record<string, unknown> | undefined) ?? {}
108
109
 
109
- const start = (toolConfig?.start as number) ?? 0
110
- const end = (toolConfig?.end as number) ?? 100
110
+ const widget = widgetStoreActions.getWidget<ZoomState>(id)
111
+ const start = widget?.zoomStart ?? 0
112
+ const end = widget?.zoomEnd ?? 100
111
113
 
112
114
  const legend = option?.legend as { show?: boolean } | undefined
113
115
  const hasLegend = legend?.show !== false && legend !== undefined
@@ -135,8 +137,6 @@ export function ZoomToggle({
135
137
 
136
138
  const gridBottom = currentGridBottom + sliderHeight + sliderGap
137
139
 
138
- const onEventsWithoutDataZoom = { ...currentOnEvents }
139
- delete onEventsWithoutDataZoom.dataZoom
140
140
  const onEvents = { ...currentOnEvents, dataZoom: handleDataZoom }
141
141
 
142
142
  return {
@@ -149,28 +149,36 @@ export function ZoomToggle({
149
149
  onEvents,
150
150
  }
151
151
  },
152
- config: {
153
- start: initialStart,
154
- end: initialEnd,
155
- },
156
152
  })
157
153
  return () => widgetStoreActions.unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
158
154
  }, [id, theme, handleDataZoom, defaultZoom, defaultZoomStart, defaultZoomEnd])
159
155
 
156
+ // Sync enabled from store — lightweight, no re-registration
157
+ useEffect(() => {
158
+ widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, zoom)
159
+ }, [id, zoom])
160
+
161
+ // Trigger pipeline when zoom range changes — fn reads from store at execution time,
162
+ // but the pipeline only re-runs when registeredTools reference changes.
163
+ // setToolEnabled handles the zoom on/off case, this handles range-only changes.
164
+ useEffect(() => {
165
+ widgetStoreActions.triggerToolPipeline(id)
166
+ }, [id, zoomStart, zoomEnd])
167
+
160
168
  const handleToggle = () => {
161
169
  const newZoom = !zoom
162
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, newZoom)
163
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
164
- start: newZoom ? zoomStart : 0,
165
- end: newZoom ? zoomEnd : 100,
170
+ widgetStoreActions.setWidget(id, {
171
+ zoom: newZoom,
172
+ zoomStart: newZoom ? zoomStart : 0,
173
+ zoomEnd: newZoom ? zoomEnd : 100,
166
174
  })
167
175
  }
168
176
 
169
177
  const handleReset = () => {
170
- widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
171
- widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
172
- start: defaultZoomStart,
173
- end: defaultZoomEnd,
178
+ widgetStoreActions.setWidget(id, {
179
+ zoom: true,
180
+ zoomStart: defaultZoomStart,
181
+ zoomEnd: defaultZoomEnd,
174
182
  })
175
183
  }
176
184
 
@@ -2,12 +2,10 @@ import { Box, useTheme } from '@mui/material'
2
2
  import { useWidgetSelector } from '../stores/use-widget-selector'
3
3
  import { styles } from './style'
4
4
  import type { CategoryUIProps, CategoryWidgetState } from './types'
5
- import {
6
- CategoryRowSingle,
7
- CategoryRowMulti,
8
- CategoryRowOther,
9
- CategoryLegend,
10
- } from './components'
5
+ import { CategoryRowSingle } from './components/category-row-single'
6
+ import { CategoryRowMulti } from './components/category-row-multi'
7
+ import { CategoryRowOther } from './components/category-row-other'
8
+ import { CategoryLegend } from './components/category-legend'
11
9
  import { useState } from 'react'
12
10
  import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
13
11
  import { useWidgetRef } from '../../hooks'
@@ -2,21 +2,20 @@ export { CategoryUI } from './category-ui'
2
2
  export { CategorySkeleton } from './skeleton'
3
3
  export { categoryConfig, categoryDownloadConfig } from './config'
4
4
 
5
- export {
6
- CategoryBar,
7
- CategoryRowSingle,
8
- CategoryRowMulti,
9
- CategoryLegend,
10
- CategoryRowOther,
11
- } from './components'
5
+ export { CategoryBar } from './components/category-bar'
6
+ export type { CategoryBarProps } from './components/category-bar'
12
7
 
13
- export type {
14
- CategoryBarProps,
15
- CategoryRowSingleProps,
16
- CategoryRowMultiProps,
17
- CategoryLegendProps,
18
- CategoryRowOtherProps,
19
- } from './components'
8
+ export { CategoryRowSingle } from './components/category-row-single'
9
+ export type { CategoryRowSingleProps } from './components/category-row-single'
10
+
11
+ export { CategoryRowMulti } from './components/category-row-multi'
12
+ export type { CategoryRowMultiProps } from './components/category-row-multi'
13
+
14
+ export { CategoryRowOther } from './components/category-row-other'
15
+ export type { CategoryRowOtherProps } from './components/category-row-other'
16
+
17
+ export { CategoryLegend } from './components/category-legend'
18
+ export type { CategoryLegendProps } from './components/category-legend'
20
19
 
21
20
  export type {
22
21
  CategoryUIProps,