@carto/ps-react-ui 4.4.1 → 4.4.3

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 (225) hide show
  1. package/dist/components.js.map +1 -1
  2. package/dist/download-config-Dqu78h2a.js +57 -0
  3. package/dist/download-config-Dqu78h2a.js.map +1 -0
  4. package/dist/error-CEkRPccv.js.map +1 -1
  5. package/dist/exports-Cr43OCul.js.map +1 -1
  6. package/dist/formatter-B9Bxn1k7.js +6 -0
  7. package/dist/formatter-B9Bxn1k7.js.map +1 -0
  8. package/dist/lasso-tool-BYbxrJ-7.js.map +1 -1
  9. package/dist/note-t51drNe0.js.map +1 -1
  10. package/dist/options-D9wflre6.js.map +1 -1
  11. package/dist/row-DTCV0Ocm.js.map +1 -1
  12. package/dist/series-CYNOu2Ju.js.map +1 -1
  13. package/dist/smart-tooltip-D4vwQpFf.js.map +1 -1
  14. package/dist/styles-Y8q7Jff3.js +118 -0
  15. package/dist/styles-Y8q7Jff3.js.map +1 -0
  16. package/dist/tooltip-BDnrRKrp.js.map +1 -1
  17. package/dist/types/components/basemaps/basemaps.d.ts +20 -0
  18. package/dist/types/components/geolocation-controls/geolocation-controls.d.ts +11 -0
  19. package/dist/types/components/lasso-tool/lasso-tool-inline.d.ts +17 -0
  20. package/dist/types/components/lasso-tool/lasso-tool.d.ts +21 -0
  21. package/dist/types/components/list-data/list-data.d.ts +16 -0
  22. package/dist/types/components/measurement-tools/measurement-tools.d.ts +20 -0
  23. package/dist/types/components/smart-tooltip/smart-tooltip.d.ts +17 -0
  24. package/dist/types/components/tooltip/tooltip.d.ts +13 -0
  25. package/dist/types/components/zoom-controls/zoom-controls.d.ts +16 -0
  26. package/dist/types/hooks/use-widget-ref.d.ts +4 -4
  27. package/dist/types/widgets/actions/brush-toggle/types.d.ts +8 -2
  28. package/dist/types/widgets/actions/download/download.d.ts +11 -0
  29. package/dist/types/widgets/actions/download/exports.d.ts +15 -0
  30. package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +13 -0
  31. package/dist/types/widgets/actions/index.d.ts +1 -1
  32. package/dist/types/widgets/actions/relative-data/relative-data.d.ts +1 -0
  33. package/dist/types/widgets/bar/config.d.ts +8 -4
  34. package/dist/types/widgets/category/category-ui.d.ts +3 -0
  35. package/dist/types/widgets/category/components/category-bar.d.ts +3 -0
  36. package/dist/types/widgets/category/components/category-legend.d.ts +3 -0
  37. package/dist/types/widgets/category/components/category-row-multi.d.ts +5 -1
  38. package/dist/types/widgets/category/components/category-row-other.d.ts +3 -0
  39. package/dist/types/widgets/category/components/category-row-single.d.ts +5 -1
  40. package/dist/types/widgets/category/config.d.ts +11 -0
  41. package/dist/types/widgets/category/types.d.ts +1 -0
  42. package/dist/types/widgets/echart/echart-ui.d.ts +7 -0
  43. package/dist/types/widgets/echart/echart.d.ts +6 -0
  44. package/dist/types/widgets/echart/options.d.ts +7 -0
  45. package/dist/types/widgets/echart/types.d.ts +3 -0
  46. package/dist/types/widgets/echart/utils.d.ts +41 -0
  47. package/dist/types/widgets/error/error.d.ts +10 -0
  48. package/dist/types/widgets/formula/components/item.d.ts +3 -0
  49. package/dist/types/widgets/formula/components/prefix.d.ts +3 -0
  50. package/dist/types/widgets/formula/components/row.d.ts +3 -0
  51. package/dist/types/widgets/formula/components/series.d.ts +3 -0
  52. package/dist/types/widgets/formula/components/suffix.d.ts +3 -0
  53. package/dist/types/widgets/formula/components/value.d.ts +3 -0
  54. package/dist/types/widgets/formula/config.d.ts +11 -0
  55. package/dist/types/widgets/formula/formula-ui.d.ts +3 -0
  56. package/dist/types/widgets/histogram/config.d.ts +18 -2
  57. package/dist/types/widgets/histogram/index.d.ts +2 -1
  58. package/dist/types/widgets/histogram/types.d.ts +6 -3
  59. package/dist/types/widgets/loader/loader.d.ts +22 -0
  60. package/dist/types/widgets/loader/utils.d.ts +26 -3
  61. package/dist/types/widgets/markdown/config.d.ts +10 -0
  62. package/dist/types/widgets/markdown/markdown-ui.d.ts +7 -0
  63. package/dist/types/widgets/markdown/markdown.d.ts +3 -0
  64. package/dist/types/widgets/note/note.d.ts +10 -0
  65. package/dist/types/widgets/pie/config.d.ts +8 -4
  66. package/dist/types/widgets/range/components/range-item.d.ts +3 -0
  67. package/dist/types/widgets/range/config.d.ts +5 -0
  68. package/dist/types/widgets/range/range-ui.d.ts +3 -0
  69. package/dist/types/widgets/scatterplot/config.d.ts +7 -3
  70. package/dist/types/widgets/selection-summary/selection-summary.d.ts +11 -0
  71. package/dist/types/widgets/skeleton-loader/skeleton-loader.d.ts +10 -0
  72. package/dist/types/widgets/spread/components/max-value.d.ts +3 -0
  73. package/dist/types/widgets/spread/components/min-value.d.ts +3 -0
  74. package/dist/types/widgets/spread/components/separator.d.ts +3 -0
  75. package/dist/types/widgets/spread/config.d.ts +11 -0
  76. package/dist/types/widgets/spread/spread-ui.d.ts +3 -0
  77. package/dist/types/widgets/stores/types.d.ts +2 -0
  78. package/dist/types/widgets/subheader/subheader.d.ts +11 -0
  79. package/dist/types/widgets/table/config.d.ts +8 -3
  80. package/dist/types/widgets/table/hooks/use-pagination.d.ts +11 -3
  81. package/dist/types/widgets/table/hooks/use-selection.d.ts +11 -2
  82. package/dist/types/widgets/table/hooks/use-sort.d.ts +11 -3
  83. package/dist/types/widgets/timeseries/config.d.ts +8 -4
  84. package/dist/types/widgets/utils/chart-config/download-config.d.ts +3 -0
  85. package/dist/types/widgets/{_shared → utils}/chart-config/index.d.ts +2 -0
  86. package/dist/types/widgets/{_shared → utils}/chart-config/option-builders.d.ts +14 -9
  87. package/dist/types/widgets/utils/formatter.d.ts +2 -0
  88. package/dist/types/widgets/utils/index.d.ts +7 -0
  89. package/dist/types/widgets/wrapper/components/actions.d.ts +3 -0
  90. package/dist/types/widgets/wrapper/components/options.d.ts +3 -0
  91. package/dist/types/widgets/wrapper/components/title.d.ts +3 -0
  92. package/dist/types/widgets/wrapper/wrapper-ui.d.ts +14 -0
  93. package/dist/types/widgets/wrapper/wrapper.d.ts +14 -0
  94. package/dist/use-widget-ref-wtFLDFCD.js.map +1 -1
  95. package/dist/utils-BOhInag6.js.map +1 -1
  96. package/dist/widgets/actions.js +720 -681
  97. package/dist/widgets/actions.js.map +1 -1
  98. package/dist/widgets/bar.js +78 -92
  99. package/dist/widgets/bar.js.map +1 -1
  100. package/dist/widgets/category.js +206 -197
  101. package/dist/widgets/category.js.map +1 -1
  102. package/dist/widgets/echart.js.map +1 -1
  103. package/dist/widgets/formula.js +54 -54
  104. package/dist/widgets/formula.js.map +1 -1
  105. package/dist/widgets/histogram.js +106 -86
  106. package/dist/widgets/histogram.js.map +1 -1
  107. package/dist/widgets/loader.js.map +1 -1
  108. package/dist/widgets/markdown.js.map +1 -1
  109. package/dist/widgets/pie.js +147 -112
  110. package/dist/widgets/pie.js.map +1 -1
  111. package/dist/widgets/range.js +23 -22
  112. package/dist/widgets/range.js.map +1 -1
  113. package/dist/widgets/scatterplot.js +46 -60
  114. package/dist/widgets/scatterplot.js.map +1 -1
  115. package/dist/widgets/selection-summary.js.map +1 -1
  116. package/dist/widgets/skeleton-loader.js.map +1 -1
  117. package/dist/widgets/spread.js +40 -41
  118. package/dist/widgets/spread.js.map +1 -1
  119. package/dist/widgets/subheader.js.map +1 -1
  120. package/dist/widgets/table.js.map +1 -1
  121. package/dist/widgets/timeseries.js +51 -65
  122. package/dist/widgets/timeseries.js.map +1 -1
  123. package/dist/widgets/toolbar-actions.js +101 -6693
  124. package/dist/widgets/toolbar-actions.js.map +1 -1
  125. package/dist/widgets/utils.js +33 -0
  126. package/dist/widgets/utils.js.map +1 -0
  127. package/dist/widgets/wrapper.js.map +1 -1
  128. package/package.json +9 -4
  129. package/src/components/basemaps/basemaps.tsx +20 -0
  130. package/src/components/geolocation-controls/geolocation-controls.tsx +11 -0
  131. package/src/components/lasso-tool/lasso-tool-inline.tsx +17 -0
  132. package/src/components/lasso-tool/lasso-tool.tsx +21 -0
  133. package/src/components/list-data/list-data.tsx +16 -0
  134. package/src/components/measurement-tools/measurement-tools.tsx +20 -0
  135. package/src/components/smart-tooltip/smart-tooltip.tsx +17 -0
  136. package/src/components/tooltip/tooltip.tsx +13 -0
  137. package/src/components/zoom-controls/zoom-controls.tsx +16 -0
  138. package/src/hooks/use-widget-ref.ts +4 -4
  139. package/src/widgets/README.md +13 -13
  140. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +42 -47
  141. package/src/widgets/actions/brush-toggle/types.ts +8 -2
  142. package/src/widgets/actions/download/download.tsx +11 -0
  143. package/src/widgets/actions/download/exports.tsx +15 -0
  144. package/src/widgets/actions/fullscreen/fullscreen.tsx +13 -0
  145. package/src/widgets/actions/index.ts +1 -0
  146. package/src/widgets/actions/relative-data/relative-data.test.tsx +62 -1
  147. package/src/widgets/actions/relative-data/relative-data.tsx +62 -39
  148. package/src/widgets/bar/config.ts +34 -34
  149. package/src/widgets/bar/style.ts +1 -1
  150. package/src/widgets/category/category-ui.tsx +12 -2
  151. package/src/widgets/category/components/category-bar.tsx +3 -0
  152. package/src/widgets/category/components/category-legend.tsx +3 -0
  153. package/src/widgets/category/components/category-row-multi.tsx +9 -2
  154. package/src/widgets/category/components/category-row-other.tsx +3 -0
  155. package/src/widgets/category/components/category-row-single.tsx +8 -1
  156. package/src/widgets/category/config.ts +11 -0
  157. package/src/widgets/category/types.ts +1 -0
  158. package/src/widgets/echart/echart-ui.tsx +7 -0
  159. package/src/widgets/echart/echart.tsx +6 -0
  160. package/src/widgets/echart/options.ts +7 -0
  161. package/src/widgets/echart/types.ts +3 -0
  162. package/src/widgets/echart/utils.ts +41 -0
  163. package/src/widgets/error/error.tsx +10 -0
  164. package/src/widgets/formula/components/item.tsx +3 -0
  165. package/src/widgets/formula/components/prefix.tsx +3 -0
  166. package/src/widgets/formula/components/row.tsx +3 -0
  167. package/src/widgets/formula/components/series.tsx +3 -0
  168. package/src/widgets/formula/components/suffix.tsx +3 -0
  169. package/src/widgets/formula/components/value.tsx +4 -2
  170. package/src/widgets/formula/config.ts +11 -0
  171. package/src/widgets/formula/formula-ui.tsx +3 -0
  172. package/src/widgets/histogram/config.ts +93 -21
  173. package/src/widgets/histogram/index.ts +6 -1
  174. package/src/widgets/histogram/style.ts +1 -1
  175. package/src/widgets/histogram/types.ts +9 -3
  176. package/src/widgets/loader/loader.tsx +22 -0
  177. package/src/widgets/loader/utils.ts +26 -3
  178. package/src/widgets/markdown/config.ts +10 -0
  179. package/src/widgets/markdown/markdown-ui.tsx +7 -0
  180. package/src/widgets/markdown/markdown.tsx +3 -0
  181. package/src/widgets/note/note.tsx +10 -0
  182. package/src/widgets/pie/config.ts +100 -33
  183. package/src/widgets/pie/style.ts +1 -1
  184. package/src/widgets/range/components/range-item.tsx +5 -2
  185. package/src/widgets/range/config.ts +5 -0
  186. package/src/widgets/range/range-ui.tsx +3 -0
  187. package/src/widgets/scatterplot/config.ts +19 -23
  188. package/src/widgets/scatterplot/style.ts +1 -1
  189. package/src/widgets/selection-summary/selection-summary.tsx +11 -0
  190. package/src/widgets/skeleton-loader/skeleton-loader.tsx +10 -0
  191. package/src/widgets/spread/components/max-value.tsx +4 -2
  192. package/src/widgets/spread/components/min-value.tsx +4 -2
  193. package/src/widgets/spread/components/separator.tsx +3 -0
  194. package/src/widgets/spread/config.ts +11 -0
  195. package/src/widgets/spread/spread-ui.tsx +3 -0
  196. package/src/widgets/stores/types.ts +2 -0
  197. package/src/widgets/subheader/subheader.tsx +11 -0
  198. package/src/widgets/table/config.ts +8 -3
  199. package/src/widgets/table/hooks/use-pagination.ts +11 -3
  200. package/src/widgets/table/hooks/use-selection.ts +11 -2
  201. package/src/widgets/table/hooks/use-sort.ts +11 -3
  202. package/src/widgets/timeseries/config.ts +32 -33
  203. package/src/widgets/timeseries/style.ts +1 -1
  204. package/src/widgets/utils/chart-config/download-config.ts +22 -0
  205. package/src/widgets/{_shared → utils}/chart-config/index.ts +4 -0
  206. package/src/widgets/{_shared → utils}/chart-config/option-builders.ts +23 -13
  207. package/src/widgets/utils/formatter.ts +2 -0
  208. package/src/widgets/utils/index.ts +26 -0
  209. package/src/widgets/wrapper/components/actions.tsx +3 -0
  210. package/src/widgets/wrapper/components/options.tsx +3 -0
  211. package/src/widgets/wrapper/components/title.tsx +3 -0
  212. package/src/widgets/wrapper/wrapper-ui.tsx +14 -0
  213. package/src/widgets/wrapper/wrapper.tsx +14 -0
  214. package/dist/styles-CAroD5Rc.js +0 -123
  215. package/dist/styles-CAroD5Rc.js.map +0 -1
  216. /package/dist/types/widgets/{_shared → utils}/chart-config/config-factory.d.ts +0 -0
  217. /package/dist/types/widgets/{_shared → utils}/chart-config/csv-modifiers.d.ts +0 -0
  218. /package/dist/types/widgets/{_shared → utils}/chart-config/option-builders.test.d.ts +0 -0
  219. /package/dist/types/widgets/{_shared → utils}/skeleton/index.d.ts +0 -0
  220. /package/dist/types/widgets/{_shared → utils}/skeleton/styles.d.ts +0 -0
  221. /package/src/widgets/{_shared → utils}/chart-config/config-factory.ts +0 -0
  222. /package/src/widgets/{_shared → utils}/chart-config/csv-modifiers.ts +0 -0
  223. /package/src/widgets/{_shared → utils}/chart-config/option-builders.test.ts +0 -0
  224. /package/src/widgets/{_shared → utils}/skeleton/index.ts +0 -0
  225. /package/src/widgets/{_shared → utils}/skeleton/styles.ts +0 -0
