@carto/ps-react-ui 4.4.2 → 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 (65) hide show
  1. package/dist/download-config-Dqu78h2a.js +57 -0
  2. package/dist/download-config-Dqu78h2a.js.map +1 -0
  3. package/dist/formatter-B9Bxn1k7.js +6 -0
  4. package/dist/formatter-B9Bxn1k7.js.map +1 -0
  5. package/dist/styles-Y8q7Jff3.js +118 -0
  6. package/dist/styles-Y8q7Jff3.js.map +1 -0
  7. package/dist/types/widgets/actions/brush-toggle/types.d.ts +8 -2
  8. package/dist/types/widgets/category/components/category-row-multi.d.ts +2 -1
  9. package/dist/types/widgets/category/components/category-row-single.d.ts +2 -1
  10. package/dist/types/widgets/category/types.d.ts +1 -0
  11. package/dist/types/widgets/echart/types.d.ts +2 -0
  12. package/dist/types/widgets/histogram/config.d.ts +15 -3
  13. package/dist/types/widgets/histogram/index.d.ts +2 -1
  14. package/dist/types/widgets/histogram/types.d.ts +6 -3
  15. package/dist/types/widgets/stores/types.d.ts +2 -0
  16. package/dist/types/widgets/utils/chart-config/index.d.ts +1 -1
  17. package/dist/types/widgets/utils/chart-config/option-builders.d.ts +13 -8
  18. package/dist/types/widgets/utils/formatter.d.ts +1 -0
  19. package/dist/types/widgets/utils/index.d.ts +1 -1
  20. package/dist/widgets/actions.js +455 -439
  21. package/dist/widgets/actions.js.map +1 -1
  22. package/dist/widgets/bar.js +52 -46
  23. package/dist/widgets/bar.js.map +1 -1
  24. package/dist/widgets/category.js +206 -197
  25. package/dist/widgets/category.js.map +1 -1
  26. package/dist/widgets/formula.js +1 -1
  27. package/dist/widgets/histogram.js +119 -79
  28. package/dist/widgets/histogram.js.map +1 -1
  29. package/dist/widgets/pie.js +110 -98
  30. package/dist/widgets/pie.js.map +1 -1
  31. package/dist/widgets/range.js +1 -1
  32. package/dist/widgets/scatterplot.js +49 -43
  33. package/dist/widgets/scatterplot.js.map +1 -1
  34. package/dist/widgets/spread.js +1 -1
  35. package/dist/widgets/timeseries.js +51 -45
  36. package/dist/widgets/timeseries.js.map +1 -1
  37. package/dist/widgets/toolbar-actions.js +101 -6693
  38. package/dist/widgets/toolbar-actions.js.map +1 -1
  39. package/dist/widgets/utils.js +16 -14
  40. package/dist/widgets/utils.js.map +1 -1
  41. package/package.json +5 -4
  42. package/src/widgets/README.md +3 -3
  43. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +42 -47
  44. package/src/widgets/actions/brush-toggle/types.ts +8 -2
  45. package/src/widgets/bar/config.ts +22 -14
  46. package/src/widgets/category/category-ui.tsx +9 -1
  47. package/src/widgets/category/components/category-row-multi.tsx +6 -2
  48. package/src/widgets/category/components/category-row-single.tsx +5 -1
  49. package/src/widgets/category/types.ts +1 -0
  50. package/src/widgets/echart/types.ts +2 -0
  51. package/src/widgets/histogram/config.ts +101 -20
  52. package/src/widgets/histogram/index.ts +6 -1
  53. package/src/widgets/histogram/types.ts +9 -3
  54. package/src/widgets/pie/config.ts +17 -5
  55. package/src/widgets/scatterplot/config.ts +8 -3
  56. package/src/widgets/stores/types.ts +2 -0
  57. package/src/widgets/timeseries/config.ts +21 -13
  58. package/src/widgets/utils/chart-config/index.ts +1 -1
  59. package/src/widgets/utils/chart-config/option-builders.ts +22 -12
  60. package/src/widgets/utils/formatter.ts +2 -1
  61. package/src/widgets/utils/index.ts +1 -1
  62. package/dist/formatter-B1Xh8XDH.js +0 -5
  63. package/dist/formatter-B1Xh8XDH.js.map +0 -1
  64. package/dist/styles-C_8vOEep.js +0 -167
  65. package/dist/styles-C_8vOEep.js.map +0 -1
