@carto/ps-react-ui 4.4.2 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/download-config-DemuQ3Jm.js +56 -0
  2. package/dist/download-config-DemuQ3Jm.js.map +1 -0
  3. package/dist/error-Cj8eUMrl.js +40 -0
  4. package/dist/error-Cj8eUMrl.js.map +1 -0
  5. package/dist/formatter-B9Bxn1k7.js +6 -0
  6. package/dist/formatter-B9Bxn1k7.js.map +1 -0
  7. package/dist/no-data-DkIt7Qt1.js +61 -0
  8. package/dist/no-data-DkIt7Qt1.js.map +1 -0
  9. package/dist/row-D4VOhcNI.js +34 -0
  10. package/dist/row-D4VOhcNI.js.map +1 -0
  11. package/dist/series-Bola3CmD.js +90 -0
  12. package/dist/series-Bola3CmD.js.map +1 -0
  13. package/dist/styles-Y8q7Jff3.js +118 -0
  14. package/dist/styles-Y8q7Jff3.js.map +1 -0
  15. package/dist/types/widgets/actions/brush-toggle/types.d.ts +8 -2
  16. package/dist/types/widgets/category/components/category-row-multi.d.ts +2 -1
  17. package/dist/types/widgets/category/components/category-row-single.d.ts +2 -1
  18. package/dist/types/widgets/category/types.d.ts +1 -0
  19. package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
  20. package/dist/types/widgets/echart/types.d.ts +2 -0
  21. package/dist/types/widgets/histogram/config.d.ts +15 -3
  22. package/dist/types/widgets/histogram/index.d.ts +2 -1
  23. package/dist/types/widgets/histogram/types.d.ts +6 -3
  24. package/dist/types/widgets/stores/index.d.ts +2 -1
  25. package/dist/types/widgets/stores/types.d.ts +2 -0
  26. package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
  27. package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
  28. package/dist/types/widgets/stores/widget-store.d.ts +49 -27
  29. package/dist/types/widgets/table/types.d.ts +1 -1
  30. package/dist/types/widgets/utils/chart-config/index.d.ts +1 -1
  31. package/dist/types/widgets/utils/chart-config/option-builders.d.ts +13 -8
  32. package/dist/types/widgets/utils/formatter.d.ts +1 -0
  33. package/dist/types/widgets/utils/index.d.ts +1 -1
  34. package/dist/use-widget-ref-BFazQvJK.js +22 -0
  35. package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
  36. package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
  37. package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
  38. package/dist/widget-store-CIrb9RKP.js +263 -0
  39. package/dist/widget-store-CIrb9RKP.js.map +1 -0
  40. package/dist/widgets/actions.js +799 -817
  41. package/dist/widgets/actions.js.map +1 -1
  42. package/dist/widgets/bar.js +53 -47
  43. package/dist/widgets/bar.js.map +1 -1
  44. package/dist/widgets/category.js +261 -255
  45. package/dist/widgets/category.js.map +1 -1
  46. package/dist/widgets/echart.js +109 -99
  47. package/dist/widgets/echart.js.map +1 -1
  48. package/dist/widgets/error.js +1 -1
  49. package/dist/widgets/formula.js +71 -63
  50. package/dist/widgets/formula.js.map +1 -1
  51. package/dist/widgets/histogram.js +119 -80
  52. package/dist/widgets/histogram.js.map +1 -1
  53. package/dist/widgets/loader.js +53 -60
  54. package/dist/widgets/loader.js.map +1 -1
  55. package/dist/widgets/markdown.js +51 -50
  56. package/dist/widgets/markdown.js.map +1 -1
  57. package/dist/widgets/no-data.js +1 -1
  58. package/dist/widgets/pie.js +111 -99
  59. package/dist/widgets/pie.js.map +1 -1
  60. package/dist/widgets/range.js +146 -144
  61. package/dist/widgets/range.js.map +1 -1
  62. package/dist/widgets/scatterplot.js +50 -44
  63. package/dist/widgets/scatterplot.js.map +1 -1
  64. package/dist/widgets/skeleton-loader.js +18 -17
  65. package/dist/widgets/skeleton-loader.js.map +1 -1
  66. package/dist/widgets/spread.js +110 -94
  67. package/dist/widgets/spread.js.map +1 -1
  68. package/dist/widgets/stores.js +5 -2
  69. package/dist/widgets/stores.js.map +1 -1
  70. package/dist/widgets/table.js +422 -436
  71. package/dist/widgets/table.js.map +1 -1
  72. package/dist/widgets/timeseries.js +52 -46
  73. package/dist/widgets/timeseries.js.map +1 -1
  74. package/dist/widgets/toolbar-actions.js +101 -6693
  75. package/dist/widgets/toolbar-actions.js.map +1 -1
  76. package/dist/widgets/utils.js +16 -14
  77. package/dist/widgets/utils.js.map +1 -1
  78. package/dist/widgets/wrapper.js +156 -158
  79. package/dist/widgets/wrapper.js.map +1 -1
  80. package/dist/widgets.js +4 -4
  81. package/package.json +5 -4
  82. package/src/hooks/use-widget-ref.ts +3 -4
  83. package/src/widgets/README.md +3 -3
  84. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +60 -79
  85. package/src/widgets/actions/brush-toggle/types.ts +8 -2
  86. package/src/widgets/actions/change-column/change-column.tsx +15 -15
  87. package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
  88. package/src/widgets/actions/download/download.tsx +4 -3
  89. package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
  90. package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
  91. package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
  92. package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
  93. package/src/widgets/actions/searcher/searcher.tsx +20 -21
  94. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
  95. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
  96. package/src/widgets/bar/config.ts +22 -14
  97. package/src/widgets/category/category-ui.tsx +31 -27
  98. package/src/widgets/category/components/category-row-multi.tsx +6 -2
  99. package/src/widgets/category/components/category-row-single.tsx +5 -1
  100. package/src/widgets/category/types.ts +1 -0
  101. package/src/widgets/echart/echart-ui.test.tsx +20 -16
  102. package/src/widgets/echart/echart-ui.tsx +6 -12
  103. package/src/widgets/echart/echart.tsx +13 -27
  104. package/src/widgets/echart/shared-resize-observer.ts +45 -0
  105. package/src/widgets/echart/types.ts +2 -0
  106. package/src/widgets/error/error.tsx +7 -9
  107. package/src/widgets/formula/components/prefix.tsx +4 -6
  108. package/src/widgets/formula/components/row.tsx +4 -4
  109. package/src/widgets/formula/components/series.tsx +4 -6
  110. package/src/widgets/formula/components/suffix.tsx +4 -6
  111. package/src/widgets/formula/components/value.tsx +9 -16
  112. package/src/widgets/histogram/config.ts +101 -20
  113. package/src/widgets/histogram/index.ts +6 -1
  114. package/src/widgets/histogram/types.ts +9 -3
  115. package/src/widgets/loader/loader.tsx +31 -44
  116. package/src/widgets/markdown/markdown.tsx +4 -7
  117. package/src/widgets/no-data/no-data.tsx +7 -10
  118. package/src/widgets/pie/config.ts +17 -5
  119. package/src/widgets/range/components/range-item.tsx +20 -18
  120. package/src/widgets/scatterplot/config.ts +8 -3
  121. package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
  122. package/src/widgets/spread/components/max-value.tsx +14 -16
  123. package/src/widgets/spread/components/min-value.tsx +14 -16
  124. package/src/widgets/stores/index.ts +2 -1
  125. package/src/widgets/stores/types.ts +2 -0
  126. package/src/widgets/stores/use-widget-selector.ts +47 -0
  127. package/src/widgets/stores/widget-store-performance.test.ts +750 -0
  128. package/src/widgets/stores/widget-store.test.ts +81 -0
  129. package/src/widgets/stores/widget-store.ts +225 -44
  130. package/src/widgets/table/config.ts +0 -1
  131. package/src/widgets/table/hooks/use-pagination.ts +28 -52
  132. package/src/widgets/table/hooks/use-selection.ts +20 -24
  133. package/src/widgets/table/hooks/use-sort.ts +22 -39
  134. package/src/widgets/table/types.ts +1 -1
  135. package/src/widgets/timeseries/config.ts +21 -13
  136. package/src/widgets/utils/chart-config/index.ts +1 -1
  137. package/src/widgets/utils/chart-config/option-builders.ts +22 -12
  138. package/src/widgets/utils/formatter.ts +2 -1
  139. package/src/widgets/utils/index.ts +1 -1
  140. package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
  141. package/src/widgets/wrapper/wrapper.tsx +4 -6
  142. package/dist/error-CEkRPccv.js +0 -39
  143. package/dist/error-CEkRPccv.js.map +0 -1
  144. package/dist/formatter-B1Xh8XDH.js +0 -5
  145. package/dist/formatter-B1Xh8XDH.js.map +0 -1
  146. package/dist/no-data-hR3KcJ-_.js +0 -60
  147. package/dist/no-data-hR3KcJ-_.js.map +0 -1
  148. package/dist/row-DTCV0Ocm.js +0 -35
  149. package/dist/row-DTCV0Ocm.js.map +0 -1
  150. package/dist/series-CYNOu2Ju.js +0 -91
  151. package/dist/series-CYNOu2Ju.js.map +0 -1
  152. package/dist/styles-C_8vOEep.js +0 -167
  153. package/dist/styles-C_8vOEep.js.map +0 -1
  154. package/dist/use-widget-ref-wtFLDFCD.js +0 -25
  155. package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
  156. package/dist/widget-store-CzDt8oSK.js +0 -163
  157. package/dist/widget-store-CzDt8oSK.js.map +0 -1
