@carto/ps-react-ui 5.3.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.
- package/dist/{actions-Dhlz_adK.js → actions-CwjqEm8Y.js} +56 -46
- package/dist/actions-CwjqEm8Y.js.map +1 -0
- package/dist/category-C6myUFUJ.js +936 -0
- package/dist/category-C6myUFUJ.js.map +1 -0
- package/dist/item-color-ZYxH3NM5.js +20 -0
- package/dist/item-color-ZYxH3NM5.js.map +1 -0
- package/dist/types/widgets-v2/bar/types.d.ts +6 -0
- package/dist/types/widgets-v2/category/category-ui.d.ts +10 -2
- package/dist/types/widgets-v2/category/category.d.ts +7 -1
- package/dist/types/widgets-v2/category/components/category-bar-stacked.d.ts +8 -1
- package/dist/types/widgets-v2/category/components/category-bar.d.ts +24 -2
- package/dist/types/widgets-v2/category/components/category-row-multi.d.ts +3 -1
- package/dist/types/widgets-v2/category/components/category-row-single.d.ts +3 -1
- package/dist/types/widgets-v2/category/components/category-row-stacked.d.ts +10 -1
- package/dist/types/widgets-v2/category/style.d.ts +33 -6
- 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/category.js +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 +2 -2
- 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/category/category-ui.test.tsx +139 -0
- package/src/widgets-v2/category/category-ui.tsx +64 -11
- package/src/widgets-v2/category/category.test.tsx +67 -0
- package/src/widgets-v2/category/category.tsx +32 -15
- package/src/widgets-v2/category/components/category-bar-stacked.test.tsx +18 -0
- package/src/widgets-v2/category/components/category-bar-stacked.tsx +42 -29
- package/src/widgets-v2/category/components/category-bar.test.tsx +66 -0
- package/src/widgets-v2/category/components/category-bar.tsx +65 -9
- package/src/widgets-v2/category/components/category-row-multi.tsx +21 -0
- package/src/widgets-v2/category/components/category-row-single.tsx +4 -0
- package/src/widgets-v2/category/components/category-row-stacked.tsx +24 -1
- package/src/widgets-v2/category/style.ts +58 -14
- 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 +31 -14
- 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.map +0 -1
- package/dist/category-D01ruG-l.js +0 -842
- package/dist/category-D01ruG-l.js.map +0 -1
|
@@ -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,
|
|
@@ -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 { createScatterplotOptionFactory, scatterplotOptions } from './options'
|
|
4
5
|
|
|
@@ -409,3 +410,80 @@ describe('createScatterplotOptionFactory', () => {
|
|
|
409
410
|
expect(insideOnly.grid.right).toBe(baseline.grid.right)
|
|
410
411
|
})
|
|
411
412
|
})
|
|
413
|
+
|
|
414
|
+
describe('createScatterplotOptionFactory — per-item colour', () => {
|
|
415
|
+
interface Out {
|
|
416
|
+
dataset: { source: number[][] }[]
|
|
417
|
+
series: { itemStyle: { color: (p: unknown) => string } }[]
|
|
418
|
+
xAxis: { min: number; max: number }
|
|
419
|
+
yAxis: { min: number; max: number }
|
|
420
|
+
}
|
|
421
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
422
|
+
const data = [
|
|
423
|
+
[{ x: 1, y: 2, color: '#00ff00' }, [3, 4] as const, { x: -5, y: 60 }],
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
it('flattens object points to [x, y] rows and bounds them', () => {
|
|
427
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
428
|
+
const out = merge({}, data) as Out
|
|
429
|
+
expect(out.dataset[0]!.source).toEqual([
|
|
430
|
+
[1, 2],
|
|
431
|
+
[3, 4],
|
|
432
|
+
[-5, 60],
|
|
433
|
+
])
|
|
434
|
+
expect(out.xAxis.min).toBeLessThanOrEqual(-5)
|
|
435
|
+
expect(out.xAxis.max).toBeGreaterThanOrEqual(3)
|
|
436
|
+
expect(out.yAxis.max).toBeGreaterThanOrEqual(60)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('keeps tuple-only series as the dataset source', () => {
|
|
440
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
441
|
+
const series = [[1, 2] as const, [3, 4] as const]
|
|
442
|
+
const out = merge({}, [series]) as Out
|
|
443
|
+
expect(out.dataset[0]!.source).toBe(series)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('point colour wins over the series colour; template next; dims', () => {
|
|
447
|
+
const merge = createScatterplotOptionFactory({
|
|
448
|
+
theme,
|
|
449
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
450
|
+
selection: ['0:0', '0:1'],
|
|
451
|
+
})
|
|
452
|
+
const out = merge(
|
|
453
|
+
{ series: [{ type: 'scatter', itemStyle: { color: '#123456' } }] },
|
|
454
|
+
data,
|
|
455
|
+
) as Out
|
|
456
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
457
|
+
expect(colorFn({ dataIndex: 0, color: '#0000ff' })).toBe('#00ff00')
|
|
458
|
+
expect(colorFn({ dataIndex: 1, color: '#0000ff' })).toBe('#123456')
|
|
459
|
+
expect(colorFn({ dataIndex: 2, color: '#0000ff' })).toBe(dim('#123456'))
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('calls a function template itemStyle.color via optionsOverride', () => {
|
|
463
|
+
const factory = createScatterplotOptionFactory({
|
|
464
|
+
theme,
|
|
465
|
+
optionsOverride: {
|
|
466
|
+
series: [
|
|
467
|
+
{
|
|
468
|
+
type: 'scatter',
|
|
469
|
+
itemStyle: {
|
|
470
|
+
color: (p: { dataIndex: number }) => `#c${p.dataIndex}`,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
const out = factory(factory(undefined, undefined), [[[1, 2]]]) as Out
|
|
477
|
+
expect(out.series[0]!.itemStyle.color({ dataIndex: 0, color: '#x' })).toBe(
|
|
478
|
+
'#c0',
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('keeps the series / palette colour for plain tuples', () => {
|
|
483
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
484
|
+
const out = merge({}, [[[1, 2]]]) as Out
|
|
485
|
+
expect(
|
|
486
|
+
out.series[0]!.itemStyle.color({ dataIndex: 0, color: '#abc' }),
|
|
487
|
+
).toBe('#abc')
|
|
488
|
+
})
|
|
489
|
+
})
|
|
@@ -12,10 +12,13 @@ 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 { getScatterplotXY } from './points'
|
|
15
17
|
import type {
|
|
16
18
|
ScatterplotEChartsOption,
|
|
17
19
|
ScatterplotOptionFactoryInput,
|
|
18
20
|
ScatterplotOptionsInput,
|
|
21
|
+
ScatterplotItem,
|
|
19
22
|
ScatterplotWidgetData,
|
|
20
23
|
} from './types'
|
|
21
24
|
|
|
@@ -156,7 +159,9 @@ export function createScatterplotOptionFactory(
|
|
|
156
159
|
: structural
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
const seriesArr = Array.isArray(data)
|
|
162
|
+
const seriesArr = Array.isArray(data)
|
|
163
|
+
? (data as ScatterplotWidgetData<ScatterplotItem>)
|
|
164
|
+
: []
|
|
160
165
|
if (seriesArr.length === 0) {
|
|
161
166
|
return { ...option, dataset: [], series: [] }
|
|
162
167
|
}
|
|
@@ -198,15 +203,26 @@ export function createScatterplotOptionFactory(
|
|
|
198
203
|
// Keeping the key always-present means a plain setOption merge swaps
|
|
199
204
|
// the callback in place, no `replaceMerge` and no entry-animation
|
|
200
205
|
// flash on selection on/off.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
//
|
|
207
|
+
// The base colour is the point's own `color` (object points), else the
|
|
208
|
+
// template's `itemStyle.color`, else the series colour / palette.
|
|
209
|
+
const seriesTemplates = Array.isArray(option.series) ? option.series : []
|
|
210
|
+
const makeDimColor = (seriesIdx: number, datums: readonly unknown[]) => {
|
|
211
|
+
const baseColor = createItemColorResolver({
|
|
212
|
+
theme,
|
|
213
|
+
datums,
|
|
214
|
+
template: seriesTemplates[seriesIdx] ?? seriesTemplates[0],
|
|
215
|
+
fallback: (params) => params.color as string,
|
|
216
|
+
})
|
|
217
|
+
return (params: CallbackDataParams) => {
|
|
218
|
+
const base = baseColor(params)
|
|
204
219
|
if (!selectionSet) return base
|
|
205
220
|
const key = `${seriesIdx}:${params.dataIndex}`
|
|
206
221
|
return selectionSet.has(key)
|
|
207
222
|
? base
|
|
208
223
|
: echarts.color.modifyAlpha(base, 0.15)
|
|
209
224
|
}
|
|
225
|
+
}
|
|
210
226
|
|
|
211
227
|
// Zoom slider layout: when ZoomToggle has installed `dataZoom`,
|
|
212
228
|
// reserve grid space and lift any x-slider above the legend (if
|
|
@@ -232,9 +248,15 @@ export function createScatterplotOptionFactory(
|
|
|
232
248
|
return {
|
|
233
249
|
...option,
|
|
234
250
|
// ECharts dataset.source wants a mutable [number, number][] shape; we
|
|
235
|
-
// hold readonly tuples internally, so cast at the boundary.
|
|
236
|
-
|
|
237
|
-
|
|
251
|
+
// hold readonly tuples internally, so cast at the boundary. Object
|
|
252
|
+
// points are flattened to `[x, y]` so the positional encode and the
|
|
253
|
+
// tooltip keep reading tuples.
|
|
254
|
+
dataset: seriesArr.map((s) => ({
|
|
255
|
+
source: (s.every(Array.isArray)
|
|
256
|
+
? s
|
|
257
|
+
: s.map(getScatterplotXY)) as unknown as number[][],
|
|
258
|
+
})),
|
|
259
|
+
series: seriesArr.map((s, i) => {
|
|
238
260
|
const overrideColor = resolveThemeColor(theme, series?.[i]?.color)
|
|
239
261
|
return {
|
|
240
262
|
type: 'scatter' as const,
|
|
@@ -243,7 +265,7 @@ export function createScatterplotOptionFactory(
|
|
|
243
265
|
encode: { x: 0, y: 1 },
|
|
244
266
|
symbolSize,
|
|
245
267
|
emphasis: { focus: 'series' },
|
|
246
|
-
itemStyle: { color: makeDimColor(i) },
|
|
268
|
+
itemStyle: { color: makeDimColor(i, s) },
|
|
247
269
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
248
270
|
}
|
|
249
271
|
}),
|
|
@@ -386,7 +408,9 @@ function buildScatterTooltipFormatter(
|
|
|
386
408
|
})
|
|
387
409
|
}
|
|
388
410
|
|
|
389
|
-
function computeScatterBounds(
|
|
411
|
+
function computeScatterBounds(
|
|
412
|
+
seriesArr: ScatterplotWidgetData<ScatterplotItem>,
|
|
413
|
+
): {
|
|
390
414
|
niceMinX: number
|
|
391
415
|
niceMaxX: number
|
|
392
416
|
niceMinY: number
|
|
@@ -398,8 +422,8 @@ function computeScatterBounds(seriesArr: ScatterplotWidgetData): {
|
|
|
398
422
|
let maxY = -Infinity
|
|
399
423
|
for (const series of seriesArr) {
|
|
400
424
|
for (const point of series) {
|
|
401
|
-
|
|
402
|
-
const y = point
|
|
425
|
+
if (point == null) continue
|
|
426
|
+
const [x, y] = getScatterplotXY(point)
|
|
403
427
|
if (typeof x === 'number' && Number.isFinite(x)) {
|
|
404
428
|
if (x < minX) minX = x
|
|
405
429
|
if (x > maxX) maxX = x
|
|
@@ -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
|
+
})
|