@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,7 +1,8 @@
1
1
  import { TextField, InputAdornment, IconButton } from '@mui/material'
2
2
  import { ClearOutlined, SearchOutlined } from '@mui/icons-material'
3
3
  import { useEffect, useRef, useCallback } 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 { SearcherProps, SearcherFilterFn, SearcherState } from './types'
6
7
  import type { EchartWidgetData } from '../../echart/types'
7
8
  import { LOCK_SELECTION_TOOL_ID } from '../lock-selection/lock-selection'
@@ -41,30 +42,24 @@ export function Searcher({
41
42
  const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
42
43
 
43
44
  // Read enabled state and search text from widget store
44
- const widgetState = useWidgetStore((state) =>
45
- state.getWidget<SearcherState>(id),
46
- )
47
- const enabled = widgetState?.isSearchEnabled ?? false
48
- const searchText = widgetState?.searchText ?? ''
45
+ const { enabled, searchText } = useWidgetSelector(id, (w) => ({
46
+ enabled: (w as SearcherState | undefined)?.isSearchEnabled ?? false,
47
+ searchText: (w as SearcherState | undefined)?.searchText ?? '',
48
+ }))
49
49
  const prevEnabledRef = useRef(enabled)
50
50
 
51
51
  const filter = filterFn ?? defaultFilterFn
52
52
 
53
- const setWidget = useWidgetStore((state) => state.setWidget)
54
- const registerTool = useWidgetStore((state) => state.registerTool)
55
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
56
- const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
57
-
58
53
  const setSearchText = useCallback(
59
54
  (text: string) => {
60
- setWidget(id, { searchText: text })
55
+ widgetStoreActions.setWidget(id, { searchText: text })
61
56
  },
62
- [id, setWidget],
57
+ [id],
63
58
  )
64
59
 
65
- // Register tool on mount
60
+ // Register tool with all reactive deps — store's no-op detection handles performance
66
61
  useEffect(() => {
67
- registerTool(id, {
62
+ widgetStoreActions.registerTool(id, {
68
63
  id: SEARCHER_TOOL_ID,
69
64
  order,
70
65
  enabled,
@@ -81,8 +76,8 @@ export function Searcher({
81
76
  disables: [LOCK_SELECTION_TOOL_ID],
82
77
  })
83
78
 
84
- return () => unregisterTool(id, SEARCHER_TOOL_ID)
85
- }, [id, order, filter, registerTool, unregisterTool, enabled, searchText])
79
+ return () => widgetStoreActions.unregisterTool(id, SEARCHER_TOOL_ID)
80
+ }, [id, order, enabled, searchText, filter])
86
81
 
87
82
  // Update config when search text changes (debounced)
88
83
  const debouncedUpdateConfig = useCallback(
@@ -91,10 +86,12 @@ export function Searcher({
91
86
  clearTimeout(debounceTimeoutRef.current)
92
87
  }
93
88
  debounceTimeoutRef.current = setTimeout(() => {
94
- updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: text })
89
+ widgetStoreActions.updateToolConfig(id, SEARCHER_TOOL_ID, {
90
+ searchText: text,
91
+ })
95
92
  }, debounceDelay)
96
93
  },
97
- [id, updateToolConfig, debounceDelay],
94
+ [id, debounceDelay],
98
95
  )
99
96
 
100
97
  // Auto-focus when enabled becomes true
@@ -127,11 +124,13 @@ export function Searcher({
127
124
 
128
125
  const handleClear = useCallback(() => {
129
126
  setSearchText('')
130
- updateToolConfig(id, SEARCHER_TOOL_ID, { searchText: '' })
127
+ widgetStoreActions.updateToolConfig(id, SEARCHER_TOOL_ID, {
128
+ searchText: '',
129
+ })
131
130
  if (inputRef.current) {
132
131
  inputRef.current.focus()
133
132
  }
134
- }, [id, setSearchText, updateToolConfig])
133
+ }, [id, setSearchText])
135
134
 
136
135
  if (!enabled) {
137
136
  return null
@@ -1,6 +1,6 @@
1
1
  import { IconButton } from '@mui/material'
2
2
  import { useCallback, useEffect, useMemo } from 'react'
3
- import { useWidgetStore } from '../../stores/widget-store'
3
+ import { widgetStoreActions } from '../../stores/widget-store'
4
4
  import type { StackToggleProps, StackToggleState } from './types'
5
5
  import { actionButtonStyles } from '../shared/styles'
6
6
  import { Tooltip } from '../../../components'
@@ -9,7 +9,7 @@ import { getEChartStackConfig } from '../../echart/utils'
9
9
  import { DEFAULT_STACK_GROUP } from '../../echart/const'
10
10
  import type { EchartWidgetState } from '../../echart/types'
11
11
  import type { EchartOptionsProps } from '../../echart/types'
12
- import { useShallow } from 'zustand/shallow'
12
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
13
13
 
14
14
  export const STACK_TOGGLE_TOOL_ID = 'stack-toggle'
15
15
 
@@ -34,16 +34,10 @@ export function StackToggle({
34
34
  Icon,
35
35
  IconButtonProps,
36
36
  }: StackToggleProps) {
37
- const setWidget = useWidgetStore((state) => state.setWidget)
38
- const registerTool = useWidgetStore((state) => state.registerTool)
39
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
- const storeIsStacked = useWidgetStore(
41
- useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
42
- )
43
-
44
- const option = useWidgetStore(
45
- useShallow((state) => state.getWidget<EchartWidgetState>(id)?.option),
46
- )
37
+ const { storeIsStacked, option } = useWidgetSelector(id, (w) => ({
38
+ storeIsStacked: (w as StackToggleState | undefined)?.isStacked,
39
+ option: (w as EchartWidgetState | undefined)?.option,
40
+ }))
47
41
 
48
42
  const { hasMultiSeries, hasStackInSeries } = useMemo(() => {
49
43
  if (!option) return { hasMultiSeries: false, hasStackInSeries: false }
@@ -60,14 +54,14 @@ export function StackToggle({
60
54
  const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
61
55
  const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
62
56
 
63
- // Register config tool on mount
57
+ // Register config tool with all reactive deps — store's no-op detection handles performance
64
58
  useEffect(() => {
65
- registerTool(id, {
59
+ widgetStoreActions.registerTool(id, {
66
60
  id: STACK_TOGGLE_TOOL_ID,
67
61
  type: 'config',
68
62
  order: 10,
69
63
  enabled: isStacked && hasMultiSeries,
70
- fn: (currentConfig) => {
64
+ fn: (currentConfig: unknown) => {
71
65
  const config = currentConfig as Record<string, unknown>
72
66
  const option = config.option as EchartOptionsProps | undefined
73
67
  if (!option) return currentConfig
@@ -87,18 +81,18 @@ export function StackToggle({
87
81
  return { ...config, option: { ...option, series: updatedSeries } }
88
82
  },
89
83
  })
90
- return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
91
- }, [id, registerTool, unregisterTool, isStacked, hasMultiSeries])
84
+ return () => widgetStoreActions.unregisterTool(id, STACK_TOGGLE_TOOL_ID)
85
+ }, [id, isStacked, hasMultiSeries])
92
86
 
93
87
  // Initialize store with default value only if not already configured
94
88
  useEffect(() => {
95
89
  if (storeIsStacked !== undefined) return
96
- setWidget(id, { isStacked: effectiveDefaultIsStacked })
97
- }, [effectiveDefaultIsStacked, id, setWidget, storeIsStacked])
90
+ widgetStoreActions.setWidget(id, { isStacked: effectiveDefaultIsStacked })
91
+ }, [effectiveDefaultIsStacked, id, storeIsStacked])
98
92
 
99
93
  const handleToggle = useCallback(() => {
100
- setWidget(id, { isStacked: !isStacked })
101
- }, [isStacked, id, setWidget])
94
+ widgetStoreActions.setWidget(id, { isStacked: !isStacked })
95
+ }, [isStacked, id])
102
96
 
103
97
  const tooltipLabel = isStacked
104
98
  ? (labels?.unstacked ?? 'Disable stacking')
@@ -4,13 +4,13 @@ import {
4
4
  ZoomInOutlined,
5
5
  } from '@mui/icons-material'
6
6
  import { useEffect, useCallback } from 'react'
7
- import { useWidgetStore } from '../../stores/widget-store'
7
+ import { widgetStoreActions } from '../../stores/widget-store'
8
8
  import type { ZoomToggleProps } from './types'
9
9
  import { styles } from './style'
10
10
  import { Tooltip } from '../../../components'
11
11
  import { getEChartZoomConfig } from '../../echart/utils'
12
12
  import type { EchartOptionsProps } from '../../echart/types'
13
- import { useShallow } from 'zustand/shallow'
13
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
14
14
 
15
15
  export const ZOOM_TOGGLE_TOOL_ID = 'zoom-toggle'
16
16
 
@@ -44,24 +44,18 @@ export function ZoomToggle({
44
44
  IconButtonProps,
45
45
  }: ZoomToggleProps) {
46
46
  const theme = useTheme()
47
- const getWidget = useWidgetStore((state) => state.getWidget)
48
- const registerTool = useWidgetStore((state) => state.registerTool)
49
- const unregisterTool = useWidgetStore((state) => state.unregisterTool)
50
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
51
- const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
52
-
53
- const zoomTool = useWidgetStore(
54
- useShallow((state) => {
55
- const tools = state.getWidget(id)?.registeredTools ?? []
56
- return tools.find((tool) => tool.id === ZOOM_TOGGLE_TOOL_ID)
57
- }),
58
- )
59
47
 
60
- const zoom = zoomTool?.enabled ?? defaultZoom
61
- const zoomStart =
62
- (zoomTool?.config?.start as number | undefined) ?? defaultZoomStart
63
- const zoomEnd =
64
- (zoomTool?.config?.end as number | undefined) ?? defaultZoomEnd
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,
57
+ }
58
+ })
65
59
 
66
60
  // Handle dataZoom event to update zoom range in tool config
67
61
  const handleDataZoom = useCallback(
@@ -79,21 +73,21 @@ export function ZoomToggle({
79
73
  const end = zoomEvent.end ?? zoomEvent.batch?.[0]?.end
80
74
 
81
75
  if (start !== undefined && end !== undefined) {
82
- setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
83
- updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
76
+ widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
77
+ widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
84
78
  start,
85
79
  end,
86
80
  })
87
81
  }
88
82
  },
89
- [id, setToolEnabled, updateToolConfig],
83
+ [id],
90
84
  )
91
85
 
92
- // Register config tool on mount
86
+ // Register config tool with all reactive deps — store's no-op detection handles performance
93
87
  useEffect(() => {
94
- const existingTool = getWidget(id)?.registeredTools?.find(
95
- (tool) => tool.id === ZOOM_TOGGLE_TOOL_ID,
96
- )
88
+ const existingTool = widgetStoreActions
89
+ .getWidget(id)
90
+ ?.registeredTools?.find((tool) => tool.id === ZOOM_TOGGLE_TOOL_ID)
97
91
 
98
92
  const initialEnabled = existingTool?.enabled ?? defaultZoom
99
93
  const initialStart =
@@ -101,7 +95,7 @@ export function ZoomToggle({
101
95
  const initialEnd =
102
96
  (existingTool?.config?.end as number | undefined) ?? defaultZoomEnd
103
97
 
104
- registerTool(id, {
98
+ widgetStoreActions.registerTool(id, {
105
99
  id: ZOOM_TOGGLE_TOOL_ID,
106
100
  type: 'config',
107
101
  order: 20,
@@ -160,31 +154,21 @@ export function ZoomToggle({
160
154
  end: initialEnd,
161
155
  },
162
156
  })
163
- return () => unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
164
- }, [
165
- defaultZoom,
166
- defaultZoomEnd,
167
- defaultZoomStart,
168
- getWidget,
169
- handleDataZoom,
170
- id,
171
- registerTool,
172
- theme,
173
- unregisterTool,
174
- ])
157
+ return () => widgetStoreActions.unregisterTool(id, ZOOM_TOGGLE_TOOL_ID)
158
+ }, [id, theme, handleDataZoom, defaultZoom, defaultZoomStart, defaultZoomEnd])
175
159
 
176
160
  const handleToggle = () => {
177
161
  const newZoom = !zoom
178
- setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, newZoom)
179
- updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
162
+ widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, newZoom)
163
+ widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
180
164
  start: newZoom ? zoomStart : 0,
181
165
  end: newZoom ? zoomEnd : 100,
182
166
  })
183
167
  }
184
168
 
185
169
  const handleReset = () => {
186
- setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
187
- updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
170
+ widgetStoreActions.setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
171
+ widgetStoreActions.updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
188
172
  start: defaultZoomStart,
189
173
  end: defaultZoomEnd,
190
174
  })
@@ -11,6 +11,7 @@ import {
11
11
  createTooltipPositioner,
12
12
  createTooltipFormatter,
13
13
  createChartDownloadConfig,
14
+ applyXAxisFormatter,
14
15
  niceNum,
15
16
  } from '../utils/chart-config'
16
17
 
@@ -29,6 +30,7 @@ export function barConfig(props: BarConfig): BarWidgetConfig {
29
30
  type: 'bar',
30
31
  option: mergeEchartWidgetConfig(getCommonOptions(props), getOption(props)),
31
32
  formatter: props.formatter,
33
+ labelFormatter: props.labelFormatter,
32
34
  }
33
35
  }
34
36
 
@@ -36,6 +38,7 @@ function getOption({
36
38
  data = [],
37
39
  theme,
38
40
  formatter,
41
+ labelFormatter,
39
42
  }: BarConfig): EchartOptionsProps {
40
43
  const hasLegend = (data?.length ?? 0) > 1
41
44
 
@@ -43,21 +46,24 @@ function getOption({
43
46
  let niceMax = 1
44
47
 
45
48
  return {
46
- legend: buildLegendConfig(hasLegend),
49
+ legend: buildLegendConfig({ hasLegend, labelFormatter }),
47
50
  grid: buildGridConfig(hasLegend, theme),
48
- xAxis: {
49
- type: 'category',
50
- axisLine: {
51
- show: false,
52
- },
53
- axisTick: {
54
- show: false,
55
- },
56
- axisLabel: {
57
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
58
- margin: 0,
51
+ xAxis: applyXAxisFormatter(
52
+ {
53
+ type: 'category',
54
+ axisLine: {
55
+ show: false,
56
+ },
57
+ axisTick: {
58
+ show: false,
59
+ },
60
+ axisLabel: {
61
+ padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
62
+ margin: 0,
63
+ },
59
64
  },
60
- },
65
+ labelFormatter,
66
+ ),
61
67
  yAxis: {
62
68
  type: 'value' as const,
63
69
  min: (extent: { min: number }) => {
@@ -110,7 +116,9 @@ function getOption({
110
116
 
111
117
  const marker = typeof item.marker === 'string' ? item.marker : ''
112
118
  const seriesName = item.seriesName ? `${item.seriesName}: ` : ''
113
- const name = item.name ?? ''
119
+ const name = labelFormatter
120
+ ? String(labelFormatter(item.name ?? ''))
121
+ : (item.name ?? '')
114
122
 
115
123
  return { name, seriesName, marker, value: formattedValue }
116
124
  }),
@@ -1,5 +1,5 @@
1
1
  import { Box, useTheme } from '@mui/material'
2
- import { useWidgetStore } from '../stores/widget-store'
2
+ import { useWidgetSelector } from '../stores/use-widget-selector'
3
3
  import { styles } from './style'
4
4
  import type { CategoryUIProps, CategoryWidgetState } from './types'
5
5
  import {
@@ -8,41 +8,43 @@ import {
8
8
  CategoryRowOther,
9
9
  CategoryLegend,
10
10
  } from './components'
11
- import { useShallow } from 'zustand/shallow'
12
11
  import { useState } from 'react'
13
- import { defaultFormatter } from '../utils/formatter'
12
+ import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
14
13
 
15
14
  /**
16
15
  * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping.
17
16
  */
18
17
  export function CategoryUI({ id }: CategoryUIProps) {
19
18
  const theme = useTheme()
20
- const _formatter = useWidgetStore(
21
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.formatter),
22
- )
23
- const _series = useWidgetStore(
24
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.series),
25
- )
26
- const data = useWidgetStore(
27
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.data),
28
- )
29
- const maxItems = useWidgetStore(
30
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.maxItems),
31
- )
32
- const labels = useWidgetStore(
33
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.labels),
34
- )
35
- const onRowClick = useWidgetStore(
36
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.onRowClick),
37
- )
38
- const selected = useWidgetStore(
39
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.selected),
40
- )
41
- const max = useWidgetStore(
42
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.max),
43
- )
19
+
20
+ // Single consolidated subscription instead of 9 separate ones.
21
+ const {
22
+ _formatter,
23
+ _labelFormatter,
24
+ _series,
25
+ data,
26
+ maxItems,
27
+ labels,
28
+ onRowClick,
29
+ selected,
30
+ max,
31
+ } = useWidgetSelector(id, (w) => {
32
+ const cw = w as CategoryWidgetState | undefined
33
+ return {
34
+ _formatter: cw?.formatter,
35
+ _labelFormatter: cw?.labelFormatter,
36
+ _series: cw?.series,
37
+ data: cw?.data,
38
+ maxItems: cw?.maxItems,
39
+ labels: cw?.labels,
40
+ onRowClick: cw?.onRowClick,
41
+ selected: cw?.selected,
42
+ max: cw?.max,
43
+ }
44
+ })
44
45
 
45
46
  const formatter = _formatter ?? defaultFormatter
47
+ const labelFormatter = _labelFormatter ?? defaultLabelFormatter
46
48
  const series = _series ?? []
47
49
 
48
50
  const [maxHeight] = useState<string | number | undefined>(
@@ -100,6 +102,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
100
102
  maxValue={maxValue}
101
103
  colors={colors}
102
104
  formatter={formatter}
105
+ labelFormatter={labelFormatter}
103
106
  onClick={onRowClick}
104
107
  selected={selected?.(item.name) ?? true}
105
108
  />
@@ -113,6 +116,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
113
116
  maxValue={maxValue}
114
117
  color={colors[0]!}
115
118
  formatter={formatter}
119
+ labelFormatter={labelFormatter}
116
120
  onClick={onRowClick}
117
121
  />
118
122
  ))}
