@carto/ps-react-ui 4.25.0 → 4.26.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-B1ss030_.js → actions-DXfQMrll.js} +56 -46
- package/dist/actions-DXfQMrll.js.map +1 -0
- package/dist/item-color-C1cu6EJd.js +20 -0
- package/dist/item-color-C1cu6EJd.js.map +1 -0
- package/dist/types/widgets-v2/bar/types.d.ts +6 -0
- 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 +1 -1
- 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 +66 -57
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/histogram.js +103 -71
- 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 +119 -79
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +70 -61
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +1 -1
- package/dist/widgets-v2.js.map +1 -1
- package/package.json +3 -3
- 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/bar/options.test.ts +106 -0
- package/src/widgets-v2/bar/options.ts +25 -14
- package/src/widgets-v2/bar/types.ts +6 -0
- 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 +38 -17
- 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 +6 -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 +78 -0
- package/src/widgets-v2/scatterplot/options.ts +35 -11
- 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 +32 -17
- 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-B1ss030_.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ScatterplotItem } from './types'
|
|
2
|
+
|
|
3
|
+
/** `[x, y]` of a {@link ScatterplotItem}, whichever form it takes. */
|
|
4
|
+
export function getScatterplotXY(
|
|
5
|
+
point: ScatterplotItem,
|
|
6
|
+
): readonly [number, number] {
|
|
7
|
+
return isTuple(point) ? point : [point.x, point.y]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Whether `v` is a valid {@link ScatterplotItem} with finite coordinates. */
|
|
11
|
+
export function isScatterplotDatum(v: unknown): v is ScatterplotItem {
|
|
12
|
+
if (Array.isArray(v)) {
|
|
13
|
+
return v.length === 2 && isFiniteNumber(v[0]) && isFiniteNumber(v[1])
|
|
14
|
+
}
|
|
15
|
+
if (v == null || typeof v !== 'object') return false
|
|
16
|
+
const { x, y } = v as { x?: unknown; y?: unknown }
|
|
17
|
+
return isFiniteNumber(x) && isFiniteNumber(y)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Same point with new coordinates; keeps the object form (and `color`). */
|
|
21
|
+
export function withScatterplotXY(
|
|
22
|
+
point: ScatterplotItem,
|
|
23
|
+
x: number,
|
|
24
|
+
y: number,
|
|
25
|
+
): ScatterplotItem {
|
|
26
|
+
return isTuple(point) ? [x, y] : { ...point, x, y }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isTuple(point: ScatterplotItem): point is readonly [number, number] {
|
|
30
|
+
return Array.isArray(point)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isFiniteNumber(v: unknown): v is number {
|
|
34
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
35
|
+
}
|
|
@@ -99,6 +99,25 @@ describe('toRelativeScatterplotData', () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
describe('toRelativeScatterplotData (object points)', () => {
|
|
103
|
+
it('relativizes y of object points, keeping x and colour', () => {
|
|
104
|
+
expect(
|
|
105
|
+
toRelativeScatterplotData([[{ x: 1, y: 5, color: '#f00' }, [2, 15]]]),
|
|
106
|
+
).toEqual([[{ x: 1, y: 25, color: '#f00' }, [2, 75]]])
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('filterScatterplotByLockedPoints (object points)', () => {
|
|
111
|
+
it('keeps locked object points as they are', () => {
|
|
112
|
+
expect(
|
|
113
|
+
filterScatterplotByLockedPoints(
|
|
114
|
+
[[{ x: 1, y: 2, color: '#f00' }, { x: 3, y: 4 }, [5, 6]]],
|
|
115
|
+
['0:0', '0:2'],
|
|
116
|
+
),
|
|
117
|
+
).toEqual([[{ x: 1, y: 2, color: '#f00' }, [5, 6]]])
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
102
121
|
describe('filterScatterplotByLockedPoints', () => {
|
|
103
122
|
const DATA = [
|
|
104
123
|
[
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getScatterplotXY,
|
|
3
|
+
isScatterplotDatum,
|
|
4
|
+
withScatterplotXY,
|
|
5
|
+
} from './points'
|
|
6
|
+
import type { ScatterplotItem } from './types'
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Scatterplot-specific `RelativeData` transform. Scatter data is
|
|
3
|
-
* `[
|
|
10
|
+
* `ScatterplotItem[][]` — each series is a list of `[x, y]` tuples or
|
|
11
|
+
* `{ x, y, color? }` points (whose shape is kept).
|
|
4
12
|
* Relative is a values-axis concept, so this rewrites `y` to its
|
|
5
13
|
* share of the series's total `y` (0–100) and leaves `x` raw — x is
|
|
6
14
|
* coordinate space, not part of the cohort total.
|
|
@@ -11,34 +19,31 @@
|
|
|
11
19
|
* y values produce sane signed shares-of-magnitude. A series whose
|
|
12
20
|
* total y-magnitude is zero (all-zero or empty input) is returned
|
|
13
21
|
* unchanged so a stalled or empty data set doesn't show misleading 0%
|
|
14
|
-
* values. Inputs whose shape isn't `[
|
|
22
|
+
* values. Inputs whose shape isn't `ScatterplotItem[]` fall through
|
|
15
23
|
* untouched.
|
|
16
24
|
*/
|
|
17
25
|
export const toRelativeScatterplotData = (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 (!isPointArray(series)) return series
|
|
29
|
+
const total = series.reduce(
|
|
30
|
+
(acc, p) => acc + Math.abs(getScatterplotXY(p)[1]),
|
|
31
|
+
0,
|
|
32
|
+
)
|
|
22
33
|
if (total <= 0) return series
|
|
23
|
-
return series.map((
|
|
34
|
+
return series.map((p) => {
|
|
35
|
+
const [x, y] = getScatterplotXY(p)
|
|
36
|
+
return withScatterplotXY(p, x, (y / total) * 100)
|
|
37
|
+
})
|
|
24
38
|
})
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
return v.every(
|
|
30
|
-
(item) =>
|
|
31
|
-
Array.isArray(item) &&
|
|
32
|
-
item.length === 2 &&
|
|
33
|
-
typeof item[0] === 'number' &&
|
|
34
|
-
Number.isFinite(item[0]) &&
|
|
35
|
-
typeof item[1] === 'number' &&
|
|
36
|
-
Number.isFinite(item[1]),
|
|
37
|
-
)
|
|
41
|
+
function isPointArray(v: unknown): v is ScatterplotItem[] {
|
|
42
|
+
return Array.isArray(v) && v.every(isScatterplotDatum)
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
|
-
* `LockSelection` filter for Scatterplot. Data is `[
|
|
46
|
+
* `LockSelection` filter for Scatterplot. Data is `ScatterplotItem[][]` —
|
|
42
47
|
* points have no `name`, so selection (and locking) is keyed on a synthetic
|
|
43
48
|
* `${seriesIndex}:${dataIndex}` string (same format `useChartSelection`'s
|
|
44
49
|
* `keyFromClick`/`keysFromBrush` produce for this widget). Unlike Histogram,
|
|
@@ -53,7 +58,7 @@ export function filterScatterplotByLockedPoints(
|
|
|
53
58
|
if (!Array.isArray(input)) return input
|
|
54
59
|
const locked = new Set<string>(lockedItems.map(String))
|
|
55
60
|
return input.map((series: unknown, seriesIndex: number): unknown => {
|
|
56
|
-
if (!
|
|
61
|
+
if (!isPointArray(series)) return series
|
|
57
62
|
return series.filter((_, dataIndex) =>
|
|
58
63
|
locked.has(`${seriesIndex}:${dataIndex}`),
|
|
59
64
|
)
|
|
@@ -5,8 +5,31 @@ import type { WidgetSeries } from '../types'
|
|
|
5
5
|
/** A single point — `[x, y]` tuple. */
|
|
6
6
|
export type ScatterplotDatum = readonly [number, number]
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Object form of a point, to give it its own colour. `color` (CSS colour or
|
|
10
|
+
* theme palette path such as `'primary.main'`) wins over the template's
|
|
11
|
+
* `itemStyle.color` and the series colour; selection dimming still applies.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScatterplotPoint {
|
|
14
|
+
x: number
|
|
15
|
+
y: number
|
|
16
|
+
color?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Any accepted point: an `[x, y]` tuple or a {@link ScatterplotPoint}. */
|
|
20
|
+
export type ScatterplotItem = ScatterplotDatum | ScatterplotPoint
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scatterplot widget data — array of series, each a list of points.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to `[x, y]` tuples so existing code that destructures points
|
|
26
|
+
* keeps type-checking; use `ScatterplotWidgetData<ScatterplotItem>` to also
|
|
27
|
+
* accept `{ x, y, color? }` points. Every scatterplot API (option factory,
|
|
28
|
+
* transforms, download) accepts both forms.
|
|
29
|
+
*/
|
|
30
|
+
export type ScatterplotWidgetData<
|
|
31
|
+
P extends ScatterplotItem = ScatterplotDatum,
|
|
32
|
+
> = readonly (readonly P[])[]
|
|
10
33
|
|
|
11
34
|
/** Inputs to the structural-only {@link scatterplotOptions} builder. */
|
|
12
35
|
export interface ScatterplotOptionsInput {
|
|
@@ -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 { createTimeseriesOptionFactory, timeseriesOptions } from './options'
|
|
4
5
|
|
|
@@ -397,3 +398,67 @@ describe('createTimeseriesOptionFactory', () => {
|
|
|
397
398
|
expect(out.grid.bottom).toBe(96)
|
|
398
399
|
})
|
|
399
400
|
})
|
|
401
|
+
|
|
402
|
+
describe('createTimeseriesOptionFactory — per-item colour', () => {
|
|
403
|
+
interface Out {
|
|
404
|
+
series: {
|
|
405
|
+
showSymbol: boolean
|
|
406
|
+
itemStyle: { color: (p: unknown) => string }
|
|
407
|
+
}[]
|
|
408
|
+
}
|
|
409
|
+
const data = [
|
|
410
|
+
[
|
|
411
|
+
{ name: 1, value: 1, color: '#00ff00' },
|
|
412
|
+
{ name: 2, value: 2 },
|
|
413
|
+
],
|
|
414
|
+
]
|
|
415
|
+
const p = (dataIndex: number, name: number) => ({
|
|
416
|
+
dataIndex,
|
|
417
|
+
color: '#0000ff',
|
|
418
|
+
value: { name },
|
|
419
|
+
})
|
|
420
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
421
|
+
|
|
422
|
+
it('datum colour wins over the series colour; template next', () => {
|
|
423
|
+
const merge = createTimeseriesOptionFactory({
|
|
424
|
+
theme,
|
|
425
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
426
|
+
})
|
|
427
|
+
const out = merge(
|
|
428
|
+
{ series: [{ type: 'line', itemStyle: { color: '#123456' } }] },
|
|
429
|
+
data,
|
|
430
|
+
) as Out
|
|
431
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
432
|
+
expect(colorFn(p(0, 1))).toBe('#00ff00')
|
|
433
|
+
expect(colorFn(p(1, 2))).toBe('#123456')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('calls a function template itemStyle.color', () => {
|
|
437
|
+
const factory = createTimeseriesOptionFactory({
|
|
438
|
+
theme,
|
|
439
|
+
optionsOverride: {
|
|
440
|
+
series: [{ type: 'line', itemStyle: { color: () => '#ff0000' } }],
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
const out = factory(factory(undefined, undefined), [
|
|
444
|
+
[{ name: 1, value: 1 }],
|
|
445
|
+
]) as Out
|
|
446
|
+
expect(out.series[0]!.itemStyle.color(p(0, 1))).toBe('#ff0000')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('dims item colours outside the selection', () => {
|
|
450
|
+
const merge = createTimeseriesOptionFactory({ theme, selection: [2] })
|
|
451
|
+
const out = merge({}, data) as Out
|
|
452
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
453
|
+
expect(colorFn(p(0, 1))).toBe(dim('#00ff00'))
|
|
454
|
+
expect(colorFn(p(1, 2))).toBe('#0000ff')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('shows symbols when a point has its own colour, not otherwise', () => {
|
|
458
|
+
const merge = createTimeseriesOptionFactory({ theme })
|
|
459
|
+
expect((merge({}, data) as Out).series[0]!.showSymbol).toBe(true)
|
|
460
|
+
const plain = merge({}, [[{ name: 1, value: 1 }]]) as Out
|
|
461
|
+
expect(plain.series[0]!.showSymbol).toBe(false)
|
|
462
|
+
expect(plain.series[0]!.itemStyle.color(p(0, 1))).toBe('#0000ff')
|
|
463
|
+
})
|
|
464
|
+
})
|
|
@@ -12,6 +12,7 @@ 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'
|
|
15
16
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
16
17
|
import type {
|
|
17
18
|
TimeseriesEChartsOption,
|
|
@@ -237,24 +238,39 @@ export function createTimeseriesOptionFactory(
|
|
|
237
238
|
// and points would stay dimmed forever after an external clear.
|
|
238
239
|
// Always emitting lets normal merge swap the callback in place, no
|
|
239
240
|
// `replaceMerge` and no entry-animation flash on selection on/off.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
241
|
+
//
|
|
242
|
+
// The base colour is the point's own `color`, else the template's
|
|
243
|
+
// `itemStyle.color`, else the series colour / palette (`params.color`).
|
|
244
|
+
const makeDimItemStyle = (datums: readonly object[], template: unknown) => {
|
|
245
|
+
const baseColor = createItemColorResolver({
|
|
246
|
+
theme,
|
|
247
|
+
datums,
|
|
248
|
+
template,
|
|
249
|
+
fallback: (params) => params.color as string,
|
|
250
|
+
})
|
|
251
|
+
return {
|
|
252
|
+
color: (params: CallbackDataParams) => {
|
|
253
|
+
const datum = params.value as
|
|
254
|
+
{ name?: Date | number | string } | undefined
|
|
255
|
+
const raw = datum?.name
|
|
256
|
+
const base = baseColor(params)
|
|
257
|
+
if (!selectionSet || raw == null) return base
|
|
258
|
+
return selectionSet.has(normalizeName(raw))
|
|
259
|
+
? base
|
|
260
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
261
|
+
},
|
|
262
|
+
}
|
|
252
263
|
}
|
|
264
|
+
// Per-point colours only show on markers, so surface them whenever a
|
|
265
|
+
// point carries its own `color` (same reason as the selection case).
|
|
266
|
+
const hasDatumColor = seriesArr.some((s) =>
|
|
267
|
+
s.some((d) => typeof d?.color === 'string' && d.color !== ''),
|
|
268
|
+
)
|
|
253
269
|
|
|
254
270
|
return {
|
|
255
271
|
...option,
|
|
256
272
|
dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),
|
|
257
|
-
series: seriesArr.map((
|
|
273
|
+
series: seriesArr.map((s, i) => {
|
|
258
274
|
const template =
|
|
259
275
|
(seriesTemplates[i] as object | undefined) ??
|
|
260
276
|
(broadcastTemplate as object)
|
|
@@ -272,10 +288,10 @@ export function createTimeseriesOptionFactory(
|
|
|
272
288
|
// When a selection is active, surface markers so the per-point
|
|
273
289
|
// color callback has something to dim — a continuous line would
|
|
274
290
|
// hide the visual selection feedback.
|
|
275
|
-
showSymbol: selectionSet != null,
|
|
291
|
+
showSymbol: selectionSet != null || hasDatumColor,
|
|
276
292
|
...(area ? { areaStyle: {} } : {}),
|
|
277
293
|
emphasis: { focus: 'series' },
|
|
278
|
-
itemStyle:
|
|
294
|
+
itemStyle: makeDimItemStyle(s, template),
|
|
279
295
|
...(overrideColor
|
|
280
296
|
? { color: overrideColor, lineStyle: { color: overrideColor } }
|
|
281
297
|
: {}),
|
|
@@ -327,8 +343,7 @@ function buildTimeseriesTooltipFormatter(
|
|
|
327
343
|
) {
|
|
328
344
|
return createTooltipFormatter((item) => {
|
|
329
345
|
const row = item.value as
|
|
330
|
-
|
|
331
|
-
| undefined
|
|
346
|
+
{ name?: Date | number | string; value?: number } | undefined
|
|
332
347
|
const raw = row?.value
|
|
333
348
|
const formattedValue =
|
|
334
349
|
typeof raw === 'number' && formatter ? formatter(raw) : (raw ?? '')
|
|
@@ -10,6 +10,12 @@ import type { WidgetSeries } from '../types'
|
|
|
10
10
|
export interface TimeseriesDatum {
|
|
11
11
|
name: Date | number | string
|
|
12
12
|
value: number
|
|
13
|
+
/**
|
|
14
|
+
* Optional colour for this item (CSS colour or theme palette path such as
|
|
15
|
+
* `'primary.main'`). Wins over the template's `itemStyle.color` and the
|
|
16
|
+
* series colour; selection dimming still applies on top.
|
|
17
|
+
*/
|
|
18
|
+
color?: string
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
/** Timeseries widget data — one array of points per series. */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { createTheme } from '@mui/material'
|
|
3
|
+
import type { CallbackDataParams } from 'echarts/types/dist/shared'
|
|
4
|
+
import { createItemColorResolver } from './item-color'
|
|
5
|
+
|
|
6
|
+
const theme = createTheme({ palette: { primary: { main: '#1976d2' } } })
|
|
7
|
+
const params = (dataIndex: number) =>
|
|
8
|
+
({ dataIndex, color: '#fallback' }) as unknown as CallbackDataParams
|
|
9
|
+
const fallback = (p: CallbackDataParams) => p.color as string
|
|
10
|
+
|
|
11
|
+
describe('createItemColorResolver', () => {
|
|
12
|
+
it('uses the datum colour first', () => {
|
|
13
|
+
const resolve = createItemColorResolver({
|
|
14
|
+
theme,
|
|
15
|
+
datums: [{ color: '#00ff00' }],
|
|
16
|
+
template: { itemStyle: { color: '#123456' } },
|
|
17
|
+
fallback,
|
|
18
|
+
})
|
|
19
|
+
expect(resolve(params(0))).toBe('#00ff00')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('uses a string template itemStyle.color when the datum has none', () => {
|
|
23
|
+
const resolve = createItemColorResolver({
|
|
24
|
+
theme,
|
|
25
|
+
datums: [{}, 3],
|
|
26
|
+
template: { itemStyle: { color: '#123456' } },
|
|
27
|
+
fallback,
|
|
28
|
+
})
|
|
29
|
+
expect(resolve(params(0))).toBe('#123456')
|
|
30
|
+
expect(resolve(params(1))).toBe('#123456')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('calls a function template itemStyle.color with the ECharts params', () => {
|
|
34
|
+
const color = vi.fn(() => '#abcdef')
|
|
35
|
+
const resolve = createItemColorResolver({
|
|
36
|
+
theme,
|
|
37
|
+
datums: [{}],
|
|
38
|
+
template: { itemStyle: { color } },
|
|
39
|
+
fallback,
|
|
40
|
+
})
|
|
41
|
+
const p = params(0)
|
|
42
|
+
expect(resolve(p)).toBe('#abcdef')
|
|
43
|
+
expect(color).toHaveBeenCalledWith(p)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('falls back when neither datum nor template give a colour', () => {
|
|
47
|
+
const resolve = createItemColorResolver({
|
|
48
|
+
theme,
|
|
49
|
+
datums: [{ value: 1 }],
|
|
50
|
+
template: { itemStyle: { color: () => undefined } },
|
|
51
|
+
fallback,
|
|
52
|
+
})
|
|
53
|
+
expect(resolve(params(0))).toBe('#fallback')
|
|
54
|
+
expect(resolve(params(5))).toBe('#fallback')
|
|
55
|
+
const noTemplate = createItemColorResolver({
|
|
56
|
+
theme,
|
|
57
|
+
datums: [],
|
|
58
|
+
template: undefined,
|
|
59
|
+
fallback,
|
|
60
|
+
})
|
|
61
|
+
expect(noTemplate(params(0))).toBe('#fallback')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('resolves theme palette paths on datum and template colours', () => {
|
|
65
|
+
const resolve = createItemColorResolver({
|
|
66
|
+
theme,
|
|
67
|
+
datums: [{ color: 'primary.main' }, {}],
|
|
68
|
+
template: { itemStyle: { color: () => 'primary.main' } },
|
|
69
|
+
fallback,
|
|
70
|
+
})
|
|
71
|
+
expect(resolve(params(0))).toBe('#1976d2')
|
|
72
|
+
expect(resolve(params(1))).toBe('#1976d2')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Theme } from '@mui/material'
|
|
2
|
+
import type { CallbackDataParams } from 'echarts/types/dist/shared'
|
|
3
|
+
import { resolveThemeColor } from './resolve-theme-color'
|
|
4
|
+
|
|
5
|
+
/** ECharts `itemStyle.color` callback, as emitted by the chart widgets. */
|
|
6
|
+
export type ItemColorCallback = (params: CallbackDataParams) => string
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds the callback that resolves the **base** colour of every item of one
|
|
10
|
+
* series, before any selection dimming. Priority:
|
|
11
|
+
*
|
|
12
|
+
* 1. the datum's own `color` (`datums[params.dataIndex].color`);
|
|
13
|
+
* 2. the consumer's `template.itemStyle.color` (from `optionsOverride`):
|
|
14
|
+
* a string is used as is, a function is called with the ECharts params;
|
|
15
|
+
* 3. `fallback` — the widget's default (series colour / palette).
|
|
16
|
+
*
|
|
17
|
+
* The datum is looked up by `params.dataIndex` in the widget's own data
|
|
18
|
+
* rather than read off `params.data`, so it works whatever the dataset row
|
|
19
|
+
* shape is (object rows, `[label, count]` tuples, `[x, y]` tuples).
|
|
20
|
+
*
|
|
21
|
+
* Datum and template colours go through {@link resolveThemeColor}, same as
|
|
22
|
+
* per-series colours, so palette paths such as `'primary.main'` work.
|
|
23
|
+
*/
|
|
24
|
+
export function createItemColorResolver({
|
|
25
|
+
theme,
|
|
26
|
+
datums,
|
|
27
|
+
template,
|
|
28
|
+
fallback,
|
|
29
|
+
}: {
|
|
30
|
+
theme: Theme
|
|
31
|
+
datums: readonly unknown[]
|
|
32
|
+
template: unknown
|
|
33
|
+
fallback: ItemColorCallback
|
|
34
|
+
}): ItemColorCallback {
|
|
35
|
+
const templateColor = (template as { itemStyle?: { color?: unknown } })
|
|
36
|
+
?.itemStyle?.color
|
|
37
|
+
return (params) => {
|
|
38
|
+
const datumColor = readColor(datums[params.dataIndex])
|
|
39
|
+
if (datumColor) return resolveThemeColor(theme, datumColor)!
|
|
40
|
+
const custom =
|
|
41
|
+
typeof templateColor === 'function'
|
|
42
|
+
? (templateColor as (p: CallbackDataParams) => unknown)(params)
|
|
43
|
+
: templateColor
|
|
44
|
+
if (typeof custom === 'string' && custom !== '') {
|
|
45
|
+
return resolveThemeColor(theme, custom)!
|
|
46
|
+
}
|
|
47
|
+
return fallback(params)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readColor(datum: unknown): string | undefined {
|
|
52
|
+
if (datum == null || typeof datum !== 'object') return undefined
|
|
53
|
+
const color = (datum as { color?: unknown }).color
|
|
54
|
+
return typeof color === 'string' && color !== '' ? color : undefined
|
|
55
|
+
}
|