@carto/ps-react-ui 4.4.3 → 4.5.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 (116) hide show
  1. package/dist/{download-config-Dqu78h2a.js → download-config-DemuQ3Jm.js} +9 -10
  2. package/dist/{download-config-Dqu78h2a.js.map → download-config-DemuQ3Jm.js.map} +1 -1
  3. package/dist/error-Cj8eUMrl.js +40 -0
  4. package/dist/error-Cj8eUMrl.js.map +1 -0
  5. package/dist/no-data-DkIt7Qt1.js +61 -0
  6. package/dist/no-data-DkIt7Qt1.js.map +1 -0
  7. package/dist/row-D4VOhcNI.js +34 -0
  8. package/dist/row-D4VOhcNI.js.map +1 -0
  9. package/dist/series-Bola3CmD.js +90 -0
  10. package/dist/series-Bola3CmD.js.map +1 -0
  11. package/dist/types/widgets/category/style.d.ts +1 -0
  12. package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
  13. package/dist/types/widgets/stores/index.d.ts +2 -1
  14. package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
  15. package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
  16. package/dist/types/widgets/stores/widget-store.d.ts +49 -27
  17. package/dist/types/widgets/table/types.d.ts +1 -1
  18. package/dist/use-widget-ref-BFazQvJK.js +22 -0
  19. package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
  20. package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
  21. package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
  22. package/dist/widget-store-CIrb9RKP.js +263 -0
  23. package/dist/widget-store-CIrb9RKP.js.map +1 -0
  24. package/dist/widgets/actions.js +783 -817
  25. package/dist/widgets/actions.js.map +1 -1
  26. package/dist/widgets/bar.js +2 -2
  27. package/dist/widgets/category.js +259 -258
  28. package/dist/widgets/category.js.map +1 -1
  29. package/dist/widgets/echart.js +109 -99
  30. package/dist/widgets/echart.js.map +1 -1
  31. package/dist/widgets/error.js +1 -1
  32. package/dist/widgets/formula.js +71 -63
  33. package/dist/widgets/formula.js.map +1 -1
  34. package/dist/widgets/histogram.js +7 -8
  35. package/dist/widgets/histogram.js.map +1 -1
  36. package/dist/widgets/loader.js +53 -60
  37. package/dist/widgets/loader.js.map +1 -1
  38. package/dist/widgets/markdown.js +51 -50
  39. package/dist/widgets/markdown.js.map +1 -1
  40. package/dist/widgets/no-data.js +1 -1
  41. package/dist/widgets/pie.js +2 -2
  42. package/dist/widgets/range.js +146 -144
  43. package/dist/widgets/range.js.map +1 -1
  44. package/dist/widgets/scatterplot.js +2 -2
  45. package/dist/widgets/skeleton-loader.js +18 -17
  46. package/dist/widgets/skeleton-loader.js.map +1 -1
  47. package/dist/widgets/spread.js +110 -94
  48. package/dist/widgets/spread.js.map +1 -1
  49. package/dist/widgets/stores.js +5 -2
  50. package/dist/widgets/stores.js.map +1 -1
  51. package/dist/widgets/subheader.js +29 -29
  52. package/dist/widgets/subheader.js.map +1 -1
  53. package/dist/widgets/table.js +422 -436
  54. package/dist/widgets/table.js.map +1 -1
  55. package/dist/widgets/timeseries.js +2 -2
  56. package/dist/widgets/utils.js +1 -1
  57. package/dist/widgets/wrapper.js +156 -158
  58. package/dist/widgets/wrapper.js.map +1 -1
  59. package/dist/widgets.js +4 -4
  60. package/package.json +1 -1
  61. package/src/hooks/use-widget-ref.ts +3 -4
  62. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -32
  63. package/src/widgets/actions/change-column/change-column.tsx +15 -15
  64. package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
  65. package/src/widgets/actions/download/download.tsx +4 -3
  66. package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
  67. package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
  68. package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
  69. package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
  70. package/src/widgets/actions/searcher/searcher.tsx +20 -21
  71. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
  72. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
  73. package/src/widgets/category/category-ui.tsx +30 -31
  74. package/src/widgets/category/style.ts +1 -0
  75. package/src/widgets/echart/echart-ui.test.tsx +20 -16
  76. package/src/widgets/echart/echart-ui.tsx +6 -12
  77. package/src/widgets/echart/echart.tsx +13 -27
  78. package/src/widgets/echart/shared-resize-observer.ts +45 -0
  79. package/src/widgets/error/error.tsx +7 -9
  80. package/src/widgets/formula/components/prefix.tsx +4 -6
  81. package/src/widgets/formula/components/row.tsx +4 -4
  82. package/src/widgets/formula/components/series.tsx +4 -6
  83. package/src/widgets/formula/components/suffix.tsx +4 -6
  84. package/src/widgets/formula/components/value.tsx +9 -16
  85. package/src/widgets/loader/loader.tsx +31 -44
  86. package/src/widgets/markdown/markdown.tsx +4 -7
  87. package/src/widgets/no-data/no-data.tsx +7 -10
  88. package/src/widgets/range/components/range-item.tsx +20 -18
  89. package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
  90. package/src/widgets/spread/components/max-value.tsx +14 -16
  91. package/src/widgets/spread/components/min-value.tsx +14 -16
  92. package/src/widgets/stores/index.ts +2 -1
  93. package/src/widgets/stores/use-widget-selector.ts +47 -0
  94. package/src/widgets/stores/widget-store-performance.test.ts +750 -0
  95. package/src/widgets/stores/widget-store.test.ts +81 -0
  96. package/src/widgets/stores/widget-store.ts +225 -44
  97. package/src/widgets/subheader/subheader.tsx +11 -3
  98. package/src/widgets/table/config.ts +0 -1
  99. package/src/widgets/table/hooks/use-pagination.ts +28 -52
  100. package/src/widgets/table/hooks/use-selection.ts +20 -24
  101. package/src/widgets/table/hooks/use-sort.ts +22 -39
  102. package/src/widgets/table/types.ts +1 -1
  103. package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
  104. package/src/widgets/wrapper/wrapper.tsx +4 -6
  105. package/dist/error-CEkRPccv.js +0 -39
  106. package/dist/error-CEkRPccv.js.map +0 -1
  107. package/dist/no-data-hR3KcJ-_.js +0 -60
  108. package/dist/no-data-hR3KcJ-_.js.map +0 -1
  109. package/dist/row-DTCV0Ocm.js +0 -35
  110. package/dist/row-DTCV0Ocm.js.map +0 -1
  111. package/dist/series-CYNOu2Ju.js +0 -91
  112. package/dist/series-CYNOu2Ju.js.map +0 -1
  113. package/dist/use-widget-ref-wtFLDFCD.js +0 -25
  114. package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
  115. package/dist/widget-store-CzDt8oSK.js +0 -163
  116. package/dist/widget-store-CzDt8oSK.js.map +0 -1
