@carto/ps-react-ui 5.4.0 → 5.6.0
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/actions-5-dVhroJ.js +1289 -0
- package/dist/actions-5-dVhroJ.js.map +1 -0
- package/dist/{category-C6myUFUJ.js → category-DBUD2ZBo.js} +267 -250
- package/dist/category-DBUD2ZBo.js.map +1 -0
- package/dist/item-color-ZYxH3NM5.js +20 -0
- package/dist/item-color-ZYxH3NM5.js.map +1 -0
- package/dist/scale-0elCv3tk.js +29 -0
- package/dist/scale-0elCv3tk.js.map +1 -0
- package/dist/types/widgets-v2/actions/index.d.ts +1 -1
- package/dist/types/widgets-v2/actions/relative-data/index.d.ts +1 -0
- package/dist/types/widgets-v2/actions/relative-data/labels.d.ts +3 -0
- package/dist/types/widgets-v2/actions/relative-data/relative-data.d.ts +16 -2
- package/dist/types/widgets-v2/actions/relative-data/scale.d.ts +26 -0
- package/dist/types/widgets-v2/actions/relative-data/style.d.ts +12 -0
- package/dist/types/widgets-v2/bar/types.d.ts +6 -0
- package/dist/types/widgets-v2/category/category.d.ts +2 -1
- package/dist/types/widgets-v2/histogram/bins.d.ts +7 -0
- package/dist/types/widgets-v2/histogram/download.d.ts +2 -2
- package/dist/types/widgets-v2/histogram/index.d.ts +2 -1
- package/dist/types/widgets-v2/histogram/options.d.ts +1 -1
- package/dist/types/widgets-v2/histogram/transforms.d.ts +5 -4
- package/dist/types/widgets-v2/histogram/types.d.ts +22 -5
- package/dist/types/widgets-v2/index.d.ts +2 -2
- package/dist/types/widgets-v2/pie/types.d.ts +6 -0
- package/dist/types/widgets-v2/scatterplot/download.d.ts +2 -2
- package/dist/types/widgets-v2/scatterplot/index.d.ts +2 -1
- package/dist/types/widgets-v2/scatterplot/points.d.ts +7 -0
- package/dist/types/widgets-v2/scatterplot/transforms.d.ts +4 -3
- package/dist/types/widgets-v2/scatterplot/types.d.ts +21 -2
- package/dist/types/widgets-v2/timeseries/types.d.ts +6 -0
- package/dist/types/widgets-v2/utils/item-color.d.ts +26 -0
- package/dist/widgets-v2/actions.js +1 -1
- package/dist/widgets-v2/bar.js +91 -81
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +1 -1
- package/dist/widgets-v2/histogram.js +133 -100
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/pie.js +98 -78
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +151 -110
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +96 -86
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +2 -2
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +1 -1
- package/src/widgets-v2/actions/fullscreen/fullscreen-store.test.tsx +77 -13
- package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +48 -5
- package/src/widgets-v2/actions/index.ts +1 -0
- package/src/widgets-v2/actions/relative-data/index.ts +1 -0
- package/src/widgets-v2/actions/relative-data/labels.ts +6 -0
- package/src/widgets-v2/actions/relative-data/relative-data.test.tsx +78 -1
- package/src/widgets-v2/actions/relative-data/relative-data.tsx +137 -20
- package/src/widgets-v2/actions/relative-data/scale.ts +48 -0
- package/src/widgets-v2/actions/relative-data/style.ts +10 -0
- package/src/widgets-v2/bar/options.test.ts +122 -0
- package/src/widgets-v2/bar/options.ts +31 -16
- package/src/widgets-v2/bar/types.ts +6 -0
- package/src/widgets-v2/category/category.relative-data.test.tsx +51 -63
- package/src/widgets-v2/category/category.tsx +25 -11
- package/src/widgets-v2/histogram/bins.ts +22 -0
- package/src/widgets-v2/histogram/download.test.ts +10 -0
- package/src/widgets-v2/histogram/download.ts +7 -3
- package/src/widgets-v2/histogram/index.ts +3 -0
- package/src/widgets-v2/histogram/options.test.ts +62 -0
- package/src/widgets-v2/histogram/options.ts +44 -19
- package/src/widgets-v2/histogram/transforms.test.ts +21 -7
- package/src/widgets-v2/histogram/transforms.ts +26 -12
- package/src/widgets-v2/histogram/types.ts +25 -5
- package/src/widgets-v2/index.ts +7 -1
- package/src/widgets-v2/pie/options.test.ts +91 -0
- package/src/widgets-v2/pie/options.ts +50 -20
- package/src/widgets-v2/pie/types.ts +6 -0
- package/src/widgets-v2/scatterplot/download.test.ts +9 -0
- package/src/widgets-v2/scatterplot/download.ts +5 -3
- package/src/widgets-v2/scatterplot/index.ts +3 -0
- package/src/widgets-v2/scatterplot/options.test.ts +90 -0
- package/src/widgets-v2/scatterplot/options.ts +53 -15
- package/src/widgets-v2/scatterplot/points.ts +35 -0
- package/src/widgets-v2/scatterplot/transforms.test.ts +19 -0
- package/src/widgets-v2/scatterplot/transforms.ts +23 -18
- package/src/widgets-v2/scatterplot/types.ts +25 -2
- package/src/widgets-v2/timeseries/options.test.ts +65 -0
- package/src/widgets-v2/timeseries/options.ts +37 -16
- package/src/widgets-v2/timeseries/types.ts +6 -0
- package/src/widgets-v2/utils/item-color.test.ts +74 -0
- package/src/widgets-v2/utils/item-color.ts +55 -0
- package/dist/actions-Dhlz_adK.js +0 -1167
- package/dist/actions-Dhlz_adK.js.map +0 -1
- package/dist/category-C6myUFUJ.js.map +0 -1
|
@@ -12,13 +12,17 @@ import {
|
|
|
12
12
|
import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
|
|
13
13
|
import type { OptionFactory } from '../echart'
|
|
14
14
|
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
15
|
+
import { createItemColorResolver } from '../utils/item-color'
|
|
16
|
+
import { getHistogramBinValue } from './bins'
|
|
15
17
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
16
18
|
import type {
|
|
17
19
|
HistogramEChartsOption,
|
|
18
20
|
HistogramOptionFactoryInput,
|
|
19
21
|
HistogramOptionsInput,
|
|
22
|
+
HistogramBin,
|
|
20
23
|
HistogramWidgetData,
|
|
21
24
|
} from './types'
|
|
25
|
+
import { fixedAxisBounds } from '../actions/relative-data/scale'
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Builds the **structural** ECharts option for a histogram widget — axes,
|
|
@@ -147,7 +151,7 @@ export function histogramOptions({
|
|
|
147
151
|
* the consumer-supplied `optionsOverride`. Called once by Provider to
|
|
148
152
|
* seed `rawOptions` in the store.
|
|
149
153
|
* - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`
|
|
150
|
-
* (`HistogramWidgetData` — `
|
|
154
|
+
* (`HistogramWidgetData` — `HistogramBin[][]`) into the option via the
|
|
151
155
|
* dataset API. Each series gets its own dataset of `[binLabel, count]`
|
|
152
156
|
* pairs derived from `ticks`; series reference their dataset by
|
|
153
157
|
* `datasetIndex` and are encoded by column position. Reactive
|
|
@@ -199,8 +203,8 @@ export function createHistogramOptionFactory(
|
|
|
199
203
|
: structural
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
const seriesArr: HistogramWidgetData = Array.isArray(data)
|
|
203
|
-
? (data as HistogramWidgetData)
|
|
206
|
+
const seriesArr: HistogramWidgetData<HistogramBin> = Array.isArray(data)
|
|
207
|
+
? (data as HistogramWidgetData<HistogramBin>)
|
|
204
208
|
: []
|
|
205
209
|
if (seriesArr.length === 0 || binLabels.length === 0) {
|
|
206
210
|
return { ...option, dataset: [], series: [] }
|
|
@@ -213,6 +217,7 @@ export function createHistogramOptionFactory(
|
|
|
213
217
|
typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)
|
|
214
218
|
? option.yAxis
|
|
215
219
|
: {}
|
|
220
|
+
const fixedY = fixedAxisBounds(baseYAxis)
|
|
216
221
|
const baseGrid =
|
|
217
222
|
typeof option.grid === 'object' && !Array.isArray(option.grid)
|
|
218
223
|
? option.grid
|
|
@@ -261,14 +266,28 @@ export function createHistogramOptionFactory(
|
|
|
261
266
|
// and bins would stay dimmed forever after an external clear. Always
|
|
262
267
|
// emitting lets normal merge swap the callback in place, no
|
|
263
268
|
// `replaceMerge` and no entry-animation flash on selection on/off.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
//
|
|
270
|
+
// The base colour is the bin's own `color` (object bins), else the
|
|
271
|
+
// template's `itemStyle.color`, else the series colour / palette.
|
|
272
|
+
const makeDimItemStyle = (
|
|
273
|
+
datums: readonly unknown[],
|
|
274
|
+
template: unknown,
|
|
275
|
+
) => {
|
|
276
|
+
const baseColor = createItemColorResolver({
|
|
277
|
+
theme,
|
|
278
|
+
datums,
|
|
279
|
+
template,
|
|
280
|
+
fallback: (params) => params.color as string,
|
|
281
|
+
})
|
|
282
|
+
return {
|
|
283
|
+
color: (params: CallbackDataParams) => {
|
|
284
|
+
const base = baseColor(params)
|
|
285
|
+
if (!selectionSet) return base
|
|
286
|
+
return selectionSet.has(params.dataIndex)
|
|
287
|
+
? base
|
|
288
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
289
|
+
},
|
|
290
|
+
}
|
|
272
291
|
}
|
|
273
292
|
|
|
274
293
|
// A consumer's `optionsOverride.xAxis` may legitimately be an array
|
|
@@ -306,12 +325,16 @@ export function createHistogramOptionFactory(
|
|
|
306
325
|
return {
|
|
307
326
|
...option,
|
|
308
327
|
xAxis: nextXAxis,
|
|
309
|
-
dataset: seriesArr.map((
|
|
310
|
-
source: binLabels.map(
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
dataset: seriesArr.map((bins) => ({
|
|
329
|
+
source: binLabels.map((label, i) => {
|
|
330
|
+
const bin = bins[i]
|
|
331
|
+
return [label, bin == null ? 0 : getHistogramBinValue(bin)] as [
|
|
332
|
+
string,
|
|
333
|
+
number,
|
|
334
|
+
]
|
|
335
|
+
}),
|
|
313
336
|
})),
|
|
314
|
-
series: seriesArr.map((
|
|
337
|
+
series: seriesArr.map((bins, i) => {
|
|
315
338
|
const template =
|
|
316
339
|
(seriesTemplates[i] as object | undefined) ??
|
|
317
340
|
(broadcastTemplate as object)
|
|
@@ -324,7 +347,7 @@ export function createHistogramOptionFactory(
|
|
|
324
347
|
encode: { x: 0, y: 1 },
|
|
325
348
|
barCategoryGap: '0%',
|
|
326
349
|
emphasis: { focus: 'series' },
|
|
327
|
-
itemStyle:
|
|
350
|
+
itemStyle: makeDimItemStyle(bins, template),
|
|
328
351
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
329
352
|
}
|
|
330
353
|
}),
|
|
@@ -333,12 +356,14 @@ export function createHistogramOptionFactory(
|
|
|
333
356
|
...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),
|
|
334
357
|
yAxis: {
|
|
335
358
|
...baseYAxis,
|
|
359
|
+
// A config transform (RelativeData's `'total'` scale) may pin the
|
|
360
|
+
// ends; each applies only on the side the data reaches.
|
|
336
361
|
min: (extent: { min: number }) => {
|
|
337
|
-
niceMin = extent.min < 0 ? niceNum(extent.min) : 0
|
|
362
|
+
niceMin = extent.min < 0 ? (fixedY.min ?? niceNum(extent.min)) : 0
|
|
338
363
|
return niceMin
|
|
339
364
|
},
|
|
340
365
|
max: (extent: { min: number; max: number }) => {
|
|
341
|
-
niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
|
|
366
|
+
niceMax = extent.max <= 0 ? 1 : (fixedY.max ?? niceNum(extent.max))
|
|
342
367
|
return niceMax
|
|
343
368
|
},
|
|
344
369
|
axisLabel: {
|
|
@@ -36,15 +36,20 @@ describe('toRelativeHistogramData', () => {
|
|
|
36
36
|
expect(out[0]?.[1]).toBeCloseTo(-49.74874, 4)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it('returns the input unchanged when shape does not match
|
|
39
|
+
it('returns the input unchanged when shape does not match HistogramBin[][]', () => {
|
|
40
40
|
expect(toRelativeHistogramData(null)).toBe(null)
|
|
41
41
|
expect(toRelativeHistogramData('not an array')).toBe('not an array')
|
|
42
|
-
// Series that isn't a
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
expect(toRelativeHistogramData([[
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// Series that isn't a list of bins (e.g. strings or tuples) is returned
|
|
43
|
+
// as-is — keeps the transform safe to attach to a widget whose shape
|
|
44
|
+
// changes through other actions.
|
|
45
|
+
expect(toRelativeHistogramData([['a', 'b']])).toEqual([['a', 'b']])
|
|
46
|
+
expect(toRelativeHistogramData([[[1, 2]]])).toEqual([[[1, 2]]])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('relativizes object bins, keeping their colour', () => {
|
|
50
|
+
expect(
|
|
51
|
+
toRelativeHistogramData([[{ value: 5, color: '#f00' }, 15]]),
|
|
52
|
+
).toEqual([[{ value: 25, color: '#f00' }, 75]])
|
|
48
53
|
})
|
|
49
54
|
})
|
|
50
55
|
|
|
@@ -75,6 +80,15 @@ describe('filterHistogramByLockedBins', () => {
|
|
|
75
80
|
])
|
|
76
81
|
})
|
|
77
82
|
|
|
83
|
+
it('zeroes non-locked object bins, keeping their colour', () => {
|
|
84
|
+
expect(
|
|
85
|
+
filterHistogramByLockedBins(
|
|
86
|
+
[[{ value: 10, color: '#f00' }, { value: 20 }, 30]],
|
|
87
|
+
[1],
|
|
88
|
+
),
|
|
89
|
+
).toEqual([[{ value: 0, color: '#f00' }, { value: 20 }, 0]])
|
|
90
|
+
})
|
|
91
|
+
|
|
78
92
|
it('returns input unchanged when input is not array-shaped', () => {
|
|
79
93
|
expect(filterHistogramByLockedBins('nope', [0])).toBe('nope')
|
|
80
94
|
})
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getHistogramBinValue,
|
|
3
|
+
isHistogramBin,
|
|
4
|
+
withHistogramBinValue,
|
|
5
|
+
} from './bins'
|
|
6
|
+
import type { HistogramBin } from './types'
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Histogram-specific `RelativeData` transform. Histogram data is
|
|
3
|
-
* `
|
|
10
|
+
* `HistogramBin[][]` — each series is a flat list of bin counts (plain
|
|
11
|
+
* numbers or `{ value, color? }`, whose shape is kept). This
|
|
4
12
|
* rewrites each count to its share of the series total (0–100), so
|
|
5
13
|
* pairing with `createPercentFormatter` renders the y-axis as
|
|
6
14
|
* percentages.
|
|
@@ -11,27 +19,31 @@
|
|
|
11
19
|
* inputs produce sane signed shares-of-magnitude. A series whose total
|
|
12
20
|
* magnitude is zero (all-zero or empty input) is returned unchanged so
|
|
13
21
|
* a stalled or empty data set doesn't show misleading 0% values. Inputs
|
|
14
|
-
* whose shape isn't `
|
|
22
|
+
* whose shape isn't `HistogramBin[][]` fall through untouched (same defensive
|
|
15
23
|
* shape-check pattern as the default transform).
|
|
16
24
|
*/
|
|
17
25
|
export const toRelativeHistogramData = (input: unknown): unknown => {
|
|
18
26
|
if (!Array.isArray(input)) return input
|
|
19
27
|
return input.map((series: unknown): unknown => {
|
|
20
|
-
if (!
|
|
21
|
-
const total = series.reduce(
|
|
28
|
+
if (!isBinArray(series)) return series
|
|
29
|
+
const total = series.reduce<number>(
|
|
30
|
+
(acc, b) => acc + Math.abs(getHistogramBinValue(b)),
|
|
31
|
+
0,
|
|
32
|
+
)
|
|
22
33
|
if (total <= 0) return series
|
|
23
|
-
return series.map((
|
|
34
|
+
return series.map((b) =>
|
|
35
|
+
withHistogramBinValue(b, (getHistogramBinValue(b) / total) * 100),
|
|
36
|
+
)
|
|
24
37
|
})
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
return v.every((item) => typeof item === 'number' && Number.isFinite(item))
|
|
40
|
+
function isBinArray(v: unknown): v is HistogramBin[] {
|
|
41
|
+
return Array.isArray(v) && v.every(isHistogramBin)
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
|
-
* `LockSelection` filter for Histogram. Data is `
|
|
34
|
-
* of bin counts per series, positionally paired with `ticks` (bin `i` covers
|
|
45
|
+
* `LockSelection` filter for Histogram. Data is `HistogramBin[][]` — a flat
|
|
46
|
+
* list of bin counts per series, positionally paired with `ticks` (bin `i` covers
|
|
35
47
|
* `[ticks[i], ticks[i+1])`). Unlike name-keyed widgets, locked-out bins can't
|
|
36
48
|
* be REMOVED — that would shift every later bin's index and desync it from
|
|
37
49
|
* its `ticks` range. Zeroing them out instead preserves array length/position,
|
|
@@ -47,7 +59,9 @@ export function filterHistogramByLockedBins(
|
|
|
47
59
|
if (!Array.isArray(input)) return input
|
|
48
60
|
const locked = new Set<number>(lockedItems.map(Number))
|
|
49
61
|
return input.map((series: unknown): unknown => {
|
|
50
|
-
if (!
|
|
51
|
-
return series.map((
|
|
62
|
+
if (!isBinArray(series)) return series
|
|
63
|
+
return series.map((bin, i) =>
|
|
64
|
+
locked.has(i) ? bin : withHistogramBinValue(bin, 0),
|
|
65
|
+
)
|
|
52
66
|
})
|
|
53
67
|
}
|
|
@@ -3,12 +3,32 @@ import type { EChartsOption } from 'echarts'
|
|
|
3
3
|
import type { WidgetSeries } from '../types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* One histogram bin: either its count, or `{ value, color? }` to give the
|
|
7
|
+
* bar its own colour (CSS colour or theme palette path such as
|
|
8
|
+
* `'primary.main'`). A bin `color` wins over the template's
|
|
9
|
+
* `itemStyle.color` and the series colour; selection dimming still applies.
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
11
|
+
export type HistogramBin = number | HistogramBinObject
|
|
12
|
+
|
|
13
|
+
/** Object form of a {@link HistogramBin}. */
|
|
14
|
+
export interface HistogramBinObject {
|
|
15
|
+
value: number
|
|
16
|
+
color?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Histogram widget data — array of series, each a flat array of bins. The
|
|
21
|
+
* option factory's merge phase (see {@link createHistogramOptionFactory})
|
|
22
|
+
* combines this with `ticks` to produce a 2-column dataset
|
|
23
|
+
* (`[binLabel, count]`) for ECharts.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to plain counts (`number[][]`) so existing code that reads bins
|
|
26
|
+
* as numbers keeps type-checking; use `HistogramWidgetData<HistogramBin>`
|
|
27
|
+
* to also accept `{ value, color? }` bins. Every histogram API (option
|
|
28
|
+
* factory, transforms, download) accepts both forms.
|
|
29
|
+
*/
|
|
30
|
+
export type HistogramWidgetData<B extends HistogramBin = number> =
|
|
31
|
+
readonly (readonly B[])[]
|
|
12
32
|
|
|
13
33
|
/** Inputs to the structural-only {@link histogramOptions} builder. */
|
|
14
34
|
export interface HistogramOptionsInput {
|
package/src/widgets-v2/index.ts
CHANGED
|
@@ -155,6 +155,7 @@ export type {
|
|
|
155
155
|
ZoomToggleProps,
|
|
156
156
|
BrushToggleProps,
|
|
157
157
|
RelativeDataProps,
|
|
158
|
+
RelativeScale,
|
|
158
159
|
DownloadProps,
|
|
159
160
|
DownloadItem,
|
|
160
161
|
DownloadContext,
|
|
@@ -175,7 +176,12 @@ export type {
|
|
|
175
176
|
export type { MarkdownUIProps, MarkdownWidgetData } from './markdown'
|
|
176
177
|
export type { PieDatum, PieWidgetData } from './pie'
|
|
177
178
|
export type { TimeseriesDatum, TimeseriesWidgetData } from './timeseries'
|
|
178
|
-
export type {
|
|
179
|
+
export type {
|
|
180
|
+
ScatterplotDatum,
|
|
181
|
+
ScatterplotPoint,
|
|
182
|
+
ScatterplotItem,
|
|
183
|
+
ScatterplotWidgetData,
|
|
184
|
+
} from './scatterplot'
|
|
179
185
|
export type {
|
|
180
186
|
SpreadUIProps,
|
|
181
187
|
SpreadDataItem,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import * as echarts from 'echarts'
|
|
2
3
|
import { createTheme, type Theme } from '@mui/material'
|
|
3
4
|
import { createPieOptionFactory, pieOptions } from './options'
|
|
4
5
|
|
|
@@ -936,3 +937,93 @@ describe('createPieOptionFactory — multi-series fallback (horizontal bar)', ()
|
|
|
936
937
|
})
|
|
937
938
|
})
|
|
938
939
|
})
|
|
940
|
+
|
|
941
|
+
describe('createPieOptionFactory — per-item colour', () => {
|
|
942
|
+
interface Out {
|
|
943
|
+
series: { itemStyle: { color: (p: unknown) => string } }[]
|
|
944
|
+
}
|
|
945
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
946
|
+
const palette = ['#p0', '#p1', '#p2']
|
|
947
|
+
|
|
948
|
+
it('donut: slice colour wins over the palette; template next', () => {
|
|
949
|
+
const merge = createPieOptionFactory({ theme, selection: ['A', 'B'] })
|
|
950
|
+
const out = merge(
|
|
951
|
+
{
|
|
952
|
+
color: palette,
|
|
953
|
+
series: [{ type: 'pie', itemStyle: { color: '#123456' } }],
|
|
954
|
+
},
|
|
955
|
+
[
|
|
956
|
+
[
|
|
957
|
+
{ name: 'A', value: 1, color: '#00ff00' },
|
|
958
|
+
{ name: 'B', value: 2 },
|
|
959
|
+
{ name: 'C', value: 3 },
|
|
960
|
+
],
|
|
961
|
+
],
|
|
962
|
+
) as Out
|
|
963
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
964
|
+
expect(colorFn({ dataIndex: 0, name: 'A', color: '#x' })).toBe('#00ff00')
|
|
965
|
+
expect(colorFn({ dataIndex: 1, name: 'B', color: '#x' })).toBe('#123456')
|
|
966
|
+
expect(colorFn({ dataIndex: 2, name: 'C', color: '#x' })).toBe(
|
|
967
|
+
dim('#123456'),
|
|
968
|
+
)
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('donut: keeps the per-data palette when no item colour is given', () => {
|
|
972
|
+
const merge = createPieOptionFactory({ theme })
|
|
973
|
+
const out = merge({ color: palette }, [
|
|
974
|
+
[
|
|
975
|
+
{ name: 'A', value: 1, color: '#00ff00' },
|
|
976
|
+
{ name: 'B', value: 2 },
|
|
977
|
+
],
|
|
978
|
+
]) as Out
|
|
979
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
980
|
+
expect(colorFn({ dataIndex: 0, name: 'A', color: '#x' })).toBe('#00ff00')
|
|
981
|
+
expect(colorFn({ dataIndex: 1, name: 'B', color: '#x' })).toBe('#p1')
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
it('donut: template itemStyle.color callback via optionsOverride', () => {
|
|
985
|
+
const factory = createPieOptionFactory({
|
|
986
|
+
theme,
|
|
987
|
+
optionsOverride: {
|
|
988
|
+
series: [
|
|
989
|
+
{
|
|
990
|
+
type: 'pie',
|
|
991
|
+
itemStyle: {
|
|
992
|
+
color: (p: { dataIndex: number }) => `#c${p.dataIndex}`,
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
],
|
|
996
|
+
},
|
|
997
|
+
})
|
|
998
|
+
const out = factory(factory(undefined, undefined), [
|
|
999
|
+
[{ name: 'A', value: 1 }],
|
|
1000
|
+
]) as Out
|
|
1001
|
+
expect(
|
|
1002
|
+
out.series[0]!.itemStyle.color({ dataIndex: 0, name: 'A', color: '#x' }),
|
|
1003
|
+
).toBe('#c0')
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
it('multi-series bar fallback: datum colour wins over the series palette', () => {
|
|
1007
|
+
const merge = createPieOptionFactory({ theme, selection: ['B'] })
|
|
1008
|
+
const out = merge({ color: palette }, [
|
|
1009
|
+
[{ name: 'A', value: 1, color: '#00ff00' }],
|
|
1010
|
+
[{ name: 'A', value: 2 }],
|
|
1011
|
+
]) as Out
|
|
1012
|
+
expect(
|
|
1013
|
+
out.series[0]!.itemStyle.color({
|
|
1014
|
+
dataIndex: 0,
|
|
1015
|
+
seriesIndex: 0,
|
|
1016
|
+
name: 'A',
|
|
1017
|
+
color: '#x',
|
|
1018
|
+
}),
|
|
1019
|
+
).toBe(dim('#00ff00'))
|
|
1020
|
+
expect(
|
|
1021
|
+
out.series[1]!.itemStyle.color({
|
|
1022
|
+
dataIndex: 0,
|
|
1023
|
+
seriesIndex: 1,
|
|
1024
|
+
name: 'B',
|
|
1025
|
+
color: '#x',
|
|
1026
|
+
}),
|
|
1027
|
+
).toBe('#p1')
|
|
1028
|
+
})
|
|
1029
|
+
})
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../../widgets/utils/chart-config'
|
|
13
13
|
import type { OptionFactory, OptionFactoryContext } from '../echart'
|
|
14
14
|
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
15
|
+
import { createItemColorResolver } from '../utils/item-color'
|
|
15
16
|
import type { WidgetSeries } from '../types'
|
|
16
17
|
import type {
|
|
17
18
|
PieEChartsOption,
|
|
@@ -168,6 +169,7 @@ export function createPieOptionFactory(
|
|
|
168
169
|
return buildSingleSeriesPieFusion(
|
|
169
170
|
option,
|
|
170
171
|
seriesArr,
|
|
172
|
+
theme,
|
|
171
173
|
radius,
|
|
172
174
|
series,
|
|
173
175
|
ctx,
|
|
@@ -185,6 +187,7 @@ export function createPieOptionFactory(
|
|
|
185
187
|
function buildSingleSeriesPieFusion(
|
|
186
188
|
option: EChartsOption,
|
|
187
189
|
seriesArr: PieWidgetData,
|
|
190
|
+
theme: Theme,
|
|
188
191
|
radius: readonly [string, string],
|
|
189
192
|
series: readonly WidgetSeries[] | undefined,
|
|
190
193
|
ctx: OptionFactoryContext | undefined,
|
|
@@ -231,14 +234,25 @@ function buildSingleSeriesPieFusion(
|
|
|
231
234
|
// empty Set — the collapse above (`selection.length === 0 → null`) rules
|
|
232
235
|
// that out — so there's no "dim everything" case here, unlike a widget
|
|
233
236
|
// that gives `[]` its own "selected down to nothing" meaning.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
//
|
|
238
|
+
// The base colour is the slice's own `color`, else the template's
|
|
239
|
+
// `itemStyle.color`, else the per-data palette swatch.
|
|
240
|
+
const makeColorFn = (datums: readonly object[], template: unknown) => {
|
|
241
|
+
const baseColor = createItemColorResolver({
|
|
242
|
+
theme,
|
|
243
|
+
datums,
|
|
244
|
+
template,
|
|
245
|
+
fallback: resolvePaletteColor,
|
|
246
|
+
})
|
|
247
|
+
return (params: CallbackDataParams): string => {
|
|
248
|
+
const base = baseColor(params)
|
|
249
|
+
if (!selectionSet) return base
|
|
250
|
+
const datum = params.value as { name?: string | number } | undefined
|
|
251
|
+
const name = datum?.name ?? params.name
|
|
252
|
+
return name != null && selectionSet.has(name)
|
|
253
|
+
? base
|
|
254
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
255
|
+
}
|
|
242
256
|
}
|
|
243
257
|
|
|
244
258
|
// ECharts anchors EVERY slice's `position: 'center'` label at the same
|
|
@@ -316,7 +330,7 @@ function buildSingleSeriesPieFusion(
|
|
|
316
330
|
return {
|
|
317
331
|
...option,
|
|
318
332
|
dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),
|
|
319
|
-
series: seriesArr.map((
|
|
333
|
+
series: seriesArr.map((s, i) => {
|
|
320
334
|
const template =
|
|
321
335
|
(seriesTemplates[i] as object | undefined) ??
|
|
322
336
|
(broadcastTemplate as object)
|
|
@@ -344,7 +358,7 @@ function buildSingleSeriesPieFusion(
|
|
|
344
358
|
},
|
|
345
359
|
itemStyle: {
|
|
346
360
|
...templateItemStyle,
|
|
347
|
-
color:
|
|
361
|
+
color: makeColorFn(s, templateObj),
|
|
348
362
|
},
|
|
349
363
|
}
|
|
350
364
|
}),
|
|
@@ -419,17 +433,31 @@ function buildMultiSeriesBarFusion(
|
|
|
419
433
|
const palette = Array.isArray(option.color)
|
|
420
434
|
? (option.color as readonly (string | undefined)[])
|
|
421
435
|
: []
|
|
422
|
-
|
|
436
|
+
// The base colour is the datum's own `color`, else the template's
|
|
437
|
+
// `itemStyle.color`, else the per-series palette swatch.
|
|
438
|
+
const seriesTemplates = Array.isArray(option.series) ? option.series : []
|
|
439
|
+
const paletteColor = (params: CallbackDataParams): string => {
|
|
423
440
|
const seriesIdx = params.seriesIndex ?? 0
|
|
424
441
|
const seriesSwatch =
|
|
425
442
|
palette.length > 0 ? palette[seriesIdx % palette.length] : undefined
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
443
|
+
return (seriesSwatch ?? params.color) as string
|
|
444
|
+
}
|
|
445
|
+
const makeBarColorFn = (datums: readonly object[], template: unknown) => {
|
|
446
|
+
const baseColor = createItemColorResolver({
|
|
447
|
+
theme,
|
|
448
|
+
datums,
|
|
449
|
+
template,
|
|
450
|
+
fallback: paletteColor,
|
|
451
|
+
})
|
|
452
|
+
return (params: CallbackDataParams): string => {
|
|
453
|
+
const base = baseColor(params)
|
|
454
|
+
if (!selectionSet) return base
|
|
455
|
+
const datum = params.value as { name?: string | number } | undefined
|
|
456
|
+
const name = datum?.name ?? params.name
|
|
457
|
+
return name != null && selectionSet.has(name)
|
|
458
|
+
? base
|
|
459
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
460
|
+
}
|
|
433
461
|
}
|
|
434
462
|
|
|
435
463
|
return {
|
|
@@ -444,7 +472,7 @@ function buildMultiSeriesBarFusion(
|
|
|
444
472
|
// Drop pie-specific structural keys that shouldn't render in the
|
|
445
473
|
// bar fallback. ECharts ignores undefined keys, so this is a clean
|
|
446
474
|
// override.
|
|
447
|
-
series: seriesArr.map((
|
|
475
|
+
series: seriesArr.map((s, i) => {
|
|
448
476
|
const overrideColor = resolveThemeColor(theme, series?.[i]?.color)
|
|
449
477
|
return {
|
|
450
478
|
datasetIndex: i,
|
|
@@ -454,7 +482,9 @@ function buildMultiSeriesBarFusion(
|
|
|
454
482
|
legendHoverLink: false,
|
|
455
483
|
emphasis: { focus: 'series' },
|
|
456
484
|
...buildSeriesLabelConfig(formatter, 'x'),
|
|
457
|
-
itemStyle: {
|
|
485
|
+
itemStyle: {
|
|
486
|
+
color: makeBarColorFn(s, seriesTemplates[i] ?? seriesTemplates[0]),
|
|
487
|
+
},
|
|
458
488
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
459
489
|
}
|
|
460
490
|
}),
|
|
@@ -6,6 +6,12 @@ import type { WidgetSeries } from '../types'
|
|
|
6
6
|
export interface PieDatum {
|
|
7
7
|
name: string | number
|
|
8
8
|
value: number
|
|
9
|
+
/**
|
|
10
|
+
* Optional colour for this item (CSS colour or theme palette path such as
|
|
11
|
+
* `'primary.main'`). Wins over the template's `itemStyle.color` and the
|
|
12
|
+
* series colour; selection dimming still applies on top.
|
|
13
|
+
*/
|
|
14
|
+
color?: string
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
/** Pie widget data — one slice array per series (rendered as side-by-side donuts). */
|
|
@@ -61,6 +61,15 @@ describe('createScatterplotDownloadConfig', () => {
|
|
|
61
61
|
)
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
it('CSV writes x/y of object points (colour is not exported)', async () => {
|
|
65
|
+
const items = createScatterplotDownloadConfig({
|
|
66
|
+
filename: 's',
|
|
67
|
+
getData: () => [[{ x: 1, y: 10, color: '#f00' }, [2, 20]]],
|
|
68
|
+
})
|
|
69
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
70
|
+
expect(csvText).toBe('series,x,y\nseries_1,1,10\nseries_1,2,20')
|
|
71
|
+
})
|
|
72
|
+
|
|
64
73
|
it('CSV honours supplied seriesNames', async () => {
|
|
65
74
|
const items = createScatterplotDownloadConfig({
|
|
66
75
|
filename: 's',
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type DownloadContext,
|
|
5
5
|
type DownloadItem,
|
|
6
6
|
} from '../actions/download'
|
|
7
|
-
import
|
|
7
|
+
import { getScatterplotXY } from './points'
|
|
8
|
+
import type { ScatterplotItem, ScatterplotWidgetData } from './types'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Download menu items for the Scatterplot widget. Always includes a CSV
|
|
@@ -14,7 +15,7 @@ import type { ScatterplotWidgetData } from './types'
|
|
|
14
15
|
*/
|
|
15
16
|
export function createScatterplotDownloadConfig(args: {
|
|
16
17
|
filename: string
|
|
17
|
-
getData: (ctx: DownloadContext) => ScatterplotWidgetData
|
|
18
|
+
getData: (ctx: DownloadContext) => ScatterplotWidgetData<ScatterplotItem>
|
|
18
19
|
seriesNames?: readonly string[]
|
|
19
20
|
getCaptureEl?: (ctx: DownloadContext) => HTMLElement | null
|
|
20
21
|
pngPixelRatio?: number
|
|
@@ -39,7 +40,8 @@ export function createScatterplotDownloadConfig(args: {
|
|
|
39
40
|
const rows: unknown[][] = [['series', 'x', 'y']]
|
|
40
41
|
for (const [i, series] of data.entries()) {
|
|
41
42
|
const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
|
|
42
|
-
for (const
|
|
43
|
+
for (const point of series) {
|
|
44
|
+
const [x, y] = getScatterplotXY(point)
|
|
43
45
|
rows.push([seriesName, x, y])
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -5,8 +5,11 @@ export {
|
|
|
5
5
|
toRelativeScatterplotData,
|
|
6
6
|
filterScatterplotByLockedPoints,
|
|
7
7
|
} from './transforms'
|
|
8
|
+
export { getScatterplotXY } from './points'
|
|
8
9
|
export type {
|
|
9
10
|
ScatterplotDatum,
|
|
11
|
+
ScatterplotPoint,
|
|
12
|
+
ScatterplotItem,
|
|
10
13
|
ScatterplotWidgetData,
|
|
11
14
|
ScatterplotOptionsInput,
|
|
12
15
|
ScatterplotOptionFactoryInput,
|