@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,203 @@
1
+ import { describe, test, expect, beforeEach, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { RangeUI } from './range-ui'
4
+ import { useWidgetStore } from '../stores/widget-store'
5
+ import type { RangeWidgetState } from './types'
6
+
7
+ describe('RangeUI', () => {
8
+ beforeEach(() => {
9
+ useWidgetStore.getState().clearWidgets()
10
+ })
11
+
12
+ test('returns null when widget does not exist', () => {
13
+ const { container } = render(<RangeUI id='non-existent' />)
14
+ expect(container.firstChild).toBeNull()
15
+ })
16
+
17
+ test('renders with single range item', () => {
18
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
19
+ type: 'range',
20
+ data: [{ min: 0, max: 100, value: [20, 80] }],
21
+ })
22
+
23
+ render(<RangeUI id='test-range' />)
24
+
25
+ const inputs = screen.getAllByRole('textbox')
26
+ expect(inputs).toHaveLength(2)
27
+ expect((inputs[0] as HTMLInputElement).value).toBe('20')
28
+ expect((inputs[1] as HTMLInputElement).value).toBe('80')
29
+ })
30
+
31
+ test('renders with multiple range items', () => {
32
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
33
+ type: 'range',
34
+ data: [
35
+ { min: 0, max: 100, value: [20, 80] },
36
+ { min: 0, max: 50, value: [10, 40] },
37
+ ],
38
+ })
39
+
40
+ render(<RangeUI id='test-range' />)
41
+
42
+ const inputs = screen.getAllByRole('textbox')
43
+ expect(inputs).toHaveLength(4)
44
+ expect((inputs[0] as HTMLInputElement).value).toBe('20')
45
+ expect((inputs[1] as HTMLInputElement).value).toBe('80')
46
+ expect((inputs[2] as HTMLInputElement).value).toBe('10')
47
+ expect((inputs[3] as HTMLInputElement).value).toBe('40')
48
+ })
49
+
50
+ test('uses formatter when provided', () => {
51
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
52
+ type: 'range',
53
+ data: [{ min: 0, max: 100, value: [20, 80] }],
54
+ formatter: (val: number) => `$${val}`,
55
+ })
56
+
57
+ render(<RangeUI id='test-range' />)
58
+
59
+ const inputs = screen.getAllByRole('textbox')
60
+ expect((inputs[0] as HTMLInputElement).value).toBe('$20')
61
+ expect((inputs[1] as HTMLInputElement).value).toBe('$80')
62
+ })
63
+
64
+ test('handles onChange callback', () => {
65
+ const onChange = vi.fn()
66
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
67
+ type: 'range',
68
+ data: [{ min: 0, max: 100, value: [20, 80] }],
69
+ onChange,
70
+ })
71
+
72
+ render(<RangeUI id='test-range' />)
73
+
74
+ const minInput = screen.getAllByRole('textbox')[0]
75
+ fireEvent.change(minInput!, { target: { value: '30' } })
76
+ fireEvent.blur(minInput!)
77
+
78
+ expect(onChange).toHaveBeenCalledWith([30, 80], 0)
79
+ })
80
+
81
+ test('renders disabled state', () => {
82
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
83
+ type: 'range',
84
+ data: [{ min: 0, max: 100, disabled: true }],
85
+ })
86
+
87
+ render(<RangeUI id='test-range' />)
88
+
89
+ const inputs = screen.getAllByRole('textbox')
90
+ expect((inputs[0] as HTMLInputElement).disabled).toBe(true)
91
+ expect((inputs[1] as HTMLInputElement).disabled).toBe(true)
92
+ })
93
+
94
+ test('handles empty data array', () => {
95
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
96
+ type: 'range',
97
+ data: [],
98
+ })
99
+
100
+ const { container } = render(<RangeUI id='test-range' />)
101
+ expect(container.firstChild).toBeNull()
102
+ })
103
+
104
+ test('handles undefined data', () => {
105
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
106
+ type: 'range',
107
+ })
108
+
109
+ const { container } = render(<RangeUI id='test-range' />)
110
+ expect(container.firstChild).toBeNull()
111
+ })
112
+
113
+ test('uses default min/max when value is not provided', () => {
114
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
115
+ type: 'range',
116
+ data: [{ min: 10, max: 90 }],
117
+ })
118
+
119
+ render(<RangeUI id='test-range' />)
120
+
121
+ const inputs = screen.getAllByRole('textbox')
122
+ expect((inputs[0] as HTMLInputElement).value).toBe('10')
123
+ expect((inputs[1] as HTMLInputElement).value).toBe('90')
124
+ })
125
+
126
+ test('validates min input on blur', () => {
127
+ const onChange = vi.fn()
128
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
129
+ type: 'range',
130
+ data: [{ min: 0, max: 100, value: [20, 80] }],
131
+ onChange,
132
+ })
133
+
134
+ render(<RangeUI id='test-range' />)
135
+
136
+ const minInput = screen.getAllByRole('textbox')[0]
137
+
138
+ // Try to set min higher than max
139
+ fireEvent.change(minInput!, { target: { value: '90' } })
140
+ fireEvent.blur(minInput!)
141
+
142
+ // Should clamp to max value
143
+ expect(onChange).toHaveBeenCalledWith([80, 80], 0)
144
+ })
145
+
146
+ test('validates max input on blur', () => {
147
+ const onChange = vi.fn()
148
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
149
+ type: 'range',
150
+ data: [{ min: 0, max: 100, value: [20, 80] }],
151
+ onChange,
152
+ })
153
+
154
+ render(<RangeUI id='test-range' />)
155
+
156
+ const maxInput = screen.getAllByRole('textbox')[1]
157
+
158
+ // Try to set max lower than min
159
+ fireEvent.change(maxInput!, { target: { value: '10' } })
160
+ fireEvent.blur(maxInput!)
161
+
162
+ // Should clamp to min value
163
+ expect(onChange).toHaveBeenCalledWith([20, 20], 0)
164
+ })
165
+
166
+ test('updates when widget data changes', () => {
167
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
168
+ type: 'range',
169
+ data: [{ min: 0, max: 100, value: [20, 80] }],
170
+ })
171
+
172
+ const { rerender } = render(<RangeUI id='test-range' />)
173
+
174
+ let inputs = screen.getAllByRole('textbox')
175
+ expect((inputs[0] as HTMLInputElement).value).toBe('20')
176
+ expect((inputs[1] as HTMLInputElement).value).toBe('80')
177
+
178
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
179
+ data: [{ min: 0, max: 100, value: [30, 70] }],
180
+ })
181
+
182
+ rerender(<RangeUI id='test-range' />)
183
+
184
+ inputs = screen.getAllByRole('textbox')
185
+ expect((inputs[0] as HTMLInputElement).value).toBe('30')
186
+ expect((inputs[1] as HTMLInputElement).value).toBe('70')
187
+ })
188
+
189
+ test('handles widget removal gracefully', () => {
190
+ useWidgetStore.getState().setWidget<RangeWidgetState>('test-range', {
191
+ type: 'range',
192
+ data: [{ min: 0, max: 100 }],
193
+ })
194
+
195
+ const { container, rerender } = render(<RangeUI id='test-range' />)
196
+ expect(container.firstChild).toBeTruthy()
197
+
198
+ useWidgetStore.getState().removeWidget('test-range')
199
+
200
+ rerender(<RangeUI id='test-range' />)
201
+ expect(container.firstChild).toBeNull()
202
+ })
203
+ })
@@ -0,0 +1,11 @@
1
+ import type { RangeUIProps } from './types'
2
+ import { Row } from '../formula/components/row'
3
+ import { RangeItem } from './components/range-item'
4
+
5
+ export function RangeUI(props: RangeUIProps) {
6
+ return (
7
+ <Row id={props.id}>
8
+ {({ index }) => <RangeItem id={props.id} index={index} />}
9
+ </Row>
10
+ )
11
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { sanitizeRangeDataItem, sanitizeRangeDataItems } from './serializer'
3
+ import type { RangeDataItem } from './types'
4
+
5
+ describe('sanitizeRangeDataItem', () => {
6
+ test('preserves all other properties', () => {
7
+ const item: RangeDataItem = {
8
+ min: 10,
9
+ max: 90,
10
+ value: [20, 80],
11
+ step: 5,
12
+ marks: true,
13
+ disabled: true,
14
+ color: '#FF0000',
15
+ }
16
+
17
+ const sanitized = sanitizeRangeDataItem(item)
18
+
19
+ expect(sanitized.min).toBe(10)
20
+ expect(sanitized.max).toBe(90)
21
+ expect(sanitized.value).toEqual([20, 80])
22
+ expect(sanitized.step).toBe(5)
23
+ expect(sanitized.marks).toBe(true)
24
+ expect(sanitized.disabled).toBe(true)
25
+ expect(sanitized.color).toBe('#FF0000')
26
+ })
27
+ })
28
+
29
+ describe('sanitizeRangeDataItems', () => {
30
+ test('sanitizes array of items', () => {
31
+ const items: RangeDataItem[] = [{ min: 10, max: 90 }]
32
+
33
+ const sanitized = sanitizeRangeDataItems(items)
34
+
35
+ expect(sanitized).toHaveLength(1)
36
+ })
37
+
38
+ test('handles undefined input', () => {
39
+ const sanitized = sanitizeRangeDataItems(undefined)
40
+
41
+ expect(sanitized).toBeUndefined()
42
+ })
43
+
44
+ test('handles empty array', () => {
45
+ const sanitized = sanitizeRangeDataItems([])
46
+
47
+ expect(sanitized).toEqual([])
48
+ })
49
+
50
+ test('preserves all item properties', () => {
51
+ const items: RangeDataItem[] = [
52
+ {
53
+ min: 0,
54
+ max: 100,
55
+ value: [25, 75],
56
+ step: 10,
57
+ marks: [
58
+ { value: 0, label: '0' },
59
+ { value: 100, label: '100' },
60
+ ],
61
+ disabled: false,
62
+ color: '#00FF00',
63
+ },
64
+ ]
65
+
66
+ const sanitized = sanitizeRangeDataItems(items)
67
+
68
+ expect(sanitized![0]).toEqual(items[0])
69
+ })
70
+ })
@@ -0,0 +1,27 @@
1
+ import type { RangeDataItem } from './types'
2
+
3
+ /**
4
+ * Sanitizes RangeDataItem by converting ReactNode note values to undefined.
5
+ * This ensures only string values are stored in the widget state.
6
+ *
7
+ * @param item - The RangeDataItem to sanitize
8
+ * @returns A new RangeDataItem with ReactNode note values converted to undefined
9
+ */
10
+ export function sanitizeRangeDataItem(item: RangeDataItem): RangeDataItem {
11
+ return {
12
+ ...item,
13
+ note: typeof item.note === 'string' ? item.note : undefined,
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Sanitizes an array of RangeDataItems by converting ReactNode note values to undefined.
19
+ *
20
+ * @param items - Array of RangeDataItems to sanitize
21
+ * @returns A new array with sanitized RangeDataItems
22
+ */
23
+ export function sanitizeRangeDataItems(
24
+ items: RangeDataItem[] | undefined,
25
+ ): RangeDataItem[] | undefined {
26
+ return items?.map(sanitizeRangeDataItem)
27
+ }
@@ -0,0 +1,14 @@
1
+ import { Box, Skeleton } from '@mui/material'
2
+ import { styles } from './style'
3
+
4
+ export function RangeSkeleton() {
5
+ return (
6
+ <Box sx={styles.rangeItem} aria-label='Range skeleton'>
7
+ <Skeleton width='100%' height={32} />
8
+ <Box sx={styles.inputsRow}>
9
+ <Skeleton width='100%' height={40} />
10
+ <Skeleton width='100%' height={40} />
11
+ </Box>
12
+ </Box>
13
+ )
14
+ }
@@ -0,0 +1,37 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles: Record<string, SxProps<Theme>> = {
4
+ root: {
5
+ display: 'flex',
6
+ flexDirection: 'column',
7
+ gap: (theme: Theme) => theme.spacing(2),
8
+ },
9
+ rangeItem: {
10
+ display: 'flex',
11
+ flexDirection: 'column',
12
+ gap: (theme: Theme) => theme.spacing(1.5),
13
+ },
14
+ sliderContainer: {
15
+ display: 'flex',
16
+ flexDirection: 'column',
17
+ gap: (theme: Theme) => theme.spacing(1),
18
+ px: (theme: Theme) => theme.spacing(1),
19
+ },
20
+ inputsRow: {
21
+ display: 'flex',
22
+ gap: (theme: Theme) => theme.spacing(2),
23
+ alignItems: 'center',
24
+ },
25
+ input: {
26
+ flex: 1,
27
+ '& .MuiInputBase-input': {
28
+ textAlign: 'center',
29
+ },
30
+ },
31
+ slider: {
32
+ width: '100%',
33
+ '&.Mui-disabled': {
34
+ color: (theme: Theme) => theme.palette.text.disabled,
35
+ },
36
+ },
37
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react'
2
+ import type { BaseWidgetState, WidgetsStoreProps } from '../stores/types'
3
+ import type { WrapperState } from '../wrapper/types'
4
+ import type { DownloadItem } from '../actions/download/types'
5
+
6
+ export interface RangeUIProps {
7
+ id: WidgetsStoreProps['id']
8
+ }
9
+
10
+ export interface RangeDataItem {
11
+ min: number
12
+ max: number
13
+ value?: number[]
14
+ step?: number
15
+ marks?: boolean | { value: number; label?: string }[]
16
+ disabled?: boolean
17
+ color?: string
18
+ note?: string | ReactNode
19
+ }
20
+
21
+ export interface RangeItemProps {
22
+ id: WidgetsStoreProps['id']
23
+ index: number
24
+ }
25
+
26
+ export type RangeWidgetData = RangeDataItem[]
27
+
28
+ export type RangeWidgetState = BaseWidgetState<
29
+ WrapperState<RangeWidgetConfig>
30
+ > & {
31
+ data: RangeWidgetData
32
+ }
33
+
34
+ export interface RangeWidgetConfig {
35
+ formatter?: (value: number) => string
36
+ onChange?: (value: number[], index: number) => void
37
+ }
38
+
39
+ export type RangeDownloadConfig = DownloadItem<RangeWidgetData>[]
@@ -0,0 +1,138 @@
1
+ import {
2
+ getCommonOptions,
3
+ mergeEchartWidgetConfig,
4
+ type EchartOptionsProps,
5
+ } from '../echart'
6
+ import type {
7
+ ScatterplotConfig,
8
+ ScatterplotWidgetConfig,
9
+ ScatterplotWidgetData,
10
+ } from './types'
11
+ import {
12
+ scatterplotDataToCSV,
13
+ buildLegendConfig,
14
+ createTooltipFormatter,
15
+ applyYAxisFormatter,
16
+ } from '../_shared/chart-config'
17
+ import { downloadToCSV, downloadToPNG, type DownloadItem } from '../actions'
18
+ import type { ConfigProps } from '../loader/types'
19
+
20
+ export function scatterplotDownloadConfig({
21
+ refUI,
22
+ }: ConfigProps): DownloadItem<ScatterplotWidgetData>[] {
23
+ return [
24
+ {
25
+ ...downloadToPNG,
26
+ modifier: () => downloadToPNG.modifier(refUI),
27
+ },
28
+ {
29
+ ...downloadToCSV,
30
+ modifier: async (data) => {
31
+ const rows = scatterplotDataToCSV(data)
32
+ return downloadToCSV.modifier(rows)
33
+ },
34
+ },
35
+ ]
36
+ }
37
+
38
+ export function scatterplotConfig(
39
+ props: ScatterplotConfig,
40
+ ): ScatterplotWidgetConfig {
41
+ return {
42
+ type: 'scatterplot',
43
+ option: mergeEchartWidgetConfig(getCommonOptions(props), getOption(props)),
44
+ }
45
+ }
46
+
47
+ function getOption({
48
+ data = [],
49
+ theme,
50
+ formatter,
51
+ }: ScatterplotConfig): EchartOptionsProps {
52
+ const hasLegend = data.length > 1
53
+
54
+ const xAxis = {
55
+ type: 'value' as const,
56
+ axisLine: { show: false },
57
+ axisTick: { show: false },
58
+ axisLabel: {
59
+ fontSize: theme.typography.overlineDelicate.fontSize,
60
+ fontFamily: theme.typography.overlineDelicate.fontFamily,
61
+ showMinLabel: true,
62
+ showMaxLabel: true,
63
+ hideOverlap: true,
64
+ padding: [
65
+ parseInt(theme.spacing(0.5)),
66
+ parseInt(theme.spacing(0.5)),
67
+ 0,
68
+ parseInt(theme.spacing(0.5)),
69
+ ],
70
+ color: theme.palette.black[60],
71
+ },
72
+ splitLine: {
73
+ show: true,
74
+ lineStyle: { color: theme.palette.black[4] },
75
+ },
76
+ }
77
+
78
+ const yAxis = {
79
+ type: 'value' as const,
80
+ axisLabel: {
81
+ fontSize: theme.typography.overlineDelicate.fontSize,
82
+ fontFamily: theme.typography.overlineDelicate.fontFamily,
83
+ inside: false,
84
+ padding: [
85
+ 0,
86
+ 0,
87
+ parseInt(theme.spacing(1.25)),
88
+ parseInt(theme.spacing(3.25)),
89
+ ],
90
+ margin: 0,
91
+ show: true,
92
+ showMaxLabel: true,
93
+ showMinLabel: false,
94
+ align: 'right',
95
+ verticalAlign: 'bottom',
96
+ },
97
+ axisLine: { show: false },
98
+ axisTick: { show: false },
99
+ splitLine: {
100
+ show: true,
101
+ lineStyle: { color: theme.palette.black[4] },
102
+ },
103
+ }
104
+
105
+ return {
106
+ legend: buildLegendConfig(hasLegend),
107
+ grid: {
108
+ ...(!hasLegend && { bottom: parseInt(theme.spacing(1)) }),
109
+ ...(hasLegend && { bottom: parseInt(theme.spacing(10)) }),
110
+ },
111
+ xAxis,
112
+ yAxis: applyYAxisFormatter(yAxis, formatter),
113
+ tooltip: {
114
+ trigger: 'item',
115
+ // position: createTooltipPositioner(theme),
116
+ formatter: createTooltipFormatter((item) => {
117
+ const value = item.value as Record<string, string | number>
118
+ const index = item.encode?.y?.at(0)
119
+ const _value = value[index ?? '']
120
+
121
+ const formattedValue =
122
+ typeof _value === 'number' && formatter
123
+ ? formatter(_value)
124
+ : (_value ?? '')
125
+ const marker = typeof item.marker === 'string' ? item.marker : ''
126
+ const name = item.seriesName ?? ''
127
+
128
+ return { name, seriesName: '', marker, value: formattedValue }
129
+ }),
130
+ },
131
+ color: Object.values(theme.palette.qualitative.bold),
132
+ series: data.map((_: unknown, index: number) => ({
133
+ datasetIndex: index,
134
+ type: 'scatter',
135
+ symbolSize: 8,
136
+ })),
137
+ } as EchartOptionsProps
138
+ }
@@ -0,0 +1,8 @@
1
+ export type {
2
+ ScatterplotWidgetData,
3
+ ScatterplotWidgetState,
4
+ ScatterplotWidgetConfig,
5
+ ScatterplotConfig,
6
+ } from './types'
7
+ export { scatterplotConfig, scatterplotDownloadConfig } from './config'
8
+ export { ScatterplotSkeleton } from './skeleton'
@@ -0,0 +1,59 @@
1
+ import { Box, Skeleton } from '@mui/material'
2
+ import { styles } from './style'
3
+
4
+ const SCATTER_POINTS = [
5
+ { left: '20%', top: '62%' },
6
+ { left: '26%', top: '70%' },
7
+ { left: '29%', top: '73%' },
8
+ { left: '38%', top: '63%' },
9
+ { left: '41%', top: '68%' },
10
+ { left: '47%', top: '62%' },
11
+ { left: '53%', top: '49%' },
12
+ { left: '56%', top: '55%' },
13
+ { left: '65%', top: '37%' },
14
+ { left: '68%', top: '43%' },
15
+ { left: '77%', top: '59%' },
16
+ { left: '25%', top: '30%' },
17
+ { left: '60%', top: '70%' },
18
+ { left: '10%', top: '55%' },
19
+ { left: '40%', top: '40%' },
20
+ ]
21
+
22
+ export function ScatterplotSkeleton() {
23
+ return (
24
+ <Box sx={styles.skeleton.graph.container}>
25
+ {SCATTER_POINTS.map((point, index) => (
26
+ <Skeleton
27
+ key={index}
28
+ variant='circular'
29
+ width={12}
30
+ height={12}
31
+ sx={{
32
+ position: 'absolute',
33
+ top: point.top,
34
+ left: point.left,
35
+ transform: 'translate(-50%, -50%)',
36
+ }}
37
+ />
38
+ ))}
39
+ {/* Legend */}
40
+ <Box sx={styles.skeleton.legend}>
41
+ {Array(2)
42
+ .fill(0)
43
+ .map((_, i) => (
44
+ <Box
45
+ key={i}
46
+ sx={{
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ gap: ({ spacing }) => spacing(1.5),
50
+ }}
51
+ >
52
+ <Skeleton variant='circular' width={8} height={8} />
53
+ <Skeleton width={48} height={8} />
54
+ </Box>
55
+ ))}
56
+ </Box>
57
+ </Box>
58
+ )
59
+ }
@@ -0,0 +1,21 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+ import { baseSkeletonStyles } from '../_shared/skeleton'
3
+
4
+ export const styles = {
5
+ skeleton: {
6
+ graph: {
7
+ container: {
8
+ ...baseSkeletonStyles.graph.container,
9
+ position: 'relative',
10
+ },
11
+ },
12
+ legend: {
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ gap: ({ spacing }) => spacing(2),
16
+ height: ({ spacing }) => spacing(5),
17
+ position: 'absolute',
18
+ bottom: 0,
19
+ },
20
+ },
21
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,17 @@
1
+ import type { EchartWidgetState } from '../echart'
2
+ import type {
3
+ EchartWidgetOptionProps,
4
+ EchartWidgetProps,
5
+ } from '../echart/types'
6
+ import type { ConfigProps } from '../loader'
7
+
8
+ export type ScatterplotWidgetData = [number, number][][]
9
+
10
+ export type ScatterplotWidgetState = EchartWidgetState
11
+
12
+ export type ScatterplotWidgetConfig = EchartWidgetProps & {
13
+ type: 'scatterplot'
14
+ }
15
+
16
+ export type ScatterplotConfig = ConfigProps &
17
+ EchartWidgetOptionProps<ScatterplotWidgetData>
@@ -0,0 +1,6 @@
1
+ export { WidgetSelectionSummary } from './selection-summary'
2
+
3
+ export type {
4
+ WidgetSelectionSummaryProps,
5
+ SelectionSummaryLabels,
6
+ } from './types'