@@ -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
  })
@@ -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,44 +8,42 @@ import {
8
8
  CategoryRowOther,
9
9
  CategoryLegend,
10
10
  } from './components'
11
- import { useShallow } from 'zustand/shallow'
12
11
  import { useState } from 'react'
13
12
  import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
13
+ import { useWidgetRef } from '../../hooks'
14
14
 
15
15
  /**
16
16
  * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping.
17
17
  */
18
18
  export function CategoryUI({ id }: CategoryUIProps) {
19
+ const { ref } = useWidgetRef<HTMLDivElement>(id)
19
20
  const theme = useTheme()
20
- const _formatter = useWidgetStore(
21
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.formatter),
22
- )
23
- const _labelFormatter = useWidgetStore(
24
- useShallow(
25
- (state) => state.getWidget<CategoryWidgetState>(id)?.labelFormatter,
26
- ),
27
- )
28
- const _series = useWidgetStore(
29
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.series),
30
- )
31
- const data = useWidgetStore(
32
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.data),
33
- )
34
- const maxItems = useWidgetStore(
35
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.maxItems),
36
- )
37
- const labels = useWidgetStore(
38
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.labels),
39
- )
40
- const onRowClick = useWidgetStore(
41
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.onRowClick),
42
- )
43
- const selected = useWidgetStore(
44
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.selected),
45
- )
46
- const max = useWidgetStore(
47
- useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.max),
48
- )
21
+
22
+ // Single consolidated subscription instead of 9 separate ones.
23
+ const {
24
+ _formatter,
25
+ _labelFormatter,
26
+ _series,
27
+ data,
28
+ maxItems,
29
+ labels,
30
+ onRowClick,
31
+ selected,
32
+ max,
33
+ } = useWidgetSelector(id, (w) => {
34
+ const cw = w as CategoryWidgetState | undefined
35
+ return {
36
+ _formatter: cw?.formatter,
37
+ _labelFormatter: cw?.labelFormatter,
38
+ _series: cw?.series,
39
+ data: cw?.data,
40
+ maxItems: cw?.maxItems,
41
+ labels: cw?.labels,
42
+ onRowClick: cw?.onRowClick,
43
+ selected: cw?.selected,
44
+ max: cw?.max,
45
+ }
46
+ })
49
47
 