@@ -2,15 +2,11 @@ import { Box, IconButton } from '@mui/material'
2
2
  import { HighlightAltOutlined } from '@mui/icons-material'
3
3
  import { useEffect, useCallback, useRef } from 'react'
4
4
  import { useWidgetStore } from '../../stores/widget-store'
5
- import type { BrushToggleProps } from './types'
5
+ import type { BrushSelectedItems, BrushToggleProps } from './types'
6
6
  import { styles } from './style'
7
7
  import { Tooltip } from '../../../components'
8
8
  import { getEChartBrushConfig } from '../../echart/utils'
9
- import type {
10
- EchartOptionsProps,
11
- EchartWidgetData,
12
- EchartWidgetState,
13
- } from '../../echart/types'
9
+ import type { EchartOptionsProps, EchartWidgetState } from '../../echart/types'
14
10
  import { useShallow } from 'zustand/shallow'
15
11
 
16
12
  export const BRUSH_TOGGLE_TOOL_ID = 'brush-toggle'
@@ -44,7 +40,7 @@ export function BrushToggle({
44
40
  const registerTool = useWidgetStore((state) => state.registerTool)
45
41
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
46
42
  const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
47
- const selected = useRef<(string | number)[]>([])
43
+ const selected = useRef<BrushSelectedItems>({ dataIndex: [], seriesIndex: 0 })
48
44
 
49
45
  const brushTool = useWidgetStore(
50
46
  useShallow((state) => {
@@ -67,7 +63,7 @@ export function BrushToggle({
67
63
  toggleTool(newBrush)
68
64
 
69
65
  if (newBrush) {
70
- onBrushSelected?.([])
66
+ onBrushSelected?.({ dataIndex: [], seriesIndex: 0 }) // Clear selection when enabling brush
71
67
  }
72
68
  }, [brush, onBrushSelected, toggleTool])
73
69
 
@@ -102,46 +98,47 @@ export function BrushToggle({
102
98
  }, [brush, getWidget, id])
103
99
 
104
100
  // Handle brushSelected event to capture selected bar indices
105
- const handleBrushSelected = useCallback(
106
- (event: unknown) => {
107
- const brushEvent = event as {
108
- batch?: {
109
- selected?: {
110
- dataIndex?: number[]
111
- seriesIndex?: number
112
- }[]
101
+ const handleBrushSelected = useCallback((event: unknown) => {
102
+ const brushEvent = event as {
103
+ batch?: {
104
+ selected?: {
105
+ dataIndex?: number[]
106
+ seriesIndex?: number
113
107
  }[]
108
+ }[]
109
+ }
110
+
111
+ const allSelected =
112
+ brushEvent.batch?.flatMap((batchItem) => batchItem.selected ?? []) ?? []
113
+
114
+ if (!allSelected.length) {
115
+ selected.current = {
116
+ dataIndex: [],
117
+ seriesIndex: 0,
114
118
  }
119
+ return
120
+ }
115
121
 
116
- // Resolve names from the widget dataset
117
- const widget = getWidget(id)
118
- const data = (widget?.data ?? []) as EchartWidgetData
119
-
120
- const items = [
121
- ...new Set(
122
- brushEvent.batch?.flatMap(
123
- (batch) =>
124
- batch.selected?.flatMap(
125
- (sel) =>
126
- sel.dataIndex?.map((dataIndex) => {
127
- const row = data[sel.seriesIndex ?? 0]?.[dataIndex]
128
- const firstValue = row ? Object.values(row)[0] : undefined
129
- const name =
130
- typeof firstValue === 'string' ||
131
- typeof firstValue === 'number'
132
- ? String(firstValue)
133
- : String(dataIndex)
134
- return name
135
- }) ?? [],
136
- ) ?? [],
137
- ),
138
- ),
139
- ]
140
-
141
- selected.current = items ?? []
142
- },
143
- [getWidget, id],
144
- )
122
+ // Use the first seriesIndex as the primary one (matches previous behavior)
123
+ const primarySeriesIndex = allSelected[0]?.seriesIndex ?? 0
124
+
125
+ const mergedDataIndex = Array.from(
126
+ new Set(
127
+ allSelected
128
+ .filter(
129
+ (item) =>
130
+ item.seriesIndex === undefined ||
131
+ item.seriesIndex === primarySeriesIndex,
132
+ )
133
+ .flatMap((item) => item.dataIndex ?? []),
134
+ ),
135
+ )
136
+
137
+ selected.current = {
138
+ dataIndex: mergedDataIndex,
139
+ seriesIndex: primarySeriesIndex,
140
+ }
141
+ }, [])
145
142
 
146
143
  const handleBrushEnd = useCallback(() => {
147
144
  onBrushSelected?.(selected.current)
@@ -169,8 +166,6 @@ export function BrushToggle({
169
166
 
170
167
  const brushConfig = getEChartBrushConfig()
171
168
 
172
- const onEventsWithoutBrush = { ...currentOnEvents }
173
- delete onEventsWithoutBrush.brushSelected
174
169
  const onEvents = {
175
170
  ...currentOnEvents,
176
171
  brushSelected: handleBrushSelected,
@@ -3,9 +3,15 @@ import type { ReactNode } from 'react'
3
3
  import type { BaseWidgetState } from '../../stores/types'
4
4
 
5
5
  /**
6
- * Represents a single item selected by the brush
6
+ * Brush selection result emitted by BrushToggle.
7
+ * Contains raw indices so consumers can resolve data according to their widget type.
7
8
  */
8
- export type BrushSelectedItems = (string | number)[]
9
+ export interface BrushSelectedItems {
10
+ /** Data indices of the selected items in the dataset */
11
+ dataIndex: number[]
12
+ /** Series index from the brush event (defaults to 0) */
13
+ seriesIndex: number
14
+ }
9
15
 
10
16
  /**
11
17
  * State stored in widget store for brush functionality
@@ -14,6 +14,17 @@ import { useShallow } from 'zustand/shallow'
14
14
 
15
15
  const EMPTY_LABELS: NonNullable<DownloadProps['labels']> = {}
16
16
 
17
+ /**
18
+ * Dropdown menu action for exporting widget data in various formats (CSV, PNG, etc.).
19
+ *
20
+ * Reads widget data from the store and triggers downloads using the modifier
21
+ * function defined in each `DownloadItem`.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <Download id={widgetId} items={barDownloadConfig({ refUI })} />
26
+ * ```
27
+ */
17
28
  export function Download({
18
29
  id,
19
30
  items,
@@ -28,6 +28,12 @@ async function downloadFileToCSV<D>(data: D[][]) {
28
28
  return Promise.resolve(URL.createObjectURL(blob))
29
29
  }
30
30
 
31
+ /**
32
+ * Pre-configured download item for exporting widget data as a CSV file.
33
+ *
34
+ * Converts a 2D array of data into CSV format with proper escaping and
35
+ * triggers a browser download. Revokes the object URL after download.
36
+ */
31
37
  export const downloadToCSV: DownloadItem<unknown[][]> = {
32
38
  id: 'csv',
33
39
  label: 'CSV',
@@ -91,6 +97,15 @@ async function downloadFileToPNG(ref: Ref<HTMLElement | null> | undefined) {
91
97
  return Promise.resolve(result)
92
98
  }
93
99
 
100
+ /**
101
+ * Pre-configured download item for exporting a widget as a PNG image.
102
+ *
103
+ * Uses html2canvas to capture the widget DOM element referenced by a React ref.
104
+ * Strips toolbar and action elements before capturing.
105
+ *
106
+ * @remarks
107
+ * The modifier expects a React ref to the widget's root HTML element, not raw data.
108
+ */
94
109
  export const downloadToPNG: Omit<DownloadItem, 'modifier'> & {
95
110
  modifier: (
96
111
  ref: Ref<HTMLElement | null> | undefined,
@@ -19,6 +19,19 @@ const EMPTY_DIALOG_CONTENT_PROPS: NonNullable<
19
19
  FullScreenProps['DialogContentProps']
20
20
  > = {}
21
21
 
22
+ /**
23
+ * Displays widget content in a fullscreen modal dialog.
24
+ *
25
+ * Manages fullscreen state via the widget store and renders a MUI Dialog
26
+ * with the widget title and a close button.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <FullScreen id="my-widget" labels={{ ariaLabel: 'Expand chart' }}>
31
+ * <ChartContent id="my-widget" />
32
+ * </FullScreen>
33
+ * ```
34
+ */
22
35
  export function FullScreen({
23
36
  id,
24
37
  labels,
@@ -11,6 +11,7 @@ export { downloadToCSV, downloadToPNG } from './download/exports'
11
11
  export {
12
12
  RelativeData,
13
13
  RELATIVE_DATA_TOOL_ID,
14
+ RELATIVE_DATA_CONFIG_TOOL_ID,
14
15
  } from './relative-data/relative-data'
15
16
  export type { RelativeDataProps } from './relative-data/types'
16
17
 
@@ -1,6 +1,10 @@
1
1
  import { describe, test, expect, beforeEach } from 'vitest'
2
2
  import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3
- import { RelativeData, RELATIVE_DATA_TOOL_ID } from './relative-data'
3
+ import {
4
+ RelativeData,
5
+ RELATIVE_DATA_TOOL_ID,
6
+ RELATIVE_DATA_CONFIG_TOOL_ID,
7
+ } from './relative-data'
4
8
  import { useWidgetStore } from '../../stores/widget-store'
5
9
  import type { EchartWidgetData } from '../../echart/types'
6
10
 
@@ -223,6 +227,63 @@ describe('RelativeData', () => {
223
227
  expect(button.hasAttribute('disabled')).toBeTruthy()
224
228
  })
225
229
 
230
+ test('registers config tool on mount', async () => {
231
+ render(<RelativeData id={widgetId} />)
232
+
233
+ await waitFor(() => {
234
+ const widget = useWidgetStore.getState().getWidget(widgetId)
235
+ const tool = widget?.registeredTools?.find(
236
+ (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
237
+ )
238
+ expect(tool).toBeTruthy()
239
+ expect(tool?.type).toBe('config')
240
+ expect(tool?.enabled).toBe(true)
241
+ })
242
+ })
243
+
244
+ test('sets formatter via config pipeline when toggling to relative mode', async () => {
245
+ useWidgetStore.getState().setWidget(widgetId, { max: 500 })
246
+
247
+ render(<RelativeData id={widgetId} />)
248
+
249
+ const button = screen.getByRole('button')
250
+ fireEvent.click(button)
251
+
252
+ await waitFor(() => {
253
+ const widget = useWidgetStore.getState().getWidget(widgetId)
254
+ const tool = widget?.registeredTools?.find(
255
+ (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
256
+ )
257
+ expect(tool?.config?.isRelative).toBe(true)
258
+ expect(tool?.config?.originalFormatter).toBeUndefined()
259
+ expect(tool?.config?.originalMax).toBe(500)
260
+ })
261
+ })
262
+
263
+ test('restores original formatter via config pipeline when toggling back', () => {
264
+ const customFormatter = (value: number) => `$${value}`
265
+ useWidgetStore
266
+ .getState()
267
+ .setWidget(widgetId, { formatter: customFormatter, max: 200 })
268
+
269
+ render(<RelativeData id={widgetId} />)
270
+
271
+ const button = screen.getByRole('button')
272
+
273
+ // Toggle to relative
274
+ fireEvent.click(button)
275
+ // Toggle back to absolute
276
+ fireEvent.click(button)
277
+
278
+ const widget = useWidgetStore.getState().getWidget(widgetId)
279
+ const tool = widget?.registeredTools?.find(
280
+ (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
281
+ )
282
+ expect(tool?.config?.isRelative).toBe(false)
283
+ expect(tool?.config?.originalFormatter).toBe(customFormatter)
284
+ expect(tool?.config?.originalMax).toBe(200)
285
+ })
286
+
226
287
  test('recalculates relative values when store data changes externally while in relative mode', async () => {
227
288
  const initialData: EchartWidgetData = [
228
289
  [
@@ -2,13 +2,14 @@ import { IconButton } from '@mui/material'
2
2
  import { PercentOutlined } from '@mui/icons-material'
3
3
  import { useCallback, useEffect, useRef } from 'react'
4
4
  import { useWidgetStore } from '../../stores/widget-store'
5
- import type { RelativeDataProps, RelativeDataState } from './types'
5
+ import type { RelativeDataProps } from './types'
6
6
  import { actionButtonStyles } from '../shared/styles'
7
7
  import { Tooltip } from '../../../components'
8
8
  import { calculateTotal, toRelativeData } from './utils'
9
9
  import type { EchartWidgetData } from '../../../widgets/echart'
10
10
 
11
11
  export const RELATIVE_DATA_TOOL_ID = 'relative-data'
12
+ export const RELATIVE_DATA_CONFIG_TOOL_ID = 'relative-data-config'
12
13
 
13
14
  /**
14
15
  * Widget action to toggle between relative (percentage) and absolute data display.
@@ -33,36 +34,30 @@ export function RelativeData({
33
34
  Icon,
34
35
  IconButtonProps,
35
36
  }: RelativeDataProps) {
36
- const previousMaxValue = useRef<number | undefined>(undefined)
37
- const originalFormatter = useRef<((value: number) => string) | undefined>(
37
+ const percentFormatterRef = useRef<((value: number) => string) | undefined>(
38
38
  undefined,
39
39
  )
40
- const setWidget = useWidgetStore((state) => state.setWidget)
41
40
  const getWidget = useWidgetStore((state) => state.getWidget)
42
41
  const registerTool = useWidgetStore((state) => state.registerTool)
43
42
  const unregisterTool = useWidgetStore((state) => state.unregisterTool)
44
43
  const setToolEnabled = useWidgetStore((state) => state.setToolEnabled)
44
+ const updateToolConfig = useWidgetStore((state) => state.updateToolConfig)
45
45
 
46
46
  const storeIsRelative = useWidgetStore(
47
- (state) => state.getWidget<RelativeDataState>(id)?.isRelative,
47
+ (state) =>
48
+ state.widgets[id]?.registeredTools?.find(
49
+ (t) => t.id === RELATIVE_DATA_CONFIG_TOOL_ID,
50
+ )?.config?.isRelative as boolean | undefined,
48
51
  )
49
52
 
50
53
  const isRelative = storeIsRelative ?? defaultIsRelative
51
54
 
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
55
  // Register tool on mount
61
56
  useEffect(() => {
62
57
  registerTool(id, {
63
58
  id: RELATIVE_DATA_TOOL_ID,
64
59
  order,
65
- enabled: isRelative,
60
+ enabled: defaultIsRelative,
66
61
  fn: (data) => {
67
62
  const echartData = data as EchartWidgetData
68
63
  const total = calculateTotal(echartData)
@@ -71,42 +66,70 @@ export function RelativeData({
71
66
  })
72
67
 
73
68
  return () => unregisterTool(id, RELATIVE_DATA_TOOL_ID)
74
- }, [id, order, registerTool, unregisterTool, isRelative])
69
+ }, [id, order, registerTool, unregisterTool, defaultIsRelative])
70
+
71
+ // Register config tool for formatter management
72
+ useEffect(() => {
73
+ registerTool(id, {
74
+ id: RELATIVE_DATA_CONFIG_TOOL_ID,
75
+ type: 'config',
76
+ order,
77
+ enabled: true,
78
+ fn: (currentConfig, toolConfig) => {
79
+ const config = currentConfig as Record<string, unknown>
80
+ if (toolConfig?.isRelative) {
81
+ if (!percentFormatterRef.current) {
82
+ const locale = toolConfig?.locale as string | undefined
83
+ percentFormatterRef.current = (value: number) =>
84
+ new Intl.NumberFormat(locale, {
85
+ style: 'percent',
86
+ minimumFractionDigits: 1,
87
+ maximumFractionDigits: 1,
88
+ }).format(value / 100)
89
+ }
90
+ return { ...config, formatter: percentFormatterRef.current, max: 100 }
91
+ }
92
+ // Switching back from relative mode
93
+ percentFormatterRef.current = undefined
94
+ if (toolConfig && 'originalFormatter' in toolConfig) {
95
+ return {
96
+ ...config,
97
+ formatter: toolConfig.originalFormatter,
98
+ max: toolConfig.originalMax,
99
+ }
100
+ }
101
+ return config
102
+ },
103
+ config: {
104
+ isRelative: defaultIsRelative,
105
+ },
106
+ })
107
+ return () => unregisterTool(id, RELATIVE_DATA_CONFIG_TOOL_ID)
108
+ }, [id, order, registerTool, unregisterTool, defaultIsRelative])
75
109
 
76
110
  const handleToggle = useCallback(() => {
77
111
  const newIsRelative = !isRelative
78
112
  setToolEnabled(id, RELATIVE_DATA_TOOL_ID, newIsRelative)
79
- let max = previousMaxValue.current
80
113
 
81
114
  if (newIsRelative) {
82
- // Backup current formatter to ref
83
115
  const widget = getWidget(id) as {
84
116
  formatter?: (value: number) => string
85
117
  locale?: string
118
+ max?: number
86
119
  }
87
- originalFormatter.current = widget?.formatter
88
-
89
- // Save current max value before setting to 100
90
- const currentMax = (getWidget(id) as { max?: number })?.max
91
- previousMaxValue.current = currentMax
92
- max = 100
120
+ updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
121
+ isRelative: true,
122
+ originalFormatter: widget?.formatter,
123
+ originalMax: widget?.max,
124
+ locale: widget?.locale,
125
+ })
126
+ } else {
127
+ percentFormatterRef.current = undefined
128
+ updateToolConfig(id, RELATIVE_DATA_CONFIG_TOOL_ID, {
129
+ isRelative: false,
130
+ })
93
131
  }
94
-
95
- setWidget(id, {
96
- isRelative: newIsRelative,
97
- max,
98
- formatter: newIsRelative
99
- ? (value: number) => {
100
- const widget = getWidget(id) as { locale?: string }
101
- return new Intl.NumberFormat(widget?.locale, {
102
- style: 'percent',
103
- minimumFractionDigits: 1,
104
- maximumFractionDigits: 1,
105
- }).format(value / 100)
106
- }
107
- : originalFormatter.current,
108
- })
109
- }, [isRelative, setWidget, id, getWidget, setToolEnabled])
132
+ }, [isRelative, id, getWidget, setToolEnabled, updateToolConfig])
110
133
 
111
134
  const tooltipLabel = isRelative
112
135
  ? (labels?.absolute ?? 'Show absolute values')
@@ -10,33 +10,27 @@ import {
10
10
  buildGridConfig,
11
11
  createTooltipPositioner,
12
12
  createTooltipFormatter,
13
+ createChartDownloadConfig,
14
+ applyXAxisFormatter,
13
15
  niceNum,
14
- } from '../_shared/chart-config'
15
- import { downloadToCSV, downloadToPNG, type DownloadItem } from '../actions'
16
- import type { ConfigProps } from '../loader/types'
16
+ } from '../utils/chart-config'
17
17
 
18
- export function barDownloadConfig({
19
- refUI,
20
- }: ConfigProps): DownloadItem<BarWidgetData>[] {
21
- return [
22
- {
23
- ...downloadToPNG,
24
- modifier: () => downloadToPNG.modifier(refUI),
25
- },
26
- {
27
- ...downloadToCSV,
28
- modifier: async (data) => {
29
- const rows = flattenObjectArrayToCSV(data)
30
- return downloadToCSV.modifier(rows)
31
- },
32
- },
33
- ]
34
- }
18
+ export const barDownloadConfig = createChartDownloadConfig<BarWidgetData>(
19
+ flattenObjectArrayToCSV,
20
+ )
35
21
 
22
+ /**
23
+ * Generates ECharts configuration for bar chart widgets (vertical and horizontal), including axis, tooltip, legend, and series options styled with the CARTO theme.
24
+ *
25
+ * @param props - Bar chart configuration including data and theme.
26
+ * @returns Widget config with ECharts option object.
27
+ */
36
28
  export function barConfig(props: BarConfig): BarWidgetConfig {
37
29
  return {
38
30
  type: 'bar',
39
31
  option: mergeEchartWidgetConfig(getCommonOptions(props), getOption(props)),
32
+ formatter: props.formatter,
33
+ labelFormatter: props.labelFormatter,
40
34
  }
41
35
  }
42
36
 
@@ -44,6 +38,7 @@ function getOption({
44
38
  data = [],
45
39
  theme,
46
40
  formatter,
41
+ labelFormatter,
47
42
  }: BarConfig): EchartOptionsProps {
48
43
  const hasLegend = (data?.length ?? 0) > 1
49
44
 
@@ -51,21 +46,24 @@ function getOption({
51
46
  let niceMax = 1
52
47
 
53
48
  return {
54
- legend: buildLegendConfig(hasLegend),
49
+ legend: buildLegendConfig({ hasLegend, labelFormatter }),
55
50
  grid: buildGridConfig(hasLegend, theme),
56
- xAxis: {
57
- type: 'category',
58
- axisLine: {
59
- show: false,
60
- },
61
- axisTick: {
62
- show: false,
63
- },
64
- axisLabel: {
65
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
66
- margin: 0,
51
+ xAxis: applyXAxisFormatter(
52
+ {
53
+ type: 'category',
54
+ axisLine: {
55
+ show: false,
56
+ },
57
+ axisTick: {
58
+ show: false,
59
+ },
60
+ axisLabel: {
61
+ padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
62
+ margin: 0,
63
+ },
67
64
  },
68
- },
65
+ labelFormatter,
66
+ ),
69
67
  yAxis: {
70
68
  type: 'value' as const,
71
69
  min: (extent: { min: number }) => {
@@ -118,7 +116,9 @@ function getOption({
118
116
 
119
117
  const marker = typeof item.marker === 'string' ? item.marker : ''
120
118
  const seriesName = item.seriesName ? `${item.seriesName}: ` : ''
121
- const name = item.name ?? ''
119
+ const name = labelFormatter
120
+ ? String(labelFormatter(item.name ?? ''))
121
+ : (item.name ?? '')
122
122
 
123
123
  return { name, seriesName, marker, value: formattedValue }
124
124
  }),
@@ -1,5 +1,5 @@
1
1
  import type { SxProps, Theme } from '@mui/material'
2
- import { baseSkeletonStyles } from '../_shared/skeleton'
2
+ import { baseSkeletonStyles } from '../utils/skeleton'
3
3
 
4
4
  export const styles = {
5
5
  skeleton: {
@@ -10,14 +10,21 @@ import {
10
10
  } from './components'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { useState } from 'react'
13
+ import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
13
14
 
14
- const defaultFormatter = (value: number) => value.toString()
15
-
15
+ /**
16
+ * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping.
17
+ */
16
18
  export function CategoryUI({ id }: CategoryUIProps) {
17
19
  const theme = useTheme()
18
20
  const _formatter = useWidgetStore(
19
21
  useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.formatter),
20
22
  )
23
+ const _labelFormatter = useWidgetStore(
24
+ useShallow(
25
+ (state) => state.getWidget<CategoryWidgetState>(id)?.labelFormatter,
26
+ ),
27
+ )
21
28
  const _series = useWidgetStore(
22
29
  useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.series),
23
30
  )
@@ -41,6 +48,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
41
48
  )
42
49
 
43
50
  const formatter = _formatter ?? defaultFormatter
51
+ const labelFormatter = _labelFormatter ?? defaultLabelFormatter
44
52
  const series = _series ?? []
45
53
 
46
54
  const [maxHeight] = useState<string | number | undefined>(
@@ -98,6 +106,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
98
106
  maxValue={maxValue}
99
107
  colors={colors}
100
108
  formatter={formatter}
109
+ labelFormatter={labelFormatter}
101
110
  onClick={onRowClick}
102
111
  selected={selected?.(item.name) ?? true}
103
112
  />
@@ -111,6 +120,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
111
120
  maxValue={maxValue}
112
121
  color={colors[0]!}
113
122
  formatter={formatter}
123
+ labelFormatter={labelFormatter}
114
124
  onClick={onRowClick}
115
125
  />
116
126
  ))}
@@ -8,6 +8,9 @@ export interface CategoryBarProps {
8
8
  selected?: boolean
9
9
  }
10
10
 
11
+ /**
12
+ * Renders a single horizontal bar fill proportional to its value relative to the maximum.
13
+ */
11
14
  export function CategoryBar({
12
15
  value,
13
16
  maxValue,
@@ -7,6 +7,9 @@ export interface CategoryLegendProps {
7
7
  colors: string[]
8
8
  }
9
9
 
10
+ /**
11
+ * Renders a color-coded legend for multi-series category widgets.
12
+ */
10
13
  export function CategoryLegend({ series, colors }: CategoryLegendProps) {
11
14
  if (series.length === 0) {
12
15
  return null
@@ -9,16 +9,21 @@ export interface CategoryRowMultiProps {
9
9
  maxValue: number
10
10
  colors: string[]
11
11
  formatter: NonNullable<CategoryWidgetConfig['formatter']>
12
+ labelFormatter?: CategoryWidgetConfig['labelFormatter']
12
13
  onClick?: CategoryWidgetConfig['onRowClick']
13
14
  selected?: boolean
14
15
  }
15
16
 
17
+ /**
18
+ * Renders a multi-series category row with a label and multiple color-coded bars stacked vertically.
19
+ */
16
20
  export function CategoryRowMulti({
17
21
  name,
18
22
  values,
19
23
  maxValue,
20
24
  colors,
21
25
  formatter,
26
+ labelFormatter,
22
27
  onClick,
23
28
  selected = true,
24
29
  }: CategoryRowMultiProps) {
@@ -27,10 +32,12 @@ export function CategoryRowMulti({
27
32
 
28
33
  return (
29
34
  <Box sx={rowStyle} onClick={handleClick}>
30
- <Typography sx={styles.rowLabel}>{name}</Typography>
35
+ <Typography sx={styles.rowLabel}>
36
+ {labelFormatter ? labelFormatter(name) : name}
37
+ </Typography>
31
38
  <Box sx={styles.barContainer}>
32
39
  {values.map((value, index) => (
33
- <Box key={`${name}-${value}`} sx={styles.multiBarRow}>
40
+ <Box key={`${name}-${value}-${index}`} sx={styles.multiBarRow}>
34
41
  <Box sx={styles.multiBarContainer}>
35
42
  <CategoryBar
36
43
  value={value}