@carto/ps-react-ui 4.17.3 → 4.18.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/category-Ct2BzADB.js +753 -0
- package/dist/category-Ct2BzADB.js.map +1 -0
- package/dist/change-column-Bhcxmp-r.js +1148 -0
- package/dist/change-column-Bhcxmp-r.js.map +1 -0
- package/dist/echart-B10dhEaM.js +262 -0
- package/dist/echart-B10dhEaM.js.map +1 -0
- package/dist/types/widgets-v2/actions/lock-selection/lock-selection.d.ts +15 -5
- package/dist/types/widgets-v2/category/components/category-legend.d.ts +18 -2
- package/dist/types/widgets-v2/category/components/category-row-stacked.d.ts +6 -1
- package/dist/types/widgets-v2/category/style.d.ts +8 -0
- package/dist/types/widgets-v2/echart/use-chart-selection.d.ts +10 -4
- package/dist/types/widgets-v2/histogram/index.d.ts +1 -1
- package/dist/types/widgets-v2/histogram/transforms.d.ts +11 -0
- package/dist/types/widgets-v2/histogram/types.d.ts +9 -0
- package/dist/types/widgets-v2/scatterplot/index.d.ts +1 -1
- package/dist/types/widgets-v2/scatterplot/transforms.d.ts +9 -0
- package/dist/types/widgets-v2/table/index.d.ts +1 -0
- package/dist/types/widgets-v2/table/transforms.d.ts +13 -0
- package/dist/types/widgets-v2/table/transforms.test.d.ts +1 -0
- package/dist/types/widgets-v2/timeseries/index.d.ts +1 -0
- package/dist/types/widgets-v2/timeseries/transforms.d.ts +10 -0
- package/dist/types/widgets-v2/timeseries/transforms.test.d.ts +1 -0
- package/dist/widgets-v2/actions.js +1 -1
- package/dist/widgets-v2/category.js +1 -1
- package/dist/widgets-v2/echart.js +1 -1
- package/dist/widgets-v2/echart.js.map +1 -1
- package/dist/widgets-v2/histogram.js +181 -158
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/pie.js +155 -116
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +91 -85
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/table.js +55 -45
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +96 -85
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +4 -4
- package/package.json +1 -1
- package/src/widgets-v2/actions/lock-selection/lock-selection.test.tsx +62 -0
- package/src/widgets-v2/actions/lock-selection/lock-selection.tsx +45 -13
- package/src/widgets-v2/category/category-ui.test.tsx +55 -0
- package/src/widgets-v2/category/category-ui.tsx +65 -6
- package/src/widgets-v2/category/components/category-legend.test.tsx +53 -2
- package/src/widgets-v2/category/components/category-legend.tsx +55 -12
- package/src/widgets-v2/category/components/category-row-stacked.test.tsx +7 -3
- package/src/widgets-v2/category/components/category-row-stacked.tsx +8 -3
- package/src/widgets-v2/category/style.ts +10 -2
- package/src/widgets-v2/echart/echart-ui.test.tsx +25 -0
- package/src/widgets-v2/echart/echart-ui.tsx +20 -0
- package/src/widgets-v2/echart/use-chart-selection.test.tsx +3 -1
- package/src/widgets-v2/echart/use-chart-selection.ts +10 -4
- package/src/widgets-v2/histogram/index.ts +4 -1
- package/src/widgets-v2/histogram/options.test.ts +86 -0
- package/src/widgets-v2/histogram/options.ts +50 -0
- package/src/widgets-v2/histogram/transforms.test.ts +36 -1
- package/src/widgets-v2/histogram/transforms.ts +23 -0
- package/src/widgets-v2/histogram/types.ts +9 -0
- package/src/widgets-v2/pie/options.test.ts +262 -0
- package/src/widgets-v2/pie/options.ts +80 -5
- package/src/widgets-v2/scatterplot/index.ts +4 -1
- package/src/widgets-v2/scatterplot/transforms.test.ts +38 -1
- package/src/widgets-v2/scatterplot/transforms.ts +23 -0
- package/src/widgets-v2/table/index.ts +1 -0
- package/src/widgets-v2/table/transforms.test.ts +40 -0
- package/src/widgets-v2/table/transforms.ts +26 -0
- package/src/widgets-v2/timeseries/index.ts +1 -0
- package/src/widgets-v2/timeseries/transforms.test.ts +49 -0
- package/src/widgets-v2/timeseries/transforms.ts +31 -0
- package/dist/category-CGS_eHr4.js +0 -719
- package/dist/category-CGS_eHr4.js.map +0 -1
- package/dist/change-column-CiVAjOUB.js +0 -1143
- package/dist/change-column-CiVAjOUB.js.map +0 -1
- package/dist/echart-Bdvbfx9s.js +0 -250
- package/dist/echart-Bdvbfx9s.js.map +0 -1
|
@@ -5,7 +5,13 @@ import type { EchartsEventHandler } from './echart-ui'
|
|
|
5
5
|
export type SelectionKey = string | number
|
|
6
6
|
|
|
7
7
|
export interface UseChartSelectionOptions<K extends SelectionKey> {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Controlled selection. `undefined`/`null`/an explicit `[]` all mean "no
|
|
10
|
+
* selection interaction" — every current merger already treats an empty
|
|
11
|
+
* selection as "nothing excluded," so this hook collapses all three to
|
|
12
|
+
* `null` rather than promising per-merger `[]` semantics it doesn't
|
|
13
|
+
* implement.
|
|
14
|
+
*/
|
|
9
15
|
selection?: readonly K[]
|
|
10
16
|
/** Receives the next selection. When omitted the wiring is a no-op. */
|
|
11
17
|
onSelectionChange?: (next: readonly K[]) => void
|
|
@@ -32,9 +38,9 @@ export interface ChartSelectionWiring<K extends SelectionKey> {
|
|
|
32
38
|
*/
|
|
33
39
|
onEvents: Record<string, EchartsEventHandler>
|
|
34
40
|
/**
|
|
35
|
-
* The current selection — `null`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
41
|
+
* The current selection — `null` for "no selection interaction"
|
|
42
|
+
* (`undefined`/`null`/an explicit `[]`), or a non-empty array meaning
|
|
43
|
+
* "render only these as fully opaque, dim the rest".
|
|
38
44
|
*/
|
|
39
45
|
selection: readonly K[] | null
|
|
40
46
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { histogramOptions, createHistogramOptionFactory } from './options'
|
|
2
2
|
export { HistogramSkeleton, type HistogramSkeletonProps } from './skeleton'
|
|
3
3
|
export { createHistogramDownloadConfig } from './download'
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
toRelativeHistogramData,
|
|
6
|
+
filterHistogramByLockedBins,
|
|
7
|
+
} from './transforms'
|
|
5
8
|
export type {
|
|
6
9
|
HistogramWidgetData,
|
|
7
10
|
HistogramOptionsInput,
|
|
@@ -273,6 +273,92 @@ describe('createHistogramOptionFactory (data → dataset merger)', () => {
|
|
|
273
273
|
expect(out.series).toEqual([])
|
|
274
274
|
})
|
|
275
275
|
|
|
276
|
+
describe('lockedItems → x-axis crop', () => {
|
|
277
|
+
it('crops xAxis.min/max to the locked bin range (data stays full-length)', () => {
|
|
278
|
+
const merge = createHistogramOptionFactory({
|
|
279
|
+
theme,
|
|
280
|
+
ticks: [0, 10, 20, 30, 40],
|
|
281
|
+
lockedItems: [1, 2],
|
|
282
|
+
})
|
|
283
|
+
const out = merge({}, [[5, 10, 15, 20]]) as {
|
|
284
|
+
xAxis: { min?: string | null; max?: string | null }
|
|
285
|
+
dataset: { source: [string, number][] }[]
|
|
286
|
+
}
|
|
287
|
+
expect(out.xAxis.min).toBe('10–20')
|
|
288
|
+
expect(out.xAxis.max).toBe('20–30')
|
|
289
|
+
// Data isn't sliced — every bin still has an entry, just scrolled
|
|
290
|
+
// out of the cropped view.
|
|
291
|
+
expect(out.dataset[0]!.source).toHaveLength(4)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('resets to null (not omitted) when lockedItems is empty, clearing a prior crop', () => {
|
|
295
|
+
const merge = createHistogramOptionFactory({
|
|
296
|
+
theme,
|
|
297
|
+
ticks: [0, 10, 20, 30],
|
|
298
|
+
lockedItems: [],
|
|
299
|
+
})
|
|
300
|
+
const out = merge({}, [[1, 2, 3]]) as {
|
|
301
|
+
xAxis: { min?: string | null; max?: string | null }
|
|
302
|
+
}
|
|
303
|
+
expect(out.xAxis.min).toBeNull()
|
|
304
|
+
expect(out.xAxis.max).toBeNull()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('preserves a consumer-supplied optionsOverride.xAxis.min/max when never locked, instead of clobbering it to null', () => {
|
|
308
|
+
const merge = createHistogramOptionFactory({
|
|
309
|
+
theme,
|
|
310
|
+
ticks: [0, 10, 20, 30],
|
|
311
|
+
lockedItems: [],
|
|
312
|
+
})
|
|
313
|
+
// `option` here stands in for the post-structural-phase option, which
|
|
314
|
+
// already has `optionsOverride.xAxis` baked in — the min/max a
|
|
315
|
+
// consumer set for an unrelated reason, with locking never engaged.
|
|
316
|
+
const out = merge({ xAxis: { min: 'custom-min', max: 'custom-max' } }, [
|
|
317
|
+
[1, 2, 3],
|
|
318
|
+
]) as { xAxis: { min?: string | null; max?: string | null } }
|
|
319
|
+
expect(out.xAxis.min).toBe('custom-min')
|
|
320
|
+
expect(out.xAxis.max).toBe('custom-max')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('handles a single locked bin (min === max)', () => {
|
|
324
|
+
const merge = createHistogramOptionFactory({
|
|
325
|
+
theme,
|
|
326
|
+
ticks: [0, 10, 20, 30],
|
|
327
|
+
lockedItems: [1],
|
|
328
|
+
})
|
|
329
|
+
const out = merge({}, [[1, 2, 3]]) as {
|
|
330
|
+
xAxis: { min?: string | null; max?: string | null }
|
|
331
|
+
}
|
|
332
|
+
expect(out.xAxis.min).toBe('10–20')
|
|
333
|
+
expect(out.xAxis.max).toBe('10–20')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('merges the crop into the primary axis and preserves the rest of an array xAxis override, instead of discarding it', () => {
|
|
337
|
+
const merge = createHistogramOptionFactory({
|
|
338
|
+
theme,
|
|
339
|
+
ticks: [0, 10, 20, 30, 40],
|
|
340
|
+
lockedItems: [1, 2],
|
|
341
|
+
})
|
|
342
|
+
const out = merge(
|
|
343
|
+
{
|
|
344
|
+
xAxis: [
|
|
345
|
+
{ type: 'category', name: 'primary' },
|
|
346
|
+
{ type: 'value', name: 'secondary' },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
[[5, 10, 15, 20]],
|
|
350
|
+
) as {
|
|
351
|
+
xAxis: { name?: string; min?: string | null; max?: string | null }[]
|
|
352
|
+
}
|
|
353
|
+
expect(Array.isArray(out.xAxis)).toBe(true)
|
|
354
|
+
expect(out.xAxis).toHaveLength(2)
|
|
355
|
+
expect(out.xAxis[0]!.name).toBe('primary')
|
|
356
|
+
expect(out.xAxis[0]!.min).toBe('10–20')
|
|
357
|
+
expect(out.xAxis[0]!.max).toBe('20–30')
|
|
358
|
+
expect(out.xAxis[1]).toEqual({ type: 'value', name: 'secondary' })
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
276
362
|
describe('tooltip formatter', () => {
|
|
277
363
|
type ItemFormatter = (
|
|
278
364
|
params: unknown,
|
|
@@ -160,6 +160,7 @@ export function createHistogramOptionFactory(
|
|
|
160
160
|
): OptionFactory {
|
|
161
161
|
const { theme, formatter, ticks, series, labelFormatter, selection } = options
|
|
162
162
|
const optionsOverride = options.optionsOverride
|
|
163
|
+
const lockedItems = options.lockedItems
|
|
163
164
|
const selectionSet =
|
|
164
165
|
selection && selection.length > 0 ? new Set<number>(selection) : null
|
|
165
166
|
|
|
@@ -171,6 +172,22 @@ export function createHistogramOptionFactory(
|
|
|
171
172
|
binLabels.push(labelFormatter ? labelFormatter(raw) : raw)
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
// Locked range → x-axis crop. `min`/`max` on a category axis accept a
|
|
176
|
+
// category value (the bin label string), bounding the visible window
|
|
177
|
+
// without touching `dataset`/`series` — the zeroed-out bins from
|
|
178
|
+
// `filterHistogramByLockedBins` stay in the data, just scrolled out of
|
|
179
|
+
// view. `null` (not omitted) on unlock, since ECharts' option merge skips
|
|
180
|
+
// `undefined` keys and would otherwise leave a stale crop in place.
|
|
181
|
+
const lockedRange =
|
|
182
|
+
lockedItems && lockedItems.length > 0
|
|
183
|
+
? (() => {
|
|
184
|
+
const indices = lockedItems.map(Number)
|
|
185
|
+
const min = binLabels[Math.min(...indices)]
|
|
186
|
+
const max = binLabels[Math.max(...indices)]
|
|
187
|
+
return min != null && max != null ? { min, max } : null
|
|
188
|
+
})()
|
|
189
|
+
: null
|
|
190
|
+
|
|
174
191
|
return (option, data, ctx) => {
|
|
175
192
|
if (option == null) {
|
|
176
193
|
const structural = histogramOptions({ theme, formatter })
|
|
@@ -254,8 +271,41 @@ export function createHistogramOptionFactory(
|
|
|
254
271
|
},
|
|
255
272
|
}
|
|
256
273
|
|
|
274
|
+
// A consumer's `optionsOverride.xAxis` may legitimately be an array
|
|
275
|
+
// (ECharts supports multiple x-axes on one grid). Merge the locked-range
|
|
276
|
+
// crop into the primary (first) axis instead of discarding the rest via
|
|
277
|
+
// an object fallback — `min`/`max` here bound a category axis by label
|
|
278
|
+
// string, which is only ambiguous if `labelFormatter` produces duplicate
|
|
279
|
+
// labels across bins (a structural limitation of label-string bounds,
|
|
280
|
+
// not fixed here).
|
|
281
|
+
//
|
|
282
|
+
// `min`/`max` are ALWAYS written explicitly below (never omitted) —
|
|
283
|
+
// ECharts' merge silently keeps whatever value it last received for an
|
|
284
|
+
// omitted key, so omitting them while unlocked would leave a stale crop
|
|
285
|
+
// in place from a previous locked render. But the fallback when there's
|
|
286
|
+
// no active crop must be the CONSUMER's own `optionsOverride.xAxis.min/max`
|
|
287
|
+
// (if they set one), not an unconditional `null` — `option.xAxis` here
|
|
288
|
+
// is the post-structural-phase axis, which only ever carries the
|
|
289
|
+
// consumer's static override (never our own prior crop — the structural
|
|
290
|
+
// phase is rebuilt from scratch on every factory recreation, so it can't
|
|
291
|
+
// have picked up a merge-phase write from a previous render).
|
|
292
|
+
const currentAxis = (
|
|
293
|
+
Array.isArray(option.xAxis) ? option.xAxis[0] : option.xAxis
|
|
294
|
+
) as { min?: unknown; max?: unknown } | undefined
|
|
295
|
+
const lockedRangeAxis = {
|
|
296
|
+
min: lockedRange?.min ?? currentAxis?.min ?? null,
|
|
297
|
+
max: lockedRange?.max ?? currentAxis?.max ?? null,
|
|
298
|
+
}
|
|
299
|
+
const nextXAxis = Array.isArray(option.xAxis)
|
|
300
|
+
? [{ ...option.xAxis[0], ...lockedRangeAxis }, ...option.xAxis.slice(1)]
|
|
301
|
+
: {
|
|
302
|
+
...(typeof option.xAxis === 'object' ? option.xAxis : {}),
|
|
303
|
+
...lockedRangeAxis,
|
|
304
|
+
}
|
|
305
|
+
|
|
257
306
|
return {
|
|
258
307
|
...option,
|
|
308
|
+
xAxis: nextXAxis,
|
|
259
309
|
dataset: seriesArr.map((counts) => ({
|
|
260
310
|
source: binLabels.map(
|
|
261
311
|
(label, i) => [label, counts[i] ?? 0] as [string, number],
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
toRelativeHistogramData,
|
|
4
|
+
filterHistogramByLockedBins,
|
|
5
|
+
} from './transforms'
|
|
3
6
|
|
|
4
7
|
describe('toRelativeHistogramData', () => {
|
|
5
8
|
it('rewrites each bin count as its share of the series total in 0-100', () => {
|
|
@@ -44,3 +47,35 @@ describe('toRelativeHistogramData', () => {
|
|
|
44
47
|
])
|
|
45
48
|
})
|
|
46
49
|
})
|
|
50
|
+
|
|
51
|
+
describe('filterHistogramByLockedBins', () => {
|
|
52
|
+
it('returns input unchanged when locked set is empty', () => {
|
|
53
|
+
const data = [[10, 20, 30]]
|
|
54
|
+
expect(filterHistogramByLockedBins(data, [])).toBe(data)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('zeroes non-locked bin counts, preserving array length and position', () => {
|
|
58
|
+
expect(filterHistogramByLockedBins([[10, 20, 30]], [0, 2])).toEqual([
|
|
59
|
+
[10, 0, 30],
|
|
60
|
+
])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('handles multiple series independently', () => {
|
|
64
|
+
expect(
|
|
65
|
+
filterHistogramByLockedBins(
|
|
66
|
+
[
|
|
67
|
+
[1, 2, 3],
|
|
68
|
+
[4, 5, 6],
|
|
69
|
+
],
|
|
70
|
+
[1],
|
|
71
|
+
),
|
|
72
|
+
).toEqual([
|
|
73
|
+
[0, 2, 0],
|
|
74
|
+
[0, 5, 0],
|
|
75
|
+
])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns input unchanged when input is not array-shaped', () => {
|
|
79
|
+
expect(filterHistogramByLockedBins('nope', [0])).toBe('nope')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -28,3 +28,26 @@ function isFlatNumberArray(v: unknown): v is number[] {
|
|
|
28
28
|
if (!Array.isArray(v)) return false
|
|
29
29
|
return v.every((item) => typeof item === 'number' && Number.isFinite(item))
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `LockSelection` filter for Histogram. Data is `number[][]` — a flat list
|
|
34
|
+
* of bin counts per series, positionally paired with `ticks` (bin `i` covers
|
|
35
|
+
* `[ticks[i], ticks[i+1])`). Unlike name-keyed widgets, locked-out bins can't
|
|
36
|
+
* be REMOVED — that would shift every later bin's index and desync it from
|
|
37
|
+
* its `ticks` range. Zeroing them out instead preserves array length/position,
|
|
38
|
+
* so locked bins just render as empty (height 0) rather than disappearing.
|
|
39
|
+
*
|
|
40
|
+
* `lockedItems` holds bin indices (numbers, from Histogram's `selection`).
|
|
41
|
+
*/
|
|
42
|
+
export function filterHistogramByLockedBins(
|
|
43
|
+
input: unknown,
|
|
44
|
+
lockedItems: readonly (string | number)[],
|
|
45
|
+
): unknown {
|
|
46
|
+
if (lockedItems.length === 0) return input
|
|
47
|
+
if (!Array.isArray(input)) return input
|
|
48
|
+
const locked = new Set<number>(lockedItems.map(Number))
|
|
49
|
+
return input.map((series: unknown): unknown => {
|
|
50
|
+
if (!isFlatNumberArray(series)) return series
|
|
51
|
+
return series.map((count, i) => (locked.has(i) ? count : 0))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -44,6 +44,15 @@ export interface HistogramOptionFactoryInput {
|
|
|
44
44
|
* (`itemStyle.opacity: 0.15`). `null`/empty means no selection.
|
|
45
45
|
*/
|
|
46
46
|
selection?: readonly number[] | null
|
|
47
|
+
/**
|
|
48
|
+
* Locked bin indices (from `LockSelection`). The x-axis crops its visible
|
|
49
|
+
* window to `[min(lockedItems), max(lockedItems)]` so the chart actually
|
|
50
|
+
* zooms into the locked range instead of showing every bin label with
|
|
51
|
+
* zeroed-out bars outside it. `null`/empty means no crop. Histogram's
|
|
52
|
+
* selection is always a contiguous run (`use-widget-filters`'s BETWEEN
|
|
53
|
+
* envelope round-trip fills any gaps), so min/max fully describes it.
|
|
54
|
+
*/
|
|
55
|
+
lockedItems?: readonly number[] | null
|
|
47
56
|
/**
|
|
48
57
|
* Consumer-supplied partial option merged into the structural option at
|
|
49
58
|
* structural-build time. Lets stories override pieces of the theme-aware
|
|
@@ -83,6 +83,20 @@ describe('pieOptions (structural)', () => {
|
|
|
83
83
|
// re-emits it as { ...baseLegend, show: true } at fusion time.
|
|
84
84
|
expect(out.legend?.show).toBe(true)
|
|
85
85
|
})
|
|
86
|
+
|
|
87
|
+
it('disables legend click-to-toggle', () => {
|
|
88
|
+
const out = pieOptions({ theme }) as {
|
|
89
|
+
legend?: { selectedMode?: boolean }
|
|
90
|
+
}
|
|
91
|
+
expect(out.legend?.selectedMode).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('disables legend hover-highlight on the series template (no legend-level knob in ECharts)', () => {
|
|
95
|
+
const out = pieOptions({ theme }) as {
|
|
96
|
+
series?: { legendHoverLink?: boolean }[]
|
|
97
|
+
}
|
|
98
|
+
expect(out.series?.[0]?.legendHoverLink).toBe(false)
|
|
99
|
+
})
|
|
86
100
|
})
|
|
87
101
|
|
|
88
102
|
describe('createPieOptionFactory — single-series (donut)', () => {
|
|
@@ -147,6 +161,16 @@ describe('createPieOptionFactory — single-series (donut)', () => {
|
|
|
147
161
|
expect(single.legend?.show).toBe(true)
|
|
148
162
|
})
|
|
149
163
|
|
|
164
|
+
it('keeps legend interactivity disabled after fusion', () => {
|
|
165
|
+
const merge = createPieOptionFactory({ theme })
|
|
166
|
+
const single = merge({}, [[{ name: 'A', value: 1 }]]) as {
|
|
167
|
+
legend?: { selectedMode?: boolean }
|
|
168
|
+
series?: { legendHoverLink?: boolean }[]
|
|
169
|
+
}
|
|
170
|
+
expect(single.legend?.selectedMode).toBe(false)
|
|
171
|
+
expect(single.series?.[0]?.legendHoverLink).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
150
174
|
it('uses series[0].name for the single series name when provided', () => {
|
|
151
175
|
const merge = createPieOptionFactory({
|
|
152
176
|
theme,
|
|
@@ -303,6 +327,215 @@ describe('createPieOptionFactory — single-series (donut)', () => {
|
|
|
303
327
|
).toBe('#FACADE')
|
|
304
328
|
})
|
|
305
329
|
|
|
330
|
+
it('label formatter renders text only for the max-valued slice when no selection is passed (regression)', () => {
|
|
331
|
+
const merge = createPieOptionFactory({ theme })
|
|
332
|
+
const out = merge({}, [
|
|
333
|
+
[
|
|
334
|
+
{ name: 'A', value: 10 },
|
|
335
|
+
{ name: 'B', value: 25 },
|
|
336
|
+
],
|
|
337
|
+
]) as {
|
|
338
|
+
series: { label: { formatter: (p: Record<string, unknown>) => string } }[]
|
|
339
|
+
}
|
|
340
|
+
const fmt = out.series[0]!.label.formatter
|
|
341
|
+
expect(
|
|
342
|
+
fmt({
|
|
343
|
+
name: 'A',
|
|
344
|
+
encode: { value: [1] },
|
|
345
|
+
data: { name: 'A', value: 10 },
|
|
346
|
+
}),
|
|
347
|
+
).toBe('')
|
|
348
|
+
expect(
|
|
349
|
+
fmt({
|
|
350
|
+
name: 'B',
|
|
351
|
+
encode: { value: [1] },
|
|
352
|
+
data: { name: 'B', value: 25 },
|
|
353
|
+
}),
|
|
354
|
+
).not.toBe('')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('label formatter shows the centre value only for the max-valued INCLUDED slice (direct/inclusive selection, computed locally)', () => {
|
|
358
|
+
const merge = createPieOptionFactory({ theme, selection: ['A', 'B'] })
|
|
359
|
+
const out = merge({}, [
|
|
360
|
+
[
|
|
361
|
+
{ name: 'A', value: 10 },
|
|
362
|
+
{ name: 'B', value: 25 },
|
|
363
|
+
{ name: 'C', value: 100 },
|
|
364
|
+
],
|
|
365
|
+
]) as {
|
|
366
|
+
series: { label: { formatter: (p: Record<string, unknown>) => string } }[]
|
|
367
|
+
}
|
|
368
|
+
const fmt = out.series[0]!.label.formatter
|
|
369
|
+
// C has the highest value overall but is not included in the selection,
|
|
370
|
+
// so it's excluded from the max computation — B (the max of A/B) wins.
|
|
371
|
+
expect(
|
|
372
|
+
fmt({
|
|
373
|
+
name: 'A',
|
|
374
|
+
encode: { value: [1] },
|
|
375
|
+
data: { name: 'A', value: 10 },
|
|
376
|
+
}),
|
|
377
|
+
).toBe('')
|
|
378
|
+
expect(
|
|
379
|
+
fmt({
|
|
380
|
+
name: 'B',
|
|
381
|
+
encode: { value: [1] },
|
|
382
|
+
data: { name: 'B', value: 25 },
|
|
383
|
+
}),
|
|
384
|
+
).not.toBe('')
|
|
385
|
+
expect(
|
|
386
|
+
fmt({
|
|
387
|
+
name: 'C',
|
|
388
|
+
encode: { value: [1] },
|
|
389
|
+
data: { name: 'C', value: 100 },
|
|
390
|
+
}),
|
|
391
|
+
).toBe('')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('label formatter treats an explicit empty selection as no selection (renders the true max, not blank)', () => {
|
|
395
|
+
const merge = createPieOptionFactory({ theme, selection: [] })
|
|
396
|
+
const out = merge({}, [[{ name: 'A', value: 10 }]]) as {
|
|
397
|
+
series: { label: { formatter: (p: Record<string, unknown>) => string } }[]
|
|
398
|
+
}
|
|
399
|
+
const html = out.series[0]!.label.formatter({
|
|
400
|
+
name: 'A',
|
|
401
|
+
encode: { value: [1] },
|
|
402
|
+
data: { name: 'A', value: 10 },
|
|
403
|
+
})
|
|
404
|
+
expect(html).not.toBe('')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('itemStyle color dims a slice NOT in the selection (direct/inclusive model, matches multi-series bar)', () => {
|
|
408
|
+
const merge = createPieOptionFactory({ theme, selection: ['A'] })
|
|
409
|
+
const out = merge({ color: ['#AAA', '#BBB'] }, [
|
|
410
|
+
[
|
|
411
|
+
{ name: 'A', value: 10 },
|
|
412
|
+
{ name: 'B', value: 5 },
|
|
413
|
+
],
|
|
414
|
+
]) as {
|
|
415
|
+
series: {
|
|
416
|
+
itemStyle: {
|
|
417
|
+
color: (p: {
|
|
418
|
+
dataIndex: number
|
|
419
|
+
color: string
|
|
420
|
+
value: object
|
|
421
|
+
name: string
|
|
422
|
+
}) => string
|
|
423
|
+
}
|
|
424
|
+
}[]
|
|
425
|
+
}
|
|
426
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
427
|
+
expect(
|
|
428
|
+
colorFn({
|
|
429
|
+
dataIndex: 0,
|
|
430
|
+
color: '#000',
|
|
431
|
+
value: { name: 'A', value: 10 },
|
|
432
|
+
name: 'A',
|
|
433
|
+
}),
|
|
434
|
+
).toBe('#AAA')
|
|
435
|
+
const dimmedB = colorFn({
|
|
436
|
+
dataIndex: 1,
|
|
437
|
+
color: '#000',
|
|
438
|
+
value: { name: 'B', value: 5 },
|
|
439
|
+
name: 'B',
|
|
440
|
+
})
|
|
441
|
+
expect(dimmedB).not.toBe('#BBB')
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('itemStyle color dims neither slice when both are in a multi-item selection (regression: locked-then-searched-and-reselected)', () => {
|
|
445
|
+
const merge = createPieOptionFactory({ theme, selection: ['A', 'B'] })
|
|
446
|
+
const out = merge({ color: ['#AAA', '#BBB'] }, [
|
|
447
|
+
[
|
|
448
|
+
{ name: 'A', value: 10 },
|
|
449
|
+
{ name: 'B', value: 5 },
|
|
450
|
+
],
|
|
451
|
+
]) as {
|
|
452
|
+
series: {
|
|
453
|
+
itemStyle: {
|
|
454
|
+
color: (p: {
|
|
455
|
+
dataIndex: number
|
|
456
|
+
color: string
|
|
457
|
+
value: object
|
|
458
|
+
name: string
|
|
459
|
+
}) => string
|
|
460
|
+
}
|
|
461
|
+
}[]
|
|
462
|
+
}
|
|
463
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
464
|
+
expect(
|
|
465
|
+
colorFn({
|
|
466
|
+
dataIndex: 0,
|
|
467
|
+
color: '#000',
|
|
468
|
+
value: { name: 'A', value: 10 },
|
|
469
|
+
name: 'A',
|
|
470
|
+
}),
|
|
471
|
+
).toBe('#AAA')
|
|
472
|
+
expect(
|
|
473
|
+
colorFn({
|
|
474
|
+
dataIndex: 1,
|
|
475
|
+
color: '#000',
|
|
476
|
+
value: { name: 'B', value: 5 },
|
|
477
|
+
name: 'B',
|
|
478
|
+
}),
|
|
479
|
+
).toBe('#BBB')
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('itemStyle color dims nothing when selection is an explicit empty array (renders fully active, not all-dimmed)', () => {
|
|
483
|
+
const merge = createPieOptionFactory({ theme, selection: [] })
|
|
484
|
+
const out = merge({ color: ['#AAA', '#BBB'] }, [
|
|
485
|
+
[
|
|
486
|
+
{ name: 'A', value: 10 },
|
|
487
|
+
{ name: 'B', value: 5 },
|
|
488
|
+
],
|
|
489
|
+
]) as {
|
|
490
|
+
series: {
|
|
491
|
+
itemStyle: {
|
|
492
|
+
color: (p: {
|
|
493
|
+
dataIndex: number
|
|
494
|
+
color: string
|
|
495
|
+
value: object
|
|
496
|
+
name: string
|
|
497
|
+
}) => string
|
|
498
|
+
}
|
|
499
|
+
}[]
|
|
500
|
+
}
|
|
501
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
502
|
+
expect(
|
|
503
|
+
colorFn({
|
|
504
|
+
dataIndex: 0,
|
|
505
|
+
color: '#000',
|
|
506
|
+
value: { name: 'A', value: 10 },
|
|
507
|
+
name: 'A',
|
|
508
|
+
}),
|
|
509
|
+
).toBe('#AAA')
|
|
510
|
+
expect(
|
|
511
|
+
colorFn({
|
|
512
|
+
dataIndex: 1,
|
|
513
|
+
color: '#000',
|
|
514
|
+
value: { name: 'B', value: 5 },
|
|
515
|
+
name: 'B',
|
|
516
|
+
}),
|
|
517
|
+
).toBe('#BBB')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('never sets legend.selected to false for a dimmed slice (would trigger ECharts native removal instead of dimming)', () => {
|
|
521
|
+
const merge = createPieOptionFactory({ theme, selection: ['A'] })
|
|
522
|
+
const out = merge({}, [
|
|
523
|
+
[
|
|
524
|
+
{ name: 'A', value: 10 },
|
|
525
|
+
{ name: 'B', value: 5 },
|
|
526
|
+
],
|
|
527
|
+
]) as { legend?: { selected?: Record<string, boolean> } }
|
|
528
|
+
expect(out.legend?.selected).toEqual({ A: true, B: true })
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('forces legend.selected back to true even with no active selection (overrides ECharts internal legend-click toggle state)', () => {
|
|
532
|
+
const merge = createPieOptionFactory({ theme })
|
|
533
|
+
const out = merge({}, [[{ name: 'A', value: 10 }]]) as {
|
|
534
|
+
legend?: { selected?: Record<string, boolean> }
|
|
535
|
+
}
|
|
536
|
+
expect(out.legend?.selected).toEqual({ A: true })
|
|
537
|
+
})
|
|
538
|
+
|
|
306
539
|
it('re-emits tooltip.formatter at fusion time so RelativeData (ctx.formatter) flows through', () => {
|
|
307
540
|
const merge = createPieOptionFactory({ theme })
|
|
308
541
|
const out = merge(
|
|
@@ -539,6 +772,35 @@ describe('createPieOptionFactory — multi-series fallback (horizontal bar)', ()
|
|
|
539
772
|
expect(out.legend?.show).toBe(true)
|
|
540
773
|
})
|
|
541
774
|
|
|
775
|
+
it('keeps legend interactivity disabled in the multi-series bar fallback', () => {
|
|
776
|
+
const merge = createPieOptionFactory({ theme })
|
|
777
|
+
const out = merge({}, MULTI) as {
|
|
778
|
+
legend?: { selectedMode?: boolean }
|
|
779
|
+
series?: { legendHoverLink?: boolean }[]
|
|
780
|
+
}
|
|
781
|
+
expect(out.legend?.selectedMode).toBe(false)
|
|
782
|
+
expect(out.series?.[0]?.legendHoverLink).toBe(false)
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
it('forces legend.selected back to true for every series (mirrors the donut branch — selectedMode:false blocks the native click that would otherwise undo a toggle)', () => {
|
|
786
|
+
const merge = createPieOptionFactory({
|
|
787
|
+
theme,
|
|
788
|
+
series: [{ name: '2024' }, { name: '2025' }],
|
|
789
|
+
})
|
|
790
|
+
const out = merge({}, MULTI) as {
|
|
791
|
+
legend?: { selected?: Record<string, boolean> }
|
|
792
|
+
}
|
|
793
|
+
expect(out.legend?.selected).toEqual({ '2024': true, '2025': true })
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('falls back to Series N for legend.selected keys when series metadata is omitted', () => {
|
|
797
|
+
const merge = createPieOptionFactory({ theme })
|
|
798
|
+
const out = merge({}, MULTI) as {
|
|
799
|
+
legend?: { selected?: Record<string, boolean> }
|
|
800
|
+
}
|
|
801
|
+
expect(out.legend?.selected).toEqual({ 'Series 1': true, 'Series 2': true })
|
|
802
|
+
})
|
|
803
|
+
|
|
542
804
|
describe('horizontal-bar tooltip formatter', () => {
|
|
543
805
|
type AxisFormatter = (
|
|
544
806
|
params: unknown,
|