@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,56 @@
1
+ /**
2
+ * Shared CSV export modifiers for chart widgets
3
+ */
4
+
5
+ /**
6
+ * Flattens object array data into CSV-ready rows.
7
+ * Used by bar, pie, histogram, and timeseries widgets.
8
+ *
9
+ * @param data - Array of series, where each series is an array of data objects
10
+ * @returns CSV rows with headers and values
11
+ */
12
+ export function flattenObjectArrayToCSV<T extends Record<string, unknown>>(
13
+ data: T[][],
14
+ ): string[][] {
15
+ const rows: string[][] = []
16
+
17
+ // Add headers from first data point if available
18
+ if (data.length > 0 && (data[0]?.length ?? 0) > 0) {
19
+ const firstDataPoint = data?.[0]?.[0] ?? {}
20
+ const headers = Object.keys(firstDataPoint)
21
+ rows.push(headers)
22
+ }
23
+
24
+ // Add data rows from all series
25
+ data.forEach((series) => {
26
+ series.forEach((dataPoint) => {
27
+ const values = Object.values(dataPoint).map((v) => String(v))
28
+ rows.push(values)
29
+ })
30
+ })
31
+
32
+ return rows
33
+ }
34
+
35
+ /**
36
+ * Creates CSV rows for scatterplot data.
37
+ * Scatterplot uses array format [x, y] instead of objects.
38
+ *
39
+ * @param data - Array of series, where each series is an array of [x, y] tuples
40
+ * @returns CSV rows with ['x', 'y'] headers
41
+ */
42
+ export function scatterplotDataToCSV(data: number[][][]): string[][] {
43
+ const rows: string[][] = []
44
+
45
+ // Add headers
46
+ rows.push(['x', 'y'])
47
+
48
+ // Add data rows from all series
49
+ data.forEach((series) => {
50
+ series.forEach((dataPoint) => {
51
+ rows.push([String(dataPoint[0]), String(dataPoint[1])])
52
+ })
53
+ })
54
+
55
+ return rows
56
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared utilities for chart widget configuration
3
+ */
4
+
5
+ export { createChartWidgetConfig } from './config-factory'
6
+ export type {
7
+ ChartWidgetBaseConfig,
8
+ CreateChartWidgetConfigParams,
9
+ } from './config-factory'
10
+
11
+ export { flattenObjectArrayToCSV, scatterplotDataToCSV } from './csv-modifiers'
12
+
13
+ export {
14
+ buildLegendConfig,
15
+ buildGridConfig,
16
+ createTooltipPositioner,
17
+ createAxisLabelFormatter,
18
+ createTooltipFormatter,
19
+ applyXAxisFormatter,
20
+ applyYAxisFormatter,
21
+ } from './option-builders'
@@ -0,0 +1,203 @@
1
+ import type { Theme } from '@mui/material'
2
+ import type { LegendComponentOption } from 'echarts'
3
+ import type {
4
+ CallbackDataParams,
5
+ TopLevelFormatterParams,
6
+ } from 'node_modules/echarts/types/dist/shared'
7
+
8
+ /**
9
+ * Shared EChart configuration builders for chart widgets
10
+ */
11
+
12
+ /**
13
+ * Builds standard legend configuration for chart widgets
14
+ *
15
+ * @param hasLegend - Whether to show the legend
16
+ * @returns Legend configuration object
17
+ */
18
+ export function buildLegendConfig(hasLegend: boolean): LegendComponentOption {
19
+ return {
20
+ show: hasLegend,
21
+ icon: 'circle' as const,
22
+ left: 0,
23
+ bottom: 0,
24
+ orient: 'horizontal',
25
+ type: 'scroll',
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Builds standard grid configuration with legend-aware spacing
31
+ *
32
+ * @param hasLegend - Whether the chart has a legend
33
+ * @param theme - MUI theme for spacing
34
+ * @param additionalConfig - Additional grid configuration to merge
35
+ * @returns Grid configuration object
36
+ */
37
+ export function buildGridConfig(hasLegend: boolean, theme: Theme) {
38
+ return {
39
+ ...(!hasLegend && { bottom: parseInt(theme.spacing(3)) }),
40
+ ...(hasLegend && { bottom: parseInt(theme.spacing(7)) }),
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Creates a tooltip position calculator that handles overflow
46
+ * Used by bar, histogram, and scatterplot widgets
47
+ *
48
+ * @param theme - MUI theme for spacing
49
+ * @returns Tooltip position function
50
+ */
51
+ export function createTooltipPositioner(theme: Theme) {
52
+ return function (
53
+ point: [number, number],
54
+ _params: unknown,
55
+ _dom: unknown,
56
+ _rect: unknown,
57
+ size: { contentSize: [number, number]; viewSize: [number, number] },
58
+ ) {
59
+ const position = { top: parseInt(theme.spacing(0.5)) } as Record<
60
+ string,
61
+ number
62
+ >
63
+
64
+ // Position tooltip left or right based on available space
65
+ if (size.contentSize[0] < size.viewSize[0] - point[0]) {
66
+ position.left = point[0]
67
+ } else {
68
+ position.right = size.viewSize[0] - point[0]
69
+ }
70
+
71
+ return position
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Creates an axis label formatter for ECharts
77
+ * Used to format numeric axis labels with a widget formatter
78
+ *
79
+ * @param formatter - Optional formatter function from widget config
80
+ * @returns Axis label formatter function or undefined
81
+ */
82
+ export function createAxisLabelFormatter(
83
+ formatter?: (value: number) => string,
84
+ ) {
85
+ if (!formatter) return undefined
86
+ return (value: number) => formatter(value)
87
+ }
88
+
89
+ /**
90
+ * Applies formatter to xAxis configuration
91
+ * Only applies to single axis objects (not arrays) with type 'value'
92
+ *
93
+ * @param xAxis - Existing xAxis configuration
94
+ * @param formatter - Optional formatter function from widget config
95
+ * @returns Updated xAxis configuration or undefined if no changes needed
96
+ */
97
+ export function applyXAxisFormatter(
98
+ xAxis: unknown,
99
+ formatter?: (value: number) => string,
100
+ ) {
101
+ let axisFormatter = createAxisLabelFormatter(formatter)
102
+
103
+ const xAxisIsObject = xAxis && !Array.isArray(xAxis)
104
+ const xAxisTyped = xAxis as { type?: string; axisLabel?: unknown }
105
+
106
+ if (!xAxisIsObject || xAxisTyped.type !== 'value') {
107
+ axisFormatter = undefined
108
+ }
109
+
110
+ return {
111
+ ...xAxisTyped,
112
+ axisLabel: {
113
+ ...(typeof xAxisTyped.axisLabel === 'object' && xAxisTyped.axisLabel
114
+ ? xAxisTyped.axisLabel
115
+ : {}),
116
+ formatter: axisFormatter,
117
+ },
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Applies formatter to yAxis configuration
123
+ * Only applies to single axis objects (not arrays) with type 'value'
124
+ *
125
+ * @param yAxis - Existing yAxis configuration
126
+ * @param formatter - Optional formatter function from widget config
127
+ * @returns Updated yAxis configuration or undefined if no changes needed
128
+ */
129
+ export function applyYAxisFormatter(
130
+ yAxis: unknown,
131
+ formatter?: (value: number) => string,
132
+ ) {
133
+ let axisFormatter = createAxisLabelFormatter(formatter)
134
+
135
+ const yAxisIsObject = yAxis && !Array.isArray(yAxis)
136
+ const yAxisTyped = yAxis as { type?: string; axisLabel?: unknown }
137
+
138
+ if (!yAxisIsObject || yAxisTyped.type !== 'value') {
139
+ axisFormatter = undefined
140
+ }
141
+
142
+ return {
143
+ ...yAxisTyped,
144
+ axisLabel: {
145
+ ...(typeof yAxisTyped.axisLabel === 'object' && yAxisTyped.axisLabel
146
+ ? yAxisTyped.axisLabel
147
+ : {}),
148
+ formatter: axisFormatter,
149
+ },
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Creates a tooltip formatter for ECharts
155
+ * Formats numeric values in tooltip using widget formatter
156
+ * Handles both axis trigger (array) and item trigger (object) modes
157
+ *
158
+ * @param formatter - Optional formatter function from widget config
159
+ * @returns Tooltip formatter function or undefined
160
+ */
161
+ export function createTooltipFormatter(
162
+ callback: (
163
+ item: CallbackDataParams,
164
+ items: CallbackDataParams[],
165
+ ) => {
166
+ name: string
167
+ seriesName: string
168
+ marker: string
169
+ value: string | number
170
+ },
171
+ ) {
172
+ return (params: TopLevelFormatterParams) => {
173
+ // Handle both array (axis trigger) and object (item trigger)
174
+ const items = Array.isArray(params) ? params : [params]
175
+
176
+ const tooltip = (name: string, callback: string) =>
177
+ `<div style="margin: 0px 0 0;line-height:1;">${name ? `<div style="font-size:11px;color:#FFFFFF;font-weight:400;line-height:1; margin-bottom: 10px">${name}</div>` : ''}<div style="margin: 0;line-height:1;">${callback}</div><div style="clear:both"></div></div>`
178
+
179
+ const values = items.map((item) => {
180
+ const { name, seriesName, marker, value } = callback(item, items)
181
+ return {
182
+ name,
183
+ seriesName,
184
+ marker,
185
+ value,
186
+ }
187
+ })
188
+
189
+ const name = values[0]?.name ?? ''
190
+ // Show margin if name exists or there are multiple items
191
+ const showMargin = name || items.length > 1
192
+ const marginStyle = showMargin
193
+ ? 'margin: 10px 0 0;line-height:1;'
194
+ : 'margin: 0;line-height:1;'
195
+
196
+ const formattedValues = values.map(
197
+ ({ seriesName, marker, value }) =>
198
+ `<div style="${marginStyle}"><div style="margin: 0px 0 0;line-height:1;">${marker}${seriesName ? `<span style="font-size:11px;color:#FFFFFF;font-weight:400;margin-left:2px;margin-right:10px">${seriesName}</span>` : ''}<span style="float:right;margin-left:10px;font-size:11px;color:#FFFFFF;font-weight:900">${value}</span></div></div>`,
199
+ )
200
+
201
+ return tooltip(name, formattedValues.join(''))
202
+ }
203
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared skeleton styles for chart widgets
3
+ */
4
+
5
+ export { baseSkeletonStyles } from './styles'
@@ -0,0 +1,20 @@
1
+ import type { Theme } from '@mui/material'
2
+
3
+ /**
4
+ * Base skeleton styles shared across all chart widgets
5
+ */
6
+ export const baseSkeletonStyles = {
7
+ graph: {
8
+ /**
9
+ * Common container style for chart widget skeletons
10
+ */
11
+ container: {
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'space-between',
15
+ flexDirection: 'column',
16
+ gap: ({ spacing }: Theme) => spacing(1),
17
+ height: ({ spacing }: Theme) => spacing(38),
18
+ },
19
+ },
20
+ } as const
@@ -0,0 +1,10 @@
1
+ export const ChangeColumnIcon = () => (
2
+ <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 18 18'>
3
+ <path
4
+ fill='currentColor'
5
+ fillRule='evenodd'
6
+ d='M14.25 2.25H3.75c-.825 0-1.5.675-1.5 1.5v10.5c0 .825.675 1.5 1.5 1.5h10.5c.825 0 1.5-.675 1.5-1.5V3.75c0-.825-.675-1.5-1.5-1.5m-3.75 12h-3V3.75h3zM3.75 3.75H6v10.5H3.75zM12 14.25V3.75h2.25v10.5z'
7
+ clipRule='evenodd'
8
+ />
9
+ </svg>
10
+ )
@@ -0,0 +1,163 @@
1
+ import { describe, test, expect, beforeEach, vi } from 'vitest'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { ChangeColumn } from './change-column'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { TableColumn } from '../../table/types'
6
+
7
+ // Mock @dnd-kit with minimal implementation for testing
8
+ vi.mock('@dnd-kit/core', async () => {
9
+ const actual = await vi.importActual('@dnd-kit/core')
10
+ return {
11
+ ...actual,
12
+ useSensor: () => ({}),
13
+ useSensors: () => [],
14
+ }
15
+ })
16
+
17
+ describe('ChangeColumn', () => {
18
+ const widgetId = 'test-change-column-widget'
19
+
20
+ const mockColumns: TableColumn[] = [
21
+ { id: 'name', label: 'Name' },
22
+ { id: 'country', label: 'Country' },
23
+ { id: 'population', label: 'Population' },
24
+ ]
25
+
26
+ beforeEach(() => {
27
+ useWidgetStore.getState().clearWidgets()
28
+ })
29
+
30
+ test('returns null when widget has no columns', () => {
31
+ const { container } = render(<ChangeColumn id={widgetId} />)
32
+ expect(container.firstChild).toBeNull()
33
+ })
34
+
35
+ test('returns null when widget has fewer than 2 columns', () => {
36
+ useWidgetStore.getState().setWidget(widgetId, {
37
+ columns: [{ id: 'name', label: 'Name' }],
38
+ })
39
+
40
+ const { container } = render(<ChangeColumn id={widgetId} />)
41
+ expect(container.firstChild).toBeNull()
42
+ })
43
+
44
+ test('renders change column button when widget has 2 or more columns', () => {
45
+ useWidgetStore.getState().setWidget(widgetId, {
46
+ columns: mockColumns,
47
+ })
48
+
49
+ render(<ChangeColumn id={widgetId} />)
50
+
51
+ const button = screen.getByRole('button')
52
+ expect(button).toBeTruthy()
53
+ })
54
+
55
+ test('shows default tooltip label', () => {
56
+ useWidgetStore.getState().setWidget(widgetId, {
57
+ columns: mockColumns,
58
+ })
59
+
60
+ render(<ChangeColumn id={widgetId} />)
61
+
62
+ const button = screen.getByRole('button', { name: 'Change column' })
63
+ expect(button).toBeTruthy()
64
+ })
65
+
66
+ test('opens menu on button click', () => {
67
+ useWidgetStore.getState().setWidget(widgetId, {
68
+ columns: mockColumns,
69
+ })
70
+
71
+ render(<ChangeColumn id={widgetId} />)
72
+
73
+ const button = screen.getByRole('button')
74
+ fireEvent.click(button)
75
+
76
+ // Menu should be open with all columns for reordering
77
+ expect(screen.getByRole('menu')).toBeTruthy()
78
+ expect(screen.getByRole('menuitem', { name: /Name/i })).toBeTruthy()
79
+ expect(screen.getByRole('menuitem', { name: /Country/i })).toBeTruthy()
80
+ expect(screen.getByRole('menuitem', { name: /Population/i })).toBeTruthy()
81
+ })
82
+
83
+ test('displays all columns in menu for drag-and-drop reordering', () => {
84
+ useWidgetStore.getState().setWidget(widgetId, {
85
+ columns: mockColumns,
86
+ })
87
+
88
+ render(<ChangeColumn id={widgetId} />)
89
+
90
+ const button = screen.getByRole('button')
91
+ fireEvent.click(button)
92
+
93
+ // All three columns should be visible
94
+ const menuItems = screen.getAllByRole('menuitem')
95
+ expect(menuItems).toHaveLength(3)
96
+ })
97
+
98
+ test('uses custom labels when provided', () => {
99
+ useWidgetStore.getState().setWidget(widgetId, {
100
+ columns: mockColumns,
101
+ })
102
+
103
+ const customLabels = {
104
+ tooltip: 'Reorder columns',
105
+ ariaLabel: 'Reorder table columns',
106
+ }
107
+
108
+ render(<ChangeColumn id={widgetId} labels={customLabels} />)
109
+
110
+ const button = screen.getByRole('button', { name: 'Reorder table columns' })
111
+ expect(button).toBeTruthy()
112
+ })
113
+
114
+ test('works with exactly 2 columns', () => {
115
+ const twoColumns: TableColumn[] = [
116
+ { id: 'first', label: 'First' },
117
+ { id: 'second', label: 'Second' },
118
+ ]
119
+
120
+ useWidgetStore.getState().setWidget(widgetId, {
121
+ columns: twoColumns,
122
+ })
123
+
124
+ render(<ChangeColumn id={widgetId} />)
125
+
126
+ const button = screen.getByRole('button')
127
+ fireEvent.click(button)
128
+
129
+ // Both columns should be available for reordering
130
+ expect(screen.getByRole('menuitem', { name: /First/i })).toBeTruthy()
131
+ expect(screen.getByRole('menuitem', { name: /Second/i })).toBeTruthy()
132
+ })
133
+
134
+ test('sets data-active attribute when menu is open', () => {
135
+ useWidgetStore.getState().setWidget(widgetId, {
136
+ columns: mockColumns,
137
+ })
138
+
139
+ render(<ChangeColumn id={widgetId} />)
140
+
141
+ const button = screen.getByRole('button')
142
+ expect(button.getAttribute('data-active')).toBe('false')
143
+
144
+ fireEvent.click(button)
145
+ expect(button.getAttribute('data-active')).toBe('true')
146
+ })
147
+
148
+ test('menu items are keyboard focusable', () => {
149
+ useWidgetStore.getState().setWidget(widgetId, {
150
+ columns: mockColumns,
151
+ })
152
+
153
+ render(<ChangeColumn id={widgetId} />)
154
+
155
+ const button = screen.getByRole('button')
156
+ fireEvent.click(button)
157
+
158
+ const menuItems = screen.getAllByRole('menuitem')
159
+ menuItems.forEach((item) => {
160
+ expect(item.getAttribute('tabindex')).toBe('0')
161
+ })
162
+ })
163
+ })
@@ -0,0 +1,141 @@
1
+ import {
2
+ DndContext,
3
+ closestCenter,
4
+ KeyboardSensor,
5
+ PointerSensor,
6
+ useSensor,
7
+ useSensors,
8
+ type DragEndEvent,
9
+ } from '@dnd-kit/core'
10
+ import {
11
+ arrayMove,
12
+ SortableContext,
13
+ sortableKeyboardCoordinates,
14
+ verticalListSortingStrategy,
15
+ } from '@dnd-kit/sortable'
16
+ import { IconButton, Menu, SvgIcon } from '@mui/material'
17
+ import { useCallback, useMemo, useState, type MouseEvent } from 'react'
18
+ import { useWidgetStore } from '../../stores/widget-store'
19
+ import type { ChangeColumnProps } from './types'
20
+ import { actionButtonStyles } from '../shared/styles'
21
+ import { Tooltip } from '../../../components'
22
+ import type { TableWidgetState } from '../../table/types'
23
+ import { ChangeColumnIcon } from './change-column-icon'
24
+ import { SortableColumnItem } from './sortable-column-item'
25
+ import { useShallow } from 'zustand/shallow'
26
+
27
+ /**
28
+ * Widget action to reorder columns in a table widget via drag-and-drop.
29
+ *
30
+ * This action reads the columns from the widget store and allows users to
31
+ * drag and drop columns to reorder them. All columns are displayed and
32
+ * can be reordered.
33
+ *
34
+ * Returns null if there are fewer than 2 columns.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <ChangeColumn id="my-table-widget" />
39
+ * ```
40
+ */
41
+ export function ChangeColumn({
42
+ id,
43
+ labels,
44
+ Icon,
45
+ IconButtonProps,
46
+ MenuProps,
47
+ }: ChangeColumnProps) {
48
+ const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
49
+ const setWidget = useWidgetStore((state) => state.setWidget)
50
+ const columns = useWidgetStore(
51
+ useShallow((state) => state.getWidget<TableWidgetState>(id)?.columns),
52
+ )
53
+
54
+ const sensors = useSensors(
55
+ useSensor(PointerSensor),
56
+ useSensor(KeyboardSensor, {
57
+ coordinateGetter: sortableKeyboardCoordinates,
58
+ }),
59
+ )
60
+
61
+ const columnIds = useMemo(
62
+ () => columns?.map((col) => col.id) ?? [],
63
+ [columns],
64
+ )
65
+
66
+ const handleToggle = useCallback((event: MouseEvent<HTMLElement>) => {
67
+ event.stopPropagation()
68
+ setAnchorEl(event.currentTarget)
69
+ }, [])
70
+
71
+ const handleClose = useCallback(() => {
72
+ setAnchorEl(null)
73
+ }, [])
74
+
75
+ const handleDragEnd = (event: DragEndEvent) => {
76
+ const { active, over } = event
77
+ if (!over || active.id === over.id || !columns) return
78
+ const oldIndex = columns.findIndex((col) => col.id === active.id)
79
+ const newIndex = columns.findIndex((col) => col.id === over.id)
80
+ if (oldIndex !== -1 && newIndex !== -1) {
81
+ const newColumns = arrayMove(columns, oldIndex, newIndex)
82
+ setWidget(id, { columns: newColumns })
83
+ }
84
+ }
85
+
86
+ // Return null if there are fewer than 2 columns
87
+ if (!columns || columns.length < 2) {
88
+ return null
89
+ }
90
+
91
+ const tooltipLabel = labels?.tooltip ?? 'Change column'
92
+ const isOpen = Boolean(anchorEl)
93
+
94
+ return (
95
+ <>
96
+ <Tooltip title={tooltipLabel}>
97
+ <IconButton
98
+ size='small'
99
+ aria-label={labels?.ariaLabel ?? tooltipLabel}
100
+ aria-controls={isOpen ? 'change-column-menu' : undefined}
101
+ aria-haspopup='true'
102
+ aria-expanded={isOpen ? 'true' : undefined}
103
+ onClick={handleToggle}
104
+ data-active={isOpen}
105
+ sx={actionButtonStyles.trigger}
106
+ {...IconButtonProps}
107
+ >
108
+ {Icon ?? (
109
+ <SvgIcon>
110
+ <ChangeColumnIcon />
111
+ </SvgIcon>
112
+ )}
113
+ </IconButton>
114
+ </Tooltip>
115
+ <DndContext
116
+ sensors={sensors}
117
+ collisionDetection={closestCenter}
118
+ onDragEnd={handleDragEnd}
119
+ >
120
+ <Menu
121
+ id='change-column-menu'
122
+ anchorEl={anchorEl}
123
+ open={isOpen}
124
+ onClose={handleClose}
125
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
126
+ transformOrigin={{ vertical: 'top', horizontal: 'right' }}
127
+ {...MenuProps}
128
+ >
129
+ <SortableContext
130
+ items={columnIds}
131
+ strategy={verticalListSortingStrategy}
132
+ >
133
+ {columns.map((column) => (
134
+ <SortableColumnItem key={column.id} column={column} />
135
+ ))}
136
+ </SortableContext>
137
+ </Menu>
138
+ </DndContext>
139
+ </>
140
+ )
141
+ }
@@ -0,0 +1,49 @@
1
+ import { useSortable } from '@dnd-kit/sortable'
2
+ import { CSS } from '@dnd-kit/utilities'
3
+ import { ListItemText, MenuItem } from '@mui/material'
4
+ import type { TableColumn } from '../../table/types'
5
+
6
+ export interface SortableColumnItemProps {
7
+ column: TableColumn
8
+ }
9
+
10
+ /**
11
+ * A draggable menu item for column reordering.
12
+ * Uses @dnd-kit/sortable for drag-and-drop functionality.
13
+ */
14
+ export function SortableColumnItem({ column }: SortableColumnItemProps) {
15
+ const {
16
+ attributes,
17
+ listeners,
18
+ setNodeRef,
19
+ transform,
20
+ transition,
21
+ isDragging,
22
+ } = useSortable({ id: column.id })
23
+
24
+ const style = {
25
+ transform: CSS.Transform.toString(transform),
26
+ transition,
27
+ opacity: isDragging ? 0.5 : 1,
28
+ cursor: isDragging ? 'grabbing' : 'grab',
29
+ }
30
+
31
+ return (
32
+ <MenuItem
33
+ ref={setNodeRef}
34
+ style={style}
35
+ {...attributes}
36
+ {...listeners}
37
+ role='menuitem'
38
+ tabIndex={0}
39
+ sx={{
40
+ '&:focus-visible': {
41
+ outline: (theme) => `2px solid ${theme.palette.primary.main}`,
42
+ outlineOffset: -2,
43
+ },
44
+ }}
45
+ >
46
+ <ListItemText>{column.label}</ListItemText>
47
+ </MenuItem>
48
+ )
49
+ }
@@ -0,0 +1,20 @@
1
+ import type { IconButtonProps, MenuProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export interface ChangeColumnProps {
5
+ /** Widget ID to update column configuration in the widget store */
6
+ id: string
7
+ /** Custom labels for the action */
8
+ labels?: {
9
+ /** Tooltip label */
10
+ tooltip?: string
11
+ /** Accessibility label */
12
+ ariaLabel?: string
13
+ }
14
+ /** Props passed to the IconButton component */
15
+ IconButtonProps?: IconButtonProps
16
+ /** Props passed to the Menu component */
17
+ MenuProps?: Partial<MenuProps>
18
+ /** Custom icon to display */
19
+ Icon?: ReactNode
20
+ }