@@ -9,6 +9,7 @@ export interface CategoryRowMultiProps {
9
9
  maxValue: number
10
10
  colors: string[]
11
11
  formatter: NonNullable<CategoryWidgetConfig['formatter']>
12
+ labelFormatter?: CategoryWidgetConfig['labelFormatter']
12
13
  onClick?: CategoryWidgetConfig['onRowClick']
13
14
  selected?: boolean
14
15
  }
@@ -22,6 +23,7 @@ export function CategoryRowMulti({
22
23
  maxValue,
23
24
  colors,
24
25
  formatter,
26
+ labelFormatter,
25
27
  onClick,
26
28
  selected = true,
27
29
  }: CategoryRowMultiProps) {
@@ -30,10 +32,12 @@ export function CategoryRowMulti({
30
32
 
31
33
  return (
32
34
  <Box sx={rowStyle} onClick={handleClick}>
33
- <Typography sx={styles.rowLabel}>{name}</Typography>
35
+ <Typography sx={styles.rowLabel}>
36
+ {labelFormatter ? labelFormatter(name) : name}
37
+ </Typography>
34
38
  <Box sx={styles.barContainer}>
35
39
  {values.map((value, index) => (
36
- <Box key={`${name}-${value}`} sx={styles.multiBarRow}>
40
+ <Box key={`${name}-${value}-${index}`} sx={styles.multiBarRow}>
37
41
  <Box sx={styles.multiBarContainer}>
38
42
  <CategoryBar
39
43
  value={value}
@@ -9,6 +9,7 @@ export interface CategoryRowSingleProps {
9
9
  maxValue: number
10
10
  color: string
11
11
  formatter: NonNullable<CategoryWidgetConfig['formatter']>
12
+ labelFormatter?: CategoryWidgetConfig['labelFormatter']
12
13
  onClick?: CategoryWidgetConfig['onRowClick']
13
14
  selected?: boolean
14
15
  }
@@ -22,6 +23,7 @@ export function CategoryRowSingle({
22
23
  maxValue,
23
24
  color,
24
25
  formatter,
26
+ labelFormatter,
25
27
  onClick,
26
28
  selected = true,
27
29
  }: CategoryRowSingleProps) {
@@ -36,7 +38,9 @@ export function CategoryRowSingle({
36
38
  return (
37
39
  <Box sx={rowStyle} onClick={handleClick}>
38
40
  <Box sx={styles.rowHeader}>
39
- <Typography sx={styles.rowLabel}>{name}</Typography>
41
+ <Typography sx={styles.rowLabel}>
42
+ {labelFormatter ? labelFormatter(name) : name}
43
+ </Typography>
40
44
  <Typography sx={styles.rowValue}>{formatter(value)}</Typography>
41
45
  </Box>
42
46
  <CategoryBar
@@ -29,6 +29,7 @@ export type CategoryWidgetState = BaseWidgetState<
29
29
 
30
30
  export interface CategoryWidgetConfig {
31
31
  formatter?: (value: number) => string
32
+ labelFormatter?: (value: string | number) => string | number
32
33
  series?: CategorySeriesConfig[]
33
34
  maxItems?: number
34
35
  labels?: CategoryLabels
@@ -3,6 +3,7 @@ import { render } from '@testing-library/react'
3
3
  import { EchartUI } from './echart-ui'
4
4
  import type { EChartsOption } from 'echarts'
5
5
  import * as echarts from 'echarts'
6
+ import { resetSharedResizeObserver } from './shared-resize-observer'
6
7
 
7
8
  // Mock echarts module
8
9
  vi.mock('echarts', () => {
@@ -36,6 +37,7 @@ describe('EchartUI', () => {
36
37
  disconnect: ReturnType<typeof vi.fn>
37
38
  unobserve: ReturnType<typeof vi.fn>
38
39
  }
40
+ let resizeObserverCallback: ResizeObserverCallback | undefined
39
41
 
40
42
  const defaultProps = {
41
43
  id: 'test-echart',
@@ -73,6 +75,9 @@ describe('EchartUI', () => {
73
75
  mockChart as unknown as echarts.ECharts,
74
76
  )
75
77
 
78
+ // Reset the shared observer so each test gets a fresh one using the mock
79
+ resetSharedResizeObserver()
80
+
76
81
  // Mock ResizeObserver
77
82
  mockResizeObserver = {
78
83
  observe: vi.fn(),
@@ -80,7 +85,12 @@ describe('EchartUI', () => {
80
85
  unobserve: vi.fn(),
81
86
  }
82
87
 
88
+ resizeObserverCallback = undefined
89
+
83
90
  global.ResizeObserver = class {
91
+ constructor(callback: ResizeObserverCallback) {
92
+ resizeObserverCallback = callback
93
+ }
84
94
  observe = mockResizeObserver.observe
85
95
  disconnect = mockResizeObserver.disconnect
86
96
  unobserve = mockResizeObserver.unobserve
@@ -245,32 +255,26 @@ describe('EchartUI', () => {
245
255
  )
246
256
  })
247
257
 
248
- test('disconnects ResizeObserver on unmount', () => {
258
+ test('unobserves element from shared ResizeObserver on unmount', () => {
249
259
  const { unmount } = render(
250
260
  <EchartUI {...defaultProps} option={basicOption} />,
251
261
  )
252
262
 
253
263
  unmount()
254
264
 
255
- expect(mockResizeObserver.disconnect).toHaveBeenCalled()
265
+ // The shared observer calls unobserve() for the specific element, not disconnect()
266
+ expect(mockResizeObserver.unobserve).toHaveBeenCalled()
256
267
  })
257
268
 
258
269
  test('calls resize on chart when ResizeObserver triggers', () => {
259
- let resizeCallback: ResizeObserverCallback
260
-
261
- global.ResizeObserver = class {
262
- constructor(callback: ResizeObserverCallback) {
263
- resizeCallback = callback
264
- }
265
- observe = mockResizeObserver.observe
266
- disconnect = mockResizeObserver.disconnect
267
- unobserve = mockResizeObserver.unobserve
268
- } as unknown as typeof ResizeObserver
269
-
270
270
  render(<EchartUI {...defaultProps} option={basicOption} />)
271
271
 
272
- // Trigger resize callback
273
- resizeCallback!([], mockResizeObserver as unknown as ResizeObserver)
272
+ // Trigger the shared observer's callback with a mock entry targeting the chart element
273
+ const chartElement = document.getElementById(defaultProps.id)!
274
+ const entries: ResizeObserverEntry[] = [
275
+ { target: chartElement } as unknown as ResizeObserverEntry,
276
+ ]
277
+ resizeObserverCallback!(entries, {} as ResizeObserver)
274
278
 
275
279
  expect(mockChart.resize).toHaveBeenCalled()
276
280
  })
@@ -517,6 +521,6 @@ describe('EchartUI', () => {
517
521
  unmount()
518
522
 
519
523
  expect(mockChart.dispose).toHaveBeenCalled()
520
- expect(mockResizeObserver.disconnect).toHaveBeenCalled()
524
+ expect(mockResizeObserver.unobserve).toHaveBeenCalled()
521
525
  })
522
526
  })
@@ -2,6 +2,7 @@ import { useEffect, useRef, useImperativeHandle } from 'react'
2
2
  import * as echarts from 'echarts'
3
3
  import type { EchartUIProps } from './types'
4
4
  import { useWidgetRef } from '../../hooks'
5
+ import { observeResize } from './shared-resize-observer'
5
6
 
6
7
  /**
7
8
  * Presentational component that initializes and manages an Apache ECharts instance.
@@ -16,7 +17,6 @@ export function EchartUI(props: EchartUIProps) {
16
17
  const { ref: chartRef, instance: chartInstanceRef } =
17
18
  useWidgetRef<HTMLDivElement>(id)
18
19
  const chartInstance = useRef<echarts.ECharts>(null)
19
- const resizeObserverRef = useRef<ResizeObserver | null>(null)
20
20
 
21
21
  useImperativeHandle(ref, () => chartInstance.current!, [])
22
22
 
@@ -45,19 +45,13 @@ export function EchartUI(props: EchartUIProps) {
45
45
  })
46
46
  }, [option])
47
47
 
48
- // Handle resize using ResizeObserver
48
+ // Handle resize using shared ResizeObserver (single instance for all charts)
49
49
  useEffect(() => {
50
- const handleResize = () => {
51
- chartInstance.current?.resize()
52
- }
53
-
54
- resizeObserverRef.current = new ResizeObserver(handleResize)
55
- resizeObserverRef.current.observe(chartRef.current!)
50
+ if (!chartRef.current) return
56
51
 
57
- return () => {
58
- resizeObserverRef.current?.disconnect()
59
- resizeObserverRef.current = null
60
- }
52
+ return observeResize(chartRef.current, () => {
53
+ chartInstance.current?.resize()
54
+ })
61
55
  }, [chartRef])
62
56
 
63
57
  useEffect(() => {