@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
|
@@ -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
|
|
|
@@ -212,6 +213,18 @@ describe('createScatterplotOptionFactory', () => {
|
|
|
212
213
|
expect(out.yAxis.min).toBe(0)
|
|
213
214
|
})
|
|
214
215
|
|
|
216
|
+
it("pins the y ends a config transform set (RelativeData 'total')", () => {
|
|
217
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
218
|
+
const out = merge({ yAxis: { min: -100, max: 100 } }, [
|
|
219
|
+
[
|
|
220
|
+
[1, 47],
|
|
221
|
+
[2, 3],
|
|
222
|
+
],
|
|
223
|
+
]) as { yAxis: { min: number; max: number } }
|
|
224
|
+
expect(out.yAxis.max).toBe(100)
|
|
225
|
+
expect(out.yAxis.min).toBe(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
215
228
|
it('always emits itemStyle.color as a callback (passthrough when no selection)', () => {
|
|
216
229
|
const merge = createScatterplotOptionFactory({ theme })
|
|
217
230
|
const out = merge({}, [[[1, 2]]]) as {
|
|
@@ -409,3 +422,80 @@ describe('createScatterplotOptionFactory', () => {
|
|
|
409
422
|
expect(insideOnly.grid.right).toBe(baseline.grid.right)
|
|
410
423
|
})
|
|
411
424
|
})
|
|
425
|
+
|
|
426
|
+
describe('createScatterplotOptionFactory — per-item colour', () => {
|
|
427
|
+
interface Out {
|
|
428
|
+
dataset: { source: number[][] }[]
|
|
429
|
+
series: { itemStyle: { color: (p: unknown) => string } }[]
|
|
430
|
+
xAxis: { min: number; max: number }
|
|
431
|
+
yAxis: { min: number; max: number }
|
|
432
|
+
}
|
|
433
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
434
|
+
const data = [
|
|
435
|
+
[{ x: 1, y: 2, color: '#00ff00' }, [3, 4] as const, { x: -5, y: 60 }],
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
it('flattens object points to [x, y] rows and bounds them', () => {
|
|
439
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
440
|
+
const out = merge({}, data) as Out
|
|
441
|
+
expect(out.dataset[0]!.source).toEqual([
|
|
442
|
+
[1, 2],
|
|
443
|
+
[3, 4],
|
|
444
|
+
[-5, 60],
|
|
445
|
+
])
|
|
446
|
+
expect(out.xAxis.min).toBeLessThanOrEqual(-5)
|
|
447
|
+
expect(out.xAxis.max).toBeGreaterThanOrEqual(3)
|
|
448
|
+
expect(out.yAxis.max).toBeGreaterThanOrEqual(60)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('keeps tuple-only series as the dataset source', () => {
|
|
452
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
453
|
+
const series = [[1, 2] as const, [3, 4] as const]
|
|
454
|
+
const out = merge({}, [series]) as Out
|
|
455
|
+
expect(out.dataset[0]!.source).toBe(series)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('point colour wins over the series colour; template next; dims', () => {
|
|
459
|
+
const merge = createScatterplotOptionFactory({
|
|
460
|
+
theme,
|
|
461
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
462
|
+
selection: ['0:0', '0:1'],
|
|
463
|
+
})
|
|
464
|
+
const out = merge(
|
|
465
|
+
{ series: [{ type: 'scatter', itemStyle: { color: '#123456' } }] },
|
|
466
|
+
data,
|
|
467
|
+
) as Out
|
|
468
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
469
|
+
expect(colorFn({ dataIndex: 0, color: '#0000ff' })).toBe('#00ff00')
|
|
470
|
+
expect(colorFn({ dataIndex: 1, color: '#0000ff' })).toBe('#123456')
|
|
471
|
+
expect(colorFn({ dataIndex: 2, color: '#0000ff' })).toBe(dim('#123456'))
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('calls a function template itemStyle.color via optionsOverride', () => {
|
|
475
|
+
const factory = createScatterplotOptionFactory({
|
|
476
|
+
theme,
|
|
477
|
+
optionsOverride: {
|
|
478
|
+
series: [
|
|
479
|
+
{
|
|
480
|
+
type: 'scatter',
|
|
481
|
+
itemStyle: {
|
|
482
|
+
color: (p: { dataIndex: number }) => `#c${p.dataIndex}`,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
})
|
|
488
|
+
const out = factory(factory(undefined, undefined), [[[1, 2]]]) as Out
|
|
489
|
+
expect(out.series[0]!.itemStyle.color({ dataIndex: 0, color: '#x' })).toBe(
|
|
490
|
+
'#c0',
|
|
491
|
+
)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('keeps the series / palette colour for plain tuples', () => {
|
|
495
|
+
const merge = createScatterplotOptionFactory({ theme })
|
|
496
|
+
const out = merge({}, [[[1, 2]]]) as Out
|
|
497
|
+
expect(
|
|
498
|
+
out.series[0]!.itemStyle.color({ dataIndex: 0, color: '#abc' }),
|
|
499
|
+
).toBe('#abc')
|
|
500
|
+
})
|
|
501
|
+
})
|
|
@@ -12,12 +12,16 @@ 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'
|
|
24
|
+
import { fixedAxisBounds } from '../actions/relative-data/scale'
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Builds the **structural** ECharts option for a scatterplot widget — both
|
|
@@ -156,7 +160,9 @@ export function createScatterplotOptionFactory(
|
|
|
156
160
|
: structural
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
const seriesArr = Array.isArray(data)
|
|
163
|
+
const seriesArr = Array.isArray(data)
|
|
164
|
+
? (data as ScatterplotWidgetData<ScatterplotItem>)
|
|
165
|
+
: []
|
|
160
166
|
if (seriesArr.length === 0) {
|
|
161
167
|
return { ...option, dataset: [], series: [] }
|
|
162
168
|
}
|
|
@@ -184,8 +190,12 @@ export function createScatterplotOptionFactory(
|
|
|
184
190
|
|
|
185
191
|
const reactiveFormatter = ctx?.formatter
|
|
186
192
|
|
|
187
|
-
const { niceMinX, niceMaxX, niceMinY, niceMaxY } =
|
|
188
|
-
|
|
193
|
+
const { niceMinX, niceMaxX, niceMinY, niceMaxY } = computeScatterBounds(
|
|
194
|
+
seriesArr,
|
|
195
|
+
// A config transform (RelativeData's `'total'` scale) may pin the
|
|
196
|
+
// y ends; each applies only on the side the data reaches.
|
|
197
|
+
fixedAxisBounds(baseYAxis),
|
|
198
|
+
)
|
|
189
199
|
|
|
190
200
|
// Dim non-selected points via `series.itemStyle.color`. The selection
|
|
191
201
|
// key is `${seriesIndex}:${dataIndex}`; we read those off the params
|
|
@@ -198,15 +208,26 @@ export function createScatterplotOptionFactory(
|
|
|
198
208
|
// Keeping the key always-present means a plain setOption merge swaps
|
|
199
209
|
// the callback in place, no `replaceMerge` and no entry-animation
|
|
200
210
|
// flash on selection on/off.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
//
|
|
212
|
+
// The base colour is the point's own `color` (object points), else the
|
|
213
|
+
// template's `itemStyle.color`, else the series colour / palette.
|
|
214
|
+
const seriesTemplates = Array.isArray(option.series) ? option.series : []
|
|
215
|
+
const makeDimColor = (seriesIdx: number, datums: readonly unknown[]) => {
|
|
216
|
+
const baseColor = createItemColorResolver({
|
|
217
|
+
theme,
|
|
218
|
+
datums,
|
|
219
|
+
template: seriesTemplates[seriesIdx] ?? seriesTemplates[0],
|
|
220
|
+
fallback: (params) => params.color as string,
|
|
221
|
+
})
|
|
222
|
+
return (params: CallbackDataParams) => {
|
|
223
|
+
const base = baseColor(params)
|
|
204
224
|
if (!selectionSet) return base
|
|
205
225
|
const key = `${seriesIdx}:${params.dataIndex}`
|
|
206
226
|
return selectionSet.has(key)
|
|
207
227
|
? base
|
|
208
228
|
: echarts.color.modifyAlpha(base, 0.15)
|
|
209
229
|
}
|
|
230
|
+
}
|
|
210
231
|
|
|
211
232
|
// Zoom slider layout: when ZoomToggle has installed `dataZoom`,
|
|
212
233
|
// reserve grid space and lift any x-slider above the legend (if
|
|
@@ -232,9 +253,15 @@ export function createScatterplotOptionFactory(
|
|
|
232
253
|
return {
|
|
233
254
|
...option,
|
|
234
255
|
// ECharts dataset.source wants a mutable [number, number][] shape; we
|
|
235
|
-
// hold readonly tuples internally, so cast at the boundary.
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
// hold readonly tuples internally, so cast at the boundary. Object
|
|
257
|
+
// points are flattened to `[x, y]` so the positional encode and the
|
|
258
|
+
// tooltip keep reading tuples.
|
|
259
|
+
dataset: seriesArr.map((s) => ({
|
|
260
|
+
source: (s.every(Array.isArray)
|
|
261
|
+
? s
|
|
262
|
+
: s.map(getScatterplotXY)) as unknown as number[][],
|
|
263
|
+
})),
|
|
264
|
+
series: seriesArr.map((s, i) => {
|
|
238
265
|
const overrideColor = resolveThemeColor(theme, series?.[i]?.color)
|
|
239
266
|
return {
|
|
240
267
|
type: 'scatter' as const,
|
|
@@ -243,7 +270,7 @@ export function createScatterplotOptionFactory(
|
|
|
243
270
|
encode: { x: 0, y: 1 },
|
|
244
271
|
symbolSize,
|
|
245
272
|
emphasis: { focus: 'series' },
|
|
246
|
-
itemStyle: { color: makeDimColor(i) },
|
|
273
|
+
itemStyle: { color: makeDimColor(i, s) },
|
|
247
274
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
248
275
|
}
|
|
249
276
|
}),
|
|
@@ -386,7 +413,10 @@ function buildScatterTooltipFormatter(
|
|
|
386
413
|
})
|
|
387
414
|
}
|
|
388
415
|
|
|
389
|
-
function computeScatterBounds(
|
|
416
|
+
function computeScatterBounds(
|
|
417
|
+
seriesArr: ScatterplotWidgetData<ScatterplotItem>,
|
|
418
|
+
fixedY: { min?: number; max?: number } = {},
|
|
419
|
+
): {
|
|
390
420
|
niceMinX: number
|
|
391
421
|
niceMaxX: number
|
|
392
422
|
niceMinY: number
|
|
@@ -398,8 +428,8 @@ function computeScatterBounds(seriesArr: ScatterplotWidgetData): {
|
|
|
398
428
|
let maxY = -Infinity
|
|
399
429
|
for (const series of seriesArr) {
|
|
400
430
|
for (const point of series) {
|
|
401
|
-
|
|
402
|
-
const y = point
|
|
431
|
+
if (point == null) continue
|
|
432
|
+
const [x, y] = getScatterplotXY(point)
|
|
403
433
|
if (typeof x === 'number' && Number.isFinite(x)) {
|
|
404
434
|
if (x < minX) minX = x
|
|
405
435
|
if (x > maxX) maxX = x
|
|
@@ -415,8 +445,16 @@ function computeScatterBounds(seriesArr: ScatterplotWidgetData): {
|
|
|
415
445
|
// negative mins, and floor max=0 to 1 so the chart always has range.
|
|
416
446
|
// Scatter can have free coordinates so we apply this per-axis.
|
|
417
447
|
const niceMaxX = Number.isFinite(maxX) ? (maxX <= 0 ? 1 : niceNum(maxX)) : 1
|
|
418
|
-
const niceMaxY = Number.isFinite(maxY)
|
|
448
|
+
const niceMaxY = Number.isFinite(maxY)
|
|
449
|
+
? maxY <= 0
|
|
450
|
+
? 1
|
|
451
|
+
: (fixedY.max ?? niceNum(maxY))
|
|
452
|
+
: 1
|
|
419
453
|
const niceMinX = Number.isFinite(minX) ? (minX < 0 ? niceNum(minX) : 0) : 0
|
|
420
|
-
const niceMinY = Number.isFinite(minY)
|
|
454
|
+
const niceMinY = Number.isFinite(minY)
|
|
455
|
+
? minY < 0
|
|
456
|
+
? (fixedY.min ?? niceNum(minY))
|
|
457
|
+
: 0
|
|
458
|
+
: 0
|
|
421
459
|
return { niceMinX, niceMaxX, niceMinY, niceMaxY }
|
|
422
460
|
}
|
|
@@ -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,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
TimeseriesOptionsInput,
|
|
20
21
|
TimeseriesWidgetData,
|
|
21
22
|
} from './types'
|
|
23
|
+
import { fixedAxisBounds } from '../actions/relative-data/scale'
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Builds the **structural** ECharts option for a timeseries widget —
|
|
@@ -187,6 +189,7 @@ export function createTimeseriesOptionFactory(
|
|
|
187
189
|
typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)
|
|
188
190
|
? option.yAxis
|
|
189
191
|
: {}
|
|
192
|
+
const fixedY = fixedAxisBounds(baseYAxis)
|
|
190
193
|
const baseGrid =
|
|
191
194
|
typeof option.grid === 'object' && !Array.isArray(option.grid)
|
|
192
195
|
? option.grid
|
|
@@ -237,23 +240,39 @@ export function createTimeseriesOptionFactory(
|
|
|
237
240
|
// and points would stay dimmed forever after an external clear.
|
|
238
241
|
// Always emitting lets normal merge swap the callback in place, no
|
|
239
242
|
// `replaceMerge` and no entry-animation flash on selection on/off.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
//
|
|
244
|
+
// The base colour is the point's own `color`, else the template's
|
|
245
|
+
// `itemStyle.color`, else the series colour / palette (`params.color`).
|
|
246
|
+
const makeDimItemStyle = (datums: readonly object[], template: unknown) => {
|
|
247
|
+
const baseColor = createItemColorResolver({
|
|
248
|
+
theme,
|
|
249
|
+
datums,
|
|
250
|
+
template,
|
|
251
|
+
fallback: (params) => params.color as string,
|
|
252
|
+
})
|
|
253
|
+
return {
|
|
254
|
+
color: (params: CallbackDataParams) => {
|
|
255
|
+
const datum = params.value as
|
|
256
|
+
{ name?: Date | number | string } | undefined
|
|
257
|
+
const raw = datum?.name
|
|
258
|
+
const base = baseColor(params)
|
|
259
|
+
if (!selectionSet || raw == null) return base
|
|
260
|
+
return selectionSet.has(normalizeName(raw))
|
|
261
|
+
? base
|
|
262
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
263
|
+
},
|
|
264
|
+
}
|
|
251
265
|
}
|
|
266
|
+
// Per-point colours only show on markers, so surface them whenever a
|
|
267
|
+
// point carries its own `color` (same reason as the selection case).
|
|
268
|
+
const hasDatumColor = seriesArr.some((s) =>
|
|
269
|
+
s.some((d) => typeof d?.color === 'string' && d.color !== ''),
|
|
270
|
+
)
|
|
252
271
|
|
|
253
272
|
return {
|
|
254
273
|
...option,
|
|
255
274
|
dataset: seriesArr.map((s) => ({ source: s as readonly object[] })),
|
|
256
|
-
series: seriesArr.map((
|
|
275
|
+
series: seriesArr.map((s, i) => {
|
|
257
276
|
const template =
|
|
258
277
|
(seriesTemplates[i] as object | undefined) ??
|
|
259
278
|
(broadcastTemplate as object)
|
|
@@ -271,10 +290,10 @@ export function createTimeseriesOptionFactory(
|
|
|
271
290
|
// When a selection is active, surface markers so the per-point
|
|
272
291
|
// color callback has something to dim — a continuous line would
|
|
273
292
|
// hide the visual selection feedback.
|
|
274
|
-
showSymbol: selectionSet != null,
|
|
293
|
+
showSymbol: selectionSet != null || hasDatumColor,
|
|
275
294
|
...(area ? { areaStyle: {} } : {}),
|
|
276
295
|
emphasis: { focus: 'series' },
|
|
277
|
-
itemStyle:
|
|
296
|
+
itemStyle: makeDimItemStyle(s, template),
|
|
278
297
|
...(overrideColor
|
|
279
298
|
? { color: overrideColor, lineStyle: { color: overrideColor } }
|
|
280
299
|
: {}),
|
|
@@ -285,12 +304,14 @@ export function createTimeseriesOptionFactory(
|
|
|
285
304
|
...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),
|
|
286
305
|
yAxis: {
|
|
287
306
|
...baseYAxis,
|
|
307
|
+
// A config transform (RelativeData's `'total'` scale) may pin the
|
|
308
|
+
// ends; each applies only on the side the data reaches.
|
|
288
309
|
min: (extent: { min: number }) => {
|
|
289
|
-
niceMin = extent.min < 0 ? niceNum(extent.min) : 0
|
|
310
|
+
niceMin = extent.min < 0 ? (fixedY.min ?? niceNum(extent.min)) : 0
|
|
290
311
|
return niceMin
|
|
291
312
|
},
|
|
292
313
|
max: (extent: { min: number; max: number }) => {
|
|
293
|
-
niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
|
|
314
|
+
niceMax = extent.max <= 0 ? 1 : (fixedY.max ?? niceNum(extent.max))
|
|
294
315
|
return niceMax
|
|
295
316
|
},
|
|
296
317
|
axisLabel: {
|
|
@@ -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. */
|