@carto/ps-react-ui 4.3.5 → 4.3.7

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 (125) hide show
  1. package/dist/components.js +123 -123
  2. package/dist/components.js.map +1 -1
  3. package/dist/error-CEkRPccv.js +39 -0
  4. package/dist/error-CEkRPccv.js.map +1 -0
  5. package/dist/{lasso-tool-wFqOD6wk.js → lasso-tool-jl4YK02H.js} +184 -159
  6. package/dist/lasso-tool-jl4YK02H.js.map +1 -0
  7. package/dist/no-data-hR3KcJ-_.js +60 -0
  8. package/dist/no-data-hR3KcJ-_.js.map +1 -0
  9. package/dist/{row-DrHwXNvF.js → row-BKmVAUN5.js} +2 -2
  10. package/dist/{row-DrHwXNvF.js.map → row-BKmVAUN5.js.map} +1 -1
  11. package/dist/{series-D3Pc-kYX.js → series-D1pynfeh.js} +3 -3
  12. package/dist/{series-D3Pc-kYX.js.map → series-D1pynfeh.js.map} +1 -1
  13. package/dist/{styles-CCZnY17y.js → styles-DrPyd0y5.js} +28 -22
  14. package/dist/styles-DrPyd0y5.js.map +1 -0
  15. package/dist/types/components/lasso-tool/types.d.ts +1 -1
  16. package/dist/types/widgets/_shared/chart-config/index.d.ts +1 -1
  17. package/dist/types/widgets/_shared/chart-config/option-builders.d.ts +7 -0
  18. package/dist/types/widgets/_shared/chart-config/option-builders.test.d.ts +1 -0
  19. package/dist/types/widgets/actions/index.d.ts +4 -4
  20. package/dist/types/widgets/actions/lock-selection/types.d.ts +0 -13
  21. package/dist/types/widgets/actions/relative-data/types.d.ts +0 -4
  22. package/dist/types/widgets/actions/searcher/types.d.ts +0 -2
  23. package/dist/types/widgets/actions/stack-toggle/stack-toggle.d.ts +3 -2
  24. package/dist/types/widgets/actions/stack-toggle/types.d.ts +0 -4
  25. package/dist/types/widgets/actions/zoom-toggle/zoom-toggle.d.ts +4 -0
  26. package/dist/types/widgets/echart/types.d.ts +0 -4
  27. package/dist/types/widgets/echart/utils.d.ts +2 -1
  28. package/dist/types/widgets/error/error.d.ts +1 -1
  29. package/dist/types/widgets/error/types.d.ts +8 -0
  30. package/dist/types/widgets/loader/loader.d.ts +1 -1
  31. package/dist/types/widgets/loader/types.d.ts +1 -1
  32. package/dist/types/widgets/stores/index.d.ts +1 -1
  33. package/dist/types/widgets/stores/types.d.ts +15 -0
  34. package/dist/{use-widget-ref-B0aNCANx.js → use-widget-ref-P-2i0MJG.js} +2 -2
  35. package/dist/{use-widget-ref-B0aNCANx.js.map → use-widget-ref-P-2i0MJG.js.map} +1 -1
  36. package/dist/{utils-D3-eQyDR.js → utils-idmvq0Oa.js} +17 -16
  37. package/dist/utils-idmvq0Oa.js.map +1 -0
  38. package/dist/widget-store-CzDt8oSK.js +163 -0
  39. package/dist/widget-store-CzDt8oSK.js.map +1 -0
  40. package/dist/widgets/actions.js +714 -659
  41. package/dist/widgets/actions.js.map +1 -1
  42. package/dist/widgets/bar.js +67 -63
  43. package/dist/widgets/bar.js.map +1 -1
  44. package/dist/widgets/category.js +250 -241
  45. package/dist/widgets/category.js.map +1 -1
  46. package/dist/widgets/echart.js +93 -100
  47. package/dist/widgets/echart.js.map +1 -1
  48. package/dist/widgets/error.js +1 -1
  49. package/dist/widgets/formula.js +64 -72
  50. package/dist/widgets/formula.js.map +1 -1
  51. package/dist/widgets/histogram.js +75 -73
  52. package/dist/widgets/histogram.js.map +1 -1
  53. package/dist/widgets/loader.js +41 -40
  54. package/dist/widgets/loader.js.map +1 -1
  55. package/dist/widgets/markdown.js +2 -2
  56. package/dist/widgets/no-data.js +1 -1
  57. package/dist/widgets/pie.js +4 -4
  58. package/dist/widgets/range.js +97 -105
  59. package/dist/widgets/range.js.map +1 -1
  60. package/dist/widgets/scatterplot.js +8 -8
  61. package/dist/widgets/skeleton-loader.js +1 -1
  62. package/dist/widgets/spread.js +84 -100
  63. package/dist/widgets/spread.js.map +1 -1
  64. package/dist/widgets/stores.js +1 -1
  65. package/dist/widgets/table.js +493 -485
  66. package/dist/widgets/table.js.map +1 -1
  67. package/dist/widgets/timeseries.js +4 -4
  68. package/dist/widgets/wrapper.js +156 -156
  69. package/dist/widgets/wrapper.js.map +1 -1
  70. package/dist/widgets.js +4 -4
  71. package/package.json +3 -3
  72. package/src/components/lasso-tool/lasso-tool-inline.tsx +19 -17
  73. package/src/components/lasso-tool/lasso-tool.tsx +27 -22
  74. package/src/components/lasso-tool/types.ts +4 -3
  75. package/src/widgets/_shared/chart-config/index.ts +1 -0
  76. package/src/widgets/_shared/chart-config/option-builders.test.ts +40 -0
  77. package/src/widgets/_shared/chart-config/option-builders.ts +12 -0
  78. package/src/widgets/actions/fullscreen/fullscreen.tsx +5 -8
  79. package/src/widgets/actions/index.ts +4 -7
  80. package/src/widgets/actions/lock-selection/lock-selection.test.tsx +28 -30
  81. package/src/widgets/actions/lock-selection/lock-selection.tsx +25 -26
  82. package/src/widgets/actions/lock-selection/types.ts +0 -17
  83. package/src/widgets/actions/relative-data/relative-data.test.tsx +13 -13
  84. package/src/widgets/actions/relative-data/relative-data.tsx +18 -21
  85. package/src/widgets/actions/relative-data/types.ts +0 -7
  86. package/src/widgets/actions/searcher/searcher.tsx +40 -22
  87. package/src/widgets/actions/searcher/types.ts +0 -2
  88. package/src/widgets/actions/stack-toggle/stack-toggle.test.tsx +160 -16
  89. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +79 -78
  90. package/src/widgets/actions/stack-toggle/types.ts +0 -8
  91. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +137 -87
  92. package/src/widgets/bar/config.ts +37 -28
  93. package/src/widgets/category/category-ui.tsx +25 -22
  94. package/src/widgets/echart/echart-ui.test.tsx +3 -18
  95. package/src/widgets/echart/echart-ui.tsx +4 -22
  96. package/src/widgets/echart/echart.test.tsx +9 -25
  97. package/src/widgets/echart/echart.tsx +36 -29
  98. package/src/widgets/echart/types.ts +0 -4
  99. package/src/widgets/echart/utils.ts +3 -1
  100. package/src/widgets/error/error.tsx +17 -14
  101. package/src/widgets/error/types.ts +10 -0
  102. package/src/widgets/formula/components/value.tsx +13 -13
  103. package/src/widgets/histogram/config.ts +36 -29
  104. package/src/widgets/loader/loader.tsx +20 -8
  105. package/src/widgets/loader/types.ts +3 -1
  106. package/src/widgets/no-data/no-data.tsx +8 -11
  107. package/src/widgets/range/components/range-item.tsx +9 -13
  108. package/src/widgets/spread/components/max-value.tsx +13 -13
  109. package/src/widgets/spread/components/min-value.tsx +13 -13
  110. package/src/widgets/stores/index.ts +1 -0
  111. package/src/widgets/stores/types.ts +17 -0
  112. package/src/widgets/stores/widget-store.test.ts +141 -0
  113. package/src/widgets/stores/widget-store.ts +73 -2
  114. package/src/widgets/table/hooks/use-pagination.ts +44 -35
  115. package/src/widgets/table/hooks/use-sort.ts +25 -23
  116. package/src/widgets/wrapper/wrapper-ui.tsx +16 -17
  117. package/dist/error-B2IJ9d2h.js +0 -38
  118. package/dist/error-B2IJ9d2h.js.map +0 -1
  119. package/dist/lasso-tool-wFqOD6wk.js.map +0 -1
  120. package/dist/no-data-C54XJt13.js +0 -61
  121. package/dist/no-data-C54XJt13.js.map +0 -1
  122. package/dist/styles-CCZnY17y.js.map +0 -1
  123. package/dist/utils-D3-eQyDR.js.map +0 -1
  124. package/dist/widget-store-CB6Trp_0.js +0 -131
  125. package/dist/widget-store-CB6Trp_0.js.map +0 -1