@@ -1,5 +1,6 @@
1
- import { d as i } from "../formatter-B1Xh8XDH.js";
2
- import { a as l, b as p, c, d as f, e as d, f as m, g as u, h as C, i as b, j as g, n as x, s as F } from "../styles-C_8vOEep.js";
1
+ import { d as i, a as n } from "../formatter-B9Bxn1k7.js";
2
+ import { c as f, f as p, s as c } from "../download-config-Dqu78h2a.js";
3
+ import { a as m, b as u, c as C, d as b, e as x, f as g, g as F, h as y, n as A } from "../styles-Y8q7Jff3.js";
3
4
  function r({
4
5
  type: a,
5
6
  getOptions: e
@@ -13,19 +14,20 @@ function r({
13
14
  };
14
15
  }
15
16
  export {
16
- l as applyXAxisFormatter,
17
- p as applyYAxisFormatter,
18
- c as baseSkeletonStyles,
19
- f as buildGridConfig,
20
- d as buildLegendConfig,
21
- m as createAxisLabelFormatter,
22
- u as createChartDownloadConfig,
17
+ m as applyXAxisFormatter,
18
+ u as applyYAxisFormatter,
19
+ C as baseSkeletonStyles,
20
+ b as buildGridConfig,
21
+ x as buildLegendConfig,
22
+ g as createAxisLabelFormatter,
23
+ f as createChartDownloadConfig,
23
24
  r as createChartWidgetConfig,
24
- C as createTooltipFormatter,
25
- b as createTooltipPositioner,
25
+ F as createTooltipFormatter,
26
+ y as createTooltipPositioner,
26
27
  i as defaultFormatter,
27
- g as flattenObjectArrayToCSV,
28
- x as niceNum,
29
- F as scatterplotDataToCSV
28
+ n as defaultLabelFormatter,
29
+ p as flattenObjectArrayToCSV,
30
+ A as niceNum,
31
+ c as scatterplotDataToCSV
30
32
  };
31
33
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/widgets/utils/chart-config/config-factory.ts"],"sourcesContent":["import type { EchartOptionsProps, EchartWidgetData } from '../../echart'\n\n/**\n * Base configuration interface for chart widgets\n */\nexport interface ChartWidgetBaseConfig<TData = EchartWidgetData> {\n data?: TData\n}\n\n/**\n * Parameters for creating a chart widget configuration\n */\nexport interface CreateChartWidgetConfigParams<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n> {\n /** Widget type identifier (e.g., 'bar', 'pie', 'histogram') */\n type: TType\n /** Function to get EChart options from config */\n getOptions: (config: TConfig) => EchartOptionsProps\n}\n\n/**\n * Return type of the chart widget config function\n */\nexport type ChartWidgetConfigResult<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n> = TConfig & {\n type: TType\n}\n\n/**\n * Factory function to create a standardized chart widget config function.\n * This eliminates duplication across chart widgets by providing a common structure.\n *\n * @example\n * ```ts\n * export const barConfig = createChartWidgetConfig({\n * type: 'bar' as const,\n * getOptions: ({ data, theme }) => ({\n * // EChart configuration\n * }),\n * csvModifier: (data) => flattenObjectArrayToCSV(data),\n * })\n * ```\n */\nexport function createChartWidgetConfig<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n>({\n type,\n getOptions,\n}: CreateChartWidgetConfigParams<TData, TConfig, TType>): (\n config: TConfig,\n) => ChartWidgetConfigResult<TData, TConfig, TType> {\n return function (config: TConfig) {\n return {\n ...config,\n option: getOptions(config),\n type: type,\n } as ChartWidgetConfigResult<TData, TConfig, TType>\n }\n}\n"],"names":["createChartWidgetConfig","type","getOptions","config","option"],"mappings":";;AAiDO,SAASA,EAId;AAAA,EACAC,MAAAA;AAAAA,EACAC,YAAAA;AACoD,GAEF;AAClD,SAAO,SAAUC,GAAiB;AAChC,WAAO;AAAA,MACL,GAAGA;AAAAA,MACHC,QAAQF,EAAWC,CAAM;AAAA,MACzBF,MAAAA;AAAAA,IAAAA;AAAAA,EAEJ;AACF;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/widgets/utils/chart-config/config-factory.ts"],"sourcesContent":["import type { EchartOptionsProps, EchartWidgetData } from '../../echart'\n\n/**\n * Base configuration interface for chart widgets\n */\nexport interface ChartWidgetBaseConfig<TData = EchartWidgetData> {\n data?: TData\n}\n\n/**\n * Parameters for creating a chart widget configuration\n */\nexport interface CreateChartWidgetConfigParams<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n> {\n /** Widget type identifier (e.g., 'bar', 'pie', 'histogram') */\n type: TType\n /** Function to get EChart options from config */\n getOptions: (config: TConfig) => EchartOptionsProps\n}\n\n/**\n * Return type of the chart widget config function\n */\nexport type ChartWidgetConfigResult<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n> = TConfig & {\n type: TType\n}\n\n/**\n * Factory function to create a standardized chart widget config function.\n * This eliminates duplication across chart widgets by providing a common structure.\n *\n * @example\n * ```ts\n * export const barConfig = createChartWidgetConfig({\n * type: 'bar' as const,\n * getOptions: ({ data, theme }) => ({\n * // EChart configuration\n * }),\n * csvModifier: (data) => flattenObjectArrayToCSV(data),\n * })\n * ```\n */\nexport function createChartWidgetConfig<\n TData = EchartWidgetData,\n TConfig extends ChartWidgetBaseConfig<TData> = ChartWidgetBaseConfig<TData>,\n TType extends string = string,\n>({\n type,\n getOptions,\n}: CreateChartWidgetConfigParams<TData, TConfig, TType>): (\n config: TConfig,\n) => ChartWidgetConfigResult<TData, TConfig, TType> {\n return function (config: TConfig) {\n return {\n ...config,\n option: getOptions(config),\n type: type,\n } as ChartWidgetConfigResult<TData, TConfig, TType>\n }\n}\n"],"names":["createChartWidgetConfig","type","getOptions","config","option"],"mappings":";;;AAiDO,SAASA,EAId;AAAA,EACAC,MAAAA;AAAAA,EACAC,YAAAA;AACoD,GAEF;AAClD,SAAO,SAAUC,GAAiB;AAChC,WAAO;AAAA,MACL,GAAGA;AAAAA,MACHC,QAAQF,EAAWC,CAAM;AAAA,MACzBF,MAAAA;AAAAA,IAAAA;AAAAA,EAEJ;AACF;"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "4.4.2",
3
+ "version": "4.4.3",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
7
- "@carto/meridian-ds": "2.10.1",
7
+ "@carto/meridian-ds": "2.14.0",
8
8
  "@dnd-kit/core": "6.3.1",
9
9
  "@dnd-kit/sortable": "10.0.0",
10
10
  "@dnd-kit/utilities": "3.2.2",
@@ -14,15 +14,16 @@
14
14
  "echarts": "6.0.0",
15
15
  "html2canvas": "1.4.1",
16
16
  "react-markdown": "10.1.0",
17
- "zustand": "5.0.11",
17
+ "zustand": "5.0.12",
18
18
  "@carto/ps-common-types": "1.0.0",
19
19
  "@carto/ps-utils": "2.0.1"
20
20
  },
21
21
  "peerDependencies": {
22
+ "@carto/meridian-ds": "^2.0.0",
22
23
  "@dnd-kit/core": "^6.0.0",
23
24
  "@dnd-kit/sortable": "^10.0.0",
24
25
  "@dnd-kit/utilities": "^3.0.0",
25
- "@emotion/styled": "^11.14.1",
26
+ "@emotion/styled": "^11.0.0",
26
27
  "@mui/icons-material": "^5.0.0",
27
28
  "@mui/material": "^5.0.0",
28
29
  "echarts": "^6.0.0",
@@ -70,7 +70,7 @@ export const myWidgetConfig = createChartWidgetConfig({
70
70
  type: 'my-widget',
71
71
  getOptions: ({ data, theme }) => ({
72
72
  // EChart configuration
73
- legend: buildLegendConfig(hasLegend),
73
+ legend: buildLegendConfig({ hasLegend }),
74
74
  // ...
75
75
  }),
76
76
  csvModifier: (data) => flattenObjectArrayToCSV(data),
@@ -88,7 +88,7 @@ export const myWidgetConfig = createChartWidgetConfig({
88
88
 
89
89
  **File:** `utils/chart-config/option-builders.ts`
90
90
 
91
- - `buildLegendConfig(hasLegend)` - Standard legend configuration
91
+ - `buildLegendConfig({ hasLegend, labelFormatter? })` - Standard legend configuration
92
92
  - `buildGridConfig(hasLegend, theme, additionalConfig?)` - Grid with legend-aware spacing
93
93
  - `createTooltipPositioner(theme)` - Tooltip positioning with overflow handling
94
94
 
@@ -159,7 +159,7 @@ export const myWidgetConfig = createChartWidgetConfig({
159
159
  }: Omit<MyConfig, 'refUI'>): EchartOptionsProps {
160
160
  const hasLegend = (data?.length ?? 0) > 1
161
161
  return {
162
- legend: buildLegendConfig(hasLegend),
162
+ legend: buildLegendConfig({ hasLegend }),
163
163
  grid: buildGridConfig(hasLegend, theme),
164
164
  // ... widget-specific configuration
165
165
  }
@@ -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
@@ -11,6 +11,7 @@ import {
11
11
  createTooltipPositioner,
12
12
  createTooltipFormatter,
13
13
  createChartDownloadConfig,
14
+ applyXAxisFormatter,
14
15
  niceNum,
15
16
  } from '../utils/chart-config'
16
17
 
@@ -29,6 +30,7 @@ export function barConfig(props: BarConfig): BarWidgetConfig {
29
30
  type: 'bar',
30
31
  option: mergeEchartWidgetConfig(getCommonOptions(props), getOption(props)),
31
32
  formatter: props.formatter,
33
+ labelFormatter: props.labelFormatter,
32
34
  }
33
35
  }
34
36
 
@@ -36,6 +38,7 @@ function getOption({
36
38
  data = [],
37
39
  theme,
38
40
  formatter,
41
+ labelFormatter,
39
42
  }: BarConfig): EchartOptionsProps {
40
43
  const hasLegend = (data?.length ?? 0) > 1
41
44
 
@@ -43,21 +46,24 @@ function getOption({
43
46
  let niceMax = 1
44
47
 
45
48
  return {
46
- legend: buildLegendConfig(hasLegend),
49
+ legend: buildLegendConfig({ hasLegend, labelFormatter }),
47
50
  grid: buildGridConfig(hasLegend, theme),
48
- xAxis: {
49
- type: 'category',
50
- axisLine: {
51
- show: false,
52
- },
53
- axisTick: {
54
- show: false,
55
- },
56
- axisLabel: {
57
- padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
58
- 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
+ },
59
64
  },
60
- },
65
+ labelFormatter,
66
+ ),
61
67
  yAxis: {
62
68
  type: 'value' as const,
63
69
  min: (extent: { min: number }) => {
@@ -110,7 +116,9 @@ function getOption({
110
116
 
111
117
  const marker = typeof item.marker === 'string' ? item.marker : ''
112
118
  const seriesName = item.seriesName ? `${item.seriesName}: ` : ''
113
- const name = item.name ?? ''
119
+ const name = labelFormatter
120
+ ? String(labelFormatter(item.name ?? ''))
121
+ : (item.name ?? '')
114
122
 
115
123
  return { name, seriesName, marker, value: formattedValue }
116
124
  }),
@@ -10,7 +10,7 @@ import {
10
10
  } from './components'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { useState } from 'react'
13
- import { defaultFormatter } from '../utils/formatter'
13
+ import { defaultFormatter, defaultLabelFormatter } from '../utils/formatter'
14
14
 
15
15
  /**
16
16
  * Renders a category widget displaying horizontal bars for categorical data with support for single and multi-series layouts, selection, and overflow grouping.
@@ -20,6 +20,11 @@ export function CategoryUI({ id }: CategoryUIProps) {
20
20
  const _formatter = useWidgetStore(
21
21
  useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.formatter),
22
22
  )
23
+ const _labelFormatter = useWidgetStore(
24
+ useShallow(
25
+ (state) => state.getWidget<CategoryWidgetState>(id)?.labelFormatter,
26
+ ),
27
+ )
23
28
  const _series = useWidgetStore(
24
29
  useShallow((state) => state.getWidget<CategoryWidgetState>(id)?.series),
25
30
  )
@@ -43,6 +48,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
43
48
  )
44
49
 
45
50
  const formatter = _formatter ?? defaultFormatter
51
+ const labelFormatter = _labelFormatter ?? defaultLabelFormatter
46
52
  const series = _series ?? []
47
53
 
48
54
  const [maxHeight] = useState<string | number | undefined>(
@@ -100,6 +106,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
100
106
  maxValue={maxValue}
101
107
  colors={colors}
102
108
  formatter={formatter}
109
+ labelFormatter={labelFormatter}
103
110
  onClick={onRowClick}
104
111
  selected={selected?.(item.name) ?? true}
105
112
  />
@@ -113,6 +120,7 @@ export function CategoryUI({ id }: CategoryUIProps) {
113
120
  maxValue={maxValue}
114
121
  color={colors[0]!}
115
122
  formatter={formatter}
123
+ labelFormatter={labelFormatter}
116
124
  onClick={onRowClick}
117
125
  />
118
126
  ))}
@@ -9,6 +9,7 @@ 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
  }
@@ -22,6 +23,7 @@ export function CategoryRowMulti({
22
23
  maxValue,
23
24
  colors,
24
25
  formatter,
26
+ labelFormatter,
25
27
  onClick,
26
28
  selected = true,
27
29
  }: CategoryRowMultiProps) {
@@ -30,10 +32,12 @@ export function CategoryRowMulti({
30
32
 
31
33
  return (
32
34
  <Box sx={rowStyle} onClick={handleClick}>
33
- <Typography sx={styles.rowLabel}>{name}</Typography>
35
+ <Typography sx={styles.rowLabel}>
36
+ {labelFormatter ? labelFormatter(name) : name}
37
+ </Typography>
34
38
  <Box sx={styles.barContainer}>
35
39
  {values.map((value, index) => (
36
- <Box key={`${name}-${value}`} sx={styles.multiBarRow}>
40
+ <Box key={`${name}-${value}-${index}`} sx={styles.multiBarRow}>
37
41
  <Box sx={styles.multiBarContainer}>
38
42
  <CategoryBar
39
43
  value={value}
@@ -9,6 +9,7 @@ export interface CategoryRowSingleProps {
9
9
  maxValue: number
10
10
  color: string
11
11
  formatter: NonNullable<CategoryWidgetConfig['formatter']>
12
+ labelFormatter?: CategoryWidgetConfig['labelFormatter']
12
13
  onClick?: CategoryWidgetConfig['onRowClick']
13
14
  selected?: boolean
14
15
  }
@@ -22,6 +23,7 @@ export function CategoryRowSingle({
22
23
  maxValue,
23
24
  color,
24
25
  formatter,
26
+ labelFormatter,
25
27
  onClick,
26
28
  selected = true,
27
29
  }: CategoryRowSingleProps) {
@@ -36,7 +38,9 @@ export function CategoryRowSingle({
36
38
  return (
37
39
  <Box sx={rowStyle} onClick={handleClick}>
38
40
  <Box sx={styles.rowHeader}>
39
- <Typography sx={styles.rowLabel}>{name}</Typography>
41
+ <Typography sx={styles.rowLabel}>
42
+ {labelFormatter ? labelFormatter(name) : name}
43
+ </Typography>
40
44
  <Typography sx={styles.rowValue}>{formatter(value)}</Typography>
41
45
  </Box>
42
46
  <CategoryBar
@@ -29,6 +29,7 @@ export type CategoryWidgetState = BaseWidgetState<
29
29
 
30
30
  export interface CategoryWidgetConfig {
31
31
  formatter?: (value: number) => string
32
+ labelFormatter?: (value: string | number) => string | number
32
33
  series?: CategorySeriesConfig[]
33
34
  maxItems?: number
34
35
  labels?: CategoryLabels
@@ -33,6 +33,7 @@ export interface EchartWidgetOptionProps<D> {
33
33
  data?: D
34
34
  theme: typeof CartoTheme
35
35
  formatter?: (value: number) => string
36
+ labelFormatter?: (value: string | number) => string | number
36
37
  }
37
38
 
38
39
  export interface EchartWidgetProps {
@@ -40,4 +41,5 @@ export interface EchartWidgetProps {
40
41
  option: EchartUIProps['option']
41
42
  onEvents?: EchartUIProps['onEvents']
42
43
  formatter?: (value: number) => string
44
+ labelFormatter?: (value: string | number) => string | number
43
45
  }
@@ -3,27 +3,104 @@ import {
3
3
  mergeEchartWidgetConfig,
4
4
  type EchartOptionsProps,
5
5
  } from '../echart'
6
- import type {
7
- HistogramConfig,
8
- HistogramWidgetConfig,
9
- HistogramWidgetData,
10
- } from './types'
6
+ import type { HistogramConfig, HistogramWidgetConfig } from './types'
11
7
  import {
12
- flattenObjectArrayToCSV,
13
8
  buildLegendConfig,
14
9
  buildGridConfig,
15
10
  createTooltipPositioner,
16
11
  createTooltipFormatter,
17
- createChartDownloadConfig,
18
12
  niceNum,
19
13
  } from '../utils/chart-config'
14
+ import { downloadToCSV, downloadToPNG, type DownloadItem } from '../actions'
15
+ import type { ConfigProps } from '../loader/types'
16
+
17
+ export interface HistogramDownloadConfigProps extends ConfigProps {
18
+ ticks: number[]
19
+ labelFormatter?: (value: string | number) => string | number
20
+ }
21
+
22
+ export function histogramDataToCSV(
23
+ data: number[][],
24
+ ticks: number[],
25
+ labelFormatter?: (value: string | number) => string | number,
26
+ ): string[][] {
27
+ if (!data?.length || data[0]?.length === 0) return []
28
+
29
+ const dataLength = data[0]?.length ?? 0
30
+ const labels = createAxisLabels(dataLength, ticks, labelFormatter)
31
+ const seriesCount = data.length
32
+ const isMulti = seriesCount > 1
33
+
34
+ const headers = isMulti
35
+ ? [
36
+ 'Bin',
37
+ ...Array.from({ length: seriesCount }, (_, i) => `Series ${i + 1}`),
38
+ ]
39
+ : ['Bin', 'Value']
40
+
41
+ return [
42
+ headers,
43
+ ...labels.map((label, i) => [
44
+ label,
45
+ ...data.map((series) => String(series[i] ?? 0)),
46
+ ]),
47
+ ]
48
+ }
49
+
50
+ export function histogramDownloadConfig({
51
+ refUI,
52
+ ticks,
53
+ labelFormatter,
54
+ }: HistogramDownloadConfigProps): DownloadItem<number[][]>[] {
55
+ return [
56
+ {
57
+ ...downloadToPNG,
58
+ modifier: () => downloadToPNG.modifier(refUI),
59
+ },
60
+ {
61
+ ...downloadToCSV,
62
+ modifier: async (data) =>
63
+ downloadToCSV.modifier(histogramDataToCSV(data, ticks, labelFormatter)),
64
+ },
65
+ ]
66
+ }
20
67
 
21
- export const histogramDownloadConfig =
22
- createChartDownloadConfig<HistogramWidgetData>(flattenObjectArrayToCSV)
23
68
  /**
24
- * Generates ECharts configuration for distribution histogram widgets with adjacent bars (minimal gap) and axis formatting styled with the CARTO theme.
69
+ * Creates formatted axis labels from tick boundaries.
25
70
  *
26
- * @param props - Histogram configuration including bin data and theme.
71
+ * @param dataLength - Number of data points (determines number of labels).
72
+ * @param ticks - Bin boundaries. If `ticks.length === dataLength + 1`, all
73
+ * bins are ranges. If `ticks.length === dataLength`, the last bin is
74
+ * open-ended (`+`). A last tick of `Infinity` also produces `+`.
75
+ * @param labelFormatter - Optional formatter applied to each individual tick
76
+ * value when building the bin range label.
77
+ */
78
+ function createAxisLabels(
79
+ dataLength: number,
80
+ ticks: number[],
81
+ labelFormatter?: (value: string | number) => string | number,
82
+ ): string[] {
83
+ const fmt = (v: number) =>
84
+ labelFormatter ? String(labelFormatter(v)) : String(v)
85
+
86
+ return Array.from({ length: dataLength }, (_, i) => {
87
+ const low = ticks[i] ?? i
88
+ const high = ticks[i + 1]
89
+ return high !== undefined && isFinite(high)
90
+ ? `${fmt(low)}-${fmt(high)}`
91
+ : `${fmt(low)}+`
92
+ })
93
+ }
94
+
95
+ /**
96
+ * Generates ECharts configuration for distribution histogram widgets with
97
+ * adjacent bars (minimal gap) and axis formatting styled with the CARTO theme.
98
+ *
99
+ * Accepts raw `number[][]` data and `ticks` boundaries. The ticks and
100
+ * `labelFormatter` are used to create the x-axis category labels; the raw
101
+ * numeric data is embedded directly in each series.
102
+ *
103
+ * @param props - Histogram configuration including raw data, ticks, and theme.
27
104
  * @returns Widget config with ECharts option object.
28
105
  */
29
106
  export function histogramConfig(props: HistogramConfig): HistogramWidgetConfig {
@@ -33,21 +110,27 @@ export function histogramConfig(props: HistogramConfig): HistogramWidgetConfig {
33
110
  formatter: props.formatter,
34
111
  }
35
112
  }
113
+
36
114
  function getOption({
37
115
  data = [],
116
+ ticks,
38
117
  theme,
39
118
  formatter,
119
+ labelFormatter,
40
120
  }: HistogramConfig): EchartOptionsProps {
41
121
  const hasLegend = (data?.length ?? 0) > 1
122
+ const dataLength = data[0]?.length ?? 0
123
+ const axisLabels = createAxisLabels(dataLength, ticks, labelFormatter)
42
124
 
43
125
  let niceMin = 0
44
126
  let niceMax = 1
45
127
 
46
128
  return {
47
- legend: buildLegendConfig(hasLegend),
129
+ legend: buildLegendConfig({ hasLegend }),
48
130
  grid: buildGridConfig(hasLegend, theme),
49
131
  xAxis: {
50
132
  type: 'category',
133
+ data: axisLabels,
51
134
  axisLine: {
52
135
  show: false,
53
136
  },
@@ -117,14 +200,12 @@ function getOption({
117
200
  tooltip: {
118
201
  position: createTooltipPositioner(theme),
119
202
  formatter: createTooltipFormatter((item) => {
120
- const value = item.value as Record<string, string | number>
121
- const index = item.dimensionNames?.[item.encode?.y?.at(0) ?? 1]
122
- const _value = value[index ?? '']
203
+ const _value = item.value as number
123
204
 
124
205
  const formattedValue =
125
206
  typeof _value === 'number' && formatter
126
207
  ? formatter(_value)
127
- : (_value ?? '')
208
+ : String(_value ?? '')
128
209
 
129
210
  const marker = typeof item.marker === 'string' ? item.marker : ''
130
211
  const seriesName = item.seriesName ? `${item.seriesName}: ` : ''
@@ -133,11 +214,11 @@ function getOption({
133
214
  return { name, seriesName, marker, value: formattedValue }
134
215
  }),
135
216
  },
136
- series: data.map((_: unknown, index: number) => ({
137
- datasetIndex: index,
217
+ series: data.map((seriesData: number[]) => ({
138
218
  type: 'bar',
139
- barGap: '1%', // No gap between bars in histogram
140
- barCategoryGap: '1%', // No gap between categories in histogram
219
+ data: seriesData,
220
+ barGap: '1%',
221
+ barCategoryGap: '1%',
141
222
  emphasis: {
142
223
  focus: 'series',
143
224
  },
@@ -4,5 +4,10 @@ export type {
4
4
  HistogramWidgetData,
5
5
  HistogramWidgetState,
6
6
  } from './types'
7
- export { histogramConfig, histogramDownloadConfig } from './config'
7
+ export type { HistogramDownloadConfigProps } from './config'
8
+ export {
9
+ histogramConfig,
10
+ histogramDataToCSV,
11
+ histogramDownloadConfig,
12
+ } from './config'
8
13
  export { HistogramSkeleton } from './skeleton'