@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.
- package/dist/{download-config-Dqu78h2a.js → download-config-DemuQ3Jm.js} +9 -10
- package/dist/{download-config-Dqu78h2a.js.map → download-config-DemuQ3Jm.js.map} +1 -1
- package/dist/error-Cj8eUMrl.js +40 -0
- package/dist/error-Cj8eUMrl.js.map +1 -0
- package/dist/no-data-DkIt7Qt1.js +61 -0
- package/dist/no-data-DkIt7Qt1.js.map +1 -0
- package/dist/row-D4VOhcNI.js +34 -0
- package/dist/row-D4VOhcNI.js.map +1 -0
- package/dist/series-Bola3CmD.js +90 -0
- package/dist/series-Bola3CmD.js.map +1 -0
- package/dist/types/widgets/category/style.d.ts +1 -0
- package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
- package/dist/types/widgets/stores/index.d.ts +2 -1
- package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
- package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
- package/dist/types/widgets/stores/widget-store.d.ts +49 -27
- package/dist/types/widgets/table/types.d.ts +1 -1
- package/dist/use-widget-ref-BFazQvJK.js +22 -0
- package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
- package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
- package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
- package/dist/widget-store-CIrb9RKP.js +263 -0
- package/dist/widget-store-CIrb9RKP.js.map +1 -0
- package/dist/widgets/actions.js +783 -817
- package/dist/widgets/actions.js.map +1 -1
- package/dist/widgets/bar.js +2 -2
- package/dist/widgets/category.js +259 -258
- package/dist/widgets/category.js.map +1 -1
- package/dist/widgets/echart.js +109 -99
- package/dist/widgets/echart.js.map +1 -1
- package/dist/widgets/error.js +1 -1
- package/dist/widgets/formula.js +71 -63
- package/dist/widgets/formula.js.map +1 -1
- package/dist/widgets/histogram.js +7 -8
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/loader.js +53 -60
- package/dist/widgets/loader.js.map +1 -1
- package/dist/widgets/markdown.js +51 -50
- package/dist/widgets/markdown.js.map +1 -1
- package/dist/widgets/no-data.js +1 -1
- package/dist/widgets/pie.js +2 -2
- package/dist/widgets/range.js +146 -144
- package/dist/widgets/range.js.map +1 -1
- package/dist/widgets/scatterplot.js +2 -2
- package/dist/widgets/skeleton-loader.js +18 -17
- package/dist/widgets/skeleton-loader.js.map +1 -1
- package/dist/widgets/spread.js +110 -94
- package/dist/widgets/spread.js.map +1 -1
- package/dist/widgets/stores.js +5 -2
- package/dist/widgets/stores.js.map +1 -1
- package/dist/widgets/subheader.js +29 -29
- package/dist/widgets/subheader.js.map +1 -1
- package/dist/widgets/table.js +422 -436
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/timeseries.js +2 -2
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +156 -158
- package/dist/widgets/wrapper.js.map +1 -1
- package/dist/widgets.js +4 -4
- package/package.json +1 -1
- package/src/hooks/use-widget-ref.ts +3 -4
- package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -32
- package/src/widgets/actions/change-column/change-column.tsx +15 -15
- package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
- package/src/widgets/actions/download/download.tsx +4 -3
- package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
- package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
- package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
- package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
- package/src/widgets/actions/searcher/searcher.tsx +20 -21
- package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
- package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
- package/src/widgets/category/category-ui.tsx +30 -31
- package/src/widgets/category/style.ts +1 -0
- package/src/widgets/echart/echart-ui.test.tsx +20 -16
- package/src/widgets/echart/echart-ui.tsx +6 -12
- package/src/widgets/echart/echart.tsx +13 -27
- package/src/widgets/echart/shared-resize-observer.ts +45 -0
- package/src/widgets/error/error.tsx +7 -9
- package/src/widgets/formula/components/prefix.tsx +4 -6
- package/src/widgets/formula/components/row.tsx +4 -4
- package/src/widgets/formula/components/series.tsx +4 -6
- package/src/widgets/formula/components/suffix.tsx +4 -6
- package/src/widgets/formula/components/value.tsx +9 -16
- package/src/widgets/loader/loader.tsx +31 -44
- package/src/widgets/markdown/markdown.tsx +4 -7
- package/src/widgets/no-data/no-data.tsx +7 -10
- package/src/widgets/range/components/range-item.tsx +20 -18
- package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
- package/src/widgets/spread/components/max-value.tsx +14 -16
- package/src/widgets/spread/components/min-value.tsx +14 -16
- package/src/widgets/stores/index.ts +2 -1
- package/src/widgets/stores/use-widget-selector.ts +47 -0
- package/src/widgets/stores/widget-store-performance.test.ts +750 -0
- package/src/widgets/stores/widget-store.test.ts +81 -0
- package/src/widgets/stores/widget-store.ts +225 -44
- package/src/widgets/subheader/subheader.tsx +11 -3
- package/src/widgets/table/config.ts +0 -1
- package/src/widgets/table/hooks/use-pagination.ts +28 -52
- package/src/widgets/table/hooks/use-selection.ts +20 -24
- package/src/widgets/table/hooks/use-sort.ts +22 -39
- package/src/widgets/table/types.ts +1 -1
- package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
- package/src/widgets/wrapper/wrapper.tsx +4 -6
- package/dist/error-CEkRPccv.js +0 -39
- package/dist/error-CEkRPccv.js.map +0 -1
- package/dist/no-data-hR3KcJ-_.js +0 -60
- package/dist/no-data-hR3KcJ-_.js.map +0 -1
- package/dist/row-DTCV0Ocm.js +0 -35
- package/dist/row-DTCV0Ocm.js.map +0 -1
- package/dist/series-CYNOu2Ju.js +0 -91
- package/dist/series-CYNOu2Ju.js.map +0 -1
- package/dist/use-widget-ref-wtFLDFCD.js +0 -25
- package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
- package/dist/widget-store-CzDt8oSK.js +0 -163
- 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 {
|
|
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 {
|
|
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 =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
83
|
+
[id],
|
|
90
84
|
)
|
|
91
85
|
|
|
92
|
-
// Register config tool
|
|
86
|
+
// Register config tool with all reactive deps — store's no-op detection handles performance
|
|
93
87
|
useEffect(() => {
|
|
94
|
-
const existingTool =
|
|
95
|
-
(
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
}}
|
|
@@ -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('
|
|
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
|
-
|
|
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
|
|
273
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
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 =
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
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 =
|
|
12
|
-
|
|
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 {
|
|
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 =
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
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 =
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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}>
|