@carto/ps-react-ui 4.8.0 → 4.9.1
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.
- package/dist/{change-column-Cidl_M-4.js → change-column-B4IT0rh6.js} +2 -2
- package/dist/{change-column-Cidl_M-4.js.map → change-column-B4IT0rh6.js.map} +1 -1
- package/dist/components.js +4 -3
- package/dist/components.js.map +1 -1
- package/dist/{data-zoom-layout-BH0LPwSy.js → data-zoom-layout-0QSptXG_.js} +2 -2
- package/dist/{data-zoom-layout-BH0LPwSy.js.map → data-zoom-layout-0QSptXG_.js.map} +1 -1
- package/dist/{download-config-DNLkypdN.js → download-config-CzmjOT2T.js} +2 -2
- package/dist/{download-config-DNLkypdN.js.map → download-config-CzmjOT2T.js.map} +1 -1
- package/dist/{lasso-tool-BYbxrJ-7.js → lasso-tool-CDFj4zKY.js} +2 -1
- package/dist/lasso-tool-CDFj4zKY.js.map +1 -0
- package/dist/range-l4fNHLEg.js +213 -0
- package/dist/range-l4fNHLEg.js.map +1 -0
- package/dist/resolve-theme-color-BdojIw0K.js +47 -0
- package/dist/resolve-theme-color-BdojIw0K.js.map +1 -0
- package/dist/{spread-CTuIXZSM.js → spread-Y9R1f5dm.js} +2 -2
- package/dist/{spread-CTuIXZSM.js.map → spread-Y9R1f5dm.js.map} +1 -1
- package/dist/table-CQCAnDLb.js +388 -0
- package/dist/table-CQCAnDLb.js.map +1 -0
- package/dist/types/components/lasso-tool/styles.d.ts +1 -0
- package/dist/types/components/measurement-tools/styles.d.ts +1 -0
- package/dist/types/widgets/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets/actions/shared/styles.d.ts +1 -1
- package/dist/types/widgets/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets/echart/types.d.ts +1 -1
- package/dist/types/widgets/toolbar-actions/styles.d.ts +1 -1
- package/dist/types/widgets-v2/actions/brush-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/change-column/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/fullscreen/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/lock-selection/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/relative-data/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/searcher/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/stack-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/actions/zoom-toggle/style.d.ts +1 -1
- package/dist/types/widgets-v2/bar/types.d.ts +8 -3
- package/dist/types/widgets-v2/category/types.d.ts +8 -4
- package/dist/types/widgets-v2/formula/types.d.ts +10 -7
- package/dist/types/widgets-v2/histogram/types.d.ts +7 -3
- package/dist/types/widgets-v2/index.d.ts +1 -0
- package/dist/types/widgets-v2/pie/types.d.ts +10 -3
- package/dist/types/widgets-v2/range/range-ui.d.ts +12 -4
- package/dist/types/widgets-v2/range/range.d.ts +13 -8
- package/dist/types/widgets-v2/scatterplot/types.d.ts +7 -3
- package/dist/types/widgets-v2/table/style.d.ts +0 -4
- package/dist/types/widgets-v2/table/table-ui.d.ts +7 -1
- package/dist/types/widgets-v2/table/table.d.ts +1 -1
- package/dist/types/widgets-v2/table/types.d.ts +13 -2
- package/dist/types/widgets-v2/timeseries/types.d.ts +7 -3
- package/dist/types/widgets-v2/types.d.ts +25 -0
- package/dist/types/widgets-v2/utils/index.d.ts +1 -0
- package/dist/types/widgets-v2/utils/resolve-theme-color.d.ts +18 -0
- package/dist/types/widgets-v2/utils/resolve-theme-color.test.d.ts +1 -0
- package/dist/types/widgets-v2/wrapper/style.d.ts +1 -2
- package/dist/types/widgets-v2/wrapper/widget-wrapper.d.ts +6 -1
- package/dist/widgets/actions.js +1 -1
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets/wrapper.js +1 -1
- package/dist/widgets-v2/actions.js +1 -1
- package/dist/widgets-v2/bar.js +59 -56
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +1 -1
- package/dist/widgets-v2/formula.js +1 -1
- package/dist/widgets-v2/histogram.js +66 -63
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +1 -1
- package/dist/widgets-v2/pie.js +101 -95
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/range.js +1 -1
- package/dist/widgets-v2/scatterplot.js +108 -102
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +2 -2
- package/dist/widgets-v2/table.js +3 -3
- package/dist/widgets-v2/timeseries.js +86 -80
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2/utils.js +4 -3
- package/dist/widgets-v2.js +229 -229
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +5 -3
- package/src/components/lasso-tool/styles.ts +1 -0
- package/src/components/measurement-tools/styles.ts +1 -0
- package/src/widgets/echart/types.ts +1 -1
- package/src/widgets-v2/bar/options.test.ts +19 -2
- package/src/widgets-v2/bar/options.ts +9 -3
- package/src/widgets-v2/bar/types.ts +8 -3
- package/src/widgets-v2/category/types.ts +9 -4
- package/src/widgets-v2/formula/types.ts +11 -7
- package/src/widgets-v2/histogram/options.test.ts +16 -2
- package/src/widgets-v2/histogram/options.ts +5 -4
- package/src/widgets-v2/histogram/types.ts +7 -3
- package/src/widgets-v2/index.ts +3 -0
- package/src/widgets-v2/pie/options.test.ts +20 -4
- package/src/widgets-v2/pie/options.ts +21 -17
- package/src/widgets-v2/pie/types.ts +10 -3
- package/src/widgets-v2/range/range-ui.test.tsx +8 -2
- package/src/widgets-v2/range/range-ui.tsx +81 -14
- package/src/widgets-v2/range/range.tsx +14 -8
- package/src/widgets-v2/scatterplot/options.test.ts +15 -3
- package/src/widgets-v2/scatterplot/options.ts +15 -11
- package/src/widgets-v2/scatterplot/types.ts +7 -3
- package/src/widgets-v2/table/style.ts +2 -5
- package/src/widgets-v2/table/table-ui.tsx +40 -7
- package/src/widgets-v2/table/table.tsx +6 -1
- package/src/widgets-v2/table/types.ts +13 -2
- package/src/widgets-v2/timeseries/options.test.ts +17 -2
- package/src/widgets-v2/timeseries/options.ts +10 -3
- package/src/widgets-v2/timeseries/types.ts +7 -3
- package/src/widgets-v2/types.ts +25 -0
- package/src/widgets-v2/utils/index.ts +1 -0
- package/src/widgets-v2/utils/resolve-theme-color.test.ts +43 -0
- package/src/widgets-v2/utils/resolve-theme-color.ts +34 -0
- package/src/widgets-v2/wrapper/style.ts +1 -2
- package/src/widgets-v2/wrapper/widget-wrapper.test.tsx +30 -0
- package/src/widgets-v2/wrapper/widget-wrapper.tsx +11 -1
- package/dist/lasso-tool-BYbxrJ-7.js.map +0 -1
- package/dist/merge-options-DCkkHZIf.js +0 -34
- package/dist/merge-options-DCkkHZIf.js.map +0 -1
- package/dist/range-DsqTjSpg.js +0 -186
- package/dist/range-DsqTjSpg.js.map +0 -1
- package/dist/table-HIpXuq4G.js +0 -390
- package/dist/table-HIpXuq4G.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme } from '@mui/material'
|
|
2
2
|
import type { EChartsOption } from 'echarts'
|
|
3
|
+
import type { WidgetSeries } from '../types'
|
|
3
4
|
|
|
4
5
|
/** A single category entry in a bar series. */
|
|
5
6
|
export interface BarDatum {
|
|
@@ -21,14 +22,18 @@ export interface BarOptionsInput {
|
|
|
21
22
|
* Combined inputs for the option factory creator. Carries everything the
|
|
22
23
|
* widget needs across BOTH phases — the structural-build (`theme`,
|
|
23
24
|
* `formatter`, `labelFormatter`, `optionsOverride`) AND the data merge
|
|
24
|
-
* (`
|
|
25
|
+
* (`series`, `selection`).
|
|
25
26
|
*/
|
|
26
27
|
export interface BarOptionFactoryInput {
|
|
27
28
|
theme: Theme
|
|
28
29
|
formatter?: (value: number) => string
|
|
29
30
|
labelFormatter?: (value: string | number) => string | number
|
|
30
|
-
/**
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Per-series metadata — drives the legend, `series[i].name`, and
|
|
33
|
+
* (when `color` is set) per-series `itemStyle.color`. Paired with the
|
|
34
|
+
* data series by index.
|
|
35
|
+
*/
|
|
36
|
+
series?: readonly WidgetSeries[]
|
|
32
37
|
/**
|
|
33
38
|
* When set, every datum whose `name` is **not** in this list renders dimmed
|
|
34
39
|
* (`itemStyle.opacity: 0.15`, matching `outOfBrush` styling). `null`/empty
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WidgetSeries } from '../types'
|
|
2
|
+
|
|
1
3
|
/** A single row in a Category widget. `name` is the category key. */
|
|
2
4
|
export interface CategoryDataItem {
|
|
3
5
|
name: string | number
|
|
@@ -29,10 +31,13 @@ export type CategoryKey = string | number
|
|
|
29
31
|
* gracefully (extra entries ignored; missing entries get palette
|
|
30
32
|
* defaults and are absent from the legend).
|
|
31
33
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Per-series metadata. Type alias of the cross-widget
|
|
36
|
+
* {@link WidgetSeries} shape (`{ name, color? }`) so Category, Formula,
|
|
37
|
+
* Spread, and the echart-based widgets all consume the same input
|
|
38
|
+
* shape. Kept as a named export for backwards compatibility.
|
|
39
|
+
*/
|
|
40
|
+
export type CategorySeriesConfig = WidgetSeries
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* Labels for the "Other" overflow row (rendered when `data.length`
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
1
|
+
import type { WidgetSeries } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Series metadata rendered as a coloured avatar at the start of a row.
|
|
5
|
+
*
|
|
6
|
+
* Type alias of the cross-widget {@link WidgetSeries} shape so the same
|
|
7
|
+
* `{ name, color? }` object can drive Formula avatars, Spread avatars,
|
|
8
|
+
* Category legends, and echart-widget legends with no per-widget shape
|
|
9
|
+
* gymnastics. Kept as a named export for backwards compatibility.
|
|
10
|
+
*/
|
|
11
|
+
export type FormulaSeries = WidgetSeries
|
|
8
12
|
|
|
9
13
|
export type DeltaSeverity = 'positive' | 'negative' | 'neutral'
|
|
10
14
|
|
|
@@ -213,11 +213,11 @@ describe('createHistogramOptionFactory (data → dataset merger)', () => {
|
|
|
213
213
|
expect(out.yAxis.max).toBe(50)
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
it('uses
|
|
216
|
+
it('uses series[i].name for series[i].name when provided', () => {
|
|
217
217
|
const merge = createHistogramOptionFactory({
|
|
218
218
|
theme,
|
|
219
219
|
ticks: [0, 10, 20],
|
|
220
|
-
|
|
220
|
+
series: [{ name: '2024' }, { name: '2025' }],
|
|
221
221
|
})
|
|
222
222
|
const out = merge({}, [
|
|
223
223
|
[1, 2],
|
|
@@ -227,6 +227,20 @@ describe('createHistogramOptionFactory (data → dataset merger)', () => {
|
|
|
227
227
|
expect(out.series[1]?.name).toBe('2025')
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
+
it('applies series[i].color as a per-series itemStyle override', () => {
|
|
231
|
+
const merge = createHistogramOptionFactory({
|
|
232
|
+
theme,
|
|
233
|
+
ticks: [0, 10, 20],
|
|
234
|
+
series: [{ name: '2024', color: '#ff0000' }, { name: '2025' }],
|
|
235
|
+
})
|
|
236
|
+
const out = merge({}, [
|
|
237
|
+
[1, 2],
|
|
238
|
+
[3, 4],
|
|
239
|
+
]) as { series: { color?: string }[] }
|
|
240
|
+
expect(out.series[0]?.color).toBe('#ff0000')
|
|
241
|
+
expect(out.series[1]?.color).toBeUndefined()
|
|
242
|
+
})
|
|
243
|
+
|
|
230
244
|
it('applies labelFormatter to bin labels', () => {
|
|
231
245
|
const merge = createHistogramOptionFactory({
|
|
232
246
|
theme,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../../widgets/utils/chart-config'
|
|
11
11
|
import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
|
|
12
12
|
import type { OptionFactory } from '../echart'
|
|
13
|
-
import { mergeOptions } from '../utils'
|
|
13
|
+
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
14
14
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
15
15
|
import type {
|
|
16
16
|
HistogramEChartsOption,
|
|
@@ -152,8 +152,7 @@ export function histogramOptions({
|
|
|
152
152
|
export function createHistogramOptionFactory(
|
|
153
153
|
options: HistogramOptionFactoryInput,
|
|
154
154
|
): OptionFactory {
|
|
155
|
-
const { theme, formatter, ticks,
|
|
156
|
-
options
|
|
155
|
+
const { theme, formatter, ticks, series, labelFormatter, selection } = options
|
|
157
156
|
const optionsOverride = options.optionsOverride
|
|
158
157
|
const selectionSet =
|
|
159
158
|
selection && selection.length > 0 ? new Set<number>(selection) : null
|
|
@@ -253,15 +252,17 @@ export function createHistogramOptionFactory(
|
|
|
253
252
|
const template =
|
|
254
253
|
(seriesTemplates[i] as object | undefined) ??
|
|
255
254
|
(broadcastTemplate as object)
|
|
255
|
+
const overrideColor = resolveThemeColor(theme, series?.[i]?.color)
|
|
256
256
|
return {
|
|
257
257
|
...(typeof template === 'object' ? template : {}),
|
|
258
258
|
type: 'bar' as const,
|
|
259
259
|
datasetIndex: i,
|
|
260
|
-
name:
|
|
260
|
+
name: series?.[i]?.name ?? `Series ${i + 1}`,
|
|
261
261
|
encode: { x: 0, y: 1 },
|
|
262
262
|
barCategoryGap: '0%',
|
|
263
263
|
emphasis: { focus: 'series' },
|
|
264
264
|
itemStyle: dimItemStyle,
|
|
265
|
+
...(overrideColor ? { color: overrideColor } : {}),
|
|
265
266
|
}
|
|
266
267
|
}),
|
|
267
268
|
legend: { ...baseLegend, show: hasLegend },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme } from '@mui/material'
|
|
2
2
|
import type { EChartsOption } from 'echarts'
|
|
3
|
+
import type { WidgetSeries } from '../types'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Histogram widget data — array of series, each a flat array of bin counts.
|
|
@@ -19,15 +20,18 @@ export interface HistogramOptionsInput {
|
|
|
19
20
|
* Combined inputs for the histogram option factory creator. Carries
|
|
20
21
|
* everything the widget needs across BOTH phases — the structural-build
|
|
21
22
|
* (`theme`, `formatter`, `optionsOverride`) AND the data merge (`ticks`,
|
|
22
|
-
* `
|
|
23
|
+
* `series`, `labelFormatter`, `selection`).
|
|
23
24
|
*/
|
|
24
25
|
export interface HistogramOptionFactoryInput {
|
|
25
26
|
theme: Theme
|
|
26
27
|
formatter?: (value: number) => string
|
|
27
28
|
/** Bin boundaries — length = bins + 1. */
|
|
28
29
|
ticks: readonly number[]
|
|
29
|
-
/**
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Per-series metadata — drives the legend, `series[i].name`, and
|
|
32
|
+
* (when `color` is set) a per-series colour override on the bars.
|
|
33
|
+
*/
|
|
34
|
+
series?: readonly WidgetSeries[]
|
|
31
35
|
/**
|
|
32
36
|
* Bin-range label transform (e.g. `"0–10"` → `"[0–10)"`). Distinct from
|
|
33
37
|
* the standard `OptionFactoryContext.labelFormatter` (which operates on
|
package/src/widgets-v2/index.ts
CHANGED
|
@@ -147,8 +147,11 @@ describe('createPieOptionFactory — single-series (donut)', () => {
|
|
|
147
147
|
expect(single.legend?.show).toBe(true)
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
-
it('uses
|
|
151
|
-
const merge = createPieOptionFactory({
|
|
150
|
+
it('uses series[0].name for the single series name when provided', () => {
|
|
151
|
+
const merge = createPieOptionFactory({
|
|
152
|
+
theme,
|
|
153
|
+
series: [{ name: '2024' }],
|
|
154
|
+
})
|
|
152
155
|
const out = merge({}, [[{ name: 'A', value: 1 }]]) as {
|
|
153
156
|
series: { name: string }[]
|
|
154
157
|
}
|
|
@@ -507,16 +510,29 @@ describe('createPieOptionFactory — multi-series fallback (horizontal bar)', ()
|
|
|
507
510
|
).toBe('#BBB')
|
|
508
511
|
})
|
|
509
512
|
|
|
510
|
-
it('uses
|
|
513
|
+
it('uses series[i].name for series.name when provided', () => {
|
|
511
514
|
const merge = createPieOptionFactory({
|
|
512
515
|
theme,
|
|
513
|
-
|
|
516
|
+
series: [{ name: '2024' }, { name: '2025' }],
|
|
514
517
|
})
|
|
515
518
|
const out = merge({}, MULTI) as { series: { name: string }[] }
|
|
516
519
|
expect(out.series[0]?.name).toBe('2024')
|
|
517
520
|
expect(out.series[1]?.name).toBe('2025')
|
|
518
521
|
})
|
|
519
522
|
|
|
523
|
+
it('applies series[i].color as a per-series colour in the multi-series bar fusion', () => {
|
|
524
|
+
const merge = createPieOptionFactory({
|
|
525
|
+
theme,
|
|
526
|
+
series: [
|
|
527
|
+
{ name: '2024', color: '#ff0000' },
|
|
528
|
+
{ name: '2025' }, // no color
|
|
529
|
+
],
|
|
530
|
+
})
|
|
531
|
+
const out = merge({}, MULTI) as { series: { color?: string }[] }
|
|
532
|
+
expect(out.series[0]?.color).toBe('#ff0000')
|
|
533
|
+
expect(out.series[1]?.color).toBeUndefined()
|
|
534
|
+
})
|
|
535
|
+
|
|
520
536
|
it('keeps legend.show = true (the bar layout still surfaces series in the legend)', () => {
|
|
521
537
|
const merge = createPieOptionFactory({ theme })
|
|
522
538
|
const out = merge({}, MULTI) as { legend?: { show?: boolean } }
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
niceNum,
|
|
12
12
|
} from '../../widgets/utils/chart-config'
|
|
13
13
|
import type { OptionFactory, OptionFactoryContext } from '../echart'
|
|
14
|
-
import { mergeOptions } from '../utils'
|
|
14
|
+
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
15
|
+
import type { WidgetSeries } from '../types'
|
|
15
16
|
import type {
|
|
16
17
|
PieEChartsOption,
|
|
17
18
|
PieOptionFactoryInput,
|
|
@@ -125,8 +126,7 @@ export function pieOptions({
|
|
|
125
126
|
export function createPieOptionFactory(
|
|
126
127
|
options: PieOptionFactoryInput,
|
|
127
128
|
): OptionFactory {
|
|
128
|
-
const { theme, formatter, labelFormatter, optionsOverride,
|
|
129
|
-
options
|
|
129
|
+
const { theme, formatter, labelFormatter, optionsOverride, series } = options
|
|
130
130
|
const radius = options.radius ?? DEFAULT_RADIUS
|
|
131
131
|
const selection = options.selection
|
|
132
132
|
const selectionSet =
|
|
@@ -153,7 +153,7 @@ export function createPieOptionFactory(
|
|
|
153
153
|
option,
|
|
154
154
|
seriesArr,
|
|
155
155
|
theme,
|
|
156
|
-
|
|
156
|
+
series,
|
|
157
157
|
ctx,
|
|
158
158
|
selectionSet,
|
|
159
159
|
)
|
|
@@ -162,7 +162,7 @@ export function createPieOptionFactory(
|
|
|
162
162
|
option,
|
|
163
163
|
seriesArr,
|
|
164
164
|
radius,
|
|
165
|
-
|
|
165
|
+
series,
|
|
166
166
|
ctx,
|
|
167
167
|
selectionSet,
|
|
168
168
|
)
|
|
@@ -179,7 +179,7 @@ function buildSingleSeriesPieFusion(
|
|
|
179
179
|
option: EChartsOption,
|
|
180
180
|
seriesArr: PieWidgetData,
|
|
181
181
|
radius: readonly [string, string],
|
|
182
|
-
|
|
182
|
+
series: readonly WidgetSeries[] | undefined,
|
|
183
183
|
ctx: OptionFactoryContext | undefined,
|
|
184
184
|
selectionSet: Set<string | number> | null,
|
|
185
185
|
): EChartsOption {
|
|
@@ -270,7 +270,7 @@ function buildSingleSeriesPieFusion(
|
|
|
270
270
|
...templateObj,
|
|
271
271
|
type: 'pie' as const,
|
|
272
272
|
datasetIndex: i,
|
|
273
|
-
name:
|
|
273
|
+
name: series?.[i]?.name ?? `Series ${i + 1}`,
|
|
274
274
|
radius: [...radius],
|
|
275
275
|
// Lift the donut up so the bottom-anchored legend has the
|
|
276
276
|
// vertical real estate to wrap to multiple rows for long
|
|
@@ -312,7 +312,7 @@ function buildMultiSeriesBarFusion(
|
|
|
312
312
|
option: EChartsOption,
|
|
313
313
|
seriesArr: PieWidgetData,
|
|
314
314
|
theme: Theme,
|
|
315
|
-
|
|
315
|
+
series: readonly WidgetSeries[] | undefined,
|
|
316
316
|
ctx: OptionFactoryContext | undefined,
|
|
317
317
|
selectionSet: Set<string | number> | null,
|
|
318
318
|
): EChartsOption {
|
|
@@ -365,15 +365,19 @@ function buildMultiSeriesBarFusion(
|
|
|
365
365
|
// Drop pie-specific structural keys that shouldn't render in the
|
|
366
366
|
// bar fallback. ECharts ignores undefined keys, so this is a clean
|
|
367
367
|
// override.
|
|
368
|
-
series: seriesArr.map((_, i) =>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
368
|
+
series: seriesArr.map((_, i) => {
|
|
369
|
+
const overrideColor = resolveThemeColor(theme, series?.[i]?.color)
|
|
370
|
+
return {
|
|
371
|
+
datasetIndex: i,
|
|
372
|
+
type: 'bar' as const,
|
|
373
|
+
name: series?.[i]?.name ?? `Series ${i + 1}`,
|
|
374
|
+
barMaxWidth: 100,
|
|
375
|
+
emphasis: { focus: 'series' },
|
|
376
|
+
...buildSeriesLabelConfig(formatter, 'x'),
|
|
377
|
+
itemStyle: { color: barColorFn },
|
|
378
|
+
...(overrideColor ? { color: overrideColor } : {}),
|
|
379
|
+
}
|
|
380
|
+
}),
|
|
377
381
|
xAxis: {
|
|
378
382
|
type: 'value',
|
|
379
383
|
// Closures over the pre-computed nice bounds — ECharts calls them
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme } from '@mui/material'
|
|
2
2
|
import type { EChartsOption } from 'echarts'
|
|
3
|
+
import type { WidgetSeries } from '../types'
|
|
3
4
|
|
|
4
5
|
/** A single slice of a pie series. */
|
|
5
6
|
export interface PieDatum {
|
|
@@ -23,7 +24,7 @@ export interface PieOptionsInput {
|
|
|
23
24
|
* Combined inputs for the pie option factory creator. Carries everything
|
|
24
25
|
* the widget needs across BOTH phases — the structural-build (`theme`,
|
|
25
26
|
* `formatter`, `labelFormatter`, `optionsOverride`) AND the data merge
|
|
26
|
-
* (`
|
|
27
|
+
* (`series`, `radius`, `selection`). The merger emits different
|
|
27
28
|
* chart shapes by series count: single → donut, multi → horizontal-bar
|
|
28
29
|
* fallback (mirrors v1 pie); both branches read `theme` for styling.
|
|
29
30
|
*/
|
|
@@ -31,8 +32,14 @@ export interface PieOptionFactoryInput {
|
|
|
31
32
|
theme: Theme
|
|
32
33
|
formatter?: (value: number) => string
|
|
33
34
|
labelFormatter?: (value: string | number) => string | number
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Per-series metadata — drives the legend, `series[i].name`, and (in
|
|
37
|
+
* the multi-series bar fallback) per-series colour overrides.
|
|
38
|
+
* Single-series donuts always use the per-slice palette regardless of
|
|
39
|
+
* any `series[0].color`, since the donut palette is keyed by data
|
|
40
|
+
* index, not series index.
|
|
41
|
+
*/
|
|
42
|
+
series?: readonly WidgetSeries[]
|
|
36
43
|
/**
|
|
37
44
|
* Inner/outer radius (percent). Default `['58%', '74%']` produces a
|
|
38
45
|
* donut sized to leave room for the wrappable bottom legend. Set
|
|
@@ -106,20 +106,26 @@ describe('<RangeUI>', () => {
|
|
|
106
106
|
).toBe(true)
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
it('commits a typed value
|
|
109
|
+
it('commits a typed value on Enter (fires both onChange and onChangeCommitted), clamped to [min, max]', () => {
|
|
110
|
+
// A text-input edit is both a value change and a final commit, so it
|
|
111
|
+
// notifies `onChange` AND `onChangeCommitted` — consumers that only wired
|
|
112
|
+
// `onChange` (the pre-`onChangeCommitted` API) still update.
|
|
110
113
|
const onChange = vi.fn()
|
|
114
|
+
const onChangeCommitted = vi.fn()
|
|
111
115
|
render(
|
|
112
116
|
<RangeUI
|
|
113
117
|
items={[{ min: 0, max: 100, value: [10, 90] }]}
|
|
114
118
|
onChange={onChange}
|
|
119
|
+
onChangeCommitted={onChangeCommitted}
|
|
115
120
|
/>,
|
|
116
121
|
)
|
|
117
122
|
const min = screen.getByLabelText('Minimum value')
|
|
118
123
|
// Type 150 — should clamp to max (100), then preserve ordering (low <= high).
|
|
119
124
|
fireEvent.change(min, { target: { value: '150' } })
|
|
120
125
|
fireEvent.keyDown(min, { key: 'Enter' })
|
|
126
|
+
expect(onChangeCommitted).toHaveBeenCalled()
|
|
121
127
|
expect(onChange).toHaveBeenCalled()
|
|
122
|
-
const last =
|
|
128
|
+
const last = onChangeCommitted.mock.calls.at(-1) as [
|
|
123
129
|
number,
|
|
124
130
|
readonly [number, number],
|
|
125
131
|
]
|
|
@@ -11,11 +11,19 @@ import { styles } from './style'
|
|
|
11
11
|
export interface RangeUIProps {
|
|
12
12
|
items: readonly RangeDataItem[]
|
|
13
13
|
/**
|
|
14
|
-
* Fires
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Fires on every pointer tick while a thumb is being dragged (mirrors
|
|
15
|
+
* MUI `<Slider>`'s `onChange`). Use this to update local UI state —
|
|
16
|
+
* not to persist expensive operations like remote queries.
|
|
17
17
|
*/
|
|
18
18
|
onChange?: (index: number, value: RangeItemValue) => void
|
|
19
|
+
/**
|
|
20
|
+
* Fires once when the user *releases* a slider thumb after dragging,
|
|
21
|
+
* after an arrow-key adjustment commits, or when the text inputs
|
|
22
|
+
* blur / Enter. Mirrors MUI `<Slider>`'s `onChangeCommitted`. Use
|
|
23
|
+
* this for side-effects you want to throttle to "drag end" — e.g.
|
|
24
|
+
* writing the value to a source filter that triggers refetches.
|
|
25
|
+
*/
|
|
26
|
+
onChangeCommitted?: (index: number, value: RangeItemValue) => void
|
|
19
27
|
/** Number formatter for the slider tooltip and the text input display. */
|
|
20
28
|
formatter?: (value: number) => string
|
|
21
29
|
}
|
|
@@ -28,7 +36,12 @@ type Bound = 'min' | 'max'
|
|
|
28
36
|
* UX. Each item is always a two-thumb range; supply `value` to seed the
|
|
29
37
|
* starting selection, otherwise it defaults to `[min, max]`.
|
|
30
38
|
*/
|
|
31
|
-
export function RangeUI({
|
|
39
|
+
export function RangeUI({
|
|
40
|
+
items,
|
|
41
|
+
onChange,
|
|
42
|
+
onChangeCommitted,
|
|
43
|
+
formatter,
|
|
44
|
+
}: RangeUIProps) {
|
|
32
45
|
const fmt = formatter ?? ((n: number) => String(n))
|
|
33
46
|
return (
|
|
34
47
|
<Box sx={styles.root}>
|
|
@@ -42,6 +55,7 @@ export function RangeUI({ items, onChange, formatter }: RangeUIProps) {
|
|
|
42
55
|
item={item}
|
|
43
56
|
fmt={fmt}
|
|
44
57
|
onChange={onChange}
|
|
58
|
+
onChangeCommitted={onChangeCommitted}
|
|
45
59
|
/>
|
|
46
60
|
))}
|
|
47
61
|
</Box>
|
|
@@ -53,23 +67,57 @@ interface RangeRowProps {
|
|
|
53
67
|
item: RangeDataItem
|
|
54
68
|
fmt: (n: number) => string
|
|
55
69
|
onChange?: (index: number, value: RangeItemValue) => void
|
|
70
|
+
onChangeCommitted?: (index: number, value: RangeItemValue) => void
|
|
56
71
|
}
|
|
57
72
|
|
|
58
|
-
function RangeRow({
|
|
73
|
+
function RangeRow({
|
|
74
|
+
index,
|
|
75
|
+
item,
|
|
76
|
+
fmt,
|
|
77
|
+
onChange,
|
|
78
|
+
onChangeCommitted,
|
|
79
|
+
}: RangeRowProps) {
|
|
59
80
|
const current: readonly [number, number] = item.value ?? [item.min, item.max]
|
|
60
81
|
const [editing, setEditing] = useState<'' | Bound>('')
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
// Clamp inside [min, max] and keep `low <= high`. Pulled out so both the
|
|
84
|
+
// live `onChange` path and the commit path share the same normalization.
|
|
85
|
+
const normalize = useCallback(
|
|
86
|
+
(next: readonly [number, number]): readonly [number, number] => {
|
|
65
87
|
const [lowRaw, highRaw] = next
|
|
66
88
|
const low = Math.min(Math.max(lowRaw, item.min), item.max)
|
|
67
89
|
const high = Math.min(Math.max(highRaw, item.min), item.max)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
return low <= high ? [low, high] : [high, low]
|
|
91
|
+
},
|
|
92
|
+
[item.min, item.max],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const commit = useCallback(
|
|
96
|
+
(next: readonly [number, number]) => {
|
|
97
|
+
onChange?.(index, normalize(next))
|
|
98
|
+
},
|
|
99
|
+
[index, normalize, onChange],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const commitFinal = useCallback(
|
|
103
|
+
(next: readonly [number, number]) => {
|
|
104
|
+
onChangeCommitted?.(index, normalize(next))
|
|
71
105
|
},
|
|
72
|
-
[index,
|
|
106
|
+
[index, normalize, onChangeCommitted],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// A text-input commit (blur / Enter) is both a value change AND a final
|
|
110
|
+
// commit, so it fires `onChange` *and* `onChangeCommitted`. Firing both
|
|
111
|
+
// keeps the widget responsive for consumers that only wired `onChange`
|
|
112
|
+
// (the pre-`onChangeCommitted` API) — without it, typing a value and
|
|
113
|
+
// pressing Enter would silently no-op for them.
|
|
114
|
+
const commitText = useCallback(
|
|
115
|
+
(next: readonly [number, number]) => {
|
|
116
|
+
const value = normalize(next)
|
|
117
|
+
onChange?.(index, value)
|
|
118
|
+
onChangeCommitted?.(index, value)
|
|
119
|
+
},
|
|
120
|
+
[index, normalize, onChange, onChangeCommitted],
|
|
73
121
|
)
|
|
74
122
|
|
|
75
123
|
const handleSlider = (_: Event, raw: number | number[]) => {
|
|
@@ -81,6 +129,16 @@ function RangeRow({ index, item, fmt, onChange }: RangeRowProps) {
|
|
|
81
129
|
commit([low, high])
|
|
82
130
|
}
|
|
83
131
|
|
|
132
|
+
const handleSliderCommitted = (
|
|
133
|
+
_: Event | React.SyntheticEvent,
|
|
134
|
+
raw: number | number[],
|
|
135
|
+
) => {
|
|
136
|
+
if (!Array.isArray(raw)) return
|
|
137
|
+
const low = raw[0] ?? item.min
|
|
138
|
+
const high = raw[1] ?? item.max
|
|
139
|
+
commitFinal([low, high])
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
return (
|
|
85
143
|
<Box sx={styles.item}>
|
|
86
144
|
<Box sx={styles.sliderContainer}>
|
|
@@ -98,9 +156,13 @@ function RangeRow({ index, item, fmt, onChange }: RangeRowProps) {
|
|
|
98
156
|
valueLabelDisplay='auto'
|
|
99
157
|
valueLabelFormat={fmt}
|
|
100
158
|
onChange={handleSlider}
|
|
159
|
+
onChangeCommitted={handleSliderCommitted}
|
|
101
160
|
/>
|
|
102
161
|
</Box>
|
|
103
162
|
<Box sx={styles.inputsRow}>
|
|
163
|
+
{/* Text-input commits (blur / Enter) are final, but also notify
|
|
164
|
+
`onChange` — see `commitText` — so consumers that only wired
|
|
165
|
+
`onChange` still update on a typed value. */}
|
|
104
166
|
<BoundInput
|
|
105
167
|
// Bumping the key on a fresh external value resets the local
|
|
106
168
|
// editing state — matches v1's RangeItem behaviour.
|
|
@@ -111,7 +173,7 @@ function RangeRow({ index, item, fmt, onChange }: RangeRowProps) {
|
|
|
111
173
|
fmt={fmt}
|
|
112
174
|
editing={editing}
|
|
113
175
|
setEditing={setEditing}
|
|
114
|
-
commit={
|
|
176
|
+
commit={commitText}
|
|
115
177
|
current={current}
|
|
116
178
|
ariaLabel='Minimum value'
|
|
117
179
|
/>
|
|
@@ -123,7 +185,7 @@ function RangeRow({ index, item, fmt, onChange }: RangeRowProps) {
|
|
|
123
185
|
fmt={fmt}
|
|
124
186
|
editing={editing}
|
|
125
187
|
setEditing={setEditing}
|
|
126
|
-
commit={
|
|
188
|
+
commit={commitText}
|
|
127
189
|
current={current}
|
|
128
190
|
ariaLabel='Maximum value'
|
|
129
191
|
/>
|
|
@@ -144,6 +206,11 @@ interface BoundInputProps {
|
|
|
144
206
|
fmt: (n: number) => string
|
|
145
207
|
editing: '' | Bound
|
|
146
208
|
setEditing: (next: '' | Bound) => void
|
|
209
|
+
/**
|
|
210
|
+
* Called when the user commits a new value (blur / Enter). Text input
|
|
211
|
+
* edits never produce intermediate "live" values, so the consumer
|
|
212
|
+
* only needs one callback — the row wires this to `commitFinal`.
|
|
213
|
+
*/
|
|
147
214
|
commit: (next: readonly [number, number]) => void
|
|
148
215
|
current: readonly [number, number]
|
|
149
216
|
ariaLabel: string
|
|
@@ -17,23 +17,28 @@ const rangeSelector = (s: {
|
|
|
17
17
|
|
|
18
18
|
export interface RangeProps {
|
|
19
19
|
/**
|
|
20
|
-
* Fires
|
|
21
|
-
*
|
|
22
|
-
* `[low, high]` tuple. The consumer owns the data state — the widget
|
|
23
|
-
* surfaces changes but does not persist them.
|
|
20
|
+
* Fires on every pointer tick while a thumb is being dragged. Use this
|
|
21
|
+
* for cheap UI updates (local state, optimistic display).
|
|
24
22
|
*/
|
|
25
23
|
onChange?: (index: number, value: RangeItemValue) => void
|
|
24
|
+
/**
|
|
25
|
+
* Fires once when the user *releases* a slider thumb (or when the text
|
|
26
|
+
* inputs blur / Enter, or when an arrow-key adjustment settles). Use
|
|
27
|
+
* this for expensive side-effects you want throttled to drag end —
|
|
28
|
+
* e.g. writing the value to a source filter that refetches widgets.
|
|
29
|
+
*/
|
|
30
|
+
onChangeCommitted?: (index: number, value: RangeItemValue) => void
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Stateful Range bridge — reads `data` (post-pipeline) and `formatter` from
|
|
30
35
|
* the per-widget store and forwards them to the pure {@link RangeUI}. Per
|
|
31
36
|
* the destination-owned principle, value changes flow back through
|
|
32
|
-
* `onChange`
|
|
33
|
-
* `<Provider>`. Use `disabled` on
|
|
34
|
-
* specific rows.
|
|
37
|
+
* `onChange` (per-tick) and `onChangeCommitted` (drag end) — the consumer
|
|
38
|
+
* is expected to update the data prop on `<Provider>`. Use `disabled` on
|
|
39
|
+
* individual `RangeDataItem`s to disable specific rows.
|
|
35
40
|
*/
|
|
36
|
-
export function Range({ onChange }: RangeProps) {
|
|
41
|
+
export function Range({ onChange, onChangeCommitted }: RangeProps) {
|
|
37
42
|
const id = useWidgetId()
|
|
38
43
|
const slice = useWidgetShallow(id, rangeSelector)
|
|
39
44
|
return (
|
|
@@ -41,6 +46,7 @@ export function Range({ onChange }: RangeProps) {
|
|
|
41
46
|
items={slice.data}
|
|
42
47
|
formatter={slice.formatter}
|
|
43
48
|
onChange={onChange}
|
|
49
|
+
onChangeCommitted={onChangeCommitted}
|
|
44
50
|
/>
|
|
45
51
|
)
|
|
46
52
|
}
|
|
@@ -151,10 +151,10 @@ describe('createScatterplotOptionFactory', () => {
|
|
|
151
151
|
expect(multi.legend.show).toBe(true)
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it('uses
|
|
154
|
+
it('uses series[i].name for series.name when provided', () => {
|
|
155
155
|
const merge = createScatterplotOptionFactory({
|
|
156
156
|
theme,
|
|
157
|
-
|
|
157
|
+
series: [{ name: 'Group A' }, { name: 'Group B' }],
|
|
158
158
|
})
|
|
159
159
|
const out = merge({}, [[[1, 2]], [[3, 4]]]) as {
|
|
160
160
|
series: { name: string }[]
|
|
@@ -163,7 +163,19 @@ describe('createScatterplotOptionFactory', () => {
|
|
|
163
163
|
expect(out.series[1]?.name).toBe('Group B')
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
-
it('
|
|
166
|
+
it('applies series[i].color as a per-series colour override', () => {
|
|
167
|
+
const merge = createScatterplotOptionFactory({
|
|
168
|
+
theme,
|
|
169
|
+
series: [{ name: 'Group A', color: '#ff0000' }, { name: 'Group B' }],
|
|
170
|
+
})
|
|
171
|
+
const out = merge({}, [[[1, 2]], [[3, 4]]]) as {
|
|
172
|
+
series: { color?: string }[]
|
|
173
|
+
}
|
|
174
|
+
expect(out.series[0]?.color).toBe('#ff0000')
|
|
175
|
+
expect(out.series[1]?.color).toBeUndefined()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('falls back to `Series N` naming when series is missing', () => {
|
|
167
179
|
const merge = createScatterplotOptionFactory({ theme })
|
|
168
180
|
const out = merge({}, [[[1, 2]], [[3, 4]]]) as {
|
|
169
181
|
series: { name: string }[]
|