50
48
  const formatter = _formatter ?? defaultFormatter
51
49
  const labelFormatter = _labelFormatter ?? defaultLabelFormatter
@@ -89,6 +87,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
89
87
 
90
88
  return (
91
89
  <Box
90
+ ref={ref}
92
91
  sx={{
93
92
  ...styles.root,
94
93
  }}
@@ -5,6 +5,7 @@ export const styles = {
5
5
  display: 'flex',
6
6
  flexDirection: 'column',
7
7
  position: 'relative',
8
+ width: '100%',
8
9
  },
9
10
  list: {
10
11
  display: 'flex',
@@ -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(() => {
@@ -5,8 +5,7 @@ import type {
5
5
  EchartOptionsProps,
6
6
  } from './types'
7
7
  import { EchartUI } from './echart-ui'
8
- import { useWidgetStore } from '../stores/widget-store'
9
- import { useShallow } from 'zustand/shallow'
8
+ import { useWidgetSelector } from '../stores/use-widget-selector'
10
9
  import { useMemo } from 'react'
11
10
 
12
11
  /**
@@ -16,31 +15,18 @@ import { useMemo } from 'react'
16
15
  * Transforms widget data into ECharts dataset format and delegates rendering to {@link EchartUI}.
17
16
  */
18
17
  export function Echart(props: EchartProps) {
19
- const id = useWidgetStore(
20
- useShallow((state) => state.getWidget<EchartWidgetState>(props.id)?.id),
21
- )
22
-
23
- const data = useWidgetStore(
24
- useShallow(
25
- (state) =>
26
- state.getWidget<EchartWidgetState>(props.id)?.data as
27
- | EchartWidgetData
28
- | undefined,
29
- ),
30
- )
31
-
32
- const widgetOption = useWidgetStore(
33
- useShallow((state) => state.getWidget<EchartWidgetState>(props.id)?.option),
34
- )
35
-
36
- const onEvents = useWidgetStore(
37
- useShallow(
38
- (state) => state.getWidget<EchartWidgetState>(props.id)?.onEvents,
39
- ),
40
- )
41
-
42
- const init = useWidgetStore(
43
- (state) => state.getWidget<EchartWidgetState>(props.id)?.init,
18
+ // Single consolidated subscription instead of 5 separate ones.
19
+ const { id, data, widgetOption, onEvents, init } = useWidgetSelector(
20
+ props.id,
21
+ (w) => ({
22
+ id: w?.id,
23
+ data: (w as EchartWidgetState | undefined)?.data as
24
+ | EchartWidgetData
25
+ | undefined,
26
+ widgetOption: (w as EchartWidgetState | undefined)?.option,
27
+ onEvents: (w as EchartWidgetState | undefined)?.onEvents,
28
+ init: (w as EchartWidgetState | undefined)?.init,
29
+ }),
44
30
  )
45
31
 
46
32
  // Memoize dataset transformation to avoid re-computing on every render
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Shared ResizeObserver singleton for all ECharts instances.
3
+ *
4
+ * Instead of creating 100+ individual ResizeObserver instances (one per chart),
5
+ * this module provides a single shared observer that efficiently handles resize
6
+ * callbacks for all registered elements.
7
+ */
8
+
9
+ type ResizeCallback = () => void
10
+
11
+ const callbacks = new Map<Element, ResizeCallback>()
12
+
13
+ let observer: ResizeObserver | null = null
14
+
15
+ function getObserver(): ResizeObserver {
16
+ observer ??= new ResizeObserver((entries) => {
17
+ for (const entry of entries) {
18
+ const callback = callbacks.get(entry.target)
19
+ callback?.()
20
+ }
21
+ })
22
+ return observer
23
+ }
24
+
25
+ export function observeResize(
26
+ element: Element,
27
+ callback: ResizeCallback,
28
+ ): () => void {
29
+ callbacks.set(element, callback)
30
+ getObserver().observe(element)
31
+
32
+ return () => {
33
+ callbacks.delete(element)
34
+ getObserver().unobserve(element)
35
+ }
36
+ }
37
+
38
+ /** Reset the shared observer (for testing only). */
39
+ export function resetSharedResizeObserver(): void {
40
+ if (observer) {
41
+ observer.disconnect()
42
+ observer = null
43
+ }
44
+ callbacks.clear()
45
+ }
@@ -1,6 +1,5 @@
1
1
  import { Alert, AlertTitle } from '@mui/material'
2
- import { useWidgetStore } from '../stores/widget-store'
3
- import { useShallow } from 'zustand/shallow'
2
+ import { useWidgetSelector } from '../stores/use-widget-selector'
4
3
  import type { WidgetErrorProps } from './types'
5
4
 
6
5
  /**
@@ -19,13 +18,12 @@ export function WidgetError({
19
18
  title: titleProp,
20
19
  description,
21
20
  }: WidgetErrorProps) {
22
- const isLoading = useWidgetStore(
23
- useShallow((state) => state.widgets[id]?.isLoading),
24
- )
25
- const isFetching = useWidgetStore(
26
- useShallow((state) => state.widgets[id]?.isFetching),
27
- )
28
- const error = useWidgetStore(useShallow((state) => state.widgets[id]?.error))
21
+ // Single consolidated subscription instead of 3 separate ones.
22
+ const { isLoading, isFetching, error } = useWidgetSelector(id, (w) => ({
23
+ isLoading: w?.isLoading,
24
+ isFetching: w?.isFetching,
25
+ error: w?.error,
26
+ }))
29
27
 
30
28
  // Don't show error during loading/fetching states
31
29
  if (isLoading || isFetching) {
@@ -1,16 +1,14 @@
1
1
  import { type FormulaWidgetState, type ValueProps } from '../types'
2
- import { useWidgetStore } from '../../stores/widget-store'
3
2
  import { Item } from './item'
4
- import { useShallow } from 'zustand/shallow'
3
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
5
4
 
6
5
  /**
7
6
  * Renders the prefix content (e.g., currency symbol) before a formula value, if defined in the data item.
8
7
  */
9
8
  export function Prefix({ id, index = 0, ...props }: ValueProps) {
10
- const prefix = useWidgetStore(
11
- useShallow(
12
- (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.prefix,
13
- ),
9
+ const prefix = useWidgetSelector(
10
+ id,
11
+ (w) => (w as FormulaWidgetState | undefined)?.data?.[index]?.prefix,
14
12
  )
15
13
 
16
14
  if (!prefix) {
@@ -1,15 +1,15 @@
1
1
  import { Box } from '@mui/material'
2
2
  import { styles } from '../style'
3
3
  import type { FormulaWidgetState, RowProps } from '../types'
4
- import { useWidgetStore } from '../../stores/widget-store'
5
- import { useShallow } from 'zustand/shallow'
4
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
6
5
 
7
6
  /**
8
7
  * Iterates over the widget's data items and renders a row for each one using render props.
9
8
  */
10
9
  export function Row(props: RowProps) {
11
- const data = useWidgetStore(
12
- useShallow((state) => state.getWidget<FormulaWidgetState>(props.id)?.data),
10
+ const data = useWidgetSelector(
11
+ props.id,
12
+ (w) => (w as FormulaWidgetState | undefined)?.data,
13
13
  )
14
14
 
15
15
  return data?.map((_, index) => {
@@ -1,16 +1,14 @@
1
1
  import { Avatar } from '@mui/material'
2
2
  import type { FormulaWidgetState, ValueProps } from '../types'
3
- import { useWidgetStore } from '../../stores/widget-store'
4
- import { useShallow } from 'zustand/shallow'
3
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
5
4
 
6
5
  /**
7
6
  * Renders a colored avatar badge showing the first letter of the series name for multi-value formula widgets.
8
7
  */
9
8
  export function Series({ id, index = 0 }: Pick<ValueProps, 'id' | 'index'>) {
10
- const serie = useWidgetStore(
11
- useShallow(
12
- (state) => state.getWidget<FormulaWidgetState>(id)?.series?.[index],
13
- ),
9
+ const serie = useWidgetSelector(
10
+ id,
11
+ (w) => (w as FormulaWidgetState | undefined)?.series?.[index],
14
12
  )
15
13
  if (!serie) {
16
14
  return null
@@ -1,17 +1,15 @@
1
1
  import { type FormulaWidgetState, type ValueProps } from '../types'
2
- import { useWidgetStore } from '../../stores/widget-store'
3
2
  import { Item } from './item'
4
3
  import type { Theme } from '@mui/material'
5
- import { useShallow } from 'zustand/shallow'
4
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
6
5
 
7
6
  /**
8
7
  * Renders the suffix content (e.g., unit label) after a formula value, if defined in the data item.
9
8
  */
10
9
  export function Suffix({ id, index = 0, ...props }: ValueProps) {
11
- const suffix = useWidgetStore(
12
- useShallow(
13
- (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.suffix,
14
- ),
10
+ const suffix = useWidgetSelector(
11
+ id,
12
+ (w) => (w as FormulaWidgetState | undefined)?.data?.[index]?.suffix,
15
13
  )
16
14
 
17
15
  if (!suffix) {
@@ -1,27 +1,20 @@
1
1
  import { type FormulaWidgetState, type ValueProps } from '../types'
2
- import { useWidgetStore } from '../../stores/widget-store'
3
2
  import { Item } from './item'
4
- import { useShallow } from 'zustand/shallow'
5
3
  import { defaultFormatter } from '../../utils/formatter'
4
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
6
5
 
7
6
  /**
8
7
  * Displays the formatted numeric value for a formula widget data item, applying the widget's formatter and color.
9
8
  */
10
9
  export function Value({ id, index = 0, ...props }: ValueProps) {
11
- const value = useWidgetStore(
12
- useShallow(
13
- (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.value,
14
- ),
15
- )
16
- const color = useWidgetStore(
17
- useShallow(
18
- (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.color,
19
- ),
20
- )
21
- const formatter =
22
- useWidgetStore(
23
- useShallow((state) => state.getWidget<FormulaWidgetState>(id)?.formatter),
24
- ) ?? defaultFormatter
10
+ const { value, color, formatter } = useWidgetSelector(id, (w) => {
11
+ const widget = w as FormulaWidgetState | undefined
12
+ return {
13
+ value: widget?.data?.[index]?.value,
14
+ color: widget?.data?.[index]?.color,
15
+ formatter: widget?.formatter ?? defaultFormatter,
16
+ }
17
+ })
25
18
 
26
19
  return (
27
20
  <Item TypographyProps={{ color }} {...props}>