@carto/ps-react-ui 4.3.9 → 4.4.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 (121) hide show
  1. package/dist/components.js +692 -700
  2. package/dist/components.js.map +1 -1
  3. package/dist/{lasso-tool-jl4YK02H.js → lasso-tool-BYbxrJ-7.js} +184 -190
  4. package/dist/lasso-tool-BYbxrJ-7.js.map +1 -0
  5. package/dist/{row-BKmVAUN5.js → row-DTCV0Ocm.js} +2 -2
  6. package/dist/row-DTCV0Ocm.js.map +1 -0
  7. package/dist/{series-D1pynfeh.js → series-CYNOu2Ju.js} +2 -2
  8. package/dist/{series-D1pynfeh.js.map → series-CYNOu2Ju.js.map} +1 -1
  9. package/dist/smart-tooltip-D4vwQpFf.js +37 -0
  10. package/dist/smart-tooltip-D4vwQpFf.js.map +1 -0
  11. package/dist/{styles-DrPyd0y5.js → styles-CAroD5Rc.js} +12 -12
  12. package/dist/styles-CAroD5Rc.js.map +1 -0
  13. package/dist/types/hooks/use-widget-ref.d.ts +5 -2
  14. package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +1 -1
  15. package/dist/types/widgets/actions/brush-toggle/brush-toggle.d.ts +21 -0
  16. package/dist/types/widgets/actions/{relative-data → brush-toggle}/style.d.ts +5 -0
  17. package/dist/types/widgets/actions/brush-toggle/types.d.ts +33 -0
  18. package/dist/types/widgets/actions/index.d.ts +2 -0
  19. package/dist/types/widgets/category/config.d.ts +3 -10
  20. package/dist/types/widgets/echart/types.d.ts +2 -1
  21. package/dist/types/widgets/echart/utils.d.ts +12 -6
  22. package/dist/types/widgets/range/config.d.ts +0 -4
  23. package/dist/types/widgets/spread/config.d.ts +0 -5
  24. package/dist/types/widgets/table/config.d.ts +1 -2
  25. package/dist/types/widgets/table/table-ui.d.ts +1 -1
  26. package/dist/types/widgets/wrapper/components/options.d.ts +1 -1
  27. package/dist/use-widget-ref-wtFLDFCD.js +25 -0
  28. package/dist/use-widget-ref-wtFLDFCD.js.map +1 -0
  29. package/dist/{utils-idmvq0Oa.js → utils-Cx3gYUcL.js} +74 -68
  30. package/dist/utils-Cx3gYUcL.js.map +1 -0
  31. package/dist/widgets/actions.js +788 -700
  32. package/dist/widgets/actions.js.map +1 -1
  33. package/dist/widgets/bar.js +48 -49
  34. package/dist/widgets/bar.js.map +1 -1
  35. package/dist/widgets/category.js +28 -28
  36. package/dist/widgets/category.js.map +1 -1
  37. package/dist/widgets/echart.js +85 -82
  38. package/dist/widgets/echart.js.map +1 -1
  39. package/dist/widgets/formula.js +7 -5
  40. package/dist/widgets/formula.js.map +1 -1
  41. package/dist/widgets/histogram.js +50 -50
  42. package/dist/widgets/histogram.js.map +1 -1
  43. package/dist/widgets/markdown.js +1 -1
  44. package/dist/widgets/pie.js +10 -10
  45. package/dist/widgets/pie.js.map +1 -1
  46. package/dist/widgets/range.js +1 -1
  47. package/dist/widgets/range.js.map +1 -1
  48. package/dist/widgets/scatterplot.js +14 -14
  49. package/dist/widgets/scatterplot.js.map +1 -1
  50. package/dist/widgets/spread.js +7 -5
  51. package/dist/widgets/spread.js.map +1 -1
  52. package/dist/widgets/table.js +5 -3
  53. package/dist/widgets/table.js.map +1 -1
  54. package/dist/widgets/timeseries.js +4 -4
  55. package/dist/widgets/timeseries.js.map +1 -1
  56. package/dist/widgets/wrapper.js +152 -162
  57. package/dist/widgets/wrapper.js.map +1 -1
  58. package/dist/widgets.js +1 -1
  59. package/package.json +1 -1
  60. package/src/components/basemaps/basemaps.tsx +3 -1
  61. package/src/components/geolocation-controls/geolocation-controls.tsx +10 -6
  62. package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -2
  63. package/src/components/lasso-tool/lasso-tool.tsx +9 -3
  64. package/src/components/list-data/list-data-skeleton.tsx +1 -1
  65. package/src/components/list-data/list-data.tsx +5 -3
  66. package/src/components/measurement-tools/measurement-tools.tsx +5 -1
  67. package/src/components/smart-tooltip/smart-tooltip.tsx +3 -1
  68. package/src/hooks/use-widget-ref.ts +4 -3
  69. package/src/widgets/_shared/chart-config/option-builders.test.ts +2 -2
  70. package/src/widgets/_shared/chart-config/option-builders.ts +6 -4
  71. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +220 -0
  72. package/src/widgets/actions/{relative-data → brush-toggle}/style.ts +5 -0
  73. package/src/widgets/actions/brush-toggle/types.ts +37 -0
  74. package/src/widgets/actions/download/download.test.tsx +6 -2
  75. package/src/widgets/actions/download/download.tsx +3 -1
  76. package/src/widgets/actions/fullscreen/fullscreen.tsx +8 -1
  77. package/src/widgets/actions/index.ts +9 -0
  78. package/src/widgets/actions/lock-selection/lock-selection.tsx +0 -9
  79. package/src/widgets/actions/relative-data/relative-data.tsx +2 -6
  80. package/src/widgets/actions/searcher/searcher.tsx +0 -6
  81. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +4 -4
  82. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +2 -13
  83. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +2 -12
  84. package/src/widgets/bar/config.ts +8 -4
  85. package/src/widgets/bar/skeleton.tsx +1 -1
  86. package/src/widgets/category/components/category-row-multi.tsx +1 -1
  87. package/src/widgets/category/config.ts +1 -11
  88. package/src/widgets/echart/echart-ui.tsx +5 -2
  89. package/src/widgets/echart/echart.tsx +1 -1
  90. package/src/widgets/echart/types.ts +2 -1
  91. package/src/widgets/echart/utils.ts +20 -15
  92. package/src/widgets/formula/components/row.tsx +1 -1
  93. package/src/widgets/formula/formula-ui.tsx +1 -1
  94. package/src/widgets/histogram/config.ts +7 -2
  95. package/src/widgets/histogram/skeleton.tsx +2 -2
  96. package/src/widgets/pie/skeleton.tsx +1 -1
  97. package/src/widgets/range/config.ts +0 -5
  98. package/src/widgets/scatterplot/skeleton.tsx +2 -2
  99. package/src/widgets/spread/config.ts +0 -6
  100. package/src/widgets/spread/spread-ui.tsx +1 -1
  101. package/src/widgets/table/config.ts +1 -1
  102. package/src/widgets/table/table-ui.tsx +2 -2
  103. package/src/widgets/timeseries/skeleton.tsx +1 -1
  104. package/src/widgets/wrapper/components/actions.test.tsx +6 -2
  105. package/src/widgets/wrapper/components/actions.tsx +3 -1
  106. package/src/widgets/wrapper/components/options.test.tsx +12 -4
  107. package/src/widgets/wrapper/components/options.tsx +8 -3
  108. package/src/widgets/wrapper/wrapper-ui.tsx +5 -2
  109. package/src/widgets/wrapper/wrapper.tsx +2 -4
  110. package/dist/lasso-tool-jl4YK02H.js.map +0 -1
  111. package/dist/row-BKmVAUN5.js.map +0 -1
  112. package/dist/smart-tooltip-BEtBaIdz.js +0 -39
  113. package/dist/smart-tooltip-BEtBaIdz.js.map +0 -1
  114. package/dist/styles-DrPyd0y5.js.map +0 -1
  115. package/dist/types/widgets/actions/zoom-toggle/index.d.ts +0 -2
  116. package/dist/types/widgets/table/components/index.d.ts +0 -4
  117. package/dist/use-widget-ref-P-2i0MJG.js +0 -19
  118. package/dist/use-widget-ref-P-2i0MJG.js.map +0 -1
  119. package/dist/utils-idmvq0Oa.js.map +0 -1
  120. package/src/widgets/actions/zoom-toggle/index.ts +0 -2
  121. package/src/widgets/table/components/index.ts +0 -4
