@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,399 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import { ThemeProvider, createTheme } from '@mui/material/styles'
4
+ import { CategoryUI } from './category-ui'
5
+ import { useWidgetStore } from '../stores/widget-store'
6
+ import type { CategoryWidgetState } from './types'
7
+
8
+ // Create a test theme with qualitative colors
9
+ const testTheme = createTheme({
10
+ palette: {
11
+ primary: {
12
+ main: '#1976d2',
13
+ },
14
+ qualitative: {
15
+ bold: {
16
+ 0: '#1f77b4',
17
+ 1: '#ff7f0e',
18
+ 2: '#2ca02c',
19
+ 3: '#d62728',
20
+ 4: '#9467bd',
21
+ 5: '#8c564b',
22
+ 6: '#e377c2',
23
+ 7: '#7f7f7f',
24
+ },
25
+ },
26
+ },
27
+ })
28
+
29
+ // Helper function to render with theme
30
+ function renderWithTheme(ui: React.ReactElement) {
31
+ return render(<ThemeProvider theme={testTheme}>{ui}</ThemeProvider>)
32
+ }
33
+
34
+ describe('CategoryUI', () => {
35
+ beforeEach(() => {
36
+ useWidgetStore.getState().clearWidgets()
37
+ })
38
+
39
+ test('returns null when widget does not exist', () => {
40
+ const { container } = renderWithTheme(<CategoryUI id='non-existent' />)
41
+ expect(container.firstChild).toBeNull()
42
+ })
43
+
44
+ test('returns null when data is empty', () => {
45
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
46
+ type: 'category',
47
+ data: [[]],
48
+ })
49
+
50
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
51
+ expect(container.firstChild).toBeNull()
52
+ })
53
+
54
+ test('renders basic category data', () => {
55
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
56
+ type: 'category',
57
+ data: [
58
+ [
59
+ { name: 'Category A', value: 100 },
60
+ { name: 'Category B', value: 50 },
61
+ ],
62
+ ],
63
+ })
64
+
65
+ renderWithTheme(<CategoryUI id='test-category' />)
66
+
67
+ expect(screen.getByText('Category A')).toBeTruthy()
68
+ expect(screen.getByText('Category B')).toBeTruthy()
69
+ expect(screen.getByText('100')).toBeTruthy()
70
+ expect(screen.getByText('50')).toBeTruthy()
71
+ })
72
+
73
+ test('auto-calculates max value from data when max prop not provided', () => {
74
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
75
+ type: 'category',
76
+ data: [
77
+ [
78
+ { name: 'Category A', value: 100 },
79
+ { name: 'Category B', value: 150 },
80
+ { name: 'Category C', value: 75 },
81
+ ],
82
+ ],
83
+ })
84
+
85
+ renderWithTheme(<CategoryUI id='test-category' />)
86
+
87
+ // Verify all categories are rendered
88
+ expect(screen.getByText('Category A')).toBeTruthy()
89
+ expect(screen.getByText('Category B')).toBeTruthy()
90
+ expect(screen.getByText('Category C')).toBeTruthy()
91
+ expect(screen.getByText('100')).toBeTruthy()
92
+ expect(screen.getByText('150')).toBeTruthy()
93
+ expect(screen.getByText('75')).toBeTruthy()
94
+ })
95
+
96
+ test('uses custom max value when provided', () => {
97
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
98
+ type: 'category',
99
+ data: [
100
+ [
101
+ { name: 'Category A', value: 100 },
102
+ { name: 'Category B', value: 150 },
103
+ ],
104
+ ],
105
+ max: 200,
106
+ })
107
+
108
+ renderWithTheme(<CategoryUI id='test-category' />)
109
+
110
+ // Verify categories are rendered with custom max
111
+ expect(screen.getByText('Category A')).toBeTruthy()
112
+ expect(screen.getByText('Category B')).toBeTruthy()
113
+ expect(screen.getByText('100')).toBeTruthy()
114
+ expect(screen.getByText('150')).toBeTruthy()
115
+ })
116
+
117
+ test('handles max value of 0', () => {
118
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
119
+ type: 'category',
120
+ data: [
121
+ [
122
+ { name: 'Category A', value: 100 },
123
+ { name: 'Category B', value: 50 },
124
+ ],
125
+ ],
126
+ max: 0,
127
+ })
128
+
129
+ renderWithTheme(<CategoryUI id='test-category' />)
130
+
131
+ // Verify categories are rendered even with max=0
132
+ expect(screen.getByText('Category A')).toBeTruthy()
133
+ expect(screen.getByText('Category B')).toBeTruthy()
134
+ expect(screen.getByText('100')).toBeTruthy()
135
+ expect(screen.getByText('50')).toBeTruthy()
136
+ })
137
+
138
+ test('handles max value less than actual data values', () => {
139
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
140
+ type: 'category',
141
+ data: [
142
+ [
143
+ { name: 'Category A', value: 150 },
144
+ { name: 'Category B', value: 75 },
145
+ ],
146
+ ],
147
+ max: 100,
148
+ })
149
+
150
+ renderWithTheme(<CategoryUI id='test-category' />)
151
+
152
+ // Verify categories are rendered when values exceed max
153
+ expect(screen.getByText('Category A')).toBeTruthy()
154
+ expect(screen.getByText('Category B')).toBeTruthy()
155
+ expect(screen.getByText('150')).toBeTruthy()
156
+ expect(screen.getByText('75')).toBeTruthy()
157
+ })
158
+
159
+ test('uses custom max with multi-series data', () => {
160
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
161
+ type: 'category',
162
+ data: [
163
+ [
164
+ { name: 'Product A', value: 80 },
165
+ { name: 'Product B', value: 60 },
166
+ ],
167
+ [
168
+ { name: 'Product A', value: 90 },
169
+ { name: 'Product B', value: 70 },
170
+ ],
171
+ ],
172
+ max: 100,
173
+ series: [{ name: 'Q1' }, { name: 'Q2' }],
174
+ })
175
+
176
+ renderWithTheme(<CategoryUI id='test-category' />)
177
+
178
+ // Multi-series should show legend
179
+ expect(screen.getByText('Q1')).toBeTruthy()
180
+ expect(screen.getByText('Q2')).toBeTruthy()
181
+
182
+ // Verify product names are displayed
183
+ expect(screen.getByText('Product A')).toBeTruthy()
184
+ expect(screen.getByText('Product B')).toBeTruthy()
185
+ })
186
+
187
+ test('updates when max value changes', () => {
188
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
189
+ type: 'category',
190
+ data: [[{ name: 'Category A', value: 100 }]],
191
+ max: 200,
192
+ })
193
+
194
+ renderWithTheme(<CategoryUI id='test-category' />)
195
+
196
+ expect(screen.getByText('Category A')).toBeTruthy()
197
+ expect(screen.getByText('100')).toBeTruthy()
198
+
199
+ // Update max value
200
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
201
+ type: 'category',
202
+ data: [[{ name: 'Category A', value: 100 }]],
203
+ max: 100,
204
+ })
205
+
206
+ // Widget should still render after update
207
+ expect(screen.getByText('Category A')).toBeTruthy()
208
+ expect(screen.getByText('100')).toBeTruthy()
209
+ })
210
+
211
+ test('uses formatter when provided', () => {
212
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
213
+ type: 'category',
214
+ data: [[{ name: 'Category A', value: 1234 }]],
215
+ formatter: (value: number) => `$${value}`,
216
+ })
217
+
218
+ renderWithTheme(<CategoryUI id='test-category' />)
219
+
220
+ expect(screen.getByText('$1234')).toBeTruthy()
221
+ })
222
+
223
+ test('uses formatter with custom max', () => {
224
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
225
+ type: 'category',
226
+ data: [[{ name: 'Sales', value: 125000 }]],
227
+ max: 200000,
228
+ formatter: (value: number) =>
229
+ new Intl.NumberFormat('en-US', {
230
+ style: 'currency',
231
+ currency: 'USD',
232
+ maximumFractionDigits: 0,
233
+ }).format(value),
234
+ })
235
+
236
+ renderWithTheme(<CategoryUI id='test-category' />)
237
+
238
+ // Should show formatted value
239
+ expect(screen.getByText('$125,000')).toBeTruthy()
240
+ expect(screen.getByText('Sales')).toBeTruthy()
241
+ })
242
+
243
+ test('max prop works with maxItems configuration', () => {
244
+ useWidgetStore.getState().setWidget<CategoryWidgetState>('test-category', {
245
+ type: 'category',
246
+ data: [
247
+ [
248
+ { name: 'Item 1', value: 80 },
249
+ { name: 'Item 2', value: 60 },
250
+ { name: 'Item 3', value: 40 },
251
+ { name: 'Item 4', value: 20 },
252
+ ],
253
+ ],
254
+ max: 100,
255
+ maxItems: 2,
256
+ })
257
+
258
+ renderWithTheme(<CategoryUI id='test-category' />)
259
+
260
+ // Should show only first 2 items
261
+ expect(screen.getByText('Item 1')).toBeTruthy()
262
+ expect(screen.getByText('Item 2')).toBeTruthy()
263
+
264
+ // Should show values
265
+ expect(screen.getByText('80')).toBeTruthy()
266
+ expect(screen.getByText('60')).toBeTruthy()
267
+ })
268
+
269
+ describe('Overflow behavior', () => {
270
+ test('applies overflow auto when hiddenCount is 0 (no maxItems)', () => {
271
+ useWidgetStore
272
+ .getState()
273
+ .setWidget<CategoryWidgetState>('test-category', {
274
+ type: 'category',
275
+ data: [
276
+ [
277
+ { name: 'Item 1', value: 100 },
278
+ { name: 'Item 2', value: 50 },
279
+ ],
280
+ ],
281
+ // No maxItems set, so hiddenCount will be 0
282
+ })
283
+
284
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
285
+ // The list Box has inline style for overflow
286
+ const listBox = container.querySelector<HTMLElement>(
287
+ '[style*="overflow"]',
288
+ )!
289
+
290
+ expect(listBox).toBeTruthy()
291
+ expect(listBox.style.overflow).toBe('auto')
292
+ })
293
+
294
+ test('applies overflow hidden when hiddenCount is greater than 0', () => {
295
+ useWidgetStore
296
+ .getState()
297
+ .setWidget<CategoryWidgetState>('test-category', {
298
+ type: 'category',
299
+ data: [
300
+ [
301
+ { name: 'Item 1', value: 100 },
302
+ { name: 'Item 2', value: 80 },
303
+ { name: 'Item 3', value: 60 },
304
+ { name: 'Item 4', value: 40 },
305
+ ],
306
+ ],
307
+ maxItems: 2, // This creates hiddenCount = 2
308
+ })
309
+
310
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
311
+ // The list Box has inline style for overflow
312
+ const listBox = container.querySelector<HTMLElement>(
313
+ '[style*="overflow"]',
314
+ )!
315
+
316
+ expect(listBox).toBeTruthy()
317
+ expect(listBox.style.overflow).toBe('hidden')
318
+ })
319
+
320
+ test('maintains overflow auto when maxItems equals data length', () => {
321
+ useWidgetStore
322
+ .getState()
323
+ .setWidget<CategoryWidgetState>('test-category', {
324
+ type: 'category',
325
+ data: [
326
+ [
327
+ { name: 'Item 1', value: 100 },
328
+ { name: 'Item 2', value: 50 },
329
+ ],
330
+ ],
331
+ maxItems: 2, // Same as data length, hiddenCount = 0
332
+ })
333
+
334
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
335
+ // The list Box has inline style for overflow
336
+ const listBox = container.querySelector<HTMLElement>(
337
+ '[style*="overflow"]',
338
+ )!
339
+
340
+ expect(listBox).toBeTruthy()
341
+ expect(listBox.style.overflow).toBe('auto')
342
+ })
343
+
344
+ test('applies overflow auto when maxItems exceeds data length', () => {
345
+ useWidgetStore
346
+ .getState()
347
+ .setWidget<CategoryWidgetState>('test-category', {
348
+ type: 'category',
349
+ data: [
350
+ [
351
+ { name: 'Item 1', value: 100 },
352
+ { name: 'Item 2', value: 50 },
353
+ ],
354
+ ],
355
+ maxItems: 5, // More than data length, hiddenCount = 0
356
+ })
357
+
358
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
359
+ // The list Box has inline style for overflow
360
+ const listBox = container.querySelector<HTMLElement>(
361
+ '[style*="overflow"]',
362
+ )!
363
+
364
+ expect(listBox).toBeTruthy()
365
+ expect(listBox.style.overflow).toBe('auto')
366
+ })
367
+
368
+ test('works correctly with multi-series data', () => {
369
+ useWidgetStore
370
+ .getState()
371
+ .setWidget<CategoryWidgetState>('test-category', {
372
+ type: 'category',
373
+ data: [
374
+ [
375
+ { name: 'Product A', value: 100 },
376
+ { name: 'Product B', value: 80 },
377
+ { name: 'Product C', value: 60 },
378
+ ],
379
+ [
380
+ { name: 'Product A', value: 90 },
381
+ { name: 'Product B', value: 70 },
382
+ { name: 'Product C', value: 50 },
383
+ ],
384
+ ],
385
+ series: [{ name: 'Q1' }, { name: 'Q2' }],
386
+ maxItems: 2, // hiddenCount = 1
387
+ })
388
+
389
+ const { container } = renderWithTheme(<CategoryUI id='test-category' />)
390
+ // The list Box has inline style for overflow
391
+ const listBox = container.querySelector<HTMLElement>(
392
+ '[style*="overflow"]',
393
+ )!
394
+
395
+ expect(listBox).toBeTruthy()
396
+ expect(listBox.style.overflow).toBe('hidden')
397
+ })
398
+ })
399
+ })
@@ -0,0 +1,156 @@
1
+ import { Box, useTheme } from '@mui/material'
2
+ import { useWidgetStore } from '../stores/widget-store'
3
+ import { styles } from './style'
4
+ import type { CategoryUIProps, CategoryWidgetState } from './types'
5
+ import {
6
+ CategoryRowSingle,
7
+ CategoryRowMulti,
8
+ CategoryRowOther,
9
+ CategoryLegend,
10
+ } from './components'
11
+ import { useShallow } from 'zustand/shallow'
12
+ import { useState } from 'react'
13
+
14
+ const defaultFormatter = (value: number) => value.toString()
15
+
16
+ export function CategoryUI({ id }: CategoryUIProps) {
17
+ const theme = useTheme()
18
+ const widget = useWidgetStore(
19
+ useShallow((state) => {
20
+ const widget = state.getWidget<CategoryWidgetState>(id)
21
+ return {
22
+ formatter: widget?.formatter,
23
+ series: widget?.series,
24
+ data: widget?.data,
25
+ maxItems: widget?.maxItems,
26
+ labels: widget?.labels,
27
+ onRowClick: widget?.onRowClick,
28
+ selected: widget?.selected,
29
+ max: widget?.max,
30
+ }
31
+ }),
32
+ )
33
+
34
+ const formatter = widget?.formatter ?? defaultFormatter
35
+ const series = widget?.series ?? []
36
+ const data = widget?.data
37
+ const maxItems = widget?.maxItems
38
+ const labels = widget?.labels
39
+ const onRowClick = widget?.onRowClick
40
+ const selected = widget?.selected
41
+ const max = widget?.max
42
+
43
+ const [maxHeight] = useState<string | number | undefined>(
44
+ maxItems ? 40 * (series.length || 1) * maxItems : undefined,
45
+ )
46
+
47
+ const qualitativeColors = Object.values(theme.palette.qualitative.bold)
48
+
49
+ const colors =
50
+ series.length > 0
51
+ ? series.map(
52
+ (s, index: number) =>
53
+ s.color ??
54
+ qualitativeColors[index % qualitativeColors.length] ??
55
+ theme.palette.secondary.main,
56
+ )
57
+ : [theme.palette.secondary.main]
58
+
59
+ // Group data items by name to support multi-series display
60
+ // data is CategoryDataItem[][] where data[seriesIndex] contains items for that series
61
+ const groupedData = generateGroupedData({ data })
62
+
63
+ if (groupedData.length === 0) {
64
+ return null
65
+ }
66
+
67
+ const maxValue =
68
+ max ?? Math.max(...groupedData.flatMap((item) => item.values))
69
+
70
+ // Slice data to maxItems and compute hidden count
71
+ const visibleData =
72
+ maxItems !== undefined && maxItems >= 0
73
+ ? groupedData.slice(0, maxItems)
74
+ : groupedData
75
+
76
+ const hiddenCount = groupedData.length - visibleData.length
77
+ const isMulti = series.length > 1
78
+
79
+ return (
80
+ <Box
81
+ sx={{
82
+ ...styles.root,
83
+ }}
84
+ >
85
+ <Box
86
+ sx={styles.list}
87
+ style={{ maxHeight, overflow: hiddenCount === 0 ? 'auto' : 'hidden' }}
88
+ >
89
+ {isMulti
90
+ ? visibleData.map((item) => (
91
+ <CategoryRowMulti
92
+ key={item.name}
93
+ name={item.name}
94
+ values={item.values}
95
+ maxValue={maxValue}
96
+ colors={colors}
97
+ formatter={formatter}
98
+ onClick={onRowClick}
99
+ selected={selected?.(item.name) ?? true}
100
+ />
101
+ ))
102
+ : visibleData.map((item) => (
103
+ <CategoryRowSingle
104
+ key={item.name}
105
+ name={item.name}
106
+ value={item.values[0] ?? 0}
107
+ selected={selected?.(item.name) ?? true}
108
+ maxValue={maxValue}
109
+ color={colors[0]!}
110
+ formatter={formatter}
111
+ onClick={onRowClick}
112
+ />
113
+ ))}
114
+ {hiddenCount > 0 && (
115
+ <CategoryRowOther
116
+ hiddenCount={hiddenCount}
117
+ otherLabel={labels?.other}
118
+ otherCountLabel={labels?.otherCount}
119
+ />
120
+ )}
121
+ </Box>
122
+ {series.length > 0 && <CategoryLegend series={series} colors={colors} />}
123
+ </Box>
124
+ )
125
+ }
126
+
127
+ function generateGroupedData({
128
+ data,
129
+ }: {
130
+ data: { name: string; value: number }[][] | undefined
131
+ }) {
132
+ if (!data || data.length === 0) return []
133
+
134
+ const seriesCount = Math.max(data.length, 1)
135
+ const grouped = new Map<string, number[]>()
136
+ const nameOrder: string[] = []
137
+
138
+ // Iterate over each series (outer array)
139
+ for (let seriesIndex = 0; seriesIndex < data.length; seriesIndex++) {
140
+ const seriesData = data[seriesIndex]!
141
+ for (const item of seriesData) {
142
+ let values = grouped.get(item.name)
143
+ if (!values) {
144
+ values = new Array<number>(seriesCount).fill(0)
145
+ grouped.set(item.name, values)
146
+ nameOrder.push(item.name)
147
+ }
148
+ values[seriesIndex] = item.value
149
+ }
150
+ }
151
+
152
+ return nameOrder.map((name) => ({
153
+ name,
154
+ values: grouped.get(name)!,
155
+ }))
156
+ }
@@ -0,0 +1,28 @@
1
+ import { Box } from '@mui/material'
2
+ import { styles } from '../style'
3
+
4
+ export interface CategoryBarProps {
5
+ value: number
6
+ maxValue: number
7
+ color: string
8
+ selected?: boolean
9
+ }
10
+
11
+ export function CategoryBar({
12
+ value,
13
+ maxValue,
14
+ color,
15
+ selected = true,
16
+ }: CategoryBarProps) {
17
+ const percentage = maxValue > 0 ? (value / maxValue) * 100 : 0
18
+
19
+ const barFillSx = selected
20
+ ? { ...styles.barFill, width: `${percentage}%`, backgroundColor: color }
21
+ : { ...styles.barFill, ...styles.barFillMuted, width: `${percentage}%` }
22
+
23
+ return (
24
+ <Box sx={styles.bar}>
25
+ <Box sx={barFillSx} />
26
+ </Box>
27
+ )
28
+ }
@@ -0,0 +1,30 @@
1
+ import { Box, Typography } from '@mui/material'
2
+ import { styles } from '../style'
3
+ import type { CategorySeriesConfig } from '../types'
4
+
5
+ export interface CategoryLegendProps {
6
+ series: CategorySeriesConfig[]
7
+ colors: string[]
8
+ }
9
+
10
+ export function CategoryLegend({ series, colors }: CategoryLegendProps) {
11
+ if (series.length === 0) {
12
+ return null
13
+ }
14
+
15
+ return (
16
+ <Box sx={styles.legend}>
17
+ {series.map((item, index) => (
18
+ <Box key={item.name} sx={styles.legendItem}>
19
+ <Box
20
+ sx={{
21
+ ...styles.legendDot,
22
+ backgroundColor: item.color ?? colors[index % colors.length],
23
+ }}
24
+ />
25
+ <Typography sx={styles.legendLabel}>{item.name}</Typography>
26
+ </Box>
27
+ ))}
28
+ </Box>
29
+ )
30
+ }
@@ -0,0 +1,50 @@
1
+ import { Box, Typography } from '@mui/material'
2
+ import { styles } from '../style'
3
+ import { CategoryBar } from './category-bar'
4
+ import type { CategoryWidgetConfig } from '../types'
5
+
6
+ export interface CategoryRowMultiProps {
7
+ name: string
8
+ values: number[]
9
+ maxValue: number
10
+ colors: string[]
11
+ formatter: NonNullable<CategoryWidgetConfig['formatter']>
12
+ onClick?: CategoryWidgetConfig['onRowClick']
13
+ selected?: boolean
14
+ }
15
+
16
+ export function CategoryRowMulti({
17
+ name,
18
+ values,
19
+ maxValue,
20
+ colors,
21
+ formatter,
22
+ onClick,
23
+ selected = true,
24
+ }: CategoryRowMultiProps) {
25
+ const handleClick = onClick ? () => onClick({ name }) : undefined
26
+ const rowStyle = onClick ? styles.rowClickable : styles.row
27
+
28
+ return (
29
+ <Box sx={rowStyle} onClick={handleClick}>
30
+ <Typography sx={styles.rowLabel}>{name}</Typography>
31
+ <Box sx={styles.barContainer}>
32
+ {values.map((value, index) => (
33
+ <Box key={`${name}-${value}-${index}`} sx={styles.multiBarRow}>
34
+ <Box sx={styles.multiBarContainer}>
35
+ <CategoryBar
36
+ value={value}
37
+ maxValue={maxValue}
38
+ color={colors[index % colors.length] ?? ''}
39
+ selected={selected}
40
+ />
41
+ </Box>
42
+ <Typography sx={styles.multiBarValue}>
43
+ {formatter(value)}
44
+ </Typography>
45
+ </Box>
46
+ ))}
47
+ </Box>
48
+ </Box>
49
+ )
50
+ }
@@ -0,0 +1,23 @@
1
+ import { Box, Typography } from '@mui/material'
2
+ import { styles } from '../style'
3
+
4
+ export interface CategoryRowOtherProps {
5
+ hiddenCount: number
6
+ otherLabel?: string
7
+ otherCountLabel?: string
8
+ }
9
+
10
+ export function CategoryRowOther({
11
+ hiddenCount,
12
+ otherLabel = 'Other',
13
+ otherCountLabel = '{count} more',
14
+ }: CategoryRowOtherProps) {
15
+ const countText = otherCountLabel.replace('{count}', String(hiddenCount))
16
+
17
+ return (
18
+ <Box sx={styles.otherRow}>
19
+ <Typography sx={styles.otherLabel}>{otherLabel}</Typography>
20
+ <Typography sx={styles.otherCount}>({countText})</Typography>
21
+ </Box>
22
+ )
23
+ }