@carto/ps-react-ui 4.3.3 → 4.3.5

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 (301) hide show
  1. package/dist/components.js +3 -3
  2. package/dist/components.js.map +1 -1
  3. package/dist/{lasso-tool-BwRzEW7k.js → lasso-tool-wFqOD6wk.js} +13 -13
  4. package/dist/lasso-tool-wFqOD6wk.js.map +1 -0
  5. package/dist/types/components/common-types.d.ts +41 -0
  6. package/dist/types/components/types.d.ts +1 -1
  7. package/dist/types/widgets/echart/echart-ui.d.ts +1 -1
  8. package/dist/types/widgets/echart/types.d.ts +4 -0
  9. package/dist/widgets/actions.js +1 -1
  10. package/dist/widgets/bar.js +1 -1
  11. package/dist/widgets/category.js +1 -1
  12. package/dist/widgets/echart.js +96 -85
  13. package/dist/widgets/echart.js.map +1 -1
  14. package/dist/widgets/formula.js +1 -1
  15. package/dist/widgets/histogram.js +1 -1
  16. package/dist/widgets/markdown.js +1 -1
  17. package/dist/widgets/pie.js +1 -1
  18. package/dist/widgets/scatterplot.js +1 -1
  19. package/dist/widgets/spread.js +1 -1
  20. package/dist/widgets/table.js +1 -1
  21. package/dist/widgets/timeseries.js +1 -1
  22. package/dist/widgets/toolbar-actions.js.map +1 -1
  23. package/dist/widgets/wrapper.js +1 -1
  24. package/package.json +8 -3
  25. package/src/components/basemaps/basemaps.test.tsx +196 -0
  26. package/src/components/basemaps/basemaps.tsx +128 -0
  27. package/src/components/basemaps/const.ts +13 -0
  28. package/src/components/basemaps/group-wrapper.test.tsx +38 -0
  29. package/src/components/basemaps/group-wrapper.tsx +28 -0
  30. package/src/components/basemaps/group.test.tsx +52 -0
  31. package/src/components/basemaps/group.tsx +42 -0
  32. package/src/components/basemaps/header.test.tsx +54 -0
  33. package/src/components/basemaps/header.tsx +36 -0
  34. package/src/components/basemaps/styles.ts +76 -0
  35. package/src/components/basemaps/types.ts +30 -0
  36. package/src/components/common-types.ts +1 -0
  37. package/src/components/geolocation-controls/const.ts +6 -0
  38. package/src/components/geolocation-controls/geolocation-controls.test.tsx +133 -0
  39. package/src/components/geolocation-controls/geolocation-controls.tsx +95 -0
  40. package/src/components/geolocation-controls/types.ts +17 -0
  41. package/src/components/index.ts +64 -0
  42. package/src/components/lasso-tool/chip.tsx +37 -0
  43. package/src/components/lasso-tool/const.tsx +70 -0
  44. package/src/components/lasso-tool/icons.tsx +91 -0
  45. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +168 -0
  46. package/src/components/lasso-tool/lasso-tool-inline.tsx +245 -0
  47. package/src/components/lasso-tool/lasso-tool.test.tsx +212 -0
  48. package/src/components/lasso-tool/lasso-tool.tsx +479 -0
  49. package/src/components/lasso-tool/styles.ts +143 -0
  50. package/src/components/lasso-tool/types.ts +114 -0
  51. package/src/components/list-data/list-data-skeleton.test.tsx +10 -0
  52. package/src/components/list-data/list-data-skeleton.tsx +40 -0
  53. package/src/components/list-data/list-data.test.tsx +94 -0
  54. package/src/components/list-data/list-data.tsx +106 -0
  55. package/src/components/list-data/styles.ts +37 -0
  56. package/src/components/list-data/types.ts +25 -0
  57. package/src/components/measurement-tools/const.tsx +108 -0
  58. package/src/components/measurement-tools/icons.tsx +54 -0
  59. package/src/components/measurement-tools/measurement-tools.test.tsx +165 -0
  60. package/src/components/measurement-tools/measurement-tools.tsx +443 -0
  61. package/src/components/measurement-tools/styles.ts +91 -0
  62. package/src/components/measurement-tools/types.ts +77 -0
  63. package/src/components/no-data-alert/no-data-alert.test.tsx +31 -0
  64. package/src/components/no-data-alert/no-data-alert.tsx +59 -0
  65. package/src/components/responsive-drawer/responsive-drawer.test.tsx +91 -0
  66. package/src/components/responsive-drawer/responsive-drawer.tsx +53 -0
  67. package/src/components/smart-tooltip/smart-tooltip.test.tsx +168 -0
  68. package/src/components/smart-tooltip/smart-tooltip.tsx +40 -0
  69. package/src/components/tooltip/tooltip.test.tsx +86 -0
  70. package/src/components/tooltip/tooltip.tsx +30 -0
  71. package/src/components/types.ts +1 -0
  72. package/src/components/zoom-controls/styles.ts +27 -0
  73. package/src/components/zoom-controls/types.ts +19 -0
  74. package/src/components/zoom-controls/zoom-controls.test.tsx +101 -0
  75. package/src/components/zoom-controls/zoom-controls.tsx +114 -0
  76. package/src/hooks/index.ts +2 -0
  77. package/src/hooks/use-debounce.ts +55 -0
  78. package/src/hooks/use-skeleton.test.tsx +32 -0
  79. package/src/hooks/use-skeleton.ts +19 -0
  80. package/src/hooks/use-widget-ref.ts +33 -0
  81. package/src/widgets/README.md +277 -0
  82. package/src/widgets/_shared/chart-config/config-factory.ts +67 -0
  83. package/src/widgets/_shared/chart-config/csv-modifiers.ts +56 -0
  84. package/src/widgets/_shared/chart-config/index.ts +21 -0
  85. package/src/widgets/_shared/chart-config/option-builders.ts +203 -0
  86. package/src/widgets/_shared/skeleton/index.ts +5 -0
  87. package/src/widgets/_shared/skeleton/styles.ts +20 -0
  88. package/src/widgets/actions/change-column/change-column-icon.tsx +10 -0
  89. package/src/widgets/actions/change-column/change-column.test.tsx +163 -0
  90. package/src/widgets/actions/change-column/change-column.tsx +141 -0
  91. package/src/widgets/actions/change-column/sortable-column-item.tsx +49 -0
  92. package/src/widgets/actions/change-column/types.ts +20 -0
  93. package/src/widgets/actions/download/download.test.tsx +322 -0
  94. package/src/widgets/actions/download/download.tsx +118 -0
  95. package/src/widgets/actions/download/exports.test.tsx +275 -0
  96. package/src/widgets/actions/download/exports.tsx +103 -0
  97. package/src/widgets/actions/download/types.ts +21 -0
  98. package/src/widgets/actions/fullscreen/fullscreen.test.tsx +269 -0
  99. package/src/widgets/actions/fullscreen/fullscreen.tsx +82 -0
  100. package/src/widgets/actions/fullscreen/styles.ts +17 -0
  101. package/src/widgets/actions/fullscreen/types.ts +27 -0
  102. package/src/widgets/actions/index.ts +51 -0
  103. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +186 -0
  104. package/src/widgets/actions/lock-selection/lock-selection.tsx +133 -0
  105. package/src/widgets/actions/lock-selection/types.ts +41 -0
  106. package/src/widgets/actions/relative-data/relative-data.test.tsx +267 -0
  107. package/src/widgets/actions/relative-data/relative-data.tsx +133 -0
  108. package/src/widgets/actions/relative-data/style.ts +9 -0
  109. package/src/widgets/actions/relative-data/types.ts +31 -0
  110. package/src/widgets/actions/relative-data/utils.test.ts +223 -0
  111. package/src/widgets/actions/relative-data/utils.ts +58 -0
  112. package/src/widgets/actions/searcher/searcher-toggle.test.tsx +354 -0
  113. package/src/widgets/actions/searcher/searcher-toggle.tsx +73 -0
  114. package/src/widgets/actions/searcher/searcher.tsx +205 -0
  115. package/src/widgets/actions/searcher/types.ts +72 -0
  116. package/src/widgets/actions/shared/styles.ts +12 -0
  117. package/src/widgets/actions/stack-toggle/grouped-bar-chart-icon.tsx +14 -0
  118. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +270 -0
  119. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +146 -0
  120. package/src/widgets/actions/stack-toggle/types.ts +29 -0
  121. package/src/widgets/actions/zoom-toggle/index.ts +2 -0
  122. package/src/widgets/actions/zoom-toggle/style.ts +14 -0
  123. package/src/widgets/actions/zoom-toggle/types.ts +44 -0
  124. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +186 -0
  125. package/src/widgets/bar/config.ts +122 -0
  126. package/src/widgets/bar/index.ts +9 -0
  127. package/src/widgets/bar/skeleton.tsx +60 -0
  128. package/src/widgets/bar/style.ts +33 -0
  129. package/src/widgets/bar/types.ts +16 -0
  130. package/src/widgets/category/category-ui.test.tsx +399 -0
  131. package/src/widgets/category/category-ui.tsx +156 -0
  132. package/src/widgets/category/components/category-bar.tsx +28 -0
  133. package/src/widgets/category/components/category-legend.tsx +30 -0
  134. package/src/widgets/category/components/category-row-multi.tsx +50 -0
  135. package/src/widgets/category/components/category-row-other.tsx +23 -0
  136. package/src/widgets/category/components/category-row-single.tsx +47 -0
  137. package/src/widgets/category/components/index.ts +14 -0
  138. package/src/widgets/category/config.ts +85 -0
  139. package/src/widgets/category/index.ts +30 -0
  140. package/src/widgets/category/skeleton.tsx +24 -0
  141. package/src/widgets/category/style.ts +133 -0
  142. package/src/widgets/category/types.ts +40 -0
  143. package/src/widgets/echart/const.ts +1 -0
  144. package/src/widgets/echart/echart-ui.test.tsx +537 -0
  145. package/src/widgets/echart/echart-ui.tsx +92 -0
  146. package/src/widgets/echart/echart.test.tsx +562 -0
  147. package/src/widgets/echart/echart.tsx +68 -0
  148. package/src/widgets/echart/index.ts +16 -0
  149. package/src/widgets/echart/options.ts +53 -0
  150. package/src/widgets/echart/types.ts +45 -0
  151. package/src/widgets/echart/utils.ts +169 -0
  152. package/src/widgets/error/error.test.tsx +331 -0
  153. package/src/widgets/error/error.tsx +40 -0
  154. package/src/widgets/error/index.ts +2 -0
  155. package/src/widgets/error/types.ts +14 -0
  156. package/src/widgets/formula/components/item.test.tsx +249 -0
  157. package/src/widgets/formula/components/item.tsx +18 -0
  158. package/src/widgets/formula/components/prefix.test.tsx +341 -0
  159. package/src/widgets/formula/components/prefix.tsx +18 -0
  160. package/src/widgets/formula/components/row.test.tsx +364 -0
  161. package/src/widgets/formula/components/row.tsx +21 -0
  162. package/src/widgets/formula/components/series.tsx +34 -0
  163. package/src/widgets/formula/components/suffix.test.tsx +383 -0
  164. package/src/widgets/formula/components/suffix.tsx +28 -0
  165. package/src/widgets/formula/components/value.test.tsx +329 -0
  166. package/src/widgets/formula/components/value.tsx +29 -0
  167. package/src/widgets/formula/config.ts +27 -0
  168. package/src/widgets/formula/formula-ui.test.tsx +399 -0
  169. package/src/widgets/formula/formula-ui.tsx +27 -0
  170. package/src/widgets/formula/index.ts +24 -0
  171. package/src/widgets/formula/serializer.test.tsx +144 -0
  172. package/src/widgets/formula/serializer.ts +28 -0
  173. package/src/widgets/formula/skeleton.tsx +10 -0
  174. package/src/widgets/formula/style.ts +23 -0
  175. package/src/widgets/formula/types.ts +50 -0
  176. package/src/widgets/histogram/config.ts +143 -0
  177. package/src/widgets/histogram/index.ts +8 -0
  178. package/src/widgets/histogram/skeleton.tsx +52 -0
  179. package/src/widgets/histogram/style.ts +8 -0
  180. package/src/widgets/histogram/types.ts +17 -0
  181. package/src/widgets/index.ts +25 -0
  182. package/src/widgets/loader/index.ts +4 -0
  183. package/src/widgets/loader/loader.tsx +70 -0
  184. package/src/widgets/loader/types.ts +11 -0
  185. package/src/widgets/loader/utils.test.ts +112 -0
  186. package/src/widgets/loader/utils.ts +35 -0
  187. package/src/widgets/markdown/config.ts +18 -0
  188. package/src/widgets/markdown/index.ts +14 -0
  189. package/src/widgets/markdown/markdown-ui.test.tsx +341 -0
  190. package/src/widgets/markdown/markdown-ui.tsx +52 -0
  191. package/src/widgets/markdown/markdown.tsx +20 -0
  192. package/src/widgets/markdown/skeleton.tsx +12 -0
  193. package/src/widgets/markdown/style.ts +28 -0
  194. package/src/widgets/markdown/types.ts +28 -0
  195. package/src/widgets/no-data/index.ts +2 -0
  196. package/src/widgets/no-data/no-data.test.tsx +447 -0
  197. package/src/widgets/no-data/no-data.tsx +116 -0
  198. package/src/widgets/no-data/style.ts +18 -0
  199. package/src/widgets/no-data/types.ts +72 -0
  200. package/src/widgets/note/index.ts +2 -0
  201. package/src/widgets/note/note.test.tsx +391 -0
  202. package/src/widgets/note/note.tsx +114 -0
  203. package/src/widgets/note/style.ts +29 -0
  204. package/src/widgets/note/types.ts +9 -0
  205. package/src/widgets/pie/config.ts +177 -0
  206. package/src/widgets/pie/index.ts +8 -0
  207. package/src/widgets/pie/skeleton.tsx +70 -0
  208. package/src/widgets/pie/style.ts +8 -0
  209. package/src/widgets/pie/types.ts +16 -0
  210. package/src/widgets/range/components/range-item.tsx +213 -0
  211. package/src/widgets/range/config.ts +10 -0
  212. package/src/widgets/range/index.ts +16 -0
  213. package/src/widgets/range/range-ui.test.tsx +203 -0
  214. package/src/widgets/range/range-ui.tsx +11 -0
  215. package/src/widgets/range/serializer.test.ts +70 -0
  216. package/src/widgets/range/serializer.ts +27 -0
  217. package/src/widgets/range/skeleton.tsx +14 -0
  218. package/src/widgets/range/style.ts +37 -0
  219. package/src/widgets/range/types.ts +39 -0
  220. package/src/widgets/scatterplot/config.ts +138 -0
  221. package/src/widgets/scatterplot/index.ts +8 -0
  222. package/src/widgets/scatterplot/skeleton.tsx +59 -0
  223. package/src/widgets/scatterplot/style.ts +21 -0
  224. package/src/widgets/scatterplot/types.ts +17 -0
  225. package/src/widgets/selection-summary/index.ts +6 -0
  226. package/src/widgets/selection-summary/selection-summary.tsx +46 -0
  227. package/src/widgets/selection-summary/style.ts +10 -0
  228. package/src/widgets/selection-summary/types.ts +14 -0
  229. package/src/widgets/skeleton-loader/index.ts +2 -0
  230. package/src/widgets/skeleton-loader/skeleton-loader.test.tsx +139 -0
  231. package/src/widgets/skeleton-loader/skeleton-loader.tsx +28 -0
  232. package/src/widgets/skeleton-loader/types.ts +8 -0
  233. package/src/widgets/spread/components/max-value.tsx +29 -0
  234. package/src/widgets/spread/components/min-value.tsx +29 -0
  235. package/src/widgets/spread/components/separator.tsx +6 -0
  236. package/src/widgets/spread/config.ts +34 -0
  237. package/src/widgets/spread/index.ts +23 -0
  238. package/src/widgets/spread/skeleton.tsx +10 -0
  239. package/src/widgets/spread/spread-ui.test.tsx +368 -0
  240. package/src/widgets/spread/spread-ui.tsx +29 -0
  241. package/src/widgets/spread/style.ts +22 -0
  242. package/src/widgets/spread/types.ts +47 -0
  243. package/src/widgets/stores/index.ts +9 -0
  244. package/src/widgets/stores/types.ts +192 -0
  245. package/src/widgets/stores/widget-store.test.ts +601 -0
  246. package/src/widgets/stores/widget-store.ts +239 -0
  247. package/src/widgets/subheader/index.ts +3 -0
  248. package/src/widgets/subheader/style.ts +20 -0
  249. package/src/widgets/subheader/subheader.test.tsx +45 -0
  250. package/src/widgets/subheader/subheader.tsx +16 -0
  251. package/src/widgets/subheader/types.ts +11 -0
  252. package/src/widgets/table/components/cell-header.tsx +58 -0
  253. package/src/widgets/table/components/cell.tsx +80 -0
  254. package/src/widgets/table/components/index.ts +4 -0
  255. package/src/widgets/table/components/pagination-actions.tsx +67 -0
  256. package/src/widgets/table/components/pagination.tsx +41 -0
  257. package/src/widgets/table/components/row.tsx +60 -0
  258. package/src/widgets/table/config.ts +71 -0
  259. package/src/widgets/table/helpers.test.ts +244 -0
  260. package/src/widgets/table/helpers.ts +107 -0
  261. package/src/widgets/table/hooks/index.ts +7 -0
  262. package/src/widgets/table/hooks/use-pagination.test.ts +294 -0
  263. package/src/widgets/table/hooks/use-pagination.ts +155 -0
  264. package/src/widgets/table/hooks/use-selection.test.ts +504 -0
  265. package/src/widgets/table/hooks/use-selection.ts +189 -0
  266. package/src/widgets/table/hooks/use-sort.test.ts +296 -0
  267. package/src/widgets/table/hooks/use-sort.ts +138 -0
  268. package/src/widgets/table/index.ts +53 -0
  269. package/src/widgets/table/serializer.ts +54 -0
  270. package/src/widgets/table/skeleton.tsx +48 -0
  271. package/src/widgets/table/style.ts +34 -0
  272. package/src/widgets/table/table-ui.tsx +64 -0
  273. package/src/widgets/table/types.ts +223 -0
  274. package/src/widgets/timeseries/config.ts +135 -0
  275. package/src/widgets/timeseries/index.ts +8 -0
  276. package/src/widgets/timeseries/skeleton.tsx +55 -0
  277. package/src/widgets/timeseries/style.ts +36 -0
  278. package/src/widgets/timeseries/types.ts +17 -0
  279. package/src/widgets/toolbar-actions/index.ts +6 -0
  280. package/src/widgets/toolbar-actions/styles.ts +38 -0
  281. package/src/widgets/toolbar-actions/toolbar-actions.test.tsx +691 -0
  282. package/src/widgets/toolbar-actions/toolbar-actions.tsx +145 -0
  283. package/src/widgets/toolbar-actions/types.ts +60 -0
  284. package/src/widgets/wrapper/components/actions.test.tsx +101 -0
  285. package/src/widgets/wrapper/components/actions.tsx +30 -0
  286. package/src/widgets/wrapper/components/options.test.tsx +323 -0
  287. package/src/widgets/wrapper/components/options.tsx +73 -0
  288. package/src/widgets/wrapper/components/title.test.tsx +126 -0
  289. package/src/widgets/wrapper/components/title.tsx +32 -0
  290. package/src/widgets/wrapper/index.ts +16 -0
  291. package/src/widgets/wrapper/styles.ts +98 -0
  292. package/src/widgets/wrapper/types.ts +55 -0
  293. package/src/widgets/wrapper/wrapper-ui.test.tsx +232 -0
  294. package/src/widgets/wrapper/wrapper-ui.tsx +57 -0
  295. package/src/widgets/wrapper/wrapper.test.tsx +365 -0
  296. package/src/widgets/wrapper/wrapper.tsx +50 -0
  297. package/dist/lasso-tool-BwRzEW7k.js.map +0 -1
  298. package/dist/types/common/common.d.ts +0 -3
  299. package/dist/types/common/index.d.ts +0 -26
  300. package/dist/types/common/lasso-tools.d.ts +0 -36
  301. package/dist/types/common/measurement-tools.d.ts +0 -65
