@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,133 @@
1
+ import { IconButton } from '@mui/material'
2
+ import { PercentOutlined } from '@mui/icons-material'
3
+ import { useCallback, useEffect, useRef } from 'react'
4
+ import { useWidgetStore } from '../../stores/widget-store'
5
+ import type { RelativeDataProps, RelativeDataState } from './types'
6
+ import { actionButtonStyles } from '../shared/styles'
7
+ import { Tooltip } from '../../../components'
8
+ import { calculateTotal, toRelativeData } from './utils'
9
+ import type { EchartWidgetData } from '../../../widgets/echart'
10
+
11
+ export const RELATIVE_DATA_TOOL_ID = 'relative-data'
12
+
13
+ /**
14
+ * Widget action to toggle between relative (percentage) and absolute data display.
15
+ *
16
+ * Registers a transformation tool in the widget pipeline when mounted.
17
+ * When relative mode is active, transforms data to percentages via the pipeline.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <RelativeData
22
+ * id="my-widget"
23
+ * order={20}
24
+ * defaultIsRelative={false}
25
+ * />
26
+ * ```
27
+ */
28
+ export function RelativeData({
29
+ id,
30
+ order = 10,
31
+ defaultIsRelative = false,
32
+ labels,
33
+ Icon,
34
+ IconButtonProps,
35
+ }: RelativeDataProps) {
36
+ const previousMaxValue = useRef<number | undefined>(undefined)
37
+ const originalFormatter = useRef<((value: number) => string) | undefined>(
38
+ undefined,
39
+ )
40
+ const setWidget = useWidgetStore((state) => state.setWidget)
41
+ const getWidget = useWidgetStore((state) => state.getWidget)
42
+ const registerTool = useWidgetStore((state) => state.registerTool)
43
+ const unregisterTool = useWidgetStore((state) => state.unregisterTool)
44
+ const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
45
+
46
+ const storeIsRelative = useWidgetStore(
47
+ (state) => state.getWidget<RelativeDataState>(id)?.isRelative,
48
+ )
49
+
50
+ const isRelative = storeIsRelative ?? defaultIsRelative
51
+
52
+ // Initialize store with default value on mount
53
+ useEffect(() => {
54
+ const currentValue = getWidget<RelativeDataState>(id)?.isRelative
55
+ if (currentValue === undefined) {
56
+ setWidget(id, { isRelative: defaultIsRelative })
57
+ }
58
+ }, [defaultIsRelative, getWidget, id, setWidget])
59
+
60
+ // Register tool on mount
61
+ useEffect(() => {
62
+ registerTool(id, {
63
+ id: RELATIVE_DATA_TOOL_ID,
64
+ order,
65
+ enabled: isRelative,
66
+ fn: (data) => {
67
+ const echartData = data as EchartWidgetData
68
+ const total = calculateTotal(echartData)
69
+ return toRelativeData(echartData, total)
70
+ },
71
+ })
72
+
73
+ return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
74
+ }, [id, order, registerTool, unregisterTool, isRelative])
75
+
76
+ // Update enabled flag when toggle changes
77
+ useEffect(() => {
78
+ setToolEnabled(id, RELATIVE_DATA_TOOL_ID, isRelative)
79
+ }, [id, isRelative, setToolEnabled])
80
+
81
+ const handleToggle = useCallback(() => {
82
+ const newIsRelative = !isRelative
83
+ let max = previousMaxValue.current
84
+
85
+ if (newIsRelative) {
86
+ // Backup current formatter to ref
87
+ const widget = getWidget(id) as {
88
+ formatter?: (value: number) => string
89
+ locale?: string
90
+ }
91
+ originalFormatter.current = widget?.formatter
92
+
93
+ // Save current max value before setting to 100
94
+ const currentMax = (getWidget(id) as { max?: number })?.max
95
+ previousMaxValue.current = currentMax
96
+ max = 100
97
+ }
98
+
99
+ setWidget(id, {
100
+ isRelative: newIsRelative,
101
+ max,
102
+ formatter: newIsRelative
103
+ ? (value: number) => {
104
+ const widget = getWidget(id) as { locale?: string }
105
+ return new Intl.NumberFormat(widget?.locale, {
106
+ style: 'percent',
107
+ minimumFractionDigits: 1,
108
+ maximumFractionDigits: 1,
109
+ }).format(value / 100)
110
+ }
111
+ : originalFormatter.current,
112
+ })
113
+ }, [isRelative, setWidget, id, getWidget])
114
+
115
+ const tooltipLabel = isRelative
116
+ ? (labels?.absolute ?? 'Show absolute values')
117
+ : (labels?.relative ?? 'Show relative values')
118
+
119
+ return (
120
+ <Tooltip title={tooltipLabel}>
121
+ <IconButton
122
+ size='small'
123
+ aria-label={labels?.ariaLabel ?? tooltipLabel}
124
+ onClick={handleToggle}
125
+ sx={actionButtonStyles.trigger}
126
+ data-active={isRelative}
127
+ {...IconButtonProps}
128
+ >
129
+ {Icon ?? <PercentOutlined />}
130
+ </IconButton>
131
+ </Tooltip>
132
+ )
133
+ }
@@ -0,0 +1,9 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ trigger: {
5
+ '&[data-active="true"]': {
6
+ background: (theme: Theme) => theme.palette.primary.relatedLight,
7
+ },
8
+ },
9
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,31 @@
1
+ import type { IconButtonProps } from '@mui/material'
2
+ import type { ReactNode } from 'react'
3
+ import type { BaseWidgetState } from '../../stores/types'
4
+
5
+ export interface RelativeDataProps {
6
+ /** Widget ID to update data in the widget store */
7
+ id: string
8
+ /** Execution order in the tool pipeline. Lower values execute first. Defaults to 20. */
9
+ order?: number
10
+ /** Initial toggle state - when true, shows relative (percentage) values */
11
+ defaultIsRelative?: boolean
12
+ /** Custom labels for the action */
13
+ labels?: {
14
+ /** Tooltip when showing absolute values (button will switch to relative) */
15
+ relative?: string
16
+ /** Tooltip when showing relative values (button will switch to absolute) */
17
+ absolute?: string
18
+ /** Accessibility label */
19
+ ariaLabel?: string
20
+ }
21
+ /** Props passed to the IconButton component */
22
+ IconButtonProps?: IconButtonProps
23
+ /** Custom icon to display */
24
+ Icon?: ReactNode
25
+ }
26
+
27
+ export type RelativeDataState<T = unknown> = BaseWidgetState<
28
+ T & {
29
+ isRelative?: boolean
30
+ }
31
+ >
@@ -0,0 +1,223 @@
1
+ import { describe, test, expect } from 'vitest'
2
+ import { calculateTotal, toRelativeData } from './utils'
3
+ import type { EchartWidgetData } from '../../echart/types'
4
+
5
+ describe('calculateTotal', () => {
6
+ test('calculates sum of all numeric values in single series', () => {
7
+ const data: EchartWidgetData = [
8
+ [
9
+ { category: 'A', value: 25 },
10
+ { category: 'B', value: 75 },
11
+ ],
12
+ ]
13
+
14
+ expect(calculateTotal(data)).toBe(100)
15
+ })
16
+
17
+ test('calculates sum across multiple series', () => {
18
+ const data: EchartWidgetData = [
19
+ [
20
+ { name: 'X', count: 10 },
21
+ { name: 'Y', count: 20 },
22
+ ],
23
+ [
24
+ { name: 'X', count: 30 },
25
+ { name: 'Y', count: 40 },
26
+ ],
27
+ ]
28
+
29
+ expect(calculateTotal(data)).toBe(100)
30
+ })
31
+
32
+ test('returns zero for empty data', () => {
33
+ const data: EchartWidgetData = []
34
+
35
+ expect(calculateTotal(data)).toBe(0)
36
+ })
37
+
38
+ test('returns zero for empty series', () => {
39
+ const data: EchartWidgetData = [[]]
40
+
41
+ expect(calculateTotal(data)).toBe(0)
42
+ })
43
+
44
+ test('handles data with only zero values', () => {
45
+ const data: EchartWidgetData = [
46
+ [
47
+ { category: 'A', value: 0 },
48
+ { category: 'B', value: 0 },
49
+ ],
50
+ ]
51
+
52
+ expect(calculateTotal(data)).toBe(0)
53
+ })
54
+
55
+ test('handles negative values', () => {
56
+ const data: EchartWidgetData = [
57
+ [
58
+ { category: 'A', value: -25 },
59
+ { category: 'B', value: 75 },
60
+ ],
61
+ ]
62
+
63
+ expect(calculateTotal(data)).toBe(50)
64
+ })
65
+
66
+ test('ignores string values in calculation', () => {
67
+ const data: EchartWidgetData = [
68
+ [
69
+ { label: 'First', value: 50, extra: 'text' },
70
+ { label: 'Second', value: 50, extra: 'more' },
71
+ ],
72
+ ]
73
+
74
+ expect(calculateTotal(data)).toBe(100)
75
+ })
76
+
77
+ test('handles multiple numeric fields per item', () => {
78
+ const data: EchartWidgetData = [
79
+ [
80
+ { category: 'A', value1: 10, value2: 20 },
81
+ { category: 'B', value1: 30, value2: 40 },
82
+ ],
83
+ ]
84
+
85
+ expect(calculateTotal(data)).toBe(100)
86
+ })
87
+ })
88
+
89
+ describe('toRelativeData', () => {
90
+ test('transforms values to percentages (25/100 = 25%, 75/100 = 75%)', () => {
91
+ const data: EchartWidgetData = [
92
+ [
93
+ { category: 'A', value: 25 },
94
+ { category: 'B', value: 75 },
95
+ ],
96
+ ]
97
+ const total = 100
98
+
99
+ const result = toRelativeData(data, total)
100
+
101
+ // With total 100, 25 becomes 25% and 75 becomes 75%
102
+ expect(result[0]?.[0]?.value).toBe(25)
103
+ expect(result[0]?.[1]?.value).toBe(75)
104
+ })
105
+
106
+ test('transforms values to percentages with non-100 total (50/200 = 25%)', () => {
107
+ const data: EchartWidgetData = [
108
+ [
109
+ { category: 'A', value: 50 },
110
+ { category: 'B', value: 150 },
111
+ ],
112
+ ]
113
+ const total = 200
114
+
115
+ const result = toRelativeData(data, total)
116
+
117
+ // 50/200 = 25%, 150/200 = 75%
118
+ expect(result[0]?.[0]?.value).toBe(25)
119
+ expect(result[0]?.[1]?.value).toBe(75)
120
+ })
121
+
122
+ test('returns original data when total is zero', () => {
123
+ const data: EchartWidgetData = [
124
+ [
125
+ { category: 'A', value: 0 },
126
+ { category: 'B', value: 0 },
127
+ ],
128
+ ]
129
+ const total = 0
130
+
131
+ const result = toRelativeData(data, total)
132
+
133
+ expect(result).toEqual(data)
134
+ })
135
+
136
+ test('preserves string values during transformation', () => {
137
+ const data: EchartWidgetData = [
138
+ [
139
+ { label: 'First', value: 50, extra: 'text' },
140
+ { label: 'Second', value: 50, extra: 'more' },
141
+ ],
142
+ ]
143
+ const total = 100
144
+
145
+ const result = toRelativeData(data, total)
146
+
147
+ expect(result[0]?.[0]?.label).toBe('First')
148
+ expect(result[0]?.[0]?.extra).toBe('text')
149
+ expect(result[0]?.[1]?.label).toBe('Second')
150
+ expect(result[0]?.[1]?.extra).toBe('more')
151
+ })
152
+
153
+ test('transforms data with multiple series', () => {
154
+ const data: EchartWidgetData = [
155
+ [
156
+ { name: 'X', count: 10 },
157
+ { name: 'Y', count: 20 },
158
+ ],
159
+ [
160
+ { name: 'X', count: 30 },
161
+ { name: 'Y', count: 40 },
162
+ ],
163
+ ]
164
+ const total = 100
165
+
166
+ const result = toRelativeData(data, total)
167
+
168
+ expect(result[0]?.[0]?.count).toBe(10)
169
+ expect(result[0]?.[1]?.count).toBe(20)
170
+ expect(result[1]?.[0]?.count).toBe(30)
171
+ expect(result[1]?.[1]?.count).toBe(40)
172
+ })
173
+
174
+ test('handles fractional percentages correctly', () => {
175
+ const data: EchartWidgetData = [
176
+ [
177
+ { category: 'A', value: 1 },
178
+ { category: 'B', value: 2 },
179
+ { category: 'C', value: 3 },
180
+ ],
181
+ ]
182
+ const total = 6
183
+
184
+ const result = toRelativeData(data, total)
185
+
186
+ expect(result[0]?.[0]?.value).toBeCloseTo(16.6667, 4)
187
+ expect(result[0]?.[1]?.value).toBeCloseTo(33.3333, 4)
188
+ expect(result[0]?.[2]?.value).toBeCloseTo(50, 4)
189
+ })
190
+
191
+ test('handles negative values', () => {
192
+ const data: EchartWidgetData = [
193
+ [
194
+ { category: 'A', value: -25 },
195
+ { category: 'B', value: 75 },
196
+ ],
197
+ ]
198
+ const total = 50
199
+
200
+ const result = toRelativeData(data, total)
201
+
202
+ expect(result[0]?.[0]?.value).toBe(-50)
203
+ expect(result[0]?.[1]?.value).toBe(150)
204
+ })
205
+
206
+ test('handles empty data', () => {
207
+ const data: EchartWidgetData = []
208
+ const total = 100
209
+
210
+ const result = toRelativeData(data, total)
211
+
212
+ expect(result).toEqual([])
213
+ })
214
+
215
+ test('handles empty series', () => {
216
+ const data: EchartWidgetData = [[]]
217
+ const total = 100
218
+
219
+ const result = toRelativeData(data, total)
220
+
221
+ expect(result).toEqual([[]])
222
+ })
223
+ })
@@ -0,0 +1,58 @@
1
+ import type { EchartWidgetData } from '../../echart/types'
2
+
3
+ /**
4
+ * Calculates the sum of all numeric values in the data
5
+ */
6
+ export function calculateTotal(data: EchartWidgetData): number {
7
+ return data.reduce((total, series) => {
8
+ return series.reduce((seriesTotal, item) => {
9
+ if (Array.isArray(item)) {
10
+ const value = item.at(-1) as number
11
+ return typeof value === 'number' ? seriesTotal + value : seriesTotal
12
+ }
13
+
14
+ return Object.entries(item).reduce((itemTotal: number, [key, value]) => {
15
+ if (key === 'name') {
16
+ return itemTotal
17
+ }
18
+ return typeof value === 'number' ? itemTotal + value : itemTotal
19
+ }, seriesTotal)
20
+ }, total)
21
+ }, 0)
22
+ }
23
+
24
+ /**
25
+ * Transforms data to relative (percentage) values
26
+ */
27
+ export function toRelativeData(
28
+ data: EchartWidgetData,
29
+ total: number,
30
+ ): EchartWidgetData {
31
+ if (total === 0) return data
32
+
33
+ return data.map((series) =>
34
+ series.map((item) => {
35
+ if (Array.isArray(item)) {
36
+ return item.map((value: number, index) => {
37
+ if (index === item.length - 1 && typeof value === 'number') {
38
+ return (value / total) * 100
39
+ }
40
+ return value
41
+ }) as typeof item
42
+ }
43
+
44
+ const transformed: Record<string, string | number> = {}
45
+
46
+ for (const [key, value] of Object.entries(item)) {
47
+ if (key === 'name') {
48
+ transformed[key] = value
49
+ continue
50
+ }
51
+ transformed[key] =
52
+ typeof value === 'number' ? (value / total) * 100 : value
53
+ }
54
+
55
+ return transformed
56
+ }),
57
+ )
58
+ }