@@ -1,17 +1,13 @@
1
1
  import { Box, IconButton } from '@mui/material'
2
2
  import { HighlightAltOutlined } from '@mui/icons-material'
3
3
  import { useEffect, useCallback, useRef } from 'react'
4
- import { useWidgetStore } from '../../stores/widget-store'
5
- import type { BrushToggleProps } from './types'
4
+ import { widgetStoreActions } from '../../stores/widget-store'
5
+ import type { BrushSelectedItems, BrushToggleProps } from './types'
6
6
  import { styles } from './style'
7
7
  import { Tooltip } from '../../../components'
8
8
  import { getEChartBrushConfig } from '../../echart/utils'
9
- import type {
10
- EchartOptionsProps,
11
- EchartWidgetData,
12
- EchartWidgetState,
13
- } from '../../echart/types'
14
- import { useShallow } from 'zustand/shallow'
9
+ import type { EchartOptionsProps, EchartWidgetState } from '../../echart/types'
10
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
15
11
 
16
12
  export const BRUSH_TOGGLE_TOOL_ID = 'brush-toggle'
17
13
 
@@ -40,26 +36,18 @@ export function BrushToggle({
40
36
  Icon,
41
37
  IconButtonProps,
42
38
  }: BrushToggleProps) {
43
- const getWidget = useWidgetStore((state) => state.getWidget)
44
- const registerTool = useWidgetStore((state) => state.registerTool)
45
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
46
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
47
- const selected = useRef<(string | number)[]>([])
48
-
49
- const brushTool = useWidgetStore(
50
- useShallow((state) => {
51
- const tools = state.getWidget(id)?.registeredTools ?? []
52
- return tools.find((tool) => tool.id === BRUSH_TOGGLE_TOOL_ID)
53
- }),
54
- )
39
+ const selected = useRef<BrushSelectedItems>({ dataIndex: [], seriesIndex: 0 })
55
40
 
56
- const brush = brushTool?.enabled
41
+ const { brush } = useWidgetSelector(id, (w) => ({
42
+ brush: w?.registeredTools?.find((tool) => tool.id === BRUSH_TOGGLE_TOOL_ID)
43
+ ?.enabled,
44
+ }))
57
45
 
58
46
  const toggleTool = useCallback(
59
47
  (value: boolean) => {
60
- setToolEnabled(id, BRUSH_TOGGLE_TOOL_ID, value)
48
+ widgetStoreActions.setToolEnabled(id, BRUSH_TOGGLE_TOOL_ID, value)
61
49
  },
62
- [id, setToolEnabled],
50
+ [id],
63
51
  )
64
52
 
65
53
  const handleToggle = useCallback(() => {
@@ -67,7 +55,7 @@ export function BrushToggle({
67
55
  toggleTool(newBrush)
68
56
 
69
57
  if (newBrush) {
70
- onBrushSelected?.([])
58
+ onBrushSelected?.({ dataIndex: [], seriesIndex: 0 }) // Clear selection when enabling brush
71
59
  }
72
60
  }, [brush, onBrushSelected, toggleTool])
73
61
 
@@ -75,7 +63,8 @@ export function BrushToggle({
75
63
  useEffect(() => {
76
64
  if (!brush) return
77
65
 
78
- const instance = getWidget<EchartWidgetState>(id)?.instance?.current
66
+ const instance =
67
+ widgetStoreActions.getWidget<EchartWidgetState>(id)?.instance?.current
79
68
  if (!instance) return
80
69
 
81
70
  const handleRendered = () => {
@@ -99,64 +88,65 @@ export function BrushToggle({
99
88
  return () => {
100
89
  instance.off('rendered', handleRendered)
101
90
  }
102
- }, [brush, getWidget, id])
91
+ }, [brush, id])
103
92
 
104
93
  // Handle brushSelected event to capture selected bar indices
105
- const handleBrushSelected = useCallback(
106
- (event: unknown) => {
107
- const brushEvent = event as {
108
- batch?: {
109
- selected?: {
110
- dataIndex?: number[]
111
- seriesIndex?: number
112
- }[]
94
+ const handleBrushSelected = useCallback((event: unknown) => {
95
+ const brushEvent = event as {
96
+ batch?: {
97
+ selected?: {
98
+ dataIndex?: number[]
99
+ seriesIndex?: number
113
100
  }[]
101
+ }[]
102
+ }
103
+
104
+ const allSelected =
105
+ brushEvent.batch?.flatMap((batchItem) => batchItem.selected ?? []) ?? []
106
+
107
+ if (!allSelected.length) {
108
+ selected.current = {
109
+ dataIndex: [],
110
+ seriesIndex: 0,
114
111
  }
112
+ return
113
+ }
115
114
 
116
- // Resolve names from the widget dataset
117
- const widget = getWidget(id)
118
- const data = (widget?.data ?? []) as EchartWidgetData
119
-
120
- const items = [
121
- ...new Set(
122
- brushEvent.batch?.flatMap(
123
- (batch) =>
124
- batch.selected?.flatMap(
125
- (sel) =>
126
- sel.dataIndex?.map((dataIndex) => {
127
- const row = data[sel.seriesIndex ?? 0]?.[dataIndex]
128
- const firstValue = row ? Object.values(row)[0] : undefined
129
- const name =
130
- typeof firstValue === 'string' ||
131
- typeof firstValue === 'number'
132
- ? String(firstValue)
133
- : String(dataIndex)
134
- return name
135
- }) ?? [],
136
- ) ?? [],
137
- ),
138
- ),
139
- ]
140
-
141
- selected.current = items ?? []
142
- },
143
- [getWidget, id],
144
- )
115
+ // Use the first seriesIndex as the primary one (matches previous behavior)
116
+ const primarySeriesIndex = allSelected[0]?.seriesIndex ?? 0
117
+
118
+ const mergedDataIndex = Array.from(
119
+ new Set(
120
+ allSelected
121
+ .filter(
122
+ (item) =>
123
+ item.seriesIndex === undefined ||
124
+ item.seriesIndex === primarySeriesIndex,
125
+ )
126
+ .flatMap((item) => item.dataIndex ?? []),
127
+ ),
128
+ )
129
+
130
+ selected.current = {
131
+ dataIndex: mergedDataIndex,
132
+ seriesIndex: primarySeriesIndex,
133
+ }
134
+ }, [])
145
135
 
146
136
  const handleBrushEnd = useCallback(() => {
147
137
  onBrushSelected?.(selected.current)
148
138
  toggleTool(false) // Disable brush after selection is made
149
139
  }, [onBrushSelected, toggleTool])
150
140
 
151
- // Register config tool on mount
141
+ // Register config tool with all reactive deps — store's no-op detection handles performance
152
142
  useEffect(() => {
153
- const existingTool = getWidget(id)?.registeredTools?.find(
154
- (tool) => tool.id === BRUSH_TOGGLE_TOOL_ID,
155
- )
143
+ const existingTool = widgetStoreActions
144
+ .getWidget(id)
145
+ ?.registeredTools?.find((tool) => tool.id === BRUSH_TOGGLE_TOOL_ID)
156
146
 
157
147
  const initialEnabled = existingTool?.enabled ?? false
158
148
 
159
- registerTool(id, {
149
+ widgetStoreActions.registerTool(id, {
160
150
  id: BRUSH_TOGGLE_TOOL_ID,
161
151
  type: 'config',
162
152
  order: 25,
@@ -169,8 +159,6 @@ export function BrushToggle({
169
159
 
170
160
  const brushConfig = getEChartBrushConfig()
171
161
 
172
- const onEventsWithoutBrush = { ...currentOnEvents }
173
- delete onEventsWithoutBrush.brushSelected
174
162
  const onEvents = {
175
163
  ...currentOnEvents,
176
164
  brushSelected: handleBrushSelected,
@@ -187,15 +175,8 @@ export function BrushToggle({
187
175
  }
188
176
  },
189
177
  })
190
- return () => unregisterTool(id, BRUSH_TOGGLE_TOOL_ID)
191
- }, [
192
- getWidget,
193
- handleBrushEnd,
194
- handleBrushSelected,
195
- id,
196
- registerTool,
197
- unregisterTool,
198
- ])
178
+ return () => widgetStoreActions.unregisterTool(id, BRUSH_TOGGLE_TOOL_ID)
179
+ }, [id, handleBrushSelected, handleBrushEnd])
199
180
 
200
181
  const enableLabel = labels?.enable ?? 'Enable brush selection'
201
182
  const disableLabel = labels?.disable ?? 'Disable brush selection'
@@ -3,9 +3,15 @@ import type { ReactNode } from 'react'
3
3
  import type { BaseWidgetState } from '../../stores/types'
4
4
 
5
5
  /**
6
- * Represents a single item selected by the brush
6
+ * Brush selection result emitted by BrushToggle.
7
+ * Contains raw indices so consumers can resolve data according to their widget type.
7
8
  */
8
- export type BrushSelectedItems = (string | number)[]
9
+ export interface BrushSelectedItems {
10
+ /** Data indices of the selected items in the dataset */
11
+ dataIndex: number[]
12
+ /** Series index from the brush event (defaults to 0) */
13
+ seriesIndex: number
14
+ }
9
15
 
10
16
  /**
11
17
  * State stored in widget store for brush functionality
@@ -21,14 +21,14 @@ import {
21
21
  useState,
22
22
  type MouseEvent,
23
23
  } from 'react'
24
- import { useWidgetStore } from '../../stores/widget-store'
24
+ import { widgetStoreActions } from '../../stores/widget-store'
25
25
  import type { ChangeColumnProps } from './types'
26
26
  import { actionButtonStyles } from '../shared/styles'
27
27
  import { Tooltip } from '../../../components'
28
28
  import type { TableColumn, TableWidgetState } from '../../table/types'
29
29
  import { ChangeColumnIcon } from './change-column-icon'
30
30
  import { SortableColumnItem } from './sortable-column-item'
31
- import { useShallow } from 'zustand/shallow'
31
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
32
32
 
33
33
  export const CHANGE_COLUMN_TOOL_ID = 'change-column'
34
34
 
@@ -57,12 +57,9 @@ export function ChangeColumn({
57
57
  MenuProps,
58
58
  }: ChangeColumnProps) {
59
59
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
60
- const setWidget = useWidgetStore((state) => state.setWidget)
61
- const registerTool = useWidgetStore((state) => state.registerTool)
62
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
63
- const columns = useWidgetStore(
64
- useShallow((state) => state.getWidget<TableWidgetState>(id)?.columns),
65
- )
60
+ const { columns } = useWidgetSelector(id, (w) => ({
61
+ columns: (w as TableWidgetState | undefined)?.columns,
62
+ }))
66
63
 
67
64
  /**
68
65
  * Config tool function that reorders columns to match the current widget state.
@@ -72,9 +69,7 @@ export function ChangeColumn({
72
69
  */
73
70
  const reorderFn = useCallback(
74
71
  (currentConfig: unknown): unknown => {
75
- const widgetState = useWidgetStore
76
- .getState()
77
- .getWidget<TableWidgetState>(id)
72
+ const widgetState = widgetStoreActions.getWidget<TableWidgetState>(id)
78
73
  const currentColumns = widgetState?.columns
79
74
  if (!currentColumns || currentColumns.length === 0) return currentConfig
80
75
 
@@ -130,15 +125,15 @@ export function ChangeColumn({
130
125
 
131
126
  // Register config tool on mount
132
127
  useEffect(() => {
133
- registerTool(id, {
128
+ widgetStoreActions.registerTool(id, {
134
129
  id: CHANGE_COLUMN_TOOL_ID,
135
130
  type: 'config',
136
131
  order: 100,
137
132
  enabled: true,
138
133
  fn: reorderFn,
139
134
  })
140
- return () => unregisterTool(id, CHANGE_COLUMN_TOOL_ID)
141
- }, [id, registerTool, unregisterTool, reorderFn])
135
+ return () => widgetStoreActions.unregisterTool(id, CHANGE_COLUMN_TOOL_ID)
136
+ }, [id, reorderFn])
142
137
 
143
138
  const handleToggle = useCallback((event: MouseEvent<HTMLElement>) => {
144
139
  event.stopPropagation()
@@ -156,7 +151,7 @@ export function ChangeColumn({
156
151
  const newIndex = columns.findIndex((col) => col.id === over.id)
157
152
  if (oldIndex !== -1 && newIndex !== -1) {
158
153
  const newColumns = arrayMove(columns, oldIndex, newIndex)
159
- setWidget(id, { columns: newColumns })
154
+ widgetStoreActions.setWidget(id, { columns: newColumns })
160
155
  }
161
156
  }
162
157
 
@@ -201,6 +196,11 @@ export function ChangeColumn({
201
196
  onClose={handleClose}
202
197
  anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
203
198
  transformOrigin={{ vertical: 'top', horizontal: 'right' }}
199
+ slotProps={{
200
+ paper: {
201
+ sx: { overflow: 'hidden' },
202
+ },
203
+ }}
204
204
  {...MenuProps}
205
205
  >
206
206
  <SortableContext
@@ -22,7 +22,9 @@ export function SortableColumnItem({ column }: SortableColumnItemProps) {
22
22
  } = useSortable({ id: column.id })
23
23
 
24
24
  const style = {
25
- transform: CSS.Transform.toString(transform),
25
+ transform: CSS.Transform.toString(
26
+ transform ? { ...transform, x: 0 } : null,
27
+ ),
26
28
  transition,
27
29
  opacity: isDragging ? 0.5 : 1,
28
30
  cursor: isDragging ? 'grabbing' : 'grab',
@@ -9,8 +9,7 @@ import {
9
9
  import type { DownloadItem, DownloadProps } from './types'
10
10
  import { FileDownloadOutlined } from '@mui/icons-material'
11
11
  import { useState, type MouseEvent } from 'react'
12
- import { useWidgetStore } from '../../../widgets'
13
- import { useShallow } from 'zustand/shallow'
12
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
14
13
 
15
14
  const EMPTY_LABELS: NonNullable<DownloadProps['labels']> = {}
16
15
 
@@ -32,7 +31,9 @@ export function Download({
32
31
  Icon,
33
32
  IconButtonProps,
34
33
  }: DownloadProps) {
35
- const data = useWidgetStore(useShallow((state) => state.getWidget(id)?.data))
34
+ const { data } = useWidgetSelector(id, (w) => ({
35
+ data: w?.data,
36
+ }))
36
37
 
37
38
  const [isDownloading, setIsDownloading] = useState(false)
38
39
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
@@ -5,7 +5,8 @@ import {
5
5
  IconButton,
6
6
  Typography,
7
7
  } from '@mui/material'
8
- import { useWidgetStore } from '../../stores/widget-store'
8
+ import { widgetStoreActions } from '../../stores/widget-store'
9
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
9
10
  import { Close, FullscreenOutlined } from '@mui/icons-material'
10
11
  import type {
11
12
  FullScreenConfig,
@@ -13,7 +14,6 @@ import type {
13
14
  FullScreenState,
14
15
  } from './types'
15
16
  import { styles } from './styles'
16
- import { useShallow } from 'zustand/shallow'
17
17
 
18
18
  const EMPTY_DIALOG_CONTENT_PROPS: NonNullable<
19
19
  FullScreenProps['DialogContentProps']
@@ -44,16 +44,12 @@ export function FullScreen({
44
44
  } = EMPTY_DIALOG_CONTENT_PROPS,
45
45
  DialogProps,
46
46
  }: FullScreenProps) {
47
- const isFullScreen = useWidgetStore(
48
- useShallow((state) => state.getWidget<FullScreenState>(id)?.isFullScreen),
49
- )
50
- const title = useWidgetStore(
51
- useShallow((state) => state.getWidget<FullScreenState>(id)?.title),
52
- )
53
- const setWidget = useWidgetStore((state) => state.setWidget)
54
-
47
+ const { isFullScreen, title } = useWidgetSelector(id, (w) => ({
48
+ isFullScreen: (w as FullScreenState | undefined)?.isFullScreen,
49
+ title: (w as FullScreenState | undefined)?.title,
50
+ }))
55
51
  const updateFullScreenConfig = (updates: Partial<FullScreenConfig>) => {
56
- setWidget<FullScreenState>(id, {
52
+ widgetStoreActions.setWidget<FullScreenState>(id, {
57
53
  isFullScreen: updates.isFullScreen,
58
54
  })
59
55
  }
@@ -4,8 +4,8 @@ import { useCallback, useEffect, useMemo } from 'react'
4
4
  import type { LockSelectionProps, LockSelectionState } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
7
- import { useWidgetStore } from '../../stores/widget-store'
8
- import { useShallow } from 'zustand/shallow'
7
+ import { widgetStoreActions } from '../../stores/widget-store'
8
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
9
9
  import type { EchartWidgetData } from '../../echart/types'
10
10
 
11
11
  export const LOCK_SELECTION_TOOL_ID = 'lock-selection'
@@ -34,12 +34,9 @@ export function LockSelection({
34
34
  Icon,
35
35
  IconButtonProps,
36
36
  }: LockSelectionProps) {
37
- const setWidget = useWidgetStore((state) => state.setWidget)
38
- const registerTool = useWidgetStore((state) => state.registerTool)
39
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
- const storeIsLocked = useWidgetStore(
41
- useShallow((state) => state.getWidget<LockSelectionState>(id)?.isLocked),
42
- )
37
+ const { storeIsLocked } = useWidgetSelector(id, (w) => ({
38
+ storeIsLocked: (w as LockSelectionState | undefined)?.isLocked,
39
+ }))
43
40
 
44
41
  const isLocked = storeIsLocked ?? false
45
42
  const lockedItems = useMemo(
@@ -47,9 +44,9 @@ export function LockSelection({
47
44
  [isLocked, selectedItems],
48
45
  )
49
46
 
50
- // Register tool on mount
47
+ // Register tool with all reactive deps — store's no-op detection handles performance
51
48
  useEffect(() => {
52
- registerTool(id, {
49
+ widgetStoreActions.registerTool(id, {
53
50
  id: LOCK_SELECTION_TOOL_ID,
54
51
  order,
55
52
  enabled: isLocked,
@@ -62,24 +59,24 @@ export function LockSelection({
62
59
  config: { lockedItems },
63
60
  })
64
61
 
65
- return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
66
- }, [id, order, registerTool, unregisterTool, isLocked, lockedItems])
62
+ return () => widgetStoreActions.unregisterTool(id, LOCK_SELECTION_TOOL_ID)
63
+ }, [id, order, isLocked, lockedItems])
67
64
 
68
65
  const handleToggle = useCallback(() => {
69
66
  if (isLocked) {
70
67
  // Unlock: clear locked items and disable tool
71
- setWidget(id, {
68
+ widgetStoreActions.setWidget(id, {
72
69
  isLocked: false,
73
70
  lockedItems: [],
74
71
  })
75
72
  } else {
76
73
  // Lock: save selected items and enable tool
77
- setWidget(id, {
74
+ widgetStoreActions.setWidget(id, {
78
75
  isLocked: true,
79
76
  lockedItems: selectedItems,
80
77
  })
81
78
  }
82
- }, [id, isLocked, selectedItems, setWidget])
79
+ }, [id, isLocked, selectedItems])
83
80
 
84
81
  // Don't render if no selections
85
82
  if (selectedItems.length === 0) {
@@ -1,7 +1,8 @@
1
1
  import { IconButton } from '@mui/material'
2
2
  import { PercentOutlined } from '@mui/icons-material'
3
3
  import { useCallback, useEffect, useRef } from 'react'
4
- import { useWidgetStore } from '../../stores/widget-store'
4
+ import { widgetStoreActions } from '../../stores/widget-store'
5
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
5
6
  import type { RelativeDataProps } from './types'
6
7
  import { actionButtonStyles } from '../shared/styles'
7
8
  import { Tooltip } from '../../../components'
@@ -37,24 +38,18 @@ export function RelativeData({
37
38
  const percentFormatterRef = useRef<((value: number) => string) | undefined>(
38
39
  undefined,
39
40
  )
40
- const getWidget = useWidgetStore((state) => state.getWidget)
41
- const registerTool = useWidgetStore((state) => state.registerTool)
42
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
43
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
44
- const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
45
41
 
46
- const storeIsRelative = useWidgetStore(
47
- (state) =>
48
- state.widgets[id]?.registeredTools?.find(
49
- (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
50
- )?.config?.isRelative as boolean | undefined,
51
- )
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,
46
+ }))
52
47
 
53
48
  const isRelative = storeIsRelative ?? defaultIsRelative
54
49
 
55
- // Register tool on mount
50
+ // Register data tool with all reactive deps — store's no-op detection handles performance
56
51
  useEffect(() => {
57
- registerTool(id, {
52
+ widgetStoreActions.registerTool(id, {
58
53
  id: RELATIVE_DATA_TOOL_ID,
59
54
  order,
60
55
  enabled: defaultIsRelative,
@@ -65,17 +60,17 @@ export function RelativeData({
65
60
  },
66
61
  })
67
62
 
68
- return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
69
- }, [id, order, registerTool, unregisterTool, defaultIsRelative])
63
+ return () => widgetStoreActions.unregisterTool(id, RELATIVE_DATA_TOOL_ID)
64
+ }, [id, order, defaultIsRelative])
70
65
 
71
- // Register config tool for formatter management
66
+ // Register config tool with all reactive deps — store's no-op detection handles performance
72
67
  useEffect(() => {
73
- registerTool(id, {
68
+ widgetStoreActions.registerTool(id, {
74
69
  id: RELATIVE_DATA_CONFIG_TOOL_ID,
75
70
  type: 'config',
76
71
  order,
77
72
  enabled: true,
78
- fn: (currentConfig, toolConfig) => {
73
+ fn: (currentConfig: unknown, toolConfig?: Record<string, unknown>) => {
79
74
  const config = currentConfig as Record<string, unknown>
80
75
  if (toolConfig?.isRelative) {
81
76
  if (!percentFormatterRef.current) {
@@ -104,20 +99,21 @@ export function RelativeData({
104
99
  isRelative: defaultIsRelative,
105
100
  },
106
101
  })
107
- return () => unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID)
108
- }, [id, order, registerTool, unregisterTool, defaultIsRelative])
102
+ return () =>
103
+ widgetStoreActions.unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID)
104
+ }, [id, order, defaultIsRelative])
109
105
 
110
106
  const handleToggle = useCallback(() => {
111
107
  const newIsRelative = !isRelative
112
- setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
108
+ widgetStoreActions.setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
113
109
 
114
110
  if (newIsRelative) {
115
- const widget = getWidget(id) as {
111
+ const widget = widgetStoreActions.getWidget(id) as {
116
112
  formatter?: (value: number) => string
117
113
  locale?: string
118
114
  max?: number
119
115
  }
120
- updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
116
+ widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
121
117
  isRelative: true,
122
118
  originalFormatter: widget?.formatter,
123
119
  originalMax: widget?.max,
@@ -125,11 +121,11 @@ export function RelativeData({
125
121
  })
126
122
  } else {
127
123
  percentFormatterRef.current = undefined
128
- updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
124
+ widgetStoreActions.updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
129
125
  isRelative: false,
130
126
  })
131
127
  }
132
- }, [isRelative, id, getWidget, setToolEnabled, updateToolConfig])
128
+ }, [isRelative, id])
133
129
 
134
130
  const tooltipLabel = isRelative
135
131
  ? (labels?.absolute ?? 'Show absolute values')
@@ -4,8 +4,8 @@ import { useCallback, useEffect } from 'react'
4
4
  import type { SearcherToggleProps, SearcherState } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
7
- import { useWidgetStore } from '../../stores/widget-store'
8
- import { useShallow } from 'zustand/shallow'
7
+ import { widgetStoreActions } from '../../stores/widget-store'
8
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
9
9
 
10
10
  /**
11
11
  * Widget action button to toggle search functionality.
@@ -30,25 +30,24 @@ export function SearcherToggle({
30
30
  Icon,
31
31
  IconButtonProps,
32
32
  }: SearcherToggleProps) {
33
- const setWidget = useWidgetStore((state) => state.setWidget)
34
- const getWidget = useWidgetStore((state) => state.getWidget)
35
- const storeIsEnabled = useWidgetStore(
36
- useShallow((state) => state.getWidget<SearcherState>(id)?.isSearchEnabled),
37
- )
33
+ const { storeIsEnabled } = useWidgetSelector(id, (w) => ({
34
+ storeIsEnabled: (w as SearcherState | undefined)?.isSearchEnabled,
35
+ }))
38
36
 
39
37
  const isEnabled = storeIsEnabled ?? defaultEnabled
40
38
 
41
39
  // Initialize store with default value on mount
42
40
  useEffect(() => {
43
- const currentValue = getWidget<SearcherState>(id)?.isSearchEnabled
41
+ const currentValue =
42
+ widgetStoreActions.getWidget<SearcherState>(id)?.isSearchEnabled
44
43
  if (currentValue === undefined) {
45
- setWidget(id, { isSearchEnabled: defaultEnabled })
44
+ widgetStoreActions.setWidget(id, { isSearchEnabled: defaultEnabled })
46
45
  }
47
- }, [defaultEnabled, getWidget, id, setWidget])
46
+ }, [defaultEnabled, id])
48
47
 
49
48
  const handleToggle = useCallback(() => {
50
- setWidget(id, { isSearchEnabled: !isEnabled })
51
- }, [id, isEnabled, setWidget])
49
+ widgetStoreActions.setWidget(id, { isSearchEnabled: !isEnabled })
50
+ }, [id, isEnabled])
52
51
 
53
52
  const tooltipLabel = isEnabled
54
53
  ? (labels?.disable ?? 'Disable search')