@carto/ps-react-ui 4.17.3 → 4.19.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-Fu1DCYQ3.js +1186 -0
- package/dist/change-column-Fu1DCYQ3.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/change-column/labels.d.ts +1 -0
- package/dist/types/widgets-v2/actions/change-column/style.d.ts +28 -2
- 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/change-column/change-column.tsx +10 -1
- package/src/widgets-v2/actions/change-column/labels.ts +2 -0
- package/src/widgets-v2/actions/change-column/sortable-column-item.tsx +8 -1
- package/src/widgets-v2/actions/change-column/style.ts +28 -2
- 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
|
@@ -55,6 +55,13 @@ export function pieOptions({
|
|
|
55
55
|
left: 'center',
|
|
56
56
|
type: 'scroll',
|
|
57
57
|
itemGap: 16,
|
|
58
|
+
// The legend is presentational only here — the pie has its own
|
|
59
|
+
// click-to-select affordance on the slices themselves, so legend
|
|
60
|
+
// clicks toggling series visibility would be a second, redundant/
|
|
61
|
+
// conflicting interaction model. Hover-highlight is disabled per
|
|
62
|
+
// series via `legendHoverLink` (ECharts has no legend-level knob
|
|
63
|
+
// for it).
|
|
64
|
+
selectedMode: false,
|
|
58
65
|
},
|
|
59
66
|
tooltip: {
|
|
60
67
|
// Pie uses item-trigger (no axis); the rest of the styling mirrors
|
|
@@ -89,6 +96,7 @@ export function pieOptions({
|
|
|
89
96
|
colorBy: 'data',
|
|
90
97
|
avoidLabelOverlap: true,
|
|
91
98
|
selectedOffset: 0,
|
|
99
|
+
legendHoverLink: false,
|
|
92
100
|
emphasis: { disabled: true },
|
|
93
101
|
itemStyle: {
|
|
94
102
|
borderColor: theme.palette.background.paper,
|
|
@@ -129,10 +137,14 @@ export function createPieOptionFactory(
|
|
|
129
137
|
const { theme, formatter, labelFormatter, optionsOverride, series } = options
|
|
130
138
|
const radius = options.radius ?? DEFAULT_RADIUS
|
|
131
139
|
const selection = options.selection
|
|
140
|
+
// An explicit `[]` (deselected down to zero via chart clicks) is treated
|
|
141
|
+
// the same as no selection: the pie has no bar/histogram-style "nothing
|
|
142
|
+
// selected" affordance, so dimming every slice would just look broken —
|
|
143
|
+
// it renders fully active, same as the no-interaction state.
|
|
132
144
|
const selectionSet =
|
|
133
|
-
selection
|
|
134
|
-
?
|
|
135
|
-
:
|
|
145
|
+
selection === undefined || selection === null || selection.length === 0
|
|
146
|
+
? null
|
|
147
|
+
: new Set<string | number>(selection)
|
|
136
148
|
return (option, data, ctx) => {
|
|
137
149
|
if (option == null) {
|
|
138
150
|
const structural = pieOptions({ theme, formatter, labelFormatter })
|
|
@@ -217,6 +229,13 @@ function buildSingleSeriesPieFusion(
|
|
|
217
229
|
// not conditionally — same anti-stale-callback rationale as bar /
|
|
218
230
|
// histogram. The slice border + width come from the structural
|
|
219
231
|
// template's `itemStyle` and survive via the spread below.
|
|
232
|
+
//
|
|
233
|
+
// `selectionSet` is the *included* set, same direct semantics as
|
|
234
|
+
// bar/histogram/timeseries (no ADR-014 exclusion inversion). Dim a slice
|
|
235
|
+
// when its name is NOT in the set. `selectionSet` is never a non-null
|
|
236
|
+
// empty Set — the collapse above (`selection.length === 0 → null`) rules
|
|
237
|
+
// that out — so there's no "dim everything" case here, unlike a widget
|
|
238
|
+
// that gives `[]` its own "selected down to nothing" meaning.
|
|
220
239
|
const colorFn = (params: CallbackDataParams): string => {
|
|
221
240
|
const base = resolvePaletteColor(params)
|
|
222
241
|
if (!selectionSet) return base
|
|
@@ -227,12 +246,33 @@ function buildSingleSeriesPieFusion(
|
|
|
227
246
|
: echarts.color.modifyAlpha(base, 0.15)
|
|
228
247
|
}
|
|
229
248
|
|
|
249
|
+
// Which slice renders the centre value+name: the greatest value among
|
|
250
|
+
// the *included* slices (all of them when there's no active
|
|
251
|
+
// selection), computed locally — no query round-trip. Only that one
|
|
252
|
+
// slice's formatter call returns text; every other slice returns ''
|
|
253
|
+
// so exactly one label ever renders at the shared centre anchor. Only
|
|
254
|
+
// `typeof value === 'number'` participates — if every included slice's
|
|
255
|
+
// value is non-numeric, `centerMaxName` stays `undefined` and no slice
|
|
256
|
+
// matches it in `labelTextFormatter` below, so the chart renders with no
|
|
257
|
+
// centre label at all (graceful no-op, not a crash).
|
|
258
|
+
const centerData = seriesArr[0] ?? []
|
|
259
|
+
let centerMaxName: string | number | undefined
|
|
260
|
+
let centerMaxValue = -Infinity
|
|
261
|
+
for (const d of centerData) {
|
|
262
|
+
if (selectionSet && !selectionSet.has(d.name)) continue
|
|
263
|
+
if (typeof d.value === 'number' && d.value > centerMaxValue) {
|
|
264
|
+
centerMaxValue = d.value
|
|
265
|
+
centerMaxName = d.name
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
230
269
|
// Center label formatter — lives in the merger because it reads
|
|
231
270
|
// reactive `formatter` / `labelFormatter` from ctx. The rich tags
|
|
232
271
|
// `{c|…}` and `{b|…}` reference the rich styles baked into the
|
|
233
272
|
// structural label template.
|
|
234
273
|
const labelTextFormatter = (params: CallbackDataParams): string => {
|
|
235
274
|
const { name } = params
|
|
275
|
+
if (name == null || name !== centerMaxName) return ''
|
|
236
276
|
const encodeIndex = params.encode?.value?.[0]
|
|
237
277
|
if (encodeIndex === undefined) return ''
|
|
238
278
|
const value = (Object.values(params.data ?? {}) as unknown[]).at(
|
|
@@ -269,6 +309,7 @@ function buildSingleSeriesPieFusion(
|
|
|
269
309
|
return {
|
|
270
310
|
...templateObj,
|
|
271
311
|
type: 'pie' as const,
|
|
312
|
+
legendHoverLink: false,
|
|
272
313
|
datasetIndex: i,
|
|
273
314
|
name: series?.[i]?.name ?? `Series ${i + 1}`,
|
|
274
315
|
radius: [...radius],
|
|
@@ -291,7 +332,27 @@ function buildSingleSeriesPieFusion(
|
|
|
291
332
|
}),
|
|
292
333
|
// Legend always shows for pie (mirrors v1) — slice names drive the
|
|
293
334
|
// entries, so even a single donut benefits from a category list.
|
|
294
|
-
|
|
335
|
+
//
|
|
336
|
+
// Exclusion is conveyed purely via `colorFn`/`labelTextFormatter`
|
|
337
|
+
// dimming above, never via the legend's `selected` map: for a pie
|
|
338
|
+
// series, ECharts treats `legend.selected[name] = false` as "remove
|
|
339
|
+
// this data point," which recomputes every remaining slice's angle —
|
|
340
|
+
// the excluded slice disappears instead of rendering dimmed. Forcing
|
|
341
|
+
// every name to `true` on every render also overrides ECharts' own
|
|
342
|
+
// internal toggle state from a native legend click (which flips a
|
|
343
|
+
// name to `false` and hides it before our `legendselectchanged`
|
|
344
|
+
// handler gets a chance to react), so a legend click never leaves a
|
|
345
|
+
// slice structurally removed — only the color/label dimming reflects
|
|
346
|
+
// exclusion, and it stays in sync with the slice itself since both
|
|
347
|
+
// read `selectionSet` on the same render.
|
|
348
|
+
legend: {
|
|
349
|
+
...baseLegend,
|
|
350
|
+
show: true,
|
|
351
|
+
selectedMode: false,
|
|
352
|
+
selected: Object.fromEntries(
|
|
353
|
+
(seriesArr[0] ?? []).map((d) => [String(d.name), true]),
|
|
354
|
+
),
|
|
355
|
+
},
|
|
295
356
|
tooltip: {
|
|
296
357
|
...baseTooltip,
|
|
297
358
|
formatter: buildPieTooltipFormatter(formatter, labelFormatter),
|
|
@@ -372,6 +433,7 @@ function buildMultiSeriesBarFusion(
|
|
|
372
433
|
type: 'bar' as const,
|
|
373
434
|
name: series?.[i]?.name ?? `Series ${i + 1}`,
|
|
374
435
|
barMaxWidth: 100,
|
|
436
|
+
legendHoverLink: false,
|
|
375
437
|
emphasis: { focus: 'series' },
|
|
376
438
|
...buildSeriesLabelConfig(formatter, 'x'),
|
|
377
439
|
itemStyle: { color: barColorFn },
|
|
@@ -422,7 +484,20 @@ function buildMultiSeriesBarFusion(
|
|
|
422
484
|
}),
|
|
423
485
|
},
|
|
424
486
|
},
|
|
425
|
-
|
|
487
|
+
// Mirrors the donut branch's `selected` reset above — `selectedMode:
|
|
488
|
+
// false` blocks the native legend click that would otherwise undo an
|
|
489
|
+
// ECharts-internal toggle, so without forcing every entry back to
|
|
490
|
+
// `true` here a prior native toggle (from before `selectedMode` took
|
|
491
|
+
// effect, or a stale `keepAlive` instance) would stay hidden forever
|
|
492
|
+
// with no way to restore it.
|
|
493
|
+
legend: {
|
|
494
|
+
...baseLegend,
|
|
495
|
+
show: true,
|
|
496
|
+
selectedMode: false,
|
|
497
|
+
selected: Object.fromEntries(
|
|
498
|
+
seriesArr.map((_, i) => [series?.[i]?.name ?? `Series ${i + 1}`, true]),
|
|
499
|
+
),
|
|
500
|
+
},
|
|
426
501
|
tooltip: {
|
|
427
502
|
...baseTooltip,
|
|
428
503
|
trigger: 'axis',
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { scatterplotOptions, createScatterplotOptionFactory } from './options'
|
|
2
2
|
export { ScatterplotSkeleton, type ScatterplotSkeletonProps } from './skeleton'
|
|
3
3
|
export { createScatterplotDownloadConfig } from './download'
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
toRelativeScatterplotData,
|
|
6
|
+
filterScatterplotByLockedPoints,
|
|
7
|
+
} from './transforms'
|
|
5
8
|
export type {
|
|
6
9
|
ScatterplotDatum,
|
|
7
10
|
ScatterplotWidgetData,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
toRelativeScatterplotData,
|
|
4
|
+
filterScatterplotByLockedPoints,
|
|
5
|
+
} from './transforms'
|
|
3
6
|
|
|
4
7
|
describe('toRelativeScatterplotData', () => {
|
|
5
8
|
it('rewrites each tuple y as its share of the series total in 0-100, x stays raw', () => {
|
|
@@ -95,3 +98,37 @@ describe('toRelativeScatterplotData', () => {
|
|
|
95
98
|
])
|
|
96
99
|
})
|
|
97
100
|
})
|
|
101
|
+
|
|
102
|
+
describe('filterScatterplotByLockedPoints', () => {
|
|
103
|
+
const DATA = [
|
|
104
|
+
[
|
|
105
|
+
[1, 10],
|
|
106
|
+
[2, 20],
|
|
107
|
+
[3, 30],
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
[4, 40],
|
|
111
|
+
[5, 50],
|
|
112
|
+
],
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
it('returns input unchanged when locked set is empty', () => {
|
|
116
|
+
expect(filterScatterplotByLockedPoints(DATA, [])).toBe(DATA)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('keeps only points whose `${seriesIndex}:${dataIndex}` key is locked', () => {
|
|
120
|
+
expect(
|
|
121
|
+
filterScatterplotByLockedPoints(DATA, ['0:0', '0:2', '1:1']),
|
|
122
|
+
).toEqual([
|
|
123
|
+
[
|
|
124
|
+
[1, 10],
|
|
125
|
+
[3, 30],
|
|
126
|
+
],
|
|
127
|
+
[[5, 50]],
|
|
128
|
+
])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns input unchanged when input is not array-shaped', () => {
|
|
132
|
+
expect(filterScatterplotByLockedPoints('nope', ['0:0'])).toBe('nope')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -36,3 +36,26 @@ function isXyTupleArray(v: unknown): v is [number, number][] {
|
|
|
36
36
|
Number.isFinite(item[1]),
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `LockSelection` filter for Scatterplot. Data is `[number, number][][]` —
|
|
42
|
+
* points have no `name`, so selection (and locking) is keyed on a synthetic
|
|
43
|
+
* `${seriesIndex}:${dataIndex}` string (same format `useChartSelection`'s
|
|
44
|
+
* `keyFromClick`/`keysFromBrush` produce for this widget). Unlike Histogram,
|
|
45
|
+
* a point's position carries no external meaning (no `ticks` to desync from),
|
|
46
|
+
* so non-locked points are removed outright rather than zeroed.
|
|
47
|
+
*/
|
|
48
|
+
export function filterScatterplotByLockedPoints(
|
|
49
|
+
input: unknown,
|
|
50
|
+
lockedItems: readonly (string | number)[],
|
|
51
|
+
): unknown {
|
|
52
|
+
if (lockedItems.length === 0) return input
|
|
53
|
+
if (!Array.isArray(input)) return input
|
|
54
|
+
const locked = new Set<string>(lockedItems.map(String))
|
|
55
|
+
return input.map((series: unknown, seriesIndex: number): unknown => {
|
|
56
|
+
if (!isXyTupleArray(series)) return series
|
|
57
|
+
return series.filter((_, dataIndex) =>
|
|
58
|
+
locked.has(`${seriesIndex}:${dataIndex}`),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -2,6 +2,7 @@ export { Table, type TableProps } from './table'
|
|
|
2
2
|
export { TableUI, type TableUIProps } from './table-ui'
|
|
3
3
|
export { TableSkeleton, type TableSkeletonProps } from './skeleton'
|
|
4
4
|
export { createTableDownloadConfig } from './download'
|
|
5
|
+
export { filterTableByLockedRows } from './transforms'
|
|
5
6
|
export {
|
|
6
7
|
compareValues,
|
|
7
8
|
sortRows,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { filterTableByLockedRows } from './transforms'
|
|
3
|
+
|
|
4
|
+
describe('filterTableByLockedRows', () => {
|
|
5
|
+
const ROWS = [
|
|
6
|
+
{ id: 'a', label: 'Alpha' },
|
|
7
|
+
{ id: 'b', label: 'Beta' },
|
|
8
|
+
{ id: 'c', label: 'Gamma' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
it('returns input unchanged when locked set is empty', () => {
|
|
12
|
+
expect(filterTableByLockedRows(ROWS, [])).toBe(ROWS)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('keeps only rows whose default `id` column is locked', () => {
|
|
16
|
+
expect(filterTableByLockedRows(ROWS, ['a', 'c'])).toEqual([
|
|
17
|
+
{ id: 'a', label: 'Alpha' },
|
|
18
|
+
{ id: 'c', label: 'Gamma' },
|
|
19
|
+
])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('uses a custom keyColumn when supplied', () => {
|
|
23
|
+
const rows = [
|
|
24
|
+
{ sku: 'X1', name: 'Widget' },
|
|
25
|
+
{ sku: 'X2', name: 'Gadget' },
|
|
26
|
+
]
|
|
27
|
+
expect(filterTableByLockedRows(rows, ['X2'], 'sku')).toEqual([
|
|
28
|
+
{ sku: 'X2', name: 'Gadget' },
|
|
29
|
+
])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('falls back to row index when the identity column is nullish', () => {
|
|
33
|
+
const rows = [{ label: 'first' }, { label: 'second' }]
|
|
34
|
+
expect(filterTableByLockedRows(rows, [1])).toEqual([{ label: 'second' }])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns input unchanged when input is not array-shaped', () => {
|
|
38
|
+
expect(filterTableByLockedRows('nope', ['a'])).toBe('nope')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LockSelection` filter for Table. Data is a FLAT `TableRow[]` (unlike every
|
|
3
|
+
* other widget's `X[][]` series shape) and row identity comes from
|
|
4
|
+
* `row[keyColumn]` (default `'id'`), not `name` — so `filterByLockedItems`
|
|
5
|
+
* doesn't apply here (its outer `.map` treats each row as a "series", and a
|
|
6
|
+
* plain row object isn't an array, so it would silently pass everything
|
|
7
|
+
* through unfiltered). Falls back to the row's index when the identity
|
|
8
|
+
* column is nullish, mirroring `TableUI`'s own `resolveRowId`.
|
|
9
|
+
*
|
|
10
|
+
* `keyColumn` isn't part of `LockSelection`'s generic `filterFn` signature —
|
|
11
|
+
* close over it: `filterFn={(data, locked) => filterTableByLockedRows(data, locked, keyColumn)}`.
|
|
12
|
+
*/
|
|
13
|
+
export function filterTableByLockedRows(
|
|
14
|
+
input: unknown,
|
|
15
|
+
lockedItems: readonly (string | number)[],
|
|
16
|
+
keyColumn = 'id',
|
|
17
|
+
): unknown {
|
|
18
|
+
if (lockedItems.length === 0) return input
|
|
19
|
+
if (!Array.isArray(input)) return input
|
|
20
|
+
const locked = new Set<string | number>(lockedItems)
|
|
21
|
+
return input.filter((row: unknown, index: number): boolean => {
|
|
22
|
+
if (row == null || typeof row !== 'object') return false
|
|
23
|
+
const id = (row as Record<string, unknown>)[keyColumn] ?? index
|
|
24
|
+
return (typeof id === 'string' || typeof id === 'number') && locked.has(id)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { timeseriesOptions, createTimeseriesOptionFactory } from './options'
|
|
2
2
|
export { TimeseriesSkeleton } from './skeleton'
|
|
3
3
|
export { createTimeseriesDownloadConfig } from './download'
|
|
4
|
+
export { filterTimeseriesByLockedItems } from './transforms'
|
|
4
5
|
export type {
|
|
5
6
|
TimeseriesDatum,
|
|
6
7
|
TimeseriesWidgetData,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { filterTimeseriesByLockedItems } from './transforms'
|
|
3
|
+
|
|
4
|
+
describe('filterTimeseriesByLockedItems', () => {
|
|
5
|
+
it('returns input unchanged when locked set is empty', () => {
|
|
6
|
+
const data = [[{ name: 1000, value: 10 }]]
|
|
7
|
+
expect(filterTimeseriesByLockedItems(data, [])).toBe(data)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('keeps only items whose (numeric) name is in the locked set', () => {
|
|
11
|
+
const out = filterTimeseriesByLockedItems(
|
|
12
|
+
[
|
|
13
|
+
[
|
|
14
|
+
{ name: 1000, value: 10 },
|
|
15
|
+
{ name: 2000, value: 20 },
|
|
16
|
+
],
|
|
17
|
+
],
|
|
18
|
+
[2000],
|
|
19
|
+
)
|
|
20
|
+
expect(out).toEqual([[{ name: 2000, value: 20 }]])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('normalizes Date names to ms-since-epoch before comparing', () => {
|
|
24
|
+
const d1 = new Date('2024-01-01T00:00:00.000Z')
|
|
25
|
+
const d2 = new Date('2024-01-02T00:00:00.000Z')
|
|
26
|
+
const out = filterTimeseriesByLockedItems(
|
|
27
|
+
[
|
|
28
|
+
[
|
|
29
|
+
{ name: d1, value: 10 },
|
|
30
|
+
{ name: d2, value: 20 },
|
|
31
|
+
],
|
|
32
|
+
],
|
|
33
|
+
[d2.getTime()],
|
|
34
|
+
)
|
|
35
|
+
expect(out).toEqual([[{ name: d2, value: 20 }]])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('handles string names', () => {
|
|
39
|
+
const out = filterTimeseriesByLockedItems(
|
|
40
|
+
[[{ name: '2024-01-01', value: 10 }]],
|
|
41
|
+
['2024-01-01'],
|
|
42
|
+
)
|
|
43
|
+
expect(out).toEqual([[{ name: '2024-01-01', value: 10 }]])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns input unchanged when input is not array-shaped', () => {
|
|
47
|
+
expect(filterTimeseriesByLockedItems('nope', [1])).toBe('nope')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LockSelection` filter for Timeseries. Data is `{name, value}[][]` like
|
|
3
|
+
* Bar/Pie/Category, but `name` may arrive as `Date | number | string` (the
|
|
4
|
+
* time axis stores a `Date`). `lockedItems` is snapshotted from `selection`,
|
|
5
|
+
* whose keys are already normalized to `Date → ms` (see `options.ts`'s
|
|
6
|
+
* `normalizeName`) — so a raw `Date` name must be normalized the same way
|
|
7
|
+
* before the membership check, or every point would compare a `Date` object
|
|
8
|
+
* against a `number` and silently fail to match, filtering everything out.
|
|
9
|
+
*/
|
|
10
|
+
export function filterTimeseriesByLockedItems(
|
|
11
|
+
input: unknown,
|
|
12
|
+
lockedItems: readonly (string | number)[],
|
|
13
|
+
): unknown {
|
|
14
|
+
if (lockedItems.length === 0) return input
|
|
15
|
+
if (!Array.isArray(input)) return input
|
|
16
|
+
const locked = new Set<string | number>(lockedItems)
|
|
17
|
+
return input.map((series: unknown): unknown => {
|
|
18
|
+
if (!Array.isArray(series)) return series
|
|
19
|
+
return series.filter((item: unknown) => {
|
|
20
|
+
if (item == null || typeof item !== 'object' || !('name' in item)) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
const raw = (item as { name: unknown }).name
|
|
24
|
+
const name = raw instanceof Date ? raw.getTime() : raw
|
|
25
|
+
return (
|
|
26
|
+
(typeof name === 'string' || typeof name === 'number') &&
|
|
27
|
+
locked.has(name)
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
}
|