@@ -5,14 +5,12 @@ import type { Ref } from 'react'
5
5
  import { theme as CartoTheme } from '@carto/meridian-ds/theme'
6
6
 
7
7
  export type EchartOptionsProps = EChartsOption
8
- export type EchartReplaceMerge = string[]
9
8
 
10
9
  export interface EchartUIProps {
11
10
  id: string
12
11
  option: EchartOptionsProps
13
12
  className?: string
14
13
  init?: echarts.EChartsInitOpts
15
- replaceMerge?: EchartReplaceMerge
16
14
  style?: React.CSSProperties
17
15
  ref?: Ref<echarts.ECharts>
18
16
  onEvents?: Record<string, Parameters<echarts.ECharts['on']>[2]>
@@ -28,7 +26,6 @@ export type EchartWidgetState = BaseWidgetState<{
28
26
  option: EchartUIProps['option']
29
27
  onEvents?: EchartUIProps['onEvents']
30
28
  init?: EchartUIProps['init']
31
- replaceMerge?: EchartReplaceMerge
32
29
  }>
33
30
 
34
31
  export interface EchartWidgetOptionProps<D> {
@@ -41,5 +38,4 @@ export interface EchartWidgetProps {
41
38
  type: string
42
39
  option: EchartUIProps['option']
43
40
  onEvents?: EchartUIProps['onEvents']
44
- replaceMerge?: EchartReplaceMerge
45
41
  }
@@ -35,12 +35,14 @@ export function getEChartZoomConfig(
35
35
  ySlider = false,
36
36
  showSliders = true,
37
37
  xAxisLabelFormatter,
38
+ bottomOffset = 0,
38
39
  } = {} as {
39
40
  inside?: boolean
40
41
  xSlider?: boolean
41
42
  ySlider?: boolean
42
43
  showSliders?: boolean
43
44
  xAxisLabelFormatter?: (value: number) => string
45
+ bottomOffset?: number
44
46
  },
45
47
  theme?: Theme,
46
48
  ) {
@@ -72,7 +74,7 @@ export function getEChartZoomConfig(
72
74
  throttle: 0,
73
75
  type: 'slider',
74
76
  xAxisIndex: [0],
75
- bottom: 0,
77
+ bottom: bottomOffset,
76
78
  height: parseInt(theme?.spacing?.(4) ?? '32'),
77
79
  show: zoom && showSliders,
78
80
  zoomLock: !zoom,
@@ -3,28 +3,31 @@ import { useWidgetStore } from '../stores/widget-store'
3
3
  import { useShallow } from 'zustand/shallow'
4
4
  import type { WidgetErrorProps } from './types'
5
5
 
6
- export function WidgetError({ id, children }: WidgetErrorProps) {
7
- const widget = useWidgetStore(
8
- useShallow((state) => {
9
- const w = state.widgets[id]
10
- return {
11
- isLoading: w?.isLoading,
12
- isFetching: w?.isFetching,
13
- error: w?.error,
14
- }
15
- }),
6
+ export function WidgetError({
7
+ id,
8
+ children,
9
+ title: titleProp,
10
+ description,
11
+ }: WidgetErrorProps) {
12
+ const isLoading = useWidgetStore(
13
+ useShallow((state) => state.widgets[id]?.isLoading),
16
14
  )
15
+ const isFetching = useWidgetStore(
16
+ useShallow((state) => state.widgets[id]?.isFetching),
17
+ )
18
+ const error = useWidgetStore(useShallow((state) => state.widgets[id]?.error))
17
19
 
18
20
  // Don't show error during loading/fetching states
19
- if (widget?.isLoading || widget?.isFetching) {
21
+ if (isLoading || isFetching) {
20
22
  return children
21
23
  }
22
24
 
23
25
  // Show error UI if error exists
24
- if (widget?.error) {
25
- const errorTitle = widget.error.title ?? 'Error'
26
+ if (error) {
27
+ const errorTitle = titleProp ?? error.title ?? 'Error'
26
28
  const errorMessage =
27
- widget.error.message ??
29
+ description ??
30
+ error.message ??
28
31
  'An error occurred while loading the widget. Please try again.'
29
32
 
30
33
  return (
@@ -11,4 +11,14 @@ export interface WidgetErrorProps {
11
11
  * Children to render when no error exists
12
12
  */
13
13
  children: ReactNode
14
+
15
+ /**
16
+ * Override error title
17
+ */
18
+ title?: string
19
+
20
+ /**
21
+ * Override error description/message
22
+ */
23
+ description?: string
14
24
  }
@@ -6,20 +6,20 @@ import { useShallow } from 'zustand/shallow'
6
6
  const defaultFormatter = (value: number) => value.toString()
7
7
 
8
8
  export function Value({ id, index = 0, ...props }: ValueProps) {
9
- const {
10
- value,
11
- color,
12
- formatter = defaultFormatter,
13
- } = useWidgetStore(
14
- useShallow((state) => {
15
- const widget = state.getWidget<FormulaWidgetState>(id)
16
- return {
17
- value: widget?.data?.[index]?.value,
18
- color: widget?.data?.[index]?.color,
19
- formatter: widget?.formatter,
20
- }
21
- }),
9
+ const value = useWidgetStore(
10
+ useShallow(
11
+ (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.value,
12
+ ),
22
13
  )
14
+ const color = useWidgetStore(
15
+ useShallow(
16
+ (state) => state.getWidget<FormulaWidgetState>(id)?.data?.[index]?.color,
17
+ ),
18
+ )
19
+ const formatter =
20
+ useWidgetStore(
21
+ useShallow((state) => state.getWidget<FormulaWidgetState>(id)?.formatter),
22
+ ) ?? defaultFormatter
23
23
 
24
24
  return (
25
25
  <Item TypographyProps={{ color }} {...props}>
@@ -14,7 +14,7 @@ import {
14
14
  buildGridConfig,
15
15
  createTooltipPositioner,
16
16
  createTooltipFormatter,
17
- applyYAxisFormatter,
17
+ niceNum,
18
18
  } from '../_shared/chart-config'
19
19
  import { downloadToCSV, downloadToPNG, type DownloadItem } from '../actions'
20
20
  import type { ConfigProps } from '../loader/types'
@@ -49,33 +49,7 @@ function getOption({
49
49
  }: HistogramConfig): EchartOptionsProps {
50
50
  const hasLegend = (data?.length ?? 0) > 1
51
51
 
52
- const yAxis = {
53
- type: 'value' as const,
54
- showMaxLabel: true,
55
- showMinLabel: true,
56
- splitNumber: 1,
57
- axisLabel: {
58
- fontSize: theme.typography.overlineDelicate.fontSize,
59
- fontFamily: theme.typography.overlineDelicate.fontFamily,
60
- margin: parseInt(theme.spacing(1)),
61
- show: true,
62
- showMaxLabel: true,
63
- showMinLabel: true,
64
- verticalAlign: 'bottom' as const,
65
- },
66
- axisLine: {
67
- show: false,
68
- },
69
- axisTick: {
70
- show: false,
71
- },
72
- splitLine: {
73
- show: true,
74
- lineStyle: {
75
- color: theme.palette.black[4],
76
- },
77
- },
78
- }
52
+ let niceMax = 1
79
53
 
80
54
  return {
81
55
  legend: buildLegendConfig(hasLegend),
@@ -110,7 +84,40 @@ function getOption({
110
84
  },
111
85
  },
112
86
  },
113
- yAxis: applyYAxisFormatter(yAxis, formatter),
87
+ yAxis: {
88
+ type: 'value' as const,
89
+ min: 0,
90
+ max: (extent: { min: number; max: number }) => {
91
+ niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
92
+ return niceMax
93
+ },
94
+ splitNumber: 1,
95
+ axisLabel: {
96
+ fontSize: theme.typography.overlineDelicate.fontSize,
97
+ fontFamily: theme.typography.overlineDelicate.fontFamily,
98
+ margin: parseInt(theme.spacing(1)),
99
+ show: true,
100
+ showMaxLabel: true,
101
+ showMinLabel: true,
102
+ verticalAlign: 'bottom' as const,
103
+ formatter: (value: number) => {
104
+ if (value !== niceMax) return ''
105
+ return formatter ? formatter(value) : String(value)
106
+ },
107
+ },
108
+ axisLine: {
109
+ show: false,
110
+ },
111
+ axisTick: {
112
+ show: false,
113
+ },
114
+ splitLine: {
115
+ show: true,
116
+ lineStyle: {
117
+ color: theme.palette.black[4],
118
+ },
119
+ },
120
+ },
114
121
  tooltip: {
115
122
  position: createTooltipPositioner(theme),
116
123
  formatter: createTooltipFormatter((item) => {
@@ -3,11 +3,16 @@ import type { WidgetLoaderProps } from './types'
3
3
  import { useWidgetStore } from '../stores/widget-store'
4
4
  import type { WrapperState } from '../wrapper'
5
5
 
6
- export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
6
+ export function WidgetLoader<T extends object = Record<string, unknown>>(
7
+ props: WidgetLoaderProps<T>,
8
+ ) {
7
9
  const setWidget = useWidgetStore((state) => state.setWidget)
8
10
  const executeToolPipeline = useWidgetStore(
9
11
  (state) => state.executeToolPipeline,
10
12
  )
13
+ const executeConfigPipeline = useWidgetStore(
14
+ (state) => state.executeConfigPipeline,
15
+ )
11
16
 
12
17
  // Split into 3 effects for metadata and 1 for data pipeline:
13
18
  // Each property that can be modified independently gets its own effect to avoid
@@ -35,21 +40,19 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
35
40
  })
36
41
  }, [props.id, props.isLoading, props.isFetching, props.error, setWidget])
37
42
 
38
- // Effect 3: Config updates
43
+ // Effect 3: Config updates — run through config pipeline
39
44
  useEffect(() => {
40
45
  if (props.config) {
41
- setWidget<WrapperState>(props.id, {
42
- ...props.config,
43
- })
46
+ void executeConfigPipeline(props.id, props.config)
44
47
  }
45
- }, [props.id, props.config, setWidget])
48
+ }, [props.id, props.config, executeConfigPipeline])
46
49
 
47
50
  // Effect 4: Execute tool pipeline when props.data changes
48
51
  useEffect(() => {
49
52
  void executeToolPipeline(props.id, props.data)
50
53
  }, [props.id, props.data, executeToolPipeline])
51
54
 
52
- // Effect 5: Re-execute pipeline when tool state changes (enabled/config)
55
+ // Effect 5: Re-execute pipelines when tool state changes (enabled/config)
53
56
  useEffect(() => {
54
57
  let prevTools = useWidgetStore.getState().widgets[props.id]?.registeredTools
55
58
 
@@ -60,11 +63,20 @@ export function WidgetLoader<T>(props: WidgetLoaderProps<T>) {
60
63
  if (currentTools !== prevTools) {
61
64
  prevTools = currentTools
62
65
  void executeToolPipeline(props.id, props.data)
66
+ if (props.config) {
67
+ void executeConfigPipeline(props.id, props.config)
68
+ }
63
69
  }
64
70
  })
65
71
 
66
72
  return unsubscribe
67
- }, [props.id, props.data, executeToolPipeline])
73
+ }, [
74
+ props.id,
75
+ props.data,
76
+ props.config,
77
+ executeToolPipeline,
78
+ executeConfigPipeline,
79
+ ])
68
80
 
69
81
  return props.children
70
82
  }
@@ -1,7 +1,9 @@
1
1
  import type { ReactNode } from 'react'
2
2
  import type { WidgetsStoreProps, WidgetState } from '../stores/types'
3
3
 
4
- export interface WidgetLoaderProps<T> extends WidgetsStoreProps {
4
+ export interface WidgetLoaderProps<
5
+ T extends object = Record<string, unknown>,
6
+ > extends WidgetsStoreProps {
5
7
  children: ReactNode
6
8
  config?: T
7
9
  }
@@ -45,25 +45,22 @@ export function WidgetNoData({
45
45
  isEmpty = defaultIsEmpty,
46
46
  }: WidgetNoDataProps) {
47
47
  // Subscribe to widget store with selective subscription for optimal performance
48
- const widget = useWidgetStore(
49
- useShallow((state) => {
50
- const w = state.widgets[id]
51
- return {
52
- isLoading: w?.isLoading,
53
- isFetching: w?.isFetching,
54
- data: w?.data,
55
- }
56
- }),
48
+ const isLoading = useWidgetStore(
49
+ useShallow((state) => state.widgets[id]?.isLoading),
57
50
  )
51
+ const isFetching = useWidgetStore(
52
+ useShallow((state) => state.widgets[id]?.isFetching),
53
+ )
54
+ const data = useWidgetStore(useShallow((state) => state.widgets[id]?.data))
58
55
 
59
56
  // If loading or fetching, show children
60
57
  // SkeletonLoader handles loading state, this allows proper composition
61
- if (widget?.isLoading || widget?.isFetching) {
58
+ if (isLoading || isFetching) {
62
59
  return children
63
60
  }
64
61
 
65
62
  // Check if data is empty
66
- if (isEmpty(widget?.data)) {
63
+ if (isEmpty(data)) {
67
64
  return (
68
65
  <Box sx={styles.root}>
69
66
  <Typography variant='body2' color='text.primary'>
@@ -10,20 +10,16 @@ type EditingState = '' | 'min' | 'max'
10
10
  const defaultFormatter = (value: number) => value.toString()
11
11
 
12
12
  export function RangeItem({ id, index }: RangeItemProps) {
13
- const {
14
- item,
15
- onChange,
16
- formatter = defaultFormatter,
17
- } = useWidgetStore(
18
- useShallow((state) => {
19
- const widget = state.getWidget<RangeWidgetState>(id)
20
- return {
21
- item: widget?.data[index],
22
- onChange: widget?.onChange,
23
- formatter: widget?.formatter,
24
- }
25
- }),
13
+ const item = useWidgetStore(
14
+ useShallow((state) => state.getWidget<RangeWidgetState>(id)?.data[index]),
15
+ )
16
+ const onChange = useWidgetStore(
17
+ useShallow((state) => state.getWidget<RangeWidgetState>(id)?.onChange),
26
18
  )
19
+ const formatter =
20
+ useWidgetStore(
21
+ useShallow((state) => state.getWidget<RangeWidgetState>(id)?.formatter),
22
+ ) ?? defaultFormatter
27
23
  const getWidget = useWidgetStore((store) => store.getWidget)
28
24
  const setWidget = useWidgetStore((store) => store.setWidget)
29
25
 
@@ -6,20 +6,20 @@ import { useShallow } from 'zustand/shallow'
6
6
  const defaultFormatter = (value: number) => value.toString()
7
7
 
8
8
  export function MaxValue({ id, index = 0, ...props }: ValueProps) {
9
- const {
10
- max,
11
- color,
12
- formatter = defaultFormatter,
13
- } = useWidgetStore(
14
- useShallow((state) => {
15
- const widget = state.getWidget<SpreadWidgetState>(id)
16
- return {
17
- max: widget?.data[index]?.max,
18
- color: widget?.data[index]?.color,
19
- formatter: widget?.formatter,
20
- }
21
- }),
9
+ const max = useWidgetStore(
10
+ useShallow(
11
+ (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.max,
12
+ ),
22
13
  )
14
+ const color = useWidgetStore(
15
+ useShallow(
16
+ (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.color,
17
+ ),
18
+ )
19
+ const formatter =
20
+ useWidgetStore(
21
+ useShallow((state) => state.getWidget<SpreadWidgetState>(id)?.formatter),
22
+ ) ?? defaultFormatter
23
23
 
24
24
  return (
25
25
  <Item TypographyProps={{ color }} {...props}>
@@ -6,20 +6,20 @@ import { useShallow } from 'zustand/shallow'
6
6
  const defaultFormatter = (value: number) => value.toString()
7
7
 
8
8
  export function MinValue({ id, index = 0, ...props }: ValueProps) {
9
- const {
10
- min,
11
- color,
12
- formatter = defaultFormatter,
13
- } = useWidgetStore(
14
- useShallow((state) => {
15
- const widget = state.getWidget<SpreadWidgetState>(id)
16
- return {
17
- min: widget?.data[index]?.min,
18
- color: widget?.data[index]?.color,
19
- formatter: widget?.formatter,
20
- }
21
- }),
9
+ const min = useWidgetStore(
10
+ useShallow(
11
+ (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.min,
12
+ ),
22
13
  )
14
+ const color = useWidgetStore(
15
+ useShallow(
16
+ (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.color,
17
+ ),
18
+ )
19
+ const formatter =
20
+ useWidgetStore(
21
+ useShallow((state) => state.getWidget<SpreadWidgetState>(id)?.formatter),
22
+ ) ?? defaultFormatter
23
23
 
24
24
  return (
25
25
  <Item TypographyProps={{ color }} {...props}>
@@ -1,6 +1,7 @@
1
1
  export { useWidgetStore } from './widget-store'
2
2
  export type {
3
3
  BaseWidgetState,
4
+ ToolType,
4
5
  WidgetsStoreProps,
5
6
  WidgetState,
6
7
  WidgetStore,
@@ -1,5 +1,12 @@
1
1
  import type { RefObject } from 'react'
2
2
 
3
+ /**
4
+ * Tool type determines which pipeline a tool participates in.
5
+ * - 'data': transforms widget data (default)
6
+ * - 'config': transforms widget config/option
7
+ */
8
+ export type ToolType = 'data' | 'config'
9
+
3
10
  export interface WidgetsStoreProps {
4
11
  /** Unique identifier for the widget */
5
12
  id: string
@@ -77,6 +84,8 @@ export interface ToolRegistration {
77
84
  fn: ToolTransformFunction
78
85
  /** Whether tool is currently enabled */
79
86
  enabled: boolean
87
+ /** 'data' (default) transforms data, 'config' transforms widget config/option */
88
+ type?: ToolType
80
89
  /** Tool-specific configuration */
81
90
  config?: Record<string, unknown>
82
91
  /**
@@ -184,6 +193,14 @@ export interface WidgetStoreActions {
184
193
  * @param sourceData - Original data to transform
185
194
  */
186
195
  executeToolPipeline: (widgetId: string, sourceData: unknown) => Promise<void>
196
+
197
+ /**
198
+ * Execute the config transformation pipeline
199
+ * Applies config-type tools to the base config, then sets the result on the widget
200
+ * @param widgetId - Widget ID
201
+ * @param baseConfig - Base config to transform
202
+ */
203
+ executeConfigPipeline: (widgetId: string, baseConfig: object) => Promise<void>
187
204
  }
188
205
 
189
206
  /**
@@ -465,6 +465,147 @@ describe('WidgetStore', () => {
465
465
  })
466
466
  })
467
467
 
468
+ describe('Config Tool Pipeline', () => {
469
+ const widgetId = 'test-widget-config'
470
+
471
+ beforeEach(() => {
472
+ useWidgetStore.getState().clearWidgets()
473
+ })
474
+
475
+ it('executes config tools and sets transformed config', async () => {
476
+ useWidgetStore.getState().setWidget(widgetId, {
477
+ type: 'bar',
478
+ isLoading: false,
479
+ })
480
+
481
+ const configTool: import('./types').ToolRegistration = {
482
+ id: 'stack-tool',
483
+ type: 'config',
484
+ order: 10,
485
+ enabled: true,
486
+ fn: (config) => {
487
+ const c = config as Record<string, unknown>
488
+ const option = c.option as { series?: { name: string }[] }
489
+ const series = option?.series ?? []
490
+ return {
491
+ ...c,
492
+ option: {
493
+ ...option,
494
+ series: series.map((s) => ({ ...s, stack: 'group' })),
495
+ },
496
+ }
497
+ },
498
+ }
499
+
500
+ useWidgetStore.getState().registerTool(widgetId, configTool)
501
+
502
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
503
+ option: {
504
+ series: [{ name: 'Series 1' }, { name: 'Series 2' }],
505
+ },
506
+ })
507
+
508
+ const widget = useWidgetStore.getState().getWidget(widgetId)
509
+ const option = (widget as { option?: { series?: { stack?: string }[] } })
510
+ ?.option
511
+ expect(option?.series?.[0]?.stack).toBe('group')
512
+ expect(option?.series?.[1]?.stack).toBe('group')
513
+ })
514
+
515
+ it('passes base config through when no config tools registered', async () => {
516
+ useWidgetStore.getState().setWidget(widgetId, {
517
+ type: 'bar',
518
+ isLoading: false,
519
+ })
520
+
521
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
522
+ option: { series: [{ name: 'Series 1' }] },
523
+ })
524
+
525
+ const widget = useWidgetStore.getState().getWidget(widgetId)
526
+ const option = (widget as { option?: { series?: { name: string }[] } })
527
+ ?.option
528
+ expect(option?.series?.[0]?.name).toBe('Series 1')
529
+ })
530
+
531
+ it('does not include config tools in data pipeline', async () => {
532
+ const executionOrder: string[] = []
533
+
534
+ useWidgetStore.getState().setWidget(widgetId, {
535
+ type: 'bar',
536
+ isLoading: false,
537
+ })
538
+
539
+ const dataTool: import('./types').ToolRegistration = {
540
+ id: 'data-tool',
541
+ type: 'data',
542
+ order: 10,
543
+ enabled: true,
544
+ fn: (data) => {
545
+ executionOrder.push('data-tool')
546
+ return data
547
+ },
548
+ }
549
+
550
+ const configTool: import('./types').ToolRegistration = {
551
+ id: 'config-tool',
552
+ type: 'config',
553
+ order: 10,
554
+ enabled: true,
555
+ fn: (config) => {
556
+ executionOrder.push('config-tool')
557
+ return config
558
+ },
559
+ }
560
+
561
+ useWidgetStore.getState().registerTool(widgetId, dataTool)
562
+ useWidgetStore.getState().registerTool(widgetId, configTool)
563
+
564
+ await useWidgetStore.getState().executeToolPipeline(widgetId, {})
565
+
566
+ expect(executionOrder).toEqual(['data-tool'])
567
+ expect(executionOrder).not.toContain('config-tool')
568
+ })
569
+
570
+ it('respects disables across tool types', async () => {
571
+ const executionOrder: string[] = []
572
+
573
+ useWidgetStore.getState().setWidget(widgetId, {
574
+ type: 'bar',
575
+ isLoading: false,
576
+ })
577
+
578
+ const configTool: import('./types').ToolRegistration = {
579
+ id: 'config-tool',
580
+ type: 'config',
581
+ order: 10,
582
+ enabled: true,
583
+ fn: (config) => {
584
+ executionOrder.push('config-tool')
585
+ return config
586
+ },
587
+ }
588
+
589
+ const disablerTool: import('./types').ToolRegistration = {
590
+ id: 'disabler',
591
+ type: 'data',
592
+ order: 10,
593
+ enabled: true,
594
+ fn: (data) => data,
595
+ disables: ['config-tool'],
596
+ }
597
+
598
+ useWidgetStore.getState().registerTool(widgetId, configTool)
599
+ useWidgetStore.getState().registerTool(widgetId, disablerTool)
600
+
601
+ await useWidgetStore.getState().executeConfigPipeline(widgetId, {
602
+ option: {},
603
+ })
604
+
605
+ expect(executionOrder).not.toContain('config-tool')
606
+ })
607
+ })
608
+
468
609
  describe('Tool Dependency Management', () => {
469
610
  const widgetId = 'test-widget-deps'
470
611