@@ -0,0 +1,12 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ /**
4
+ * Shared styles for widget action buttons
5
+ */
6
+ export const actionButtonStyles = {
7
+ trigger: {
8
+ '&[data-active="true"]': {
9
+ background: (theme: Theme) => theme.palette.primary.relatedLight,
10
+ },
11
+ },
12
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,14 @@
1
+ export const GroupedBarChartIcon = () => (
2
+ <svg
3
+ width='24'
4
+ height='24'
5
+ viewBox='0 0 24 24'
6
+ fill='none'
7
+ xmlns='http://www.w3.org/2000/svg'
8
+ >
9
+ <path
10
+ d='M4 20V8H8V20H4ZM9 20V13H13V20H9ZM16 20V4H20V20H16Z'
11
+ fill='currentColor'
12
+ />
13
+ </svg>
14
+ )
@@ -0,0 +1,270 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
+ import { StackToggle } from './stack-toggle'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import { DEFAULT_STACK_GROUP } from '../../echart/const'
6
+
7
+ describe('StackToggle', () => {
8
+ const widgetId = 'test-stack-widget'
9
+
10
+ // Helper to set up widget with multiple series (required for component to render)
11
+ function setupMultiSeriesWidget(overrides = {}) {
12
+ useWidgetStore.getState().setWidget(widgetId, {
13
+ option: {
14
+ series: [
15
+ { name: 'Series 1', type: 'bar' },
16
+ { name: 'Series 2', type: 'bar' },
17
+ ],
18
+ },
19
+ ...overrides,
20
+ })
21
+ }
22
+
23
+ beforeEach(() => {
24
+ useWidgetStore.getState().clearWidgets()
25
+ setupMultiSeriesWidget()
26
+ })
27
+
28
+ test('renders stack toggle button', () => {
29
+ render(<StackToggle id={widgetId} />)
30
+
31
+ const button = screen.getByRole('button')
32
+ expect(button).toBeTruthy()
33
+ })
34
+
35
+ test('shows stacked tooltip when in unstacked mode', () => {
36
+ render(<StackToggle id={widgetId} />)
37
+
38
+ const button = screen.getByRole('button', { name: 'Enable stacking' })
39
+ expect(button).toBeTruthy()
40
+ })
41
+
42
+ test('toggles to stacked mode and updates widget store', () => {
43
+ render(<StackToggle id={widgetId} />)
44
+
45
+ const button = screen.getByRole('button')
46
+ fireEvent.click(button)
47
+
48
+ const widget = useWidgetStore.getState().getWidget(widgetId)
49
+ expect((widget as { isStacked?: boolean })?.isStacked).toBe(true)
50
+ })
51
+
52
+ test('toggles back to unstacked mode', () => {
53
+ render(<StackToggle id={widgetId} />)
54
+
55
+ const button = screen.getByRole('button')
56
+
57
+ // Toggle to stacked
58
+ fireEvent.click(button)
59
+
60
+ // Toggle back to unstacked
61
+ fireEvent.click(button)
62
+
63
+ const widget = useWidgetStore.getState().getWidget(widgetId)
64
+ expect((widget as { isStacked?: boolean })?.isStacked).toBe(false)
65
+ })
66
+
67
+ test('has active state when in stacked mode', () => {
68
+ render(<StackToggle id={widgetId} />)
69
+
70
+ const button = screen.getByRole('button')
71
+
72
+ expect(button.getAttribute('data-active')).toBe('false')
73
+
74
+ fireEvent.click(button)
75
+
76
+ expect(button.getAttribute('data-active')).toBe('true')
77
+ })
78
+
79
+ test('starts in stacked mode when defaultIsStacked is true', () => {
80
+ render(<StackToggle id={widgetId} defaultIsStacked />)
81
+
82
+ const button = screen.getByRole('button')
83
+ expect(button.getAttribute('data-active')).toBe('true')
84
+
85
+ // Should show unstacked tooltip when in stacked mode
86
+ expect(button.getAttribute('aria-label')).toBe('Disable stacking')
87
+ })
88
+
89
+ test('uses custom labels when provided', () => {
90
+ const customLabels = {
91
+ stacked: 'Stack bars',
92
+ unstacked: 'Unstack bars',
93
+ ariaLabel: 'Toggle bar stacking',
94
+ }
95
+
96
+ render(<StackToggle id={widgetId} labels={customLabels} />)
97
+
98
+ const button = screen.getByRole('button', { name: 'Toggle bar stacking' })
99
+ expect(button).toBeTruthy()
100
+ })
101
+
102
+ test('initializes store with default values on mount', () => {
103
+ render(<StackToggle id={widgetId} />)
104
+
105
+ const widget = useWidgetStore.getState().getWidget(widgetId)
106
+ expect((widget as { isStacked?: boolean })?.isStacked).toBe(false)
107
+ })
108
+
109
+ test('initializes store with stacked values when defaultIsStacked is true', () => {
110
+ render(<StackToggle id={widgetId} defaultIsStacked />)
111
+
112
+ const widget = useWidgetStore.getState().getWidget(widgetId)
113
+ expect((widget as { isStacked?: boolean })?.isStacked).toBe(true)
114
+ })
115
+
116
+ test('updates EChart series options when toggling to stacked', () => {
117
+ // setupMultiSeriesWidget already ran in beforeEach
118
+ render(<StackToggle id={widgetId} />)
119
+ const button = screen.getByRole('button')
120
+ fireEvent.click(button)
121
+
122
+ const widget = useWidgetStore.getState().getWidget(widgetId)
123
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
124
+ ?.option?.series?.[0]
125
+ expect(series?.stack).toBe(DEFAULT_STACK_GROUP)
126
+ })
127
+
128
+ test('removes stack from EChart series when toggling to unstacked', () => {
129
+ useWidgetStore.getState().setWidget(widgetId, {
130
+ option: {
131
+ series: [
132
+ { name: 'Series 1', type: 'bar', stack: DEFAULT_STACK_GROUP },
133
+ { name: 'Series 2', type: 'bar', stack: DEFAULT_STACK_GROUP },
134
+ ],
135
+ },
136
+ })
137
+
138
+ render(<StackToggle id={widgetId} />)
139
+ const button = screen.getByRole('button')
140
+
141
+ // First click unstacks (since series has stack, it starts stacked)
142
+ fireEvent.click(button)
143
+
144
+ const widget = useWidgetStore.getState().getWidget(widgetId)
145
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
146
+ ?.option?.series?.[0]
147
+ expect(series?.stack).toBeUndefined()
148
+ })
149
+
150
+ test('updates multiple series when toggling stack', () => {
151
+ useWidgetStore.getState().setWidget(widgetId, {
152
+ option: {
153
+ series: [
154
+ { name: 'Series 1', type: 'bar' },
155
+ { name: 'Series 2', type: 'bar' },
156
+ { name: 'Series 3', type: 'bar' },
157
+ ],
158
+ },
159
+ })
160
+
161
+ render(<StackToggle id={widgetId} />)
162
+ const button = screen.getByRole('button')
163
+ fireEvent.click(button)
164
+
165
+ const widget = useWidgetStore.getState().getWidget(widgetId)
166
+ const seriesArray = (
167
+ widget as { option?: { series?: { stack?: string }[] } }
168
+ )?.option?.series
169
+
170
+ expect(seriesArray).toHaveLength(3)
171
+ seriesArray?.forEach((s) => {
172
+ expect(s.stack).toBe(DEFAULT_STACK_GROUP)
173
+ })
174
+ })
175
+
176
+ test('uses default stack group when re-stacking after unstacking', () => {
177
+ const customStackGroup = 'custom-group'
178
+ useWidgetStore.getState().setWidget(widgetId, {
179
+ option: {
180
+ series: [
181
+ { name: 'Series 1', type: 'bar', stack: customStackGroup },
182
+ { name: 'Series 2', type: 'bar', stack: customStackGroup },
183
+ ],
184
+ },
185
+ })
186
+
187
+ render(<StackToggle id={widgetId} />)
188
+ const button = screen.getByRole('button')
189
+
190
+ // Toggle off then on - re-stacking uses default stack group
191
+ fireEvent.click(button) // unstacked
192
+ fireEvent.click(button) // stacked again
193
+
194
+ void waitFor(() => {
195
+ const widget = useWidgetStore.getState().getWidget(widgetId)
196
+ const series = (widget as { option?: { series?: { stack?: string }[] } })
197
+ ?.option?.series?.[0]
198
+ expect(series?.stack).toBe(customStackGroup)
199
+ })
200
+ })
201
+
202
+ test('detects existing stack in series and defaults to stacked mode', () => {
203
+ useWidgetStore.getState().setWidget(widgetId, {
204
+ option: {
205
+ series: [
206
+ { name: 'Series 1', type: 'bar', stack: 'existing-stack' },
207
+ { name: 'Series 2', type: 'bar', stack: 'existing-stack' },
208
+ ],
209
+ },
210
+ })
211
+
212
+ render(<StackToggle id={widgetId} />)
213
+
214
+ const button = screen.getByRole('button')
215
+ expect(button.getAttribute('data-active')).toBe('true')
216
+ expect(button.getAttribute('aria-label')).toBe('Disable stacking')
217
+ })
218
+
219
+ test('returns null for single series (stacking not applicable)', () => {
220
+ useWidgetStore.getState().setWidget(widgetId, {
221
+ option: {
222
+ series: { name: 'Single Series', type: 'bar' },
223
+ },
224
+ })
225
+
226
+ const { container } = render(<StackToggle id={widgetId} />)
227
+ // Component returns null when there's only one series
228
+ expect(container.firstChild).toBeNull()
229
+ })
230
+
231
+ test('returns null when widget has no option defined', () => {
232
+ // Clear the widget set up in beforeEach and create one without option
233
+ useWidgetStore.getState().clearWidgets()
234
+ useWidgetStore.getState().setWidget(widgetId, {})
235
+
236
+ const { container } = render(<StackToggle id={widgetId} />)
237
+ // Component returns null when there's no series data
238
+ expect(container.firstChild).toBeNull()
239
+ })
240
+
241
+ test('preserves other option properties when updating series', () => {
242
+ useWidgetStore.getState().setWidget(widgetId, {
243
+ option: {
244
+ title: { text: 'My Chart' },
245
+ xAxis: { type: 'category' },
246
+ series: [
247
+ { name: 'Series 1', type: 'bar' },
248
+ { name: 'Series 2', type: 'bar' },
249
+ ],
250
+ },
251
+ })
252
+
253
+ render(<StackToggle id={widgetId} />)
254
+ const button = screen.getByRole('button')
255
+ fireEvent.click(button)
256
+
257
+ const widget = useWidgetStore.getState().getWidget(widgetId)
258
+ const option = (
259
+ widget as {
260
+ option?: {
261
+ title?: { text?: string }
262
+ xAxis?: { type?: string }
263
+ }
264
+ }
265
+ )?.option
266
+
267
+ expect(option?.title?.text).toBe('My Chart')
268
+ expect(option?.xAxis?.type).toBe('category')
269
+ })
270
+ })
@@ -0,0 +1,146 @@
1
+ import { IconButton } from '@mui/material'
2
+ import { useCallback, useEffect, useMemo } from 'react'
3
+ import { useWidgetStore } from '../../stores/widget-store'
4
+ import type { StackToggleProps, StackToggleState } from './types'
5
+ import { actionButtonStyles } from '../shared/styles'
6
+ import { Tooltip } from '../../../components'
7
+ import { GroupedBarChartIcon } from './grouped-bar-chart-icon'
8
+ import { getEChartStackConfig } from '../../echart/utils'
9
+ import { DEFAULT_STACK_GROUP } from '../../echart/const'
10
+ import type { EchartWidgetState } from '../../echart/types'
11
+ import { useShallow } from 'zustand/shallow'
12
+
13
+ /**
14
+ * Widget action to toggle stacking behavior in ECharts bar and histogram widgets.
15
+ *
16
+ * Stores the stack state in the widget store and updates the ECharts option
17
+ * using getEChartStackConfig to apply stacking to all series.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <StackToggle
22
+ * id="my-widget"
23
+ * defaultIsStacked={false}
24
+ * />
25
+ * ```
26
+ */
27
+ export function StackToggle({
28
+ id,
29
+ defaultIsStacked = false,
30
+ labels,
31
+ Icon,
32
+ IconButtonProps,
33
+ }: StackToggleProps) {
34
+ const setWidget = useWidgetStore((state) => state.setWidget)
35
+ const storeIsStacked = useWidgetStore(
36
+ useShallow((state) => state.getWidget<StackToggleState>(id)?.isStacked),
37
+ )
38
+
39
+ const getWidget = useWidgetStore((state) => state.getWidget)
40
+
41
+ /**
42
+ * Checks if there are multiple series in the chart
43
+ */
44
+ const hasMultiSeries = useMemo(() => {
45
+ const option = getWidget<EchartWidgetState>(id)?.option
46
+ if (!option) return false
47
+
48
+ const series = Array.isArray(option.series)
49
+ ? option.series
50
+ : [option.series]
51
+
52
+ return series.length > 1
53
+ }, [getWidget, id])
54
+
55
+ /**
56
+ * Checks if any series in the option has a stack property defined
57
+ */
58
+ const hasStackInSeries = useMemo(() => {
59
+ const option = getWidget<EchartWidgetState>(id)?.option
60
+ if (!option) return false
61
+
62
+ const series = Array.isArray(option.series)
63
+ ? option.series
64
+ : [option.series]
65
+
66
+ return series.some((s) => (s as { stack?: string })?.stack)
67
+ }, [getWidget, id])
68
+
69
+ // If series already has stack defined, default to stacked=true
70
+ const effectiveDefaultIsStacked = hasStackInSeries || defaultIsStacked
71
+ const isStacked = storeIsStacked ?? effectiveDefaultIsStacked
72
+
73
+ /**
74
+ * Updates the ECharts option with the stack configuration
75
+ * Preserves existing stack group names from series, falling back to DEFAULT_STACK_GROUP
76
+ */
77
+ const updateOptions = useCallback(
78
+ (stacked: boolean) => {
79
+ const option = getWidget<EchartWidgetState>(id)?.option
80
+
81
+ if (!option) return
82
+
83
+ const series = Array.isArray(option.series)
84
+ ? option.series
85
+ : [option.series]
86
+
87
+ const updatedSeries = series.map((s) => {
88
+ // Extract existing stack group from series, fallback to default
89
+ const existingStack = (s as { stack?: string })?.stack
90
+ const stackGroup =
91
+ typeof existingStack === 'string'
92
+ ? existingStack
93
+ : DEFAULT_STACK_GROUP
94
+
95
+ return {
96
+ ...s,
97
+ ...getEChartStackConfig(stacked, stackGroup),
98
+ }
99
+ })
100
+
101
+ setWidget(id, {
102
+ option: {
103
+ ...option,
104
+ series: updatedSeries,
105
+ },
106
+ })
107
+ },
108
+ [getWidget, id, setWidget],
109
+ )
110
+
111
+ // Initialize store with default value only if not already configured
112
+ useEffect(() => {
113
+ setWidget(id, { isStacked: effectiveDefaultIsStacked })
114
+ }, [effectiveDefaultIsStacked, id, setWidget])
115
+
116
+ const handleToggle = useCallback(() => {
117
+ const newIsStacked = !isStacked
118
+
119
+ setWidget(id, { isStacked: newIsStacked })
120
+ updateOptions(newIsStacked)
121
+ }, [isStacked, id, setWidget, updateOptions])
122
+
123
+ const tooltipLabel = isStacked
124
+ ? (labels?.unstacked ?? 'Disable stacking')
125
+ : (labels?.stacked ?? 'Enable stacking')
126
+
127
+ // Early return if there's only one series - stacking doesn't apply
128
+ if (!hasMultiSeries) {
129
+ return null
130
+ }
131
+
132
+ return (
133
+ <Tooltip title={tooltipLabel}>
134
+ <IconButton
135
+ size='small'
136
+ aria-label={labels?.ariaLabel ?? tooltipLabel}
137
+ onClick={handleToggle}
138
+ sx={actionButtonStyles.trigger}
139
+ data-active={isStacked}
140
+ {...IconButtonProps}
141
+ >
142
+ {Icon ?? <GroupedBarChartIcon />}
143
+ </IconButton>
144
+ </Tooltip>
145
+ )
146
+ }
@@ -0,0 +1,29 @@
1
+ import type { IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../stores/types'
4
+
5
+ export interface StackToggleProps {
6
+ /** Widget ID to update stack configuration in the widget store */
7
+ id: string
8
+ /** Initial toggle state - when true, starts with stacking enabled */
9
+ defaultIsStacked?: boolean
10
+ /** Custom labels for the action */
11
+ labels?: {
12
+ /** Tooltip when stacking is disabled (button will enable stacking) */
13
+ stacked?: string
14
+ /** Tooltip when stacking is enabled (button will disable stacking) */
15
+ unstacked?: string
16
+ /** Accessibility label */
17
+ ariaLabel?: string
18
+ }
19
+ /** Props passed to the IconButton component */
20
+ IconButtonProps?: IconButtonProps
21
+ /** Custom icon to display */
22
+ Icon?: ReactNode
23
+ }
24
+
25
+ export type StackToggleState<T = unknown> = BaseWidgetState<
26
+ T & {
27
+ isStacked?: boolean
28
+ }
29
+ >
@@ -0,0 +1,2 @@
1
+ export { ZoomToggle } from './zoom-toggle'
2
+ export type { ZoomToggleProps, ZoomState, ZoomConfig } from './types'
@@ -0,0 +1,14 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ container: {
5
+ display: 'flex',
6
+ alignItems: 'center',
7
+ gap: ({ spacing }) => spacing(0.25),
8
+ },
9
+ trigger: {
10
+ '&[data-active="true"]': {
11
+ background: (theme: Theme) => theme.palette.primary.relatedLight,
12
+ },
13
+ },
14
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,44 @@
1
+ import type { IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../stores/types'
4
+
5
+ /**
6
+ * State stored in widget store for zoom functionality
7
+ */
8
+ export interface ZoomConfig {
9
+ zoom?: boolean
10
+ /** Start percentage for zoom range (0-100) */
11
+ zoomStart?: number
12
+ /** End percentage for zoom range (0-100) */
13
+ zoomEnd?: number
14
+ }
15
+
16
+ export type ZoomState<T = unknown> = BaseWidgetState<T & ZoomConfig>
17
+
18
+ export interface ZoomToggleProps {
19
+ /** Widget ID to update zoom state in the widget store */
20
+ id: string
21
+ /** Initial zoom state - when true, zoom is enabled */
22
+ defaultZoom?: boolean
23
+ /** Initial start percentage for zoom range (0-100) */
24
+ defaultZoomStart?: number
25
+ /** Initial end percentage for zoom range (0-100) */
26
+ defaultZoomEnd?: number
27
+ /** Custom labels for the action */
28
+ labels?: {
29
+ /** Tooltip when zoom is disabled (button will enable zoom) */
30
+ enable?: string
31
+ /** Tooltip when zoom is enabled (button will disable zoom) */
32
+ disable?: string
33
+ /** Tooltip for reset button */
34
+ reset?: string
35
+ /** Accessibility label */
36
+ ariaLabel?: string
37
+ }
38
+ /** Props passed to the IconButton component */
39
+ IconButtonProps?: IconButtonProps
40
+ /** Custom icon to display for zoom toggle */
41
+ Icon?: ReactNode
42
+ /** Custom icon to display for reset button */
43
+ ResetIcon?: ReactNode
44
+ }