@carto/ps-react-ui 5.4.0 → 5.5.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.
Files changed (67) hide show
  1. package/dist/{actions-Dhlz_adK.js → actions-CwjqEm8Y.js} +56 -46
  2. package/dist/actions-CwjqEm8Y.js.map +1 -0
  3. package/dist/item-color-ZYxH3NM5.js +20 -0
  4. package/dist/item-color-ZYxH3NM5.js.map +1 -0
  5. package/dist/types/widgets-v2/bar/types.d.ts +6 -0
  6. package/dist/types/widgets-v2/histogram/bins.d.ts +7 -0
  7. package/dist/types/widgets-v2/histogram/download.d.ts +2 -2
  8. package/dist/types/widgets-v2/histogram/index.d.ts +2 -1
  9. package/dist/types/widgets-v2/histogram/options.d.ts +1 -1
  10. package/dist/types/widgets-v2/histogram/transforms.d.ts +5 -4
  11. package/dist/types/widgets-v2/histogram/types.d.ts +22 -5
  12. package/dist/types/widgets-v2/index.d.ts +1 -1
  13. package/dist/types/widgets-v2/pie/types.d.ts +6 -0
  14. package/dist/types/widgets-v2/scatterplot/download.d.ts +2 -2
  15. package/dist/types/widgets-v2/scatterplot/index.d.ts +2 -1
  16. package/dist/types/widgets-v2/scatterplot/points.d.ts +7 -0
  17. package/dist/types/widgets-v2/scatterplot/transforms.d.ts +4 -3
  18. package/dist/types/widgets-v2/scatterplot/types.d.ts +21 -2
  19. package/dist/types/widgets-v2/timeseries/types.d.ts +6 -0
  20. package/dist/types/widgets-v2/utils/item-color.d.ts +26 -0
  21. package/dist/widgets-v2/actions.js +1 -1
  22. package/dist/widgets-v2/bar.js +66 -57
  23. package/dist/widgets-v2/bar.js.map +1 -1
  24. package/dist/widgets-v2/histogram.js +103 -71
  25. package/dist/widgets-v2/histogram.js.map +1 -1
  26. package/dist/widgets-v2/pie.js +98 -78
  27. package/dist/widgets-v2/pie.js.map +1 -1
  28. package/dist/widgets-v2/scatterplot.js +119 -79
  29. package/dist/widgets-v2/scatterplot.js.map +1 -1
  30. package/dist/widgets-v2/timeseries.js +70 -61
  31. package/dist/widgets-v2/timeseries.js.map +1 -1
  32. package/dist/widgets-v2.js +1 -1
  33. package/dist/widgets-v2.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/widgets-v2/actions/fullscreen/fullscreen-store.test.tsx +77 -13
  36. package/src/widgets-v2/actions/fullscreen/fullscreen.tsx +48 -5
  37. package/src/widgets-v2/bar/options.test.ts +106 -0
  38. package/src/widgets-v2/bar/options.ts +25 -14
  39. package/src/widgets-v2/bar/types.ts +6 -0
  40. package/src/widgets-v2/histogram/bins.ts +22 -0
  41. package/src/widgets-v2/histogram/download.test.ts +10 -0
  42. package/src/widgets-v2/histogram/download.ts +7 -3
  43. package/src/widgets-v2/histogram/index.ts +3 -0
  44. package/src/widgets-v2/histogram/options.test.ts +62 -0
  45. package/src/widgets-v2/histogram/options.ts +38 -17
  46. package/src/widgets-v2/histogram/transforms.test.ts +21 -7
  47. package/src/widgets-v2/histogram/transforms.ts +26 -12
  48. package/src/widgets-v2/histogram/types.ts +25 -5
  49. package/src/widgets-v2/index.ts +6 -1
  50. package/src/widgets-v2/pie/options.test.ts +91 -0
  51. package/src/widgets-v2/pie/options.ts +50 -20
  52. package/src/widgets-v2/pie/types.ts +6 -0
  53. package/src/widgets-v2/scatterplot/download.test.ts +9 -0
  54. package/src/widgets-v2/scatterplot/download.ts +5 -3
  55. package/src/widgets-v2/scatterplot/index.ts +3 -0
  56. package/src/widgets-v2/scatterplot/options.test.ts +78 -0
  57. package/src/widgets-v2/scatterplot/options.ts +35 -11
  58. package/src/widgets-v2/scatterplot/points.ts +35 -0
  59. package/src/widgets-v2/scatterplot/transforms.test.ts +19 -0
  60. package/src/widgets-v2/scatterplot/transforms.ts +23 -18
  61. package/src/widgets-v2/scatterplot/types.ts +25 -2
  62. package/src/widgets-v2/timeseries/options.test.ts +65 -0
  63. package/src/widgets-v2/timeseries/options.ts +31 -14
  64. package/src/widgets-v2/timeseries/types.ts +6 -0
  65. package/src/widgets-v2/utils/item-color.test.ts +74 -0
  66. package/src/widgets-v2/utils/item-color.ts +55 -0
  67. package/dist/actions-Dhlz_adK.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
- * `[number, number][]` — each series is a list of `[x, y]` tuples.
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 `[number, number][]` fall through
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 (!isXyTupleArray(series)) return series
21
- const total = series.reduce((acc, [, y]) => acc + Math.abs(y), 0)
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(([x, y]) => [x, (y / total) * 100] as [number, number])
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 isXyTupleArray(v: unknown): v is [number, number][] {
28
- if (!Array.isArray(v)) return false
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 `[number, number][][]` —
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 (!isXyTupleArray(series)) return series
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
- /** Scatterplot widget data — array of series, each a list of `[x, y]` tuples. */
9
- export type ScatterplotWidgetData = readonly (readonly ScatterplotDatum[])[]
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,23 +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
- const dimItemStyle = {
241
- color: (params: CallbackDataParams) => {
242
- const datum = params.value as
243
- { name?: Date | number | string } | undefined
244
- const raw = datum?.name
245
- const base = params.color as string
246
- if (!selectionSet || raw == null) return base
247
- return selectionSet.has(normalizeName(raw))
248
- ? base
249
- : echarts.color.modifyAlpha(base, 0.15)
250
- },
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
+ }
251
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
+ )
252
269
 
253
270
  return {
254
271
  ...option,
255
272
  dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),
256
- series: seriesArr.map((_, i) => {
273
+ series: seriesArr.map((s, i) => {
257
274
  const template =
258
275
  (seriesTemplates[i] as object | undefined) ??
259
276
  (broadcastTemplate as object)
@@ -271,10 +288,10 @@ export function createTimeseriesOptionFactory(
271
288
  // When a selection is active, surface markers so the per-point
272
289
  // color callback has something to dim — a continuous line would
273
290
  // hide the visual selection feedback.
274
- showSymbol: selectionSet != null,
291
+ showSymbol: selectionSet != null || hasDatumColor,
275
292
  ...(area ? { areaStyle: {} } : {}),
276
293
  emphasis: { focus: 'series' },
277
- itemStyle: dimItemStyle,
294
+ itemStyle: makeDimItemStyle(s, template),
278
295
  ...(overrideColor
279
296
  ? { color: overrideColor, lineStyle: { color: overrideColor } }
280
297
  : {}),
@@ -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
+ }