@@ -12,8 +12,10 @@ const DEFAULT_LABELS = {
12
12
  showLess: 'Show Less',
13
13
  } as const
14
14
 
15
+ const EMPTY_DATA: ListDataProps['data'] = []
16
+
15
17
  export function ListDataUI({
16
- data = [],
18
+ data = EMPTY_DATA,
17
19
  isLoading = false,
18
20
  maxItems = 5,
19
21
  labels = DEFAULT_LABELS,
@@ -47,11 +49,11 @@ export function ListDataUI({
47
49
  return (
48
50
  <>
49
51
  <List id='expandable-list' role='list'>
50
- {items.map((item, idx) => (
52
+ {items.map((item) => (
51
53
  <SmartTooltip<HTMLLIElement>
52
54
  // Default tooltip props
53
55
  followCursor={false}
54
- key={`list-item-${idx}`}
56
+ key={item.id}
55
57
  placement='top'
56
58
  arrow
57
59
  title={item.tooltipTitle}
@@ -49,6 +49,10 @@ import type {
49
49
  } from '../types'
50
50
  import { Tooltip } from '../tooltip/tooltip'
51
51
 
52
+ const EMPTY_PAPER_PROPS: NonNullable<
53
+ MeasurementToolsComponentProps['PaperProps']
54
+ > = {}
55
+
52
56
  export function MeasurementToolsUI({
53
57
  enabled,
54
58
  actionProps,
@@ -57,7 +61,7 @@ export function MeasurementToolsUI({
57
61
  modesMapping = DEFAULT_MEASUREMENT_TOOLS_MODES_MAPPING,
58
62
  unitsMapping = DEFAULT_MEASUREMENT_TOOLS_UNITS_MAPPING,
59
63
  modeSelected,
60
- PaperProps: { sx, ...PaperProps } = {},
64
+ PaperProps: { sx, ...PaperProps } = EMPTY_PAPER_PROPS,
61
65
  units,
62
66
  unitSelected,
63
67
  onActionToggle,
@@ -2,9 +2,11 @@ import type { TooltipProps } from '@mui/material'
2
2
  import { useLayoutEffect, useRef, useState, type JSX, type Ref } from 'react'
3
3
  import { Tooltip } from '../tooltip/tooltip'
4
4
 
5
+ const EMPTY_DEPENDENCIES: unknown[] = []
6
+
5
7
  export function SmartTooltip<T extends HTMLElement>({
6
8
  title,
7
- dependencies = [],
9
+ dependencies = EMPTY_DEPENDENCIES,
8
10
  timeout = 500,
9
11
  TooltipProps,
10
12
  children,
@@ -11,7 +11,7 @@ import { useWidgetStore } from '../widgets/stores/widget-store'
11
11
  * @example
12
12
  * ```tsx
13
13
  * function MyWidget({ id }: { id: string }) {
14
- * const ref = useWidgetRef<HTMLDivElement>(id)
14
+ * const { ref } = useWidgetRef<HTMLDivElement>(id)
15
15
  *
16
16
  * return <div ref={ref}>Widget content</div>
17
17
  * }
@@ -21,13 +21,14 @@ export function useWidgetRef<T extends HTMLElement = HTMLElement>(
21
21
  widgetId: string,
22
22
  ) {
23
23
  const ref = useRef<T | null>(null)
24
+ const instance = useRef<echarts.ECharts | null>(null)
24
25
  const setWidget = useWidgetStore((store) => store.setWidget)
25
26
 
26
27
  useEffect(() => {
27
28
  if (ref.current) {
28
- setWidget(widgetId, { refUI: ref })
29
+ setWidget(widgetId, { refUI: ref, instance: instance })
29
30
  }
30
31
  }, [widgetId, setWidget])
31
32
 
32
- return ref
33
+ return { ref, instance }
33
34
  }
@@ -6,8 +6,8 @@ describe('niceNum', () => {
6
6
  expect(niceNum(0)).toBe(0)
7
7
  })
8
8
 
9
- it('should return 0 for negative values', () => {
10
- expect(niceNum(-5)).toBe(0)
9
+ it('should return the same negative value for negative numbers', () => {
10
+ expect(niceNum(-5)).toBe(-5)
11
11
  })
12
12
 
13
13
  it('should round 547 to 600', () => {
@@ -13,12 +13,14 @@ import type {
13
13
  * Rounds a value up to the nearest "nice" number.
14
14
  * A nice number is a multiple of 10^floor(log10(value)).
15
15
  *
16
- * Examples: 547 → 600, 200 → 200, 1200 → 2000, 18 → 20, 5 → 5
16
+ * Examples: 547 → 600, 200 → 200, 1200 → 2000, 18 → 20, 5 → 5, -547 → -500
17
17
  */
18
18
  export function niceNum(value: number): number {
19
- if (value <= 0) return 0
20
- const base = Math.pow(10, Math.floor(Math.log10(value)))
21
- return Math.ceil(value / base) * base
19
+ if (value === 0) return 0
20
+ const absValue = Math.abs(value)
21
+ const base = Math.pow(10, Math.floor(Math.log10(absValue)))
22
+ const rounded = Math.ceil(absValue / base) * base
23
+ return value < 0 ? -rounded : rounded
22
24
  }
23
25
 
24
26
  /**
@@ -0,0 +1,220 @@
1
+ import { Box, IconButton } from '@mui/material'
2
+ import { HighlightAltOutlined } from '@mui/icons-material'
3
+ import { useEffect, useCallback, useRef } from 'react'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { BrushToggleProps } from './types'
6
+ import { styles } from './style'
7
+ import { Tooltip } from '../../../components'
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'
15
+
16
+ export const BRUSH_TOGGLE_TOOL_ID = 'brush-toggle'
17
+
18
+ /**
19
+ * Widget action to toggle EChart brush selection functionality.
20
+ *
21
+ * Registers as a config pipeline tool so that brush configuration is automatically
22
+ * re-applied when the base config is updated (e.g., by WidgetLoader).
23
+ *
24
+ * When brush is active, users can drag across bars to select a range.
25
+ * Selection clearing is handled via the chart's brush interactions/toolbox configuration.
26
+ * Only intended for use in fullscreen ToolbarActions for bar and histogram widgets.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <BrushToggle
31
+ * id="my-widget"
32
+ * onBrushSelected={(items) => console.log(items)}
33
+ * />
34
+ * ```
35
+ */
36
+ export function BrushToggle({
37
+ id,
38
+ onBrushSelected,
39
+ labels,
40
+ Icon,
41
+ IconButtonProps,
42
+ }: 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
+ )
55
+
56
+ const brush = brushTool?.enabled
57
+
58
+ const toggleTool = useCallback(
59
+ (value: boolean) => {
60
+ setToolEnabled(id, BRUSH_TOGGLE_TOOL_ID, value)
61
+ },
62
+ [id, setToolEnabled],
63
+ )
64
+
65
+ const handleToggle = useCallback(() => {
66
+ const newBrush = !brush
67
+ toggleTool(newBrush)
68
+
69
+ if (newBrush) {
70
+ onBrushSelected?.([])
71
+ }
72
+ }, [brush, onBrushSelected, toggleTool])
73
+
74
+ // Re-dispatch brush action after every ECharts render while brush is active
75
+ useEffect(() => {
76
+ if (!brush) return
77
+
78
+ const instance = getWidget<EchartWidgetState>(id)?.instance?.current
79
+ if (!instance) return
80
+
81
+ const handleRendered = () => {
82
+ const config = getEChartBrushConfig()
83
+ instance.dispatchAction({
84
+ type: 'takeGlobalCursor',
85
+ key: 'brush',
86
+ brushOption: {
87
+ brushType: config.brush.brushType,
88
+ brushMode: config.brush.brushMode,
89
+ },
90
+ })
91
+ }
92
+
93
+ // Dispatch immediately for the initial activation
94
+ handleRendered()
95
+
96
+ // Re-dispatch after each render (covers setOption with notMerge:true)
97
+ instance.on('rendered', handleRendered)
98
+
99
+ return () => {
100
+ instance.off('rendered', handleRendered)
101
+ }
102
+ }, [brush, getWidget, id])
103
+
104
+ // 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
+ }[]
113
+ }[]
114
+ }
115
+
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
+ )
145
+
146
+ const handleBrushEnd = useCallback(() => {
147
+ onBrushSelected?.(selected.current)
148
+ toggleTool(false) // Disable brush after selection is made
149
+ }, [onBrushSelected, toggleTool])
150
+
151
+ // Register config tool on mount
152
+ useEffect(() => {
153
+ const existingTool = getWidget(id)?.registeredTools?.find(
154
+ (tool) => tool.id === BRUSH_TOGGLE_TOOL_ID,
155
+ )
156
+
157
+ const initialEnabled = existingTool?.enabled ?? false
158
+
159
+ registerTool(id, {
160
+ id: BRUSH_TOGGLE_TOOL_ID,
161
+ type: 'config',
162
+ order: 25,
163
+ enabled: initialEnabled,
164
+ fn: (currentConfig) => {
165
+ const config = currentConfig as Record<string, unknown>
166
+ const option = config.option as EchartOptionsProps | undefined
167
+ const currentOnEvents =
168
+ (config.onEvents as Record<string, unknown> | undefined) ?? {}
169
+
170
+ const brushConfig = getEChartBrushConfig()
171
+
172
+ const onEventsWithoutBrush = { ...currentOnEvents }
173
+ delete onEventsWithoutBrush.brushSelected
174
+ const onEvents = {
175
+ ...currentOnEvents,
176
+ brushSelected: handleBrushSelected,
177
+ brushEnd: handleBrushEnd,
178
+ }
179
+
180
+ return {
181
+ ...config,
182
+ option: {
183
+ ...option,
184
+ ...brushConfig,
185
+ },
186
+ onEvents,
187
+ }
188
+ },
189
+ })
190
+ return () => unregisterTool(id, BRUSH_TOGGLE_TOOL_ID)
191
+ }, [
192
+ getWidget,
193
+ handleBrushEnd,
194
+ handleBrushSelected,
195
+ id,
196
+ registerTool,
197
+ unregisterTool,
198
+ ])
199
+
200
+ const enableLabel = labels?.enable ?? 'Enable brush selection'
201
+ const disableLabel = labels?.disable ?? 'Disable brush selection'
202
+ const tooltipLabel = brush ? disableLabel : enableLabel
203
+
204
+ return (
205
+ <Box sx={styles.container}>
206
+ <Tooltip title={tooltipLabel}>
207
+ <IconButton
208
+ size='small'
209
+ aria-label={labels?.ariaLabel ?? tooltipLabel}
210
+ onClick={handleToggle}
211
+ sx={styles.trigger}
212
+ data-active={brush}
213
+ {...IconButtonProps}
214
+ >
215
+ {Icon ?? <HighlightAltOutlined />}
216
+ </IconButton>
217
+ </Tooltip>
218
+ </Box>
219
+ )
220
+ }
@@ -1,6 +1,11 @@
1
1
  import type { SxProps, Theme } from '@mui/material'
2
2
 
3
3
  export const styles = {
4
+ container: {
5
+ display: 'flex',
6
+ alignItems: 'center',
7
+ gap: ({ spacing }) => spacing(0.25),
8
+ },
4
9
  trigger: {
5
10
  '&[data-active="true"]': {
6
11
  background: (theme: Theme) => theme.palette.primary.relatedLight,
@@ -0,0 +1,37 @@
1
+ import type { IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../stores/types'
4
+
5
+ /**
6
+ * Represents a single item selected by the brush
7
+ */
8
+ export type BrushSelectedItems = (string | number)[]
9
+
10
+ /**
11
+ * State stored in widget store for brush functionality
12
+ */
13
+ export interface BrushConfig {
14
+ brush?: boolean
15
+ }
16
+
17
+ export type BrushState<T = unknown> = BaseWidgetState<T & BrushConfig>
18
+
19
+ export interface BrushToggleProps {
20
+ /** Widget ID to update brush state in the widget store */
21
+ id: string
22
+ /** Callback fired when items are selected via brush */
23
+ onBrushSelected?: (items: BrushSelectedItems) => void
24
+ /** Custom labels for the action */
25
+ labels?: {
26
+ /** Tooltip when brush is disabled (button will enable brush) */
27
+ enable?: string
28
+ /** Tooltip when brush is enabled (button will disable brush) */
29
+ disable?: string
30
+ /** Accessibility label */
31
+ ariaLabel?: string
32
+ }
33
+ /** Props passed to the IconButton component */
34
+ IconButtonProps?: IconButtonProps
35
+ /** Custom icon to display for brush toggle */
36
+ Icon?: ReactNode
37
+ }
@@ -271,8 +271,12 @@ describe('Download', () => {
271
271
  const parentClickHandler = vi.fn()
272
272
 
273
273
  render(
274
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
275
- <div onClick={parentClickHandler}>
274
+ <div
275
+ role='button'
276
+ tabIndex={0}
277
+ onClick={parentClickHandler}
278
+ onKeyDown={parentClickHandler}
279
+ >
276
280
  <Download id={widgetId} items={mockDownload} />
277
281
  </div>,
278
282
  )
@@ -12,10 +12,12 @@ import { useState, type MouseEvent } from 'react'
12
12
  import { useWidgetStore } from '../../../widgets'
13
13
  import { useShallow } from 'zustand/shallow'
14
14
 
15
+ const EMPTY_LABELS: NonNullable<DownloadProps['labels']> = {}
16
+
15
17
  export function Download({
16
18
  id,
17
19
  items,
18
- labels = {},
20
+ labels = EMPTY_LABELS,
19
21
  Icon,
20
22
  IconButtonProps,
21
23
  }: DownloadProps) {
@@ -15,13 +15,20 @@ import type {
15
15
  import { styles } from './styles'
16
16
  import { useShallow } from 'zustand/shallow'
17
17
 
18
+ const EMPTY_DIALOG_CONTENT_PROPS: NonNullable<
19
+ FullScreenProps['DialogContentProps']
20
+ > = {}
21
+
18
22
  export function FullScreen({
19
23
  id,
20
24
  labels,
21
25
  children,
22
26
  Icon,
23
27
  IconButtonProps,
24
- DialogContentProps: { sx, ...DialogContentProps } = {},
28
+ DialogContentProps: {
29
+ sx,
30
+ ...DialogContentProps
31
+ } = EMPTY_DIALOG_CONTENT_PROPS,
25
32
  DialogProps,
26
33
  }: FullScreenProps) {
27
34
  const isFullScreen = useWidgetStore(
@@ -52,3 +52,12 @@ export type {
52
52
  LockSelectionProps,
53
53
  LockSelectionState,
54
54
  } from './lock-selection/types'
55
+
56
+ /* Brush Toggle Widget */
57
+ export { BrushToggle, BRUSH_TOGGLE_TOOL_ID } from './brush-toggle/brush-toggle'
58
+ export type {
59
+ BrushToggleProps,
60
+ BrushState,
61
+ BrushConfig,
62
+ BrushSelectedItems,
63
+ } from './brush-toggle/types'
@@ -37,9 +37,6 @@ export function LockSelection({
37
37
  const setWidget = useWidgetStore((state) => state.setWidget)
38
38
  const registerTool = useWidgetStore((state) => state.registerTool)
39
39
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
41
- const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
42
-
43
40
  const storeIsLocked = useWidgetStore(
44
41
  useShallow((state) => state.getWidget<LockSelectionState>(id)?.isLocked),
45
42
  )
@@ -68,12 +65,6 @@ export function LockSelection({
68
65
  return () => unregisterTool(id, LOCK_SELECTION_TOOL_ID)
69
66
  }, [id, order, registerTool, unregisterTool, isLocked, lockedItems])
70
67
 
71
- // Update enabled flag and config when they change
72
- useEffect(() => {
73
- setToolEnabled(id, LOCK_SELECTION_TOOL_ID, isLocked)
74
- updateToolConfig(id, LOCK_SELECTION_TOOL_ID, { lockedItems })
75
- }, [id, isLocked, lockedItems, setToolEnabled, updateToolConfig])
76
-
77
68
  const handleToggle = useCallback(() => {
78
69
  if (isLocked) {
79
70
  // Unlock: clear locked items and disable tool
@@ -73,13 +73,9 @@ export function RelativeData({
73
73
  return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
74
74
  }, [id, order, registerTool, unregisterTool, isRelative])
75
75
 
76
- // Update enabled flag when toggle changes
77
- useEffect(() => {
78
- setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative)
79
- }, [id, isRelative, setToolEnabled])
80
-
81
76
  const handleToggle = useCallback(() => {
82
77
  const newIsRelative = !isRelative
78
+ setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
83
79
  let max = previousMaxValue.current
84
80
 
85
81
  if (newIsRelative) {
@@ -110,7 +106,7 @@ export function RelativeData({
110
106
  }
111
107
  : originalFormatter.current,
112
108
  })
113
- }, [isRelative, setWidget, id, getWidget])
109
+ }, [isRelative, setWidget, id, getWidget, setToolEnabled])
114
110
 
115
111
  const tooltipLabel = isRelative
116
112
  ? (labels?.absolute ?? 'Show absolute values')
@@ -53,7 +53,6 @@ export function Searcher({
53
53
  const setWidget = useWidgetStore((state) => state.setWidget)
54
54
  const registerTool = useWidgetStore((state) => state.registerTool)
55
55
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
56
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
57
56
  const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
58
57
 
59
58
  const setSearchText = useCallback(
@@ -85,11 +84,6 @@ export function Searcher({
85
84
  return () => unregisterTool(id, SEARCHER_TOOL_ID)
86
85
  }, [id, order, filter, registerTool, unregisterTool, enabled, searchText])
87
86
 
88
- // Update enabled flag when it changes
89
- useEffect(() => {
90
- setToolEnabled(id, SEARCHER_TOOL_ID, enabled)
91
- }, [id, enabled, setToolEnabled])
92
-
93
87
  // Update config when search text changes (debounced)
94
88
  const debouncedUpdateConfig = useCallback(
95
89
  (text: string) => {
@@ -49,7 +49,7 @@ describe('StackToggle', () => {
49
49
  const tool = widget?.registeredTools?.find(
50
50
  (t) => t.id === STACK_TOGGLE_TOOL_ID,
51
51
  )
52
- expect(tool?.config?.stacked).toBe(true)
52
+ expect(tool?.enabled).toBe(true)
53
53
  })
54
54
 
55
55
  test('toggles back to unstacked mode', () => {
@@ -67,7 +67,7 @@ describe('StackToggle', () => {
67
67
  const tool = widget?.registeredTools?.find(
68
68
  (t) => t.id === STACK_TOGGLE_TOOL_ID,
69
69
  )
70
- expect(tool?.config?.stacked).toBe(false)
70
+ expect(tool?.enabled).toBe(false)
71
71
  })
72
72
 
73
73
  test('has active state when in stacked mode', () => {
@@ -112,7 +112,7 @@ describe('StackToggle', () => {
112
112
  const tool = widget?.registeredTools?.find(
113
113
  (t) => t.id === STACK_TOGGLE_TOOL_ID,
114
114
  )
115
- expect(tool?.config?.stacked).toBe(false)
115
+ expect(tool?.enabled).toBe(false)
116
116
  })
117
117
 
118
118
  test('initializes tool config with stacked values when defaultIsStacked is true', () => {
@@ -122,7 +122,7 @@ describe('StackToggle', () => {
122
122
  const tool = widget?.registeredTools?.find(
123
123
  (t) => t.id === STACK_TOGGLE_TOOL_ID,
124
124
  )
125
- expect(tool?.config?.stacked).toBe(true)
125
+ expect(tool?.enabled).toBe(true)
126
126
  })
127
127
 
128
128
  test('applies stack to EChart series via config pipeline when toggling to stacked', async () => {
@@ -37,9 +37,6 @@ export function StackToggle({
37
37
  const setWidget = useWidgetStore((state) => state.setWidget)
38
38
  const registerTool = useWidgetStore((state) => state.registerTool)
39
39
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
40
- const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
41
- const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
42
-
43
40
  const storeIsStacked = useWidgetStore(
44
41
  useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
45
42
  )
@@ -70,12 +67,11 @@ export function StackToggle({
70
67
  type: 'config',
71
68
  order: 10,
72
69
  enabled: isStacked && hasMultiSeries,
73
- fn: (currentConfig, toolConfig) => {
70
+ fn: (currentConfig) => {
74
71
  const config = currentConfig as Record<string, unknown>
75
72
  const option = config.option as EchartOptionsProps | undefined
76
73
  if (!option) return currentConfig
77
74
 
78
- const stacked = (toolConfig?.stacked as boolean) ?? false
79
75
  const series = Array.isArray(option.series)
80
76
  ? option.series
81
77
  : [option.series]
@@ -85,22 +81,15 @@ export function StackToggle({
85
81
  typeof existingStack === 'string'
86
82
  ? existingStack
87
83
  : DEFAULT_STACK_GROUP
88
- return { ...s, ...getEChartStackConfig(stacked, stackGroup) }
84
+ return { ...s, ...getEChartStackConfig(stackGroup) }
89
85
  })
90
86
 
91
87
  return { ...config, option: { ...option, series: updatedSeries } }
92
88
  },
93
- config: { stacked: isStacked },
94
89
  })
95
90
  return () => unregisterTool(id, STACK_TOGGLE_TOOL_ID)
96
91
  }, [id, registerTool, unregisterTool, isStacked, hasMultiSeries])
97
92
 
98
- // Sync tool enabled/config when state changes
99
- useEffect(() => {
100
- setToolEnabled(id, STACK_TOGGLE_TOOL_ID, isStacked && hasMultiSeries)
101
- updateToolConfig(id, STACK_TOGGLE_TOOL_ID, { stacked: isStacked })
102
- }, [id, isStacked, hasMultiSeries, setToolEnabled, updateToolConfig])
103
-
104
93
  // Initialize store with default value only if not already configured
105
94
  useEffect(() => {
106
95
  if (storeIsStacked !== undefined) return
@@ -81,7 +81,6 @@ export function ZoomToggle({
81
81
  if (start !== undefined && end !== undefined) {
82
82
  setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
83
83
  updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
84
- enabled: true,
85
84
  start,
86
85
  end,
87
86
  })
@@ -113,7 +112,6 @@ export function ZoomToggle({
113
112
  const currentOnEvents =
114
113
  (config.onEvents as Record<string, unknown> | undefined) ?? {}
115
114
 
116
- const enabled = (toolConfig?.enabled as boolean) ?? false
117
115
  const start = (toolConfig?.start as number) ?? 0
118
116
  const end = (toolConfig?.end as number) ?? 100
119
117
 
@@ -125,7 +123,6 @@ export function ZoomToggle({
125
123
  const legendBottomOffset = hasLegend ? 28 : 0
126
124
 
127
125
  const zoomConfig = getEChartZoomConfig(
128
- enabled,
129
126
  { start, end },
130
127
  {
131
128
  inside: true,
@@ -142,15 +139,11 @@ export function ZoomToggle({
142
139
  ? grid.bottom
143
140
  : parseInt(grid?.bottom ?? '24')
144
141
 
145
- const gridBottom = enabled
146
- ? currentGridBottom + sliderHeight + sliderGap
147
- : currentGridBottom
142
+ const gridBottom = currentGridBottom + sliderHeight + sliderGap
148
143
 
149
144
  const onEventsWithoutDataZoom = { ...currentOnEvents }
150
145
  delete onEventsWithoutDataZoom.dataZoom
151
- const onEvents = enabled
152
- ? { ...currentOnEvents, dataZoom: handleDataZoom }
153
- : onEventsWithoutDataZoom
146
+ const onEvents = { ...currentOnEvents, dataZoom: handleDataZoom }
154
147
 
155
148
  return {
156
149
  ...config,
@@ -163,7 +156,6 @@ export function ZoomToggle({
163
156
  }
164
157
  },
165
158
  config: {
166
- enabled: initialEnabled,
167
159
  start: initialStart,
168
160
  end: initialEnd,
169
161
  },
@@ -185,7 +177,6 @@ export function ZoomToggle({
185
177
  const newZoom = !zoom
186
178
  setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, newZoom)
187
179
  updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
188
- enabled: newZoom,
189
180
  start: newZoom ? zoomStart : 0,
190
181
  end: newZoom ? zoomEnd : 100,
191
182
  })
@@ -194,7 +185,6 @@ export function ZoomToggle({
194
185
  const handleReset = () => {
195
186
  setToolEnabled(id, ZOOM_TOGGLE_TOOL_ID, true)
196
187
  updateToolConfig(id, ZOOM_TOGGLE_TOOL_ID, {
197
- enabled: true,
198
188
  start: defaultZoomStart,
199
189
  end: defaultZoomEnd,
200
190
  })