@carto/ps-react-ui 6.0.0 → 6.1.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-BWlS5tpP.js +1150 -0
- package/dist/actions-BWlS5tpP.js.map +1 -0
- package/dist/category-alzjKzvQ.js +839 -0
- package/dist/category-alzjKzvQ.js.map +1 -0
- package/dist/{download-Vlzn2jzb.js → download-B4sykOFm.js} +94 -83
- package/dist/download-B4sykOFm.js.map +1 -0
- package/dist/{echart-DjcbtKgi.js → echart-CV8F50gn.js} +6 -5
- package/dist/echart-CV8F50gn.js.map +1 -0
- package/dist/{formula-CdHWtepq.js → formula-DJS-JLpk.js} +10 -16
- package/dist/{formula-CdHWtepq.js.map → formula-DJS-JLpk.js.map} +1 -1
- package/dist/legend.js +34 -21
- package/dist/legend.js.map +1 -1
- package/dist/{markdown-CY-67NRE.js → markdown-Bn2nKCbn.js} +12 -18
- package/dist/{markdown-CY-67NRE.js.map → markdown-Bn2nKCbn.js.map} +1 -1
- package/dist/{range-DHQSsl2_.js → range-BpU2zBmq.js} +2 -2
- package/dist/{range-DHQSsl2_.js.map → range-BpU2zBmq.js.map} +1 -1
- package/dist/{spread-DuM5oTI1.js → spread-CQwFIwuz.js} +10 -16
- package/dist/{spread-DuM5oTI1.js.map → spread-CQwFIwuz.js.map} +1 -1
- package/dist/{stores-B8CHbgw-.js → stores-DcBOZlU3.js} +30 -24
- package/dist/stores-DcBOZlU3.js.map +1 -0
- package/dist/{table-BE4uO9pp.js → table-DEdW_fDj.js} +7 -13
- package/dist/{table-BE4uO9pp.js.map → table-DEdW_fDj.js.map} +1 -1
- package/dist/types/legend/stores/types.d.ts +4 -2
- package/dist/types/widgets/actions/download/csv-item.d.ts +3 -3
- package/dist/types/widgets/actions/download/index.d.ts +2 -1
- package/dist/types/widgets/actions/download/png-item.d.ts +2 -2
- package/dist/types/widgets/actions/download/types.d.ts +12 -1
- package/dist/types/widgets/actions/download/widget-download.d.ts +26 -0
- package/dist/types/widgets/actions/fullscreen/fullscreen.d.ts +20 -13
- package/dist/types/widgets/actions/index.d.ts +1 -1
- package/dist/types/widgets/actions/searcher/filter.d.ts +3 -2
- package/dist/types/widgets/actions/searcher/searcher-toggle.d.ts +3 -0
- package/dist/types/widgets/bar/download.d.ts +6 -20
- package/dist/types/widgets/category/category-ui.d.ts +9 -1
- package/dist/types/widgets/category/download.d.ts +5 -10
- package/dist/types/widgets/category/helpers.d.ts +10 -0
- package/dist/types/widgets/echart/style.d.ts +1 -0
- package/dist/types/widgets/formula/download.d.ts +5 -15
- package/dist/types/widgets/histogram/download.d.ts +3 -8
- package/dist/types/widgets/index.d.ts +1 -1
- package/dist/types/widgets/markdown/download.d.ts +5 -11
- package/dist/types/widgets/pie/download.d.ts +3 -8
- package/dist/types/widgets/scatterplot/download.d.ts +6 -11
- package/dist/types/widgets/spread/download.d.ts +6 -12
- package/dist/types/widgets/stores/index.d.ts +1 -1
- package/dist/types/widgets/stores/widget-store-registry.d.ts +14 -0
- package/dist/types/widgets/table/download.d.ts +7 -13
- package/dist/types/widgets/timeseries/download.d.ts +7 -12
- package/dist/types/widgets/toolbox/toolbox.d.ts +1 -1
- package/dist/{utils-BStJWhZN.js → utils-C-eWwOhE.js} +2 -2
- package/dist/{utils-BStJWhZN.js.map → utils-C-eWwOhE.js.map} +1 -1
- package/dist/widgets/actions.js +4 -4
- package/dist/widgets/bar.js +11 -17
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/echart.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +14 -20
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +13 -19
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/range.js +1 -1
- package/dist/widgets/scatterplot.js +13 -19
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/stores.js +2 -2
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +16 -22
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets.js +274 -333
- package/dist/widgets.js.map +1 -1
- package/dist/{zoom-toggle-D_VuA01l.js → zoom-toggle-MmLjMTEw.js} +2 -2
- package/dist/{zoom-toggle-D_VuA01l.js.map → zoom-toggle-MmLjMTEw.js.map} +1 -1
- package/package.json +1 -1
- package/src/legend/components/legend-item/legend-item-ui.test.tsx +22 -1
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +22 -6
- package/src/legend/stores/types.ts +4 -2
- package/src/widgets/actions/download/csv-item.test.tsx +3 -3
- package/src/widgets/actions/download/csv-item.tsx +6 -6
- package/src/widgets/actions/download/download.tsx +5 -3
- package/src/widgets/actions/download/index.ts +2 -1
- package/src/widgets/actions/download/png-item.test.tsx +5 -3
- package/src/widgets/actions/download/png-item.tsx +4 -4
- package/src/widgets/actions/download/types.ts +15 -1
- package/src/widgets/actions/download/widget-download.test.ts +30 -0
- package/src/widgets/actions/download/widget-download.ts +41 -0
- package/src/widgets/actions/fullscreen/fullscreen-store.test.tsx +155 -0
- package/src/widgets/actions/fullscreen/fullscreen.test.tsx +39 -41
- package/src/widgets/actions/fullscreen/fullscreen.tsx +198 -96
- package/src/widgets/actions/index.ts +3 -0
- package/src/widgets/actions/searcher/filter.test.ts +28 -0
- package/src/widgets/actions/searcher/filter.ts +22 -4
- package/src/widgets/actions/searcher/searcher-toggle.tsx +16 -8
- package/src/widgets/actions/searcher/searcher.test.tsx +22 -0
- package/src/widgets/actions/searcher/searcher.tsx +17 -7
- package/src/widgets/bar/download.test.tsx +5 -3
- package/src/widgets/bar/download.tsx +11 -35
- package/src/widgets/category/category-ui.tsx +26 -14
- package/src/widgets/category/category.tsx +37 -2
- package/src/widgets/category/components/category-legend.tsx +1 -2
- package/src/widgets/category/download.test.ts +4 -2
- package/src/widgets/category/download.ts +14 -27
- package/src/widgets/category/helpers.test.ts +17 -0
- package/src/widgets/category/helpers.ts +16 -0
- package/src/widgets/echart/style.ts +6 -0
- package/src/widgets/formula/download.test.tsx +3 -1
- package/src/widgets/formula/download.tsx +10 -30
- package/src/widgets/histogram/download.test.ts +6 -4
- package/src/widgets/histogram/download.ts +13 -26
- package/src/widgets/index.ts +1 -0
- package/src/widgets/markdown/download.test.ts +2 -2
- package/src/widgets/markdown/download.tsx +11 -26
- package/src/widgets/pie/download.test.ts +7 -3
- package/src/widgets/pie/download.ts +12 -25
- package/src/widgets/scatterplot/download.test.ts +4 -2
- package/src/widgets/scatterplot/download.ts +15 -28
- package/src/widgets/spread/download.test.ts +3 -1
- package/src/widgets/spread/download.ts +13 -27
- package/src/widgets/stores/index.ts +2 -0
- package/src/widgets/stores/widget-store-registry.test.ts +25 -0
- package/src/widgets/stores/widget-store-registry.ts +22 -0
- package/src/widgets/table/download.test.ts +5 -3
- package/src/widgets/table/download.ts +15 -29
- package/src/widgets/timeseries/download.test.ts +6 -4
- package/src/widgets/timeseries/download.ts +16 -29
- package/src/widgets/toolbox/toolbox.tsx +1 -1
- package/dist/actions-KsX1xdDM.js +0 -1024
- package/dist/actions-KsX1xdDM.js.map +0 -1
- package/dist/category-CsdDqDzo.js +0 -833
- package/dist/category-CsdDqDzo.js.map +0 -1
- package/dist/download-Vlzn2jzb.js.map +0 -1
- package/dist/echart-DjcbtKgi.js.map +0 -1
- package/dist/stores-B8CHbgw-.js.map +0 -1
|
@@ -1,51 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildCsvDownloadItem,
|
|
3
|
-
|
|
3
|
+
withPngItem,
|
|
4
4
|
type DownloadItem,
|
|
5
|
+
type WidgetDownloadArgs,
|
|
5
6
|
} from '../actions/download'
|
|
6
7
|
import type { BarWidgetData } from './types'
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
filename: string
|
|
10
|
-
getData: () => BarWidgetData
|
|
11
|
-
/**
|
|
12
|
-
* Optional getter for the widget's capture element (registered by
|
|
13
|
-
* `Widget.State` on its success path). When provided, a PNG item is
|
|
14
|
-
* prepended to the returned list. Wire it to
|
|
15
|
-
* `() => getCaptureEl(id)`.
|
|
16
|
-
*/
|
|
17
|
-
getCaptureEl?: () => HTMLElement | null
|
|
18
|
-
/** PNG `pixelRatio` (default 2). */
|
|
19
|
-
pngPixelRatio?: number
|
|
20
|
-
/** PNG `backgroundColor` (default transparent). */
|
|
21
|
-
pngBackgroundColor?: string | null
|
|
22
|
-
}
|
|
9
|
+
export type BarDownloadConfigArgs = WidgetDownloadArgs<BarWidgetData>
|
|
23
10
|
|
|
24
11
|
/**
|
|
25
|
-
* Builds download items for the Bar widget. Always includes a CSV item
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
12
|
+
* Builds download items for the Bar widget. Always includes a CSV item with
|
|
13
|
+
* one `name,value` block per series, separated by an empty row when there
|
|
14
|
+
* are multiple. When `getCaptureEl` is given, also prepends a PNG item that
|
|
15
|
+
* rasterises the captured element via `html2canvas`.
|
|
29
16
|
*/
|
|
30
17
|
export function createBarDownloadConfig(
|
|
31
18
|
args: BarDownloadConfigArgs,
|
|
32
19
|
): DownloadItem[] {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
items.push(
|
|
36
|
-
buildPngDownloadItem({
|
|
37
|
-
filename: args.filename,
|
|
38
|
-
getCaptureEl: args.getCaptureEl,
|
|
39
|
-
pixelRatio: args.pngPixelRatio,
|
|
40
|
-
backgroundColor: args.pngBackgroundColor,
|
|
41
|
-
}),
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
items.push(
|
|
20
|
+
return withPngItem(
|
|
21
|
+
args,
|
|
45
22
|
buildCsvDownloadItem({
|
|
46
23
|
filename: args.filename,
|
|
47
|
-
getRows: () => {
|
|
48
|
-
const data = args.getData()
|
|
24
|
+
getRows: (ctx) => {
|
|
25
|
+
const data = args.getData(ctx)
|
|
49
26
|
const rows: unknown[][] = []
|
|
50
27
|
for (const [i, series] of data.entries()) {
|
|
51
28
|
if (i > 0) rows.push([])
|
|
@@ -56,5 +33,4 @@ export function createBarDownloadConfig(
|
|
|
56
33
|
},
|
|
57
34
|
}),
|
|
58
35
|
)
|
|
59
|
-
return items
|
|
60
36
|
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
CategoryWidgetData,
|
|
14
14
|
} from './types'
|
|
15
15
|
import { styles } from './style'
|
|
16
|
+
import { categorySeriesKey } from './helpers'
|
|
16
17
|
import { resolveThemeColor } from '../utils'
|
|
17
18
|
|
|
18
19
|
const DEFAULT_MAX_ITEMS = 20
|
|
@@ -97,6 +98,14 @@ export interface CategoryUIProps {
|
|
|
97
98
|
* No-op for single-series data — same convention as the legend.
|
|
98
99
|
*/
|
|
99
100
|
stacked?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* Series indices hidden via the legend. Leave both this and
|
|
103
|
+
* `onHiddenSeriesChange` unset and CategoryUI keeps the set itself; pass
|
|
104
|
+
* them to own it — `Category` does, so the card and the fullscreen copy
|
|
105
|
+
* share it through the widget store.
|
|
106
|
+
*/
|
|
107
|
+
hiddenSeries?: ReadonlySet<number>
|
|
108
|
+
onHiddenSeriesChange?: (next: ReadonlySet<number>) => void
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
interface GroupedRow {
|
|
@@ -182,6 +191,8 @@ export function CategoryUI({
|
|
|
182
191
|
maxOverride,
|
|
183
192
|
size = 'small',
|
|
184
193
|
stacked = false,
|
|
194
|
+
hiddenSeries: hiddenSeriesProp,
|
|
195
|
+
onHiddenSeriesChange,
|
|
185
196
|
}: CategoryUIProps) {
|
|
186
197
|
const theme = useTheme()
|
|
187
198
|
const fmt = formatter ?? ((n: number) => String(n))
|
|
@@ -304,9 +315,10 @@ export function CategoryUI({
|
|
|
304
315
|
// selection, unrelated). Never allowed to reach "every series hidden":
|
|
305
316
|
// toggling the last visible one instead resets to "all visible" rather
|
|
306
317
|
// than leaving the widget blank.
|
|
307
|
-
const [
|
|
318
|
+
const [localHidden, setLocalHidden] = useState<ReadonlySet<number>>(
|
|
308
319
|
() => new Set(),
|
|
309
320
|
)
|
|
321
|
+
const hiddenSeries = hiddenSeriesProp ?? localHidden
|
|
310
322
|
// A reload with a different series breakdown invalidates hidden indices —
|
|
311
323
|
// index 2 may now point at an entirely different series. Reset rather
|
|
312
324
|
// than keep hiding whatever now occupies a stale index. Adjusted directly
|
|
@@ -317,11 +329,11 @@ export function CategoryUI({
|
|
|
317
329
|
// `series={[...]}` literal (no useMemo) gets a new reference on every
|
|
318
330
|
// unrelated parent re-render, which would otherwise reset hiddenSeries
|
|
319
331
|
// right after the user hides one.
|
|
320
|
-
const seriesKey =
|
|
332
|
+
const seriesKey = categorySeriesKey(series)
|
|
321
333
|
const [prevSeriesKey, setPrevSeriesKey] = useState(seriesKey)
|
|
322
334
|
if (seriesKey !== prevSeriesKey) {
|
|
323
335
|
setPrevSeriesKey(seriesKey)
|
|
324
|
-
|
|
336
|
+
setLocalHidden(new Set())
|
|
325
337
|
}
|
|
326
338
|
// Real number of data series — NOT `series?.length`, which is legend
|
|
327
339
|
// metadata and may legitimately be shorter than the actual series count
|
|
@@ -331,18 +343,18 @@ export function CategoryUI({
|
|
|
331
343
|
const seriesCount = Math.max(data.length, 1)
|
|
332
344
|
const toggleSeries = useCallback(
|
|
333
345
|
(index: number) => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
346
|
+
// Start from what the user sees, not the stored set: `data` can
|
|
347
|
+
// shrink while the `series` names hold (the reset only watches the
|
|
348
|
+
// names), and a stale index would otherwise count toward the guard
|
|
349
|
+
// below — or make unchecking one series hide a different one.
|
|
350
|
+
const next = effectiveHidden(hiddenSeries, seriesCount)
|
|
351
|
+
if (next.has(index)) next.delete(index)
|
|
352
|
+
else next.add(index)
|
|
353
|
+
;(onHiddenSeriesChange ?? setLocalHidden)(
|
|
354
|
+
next.size >= seriesCount ? new Set() : next,
|
|
355
|
+
)
|
|
344
356
|
},
|
|
345
|
-
[seriesCount],
|
|
357
|
+
[hiddenSeries, onHiddenSeriesChange, seriesCount],
|
|
346
358
|
)
|
|
347
359
|
|
|
348
360
|
if (grouped.length === 0) return null
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
getWidgetStore,
|
|
4
|
+
useWidget,
|
|
5
|
+
useWidgetId,
|
|
6
|
+
useWidgetShallow,
|
|
7
|
+
type WidgetState,
|
|
8
|
+
} from '../stores'
|
|
3
9
|
import { CategoryUI } from './category-ui'
|
|
10
|
+
import { categorySeriesKey, NO_HIDDEN_SERIES } from './helpers'
|
|
4
11
|
import type {
|
|
5
12
|
CategoryKey,
|
|
6
13
|
CategoryLabels,
|
|
@@ -23,6 +30,17 @@ const categorySelector = (s: WidgetState): CategorySlice => ({
|
|
|
23
30
|
labelFormatter: s.labelFormatter,
|
|
24
31
|
})
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Legend-hidden series, kept on the widget store so every mounted Category
|
|
35
|
+
* of the widget — the card and its fullscreen copy — shows the same set.
|
|
36
|
+
* Tagged with the series breakdown it was made for: after a reload with
|
|
37
|
+
* different series the indices point elsewhere, so a stale set reads as
|
|
38
|
+
* empty instead of being reset with a write.
|
|
39
|
+
*/
|
|
40
|
+
interface CategoryWidgetState extends WidgetState {
|
|
41
|
+
categoryHiddenSeries?: { key: string; indices: ReadonlySet<number> }
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
function maxFromCategoryData(d: CategoryWidgetData | undefined): number {
|
|
27
45
|
if (!d) return 0
|
|
28
46
|
let m = 0
|
|
@@ -111,6 +129,21 @@ export function Category({
|
|
|
111
129
|
)
|
|
112
130
|
const effectiveMaxOverride =
|
|
113
131
|
maxOverride ?? (naturalMax > 0 ? naturalMax : undefined)
|
|
132
|
+
|
|
133
|
+
const seriesKey = categorySeriesKey(series)
|
|
134
|
+
const stored = useWidget<
|
|
135
|
+
CategoryWidgetState['categoryHiddenSeries'],
|
|
136
|
+
CategoryWidgetState
|
|
137
|
+
>(id, (s) => s.categoryHiddenSeries)
|
|
138
|
+
const hiddenSeries =
|
|
139
|
+
stored?.key === seriesKey ? stored.indices : NO_HIDDEN_SERIES
|
|
140
|
+
const onHiddenSeriesChange = useCallback(
|
|
141
|
+
(indices: ReadonlySet<number>) =>
|
|
142
|
+
getWidgetStore(id).setState({
|
|
143
|
+
categoryHiddenSeries: { key: seriesKey, indices },
|
|
144
|
+
} as Partial<CategoryWidgetState>),
|
|
145
|
+
[id, seriesKey],
|
|
146
|
+
)
|
|
114
147
|
return (
|
|
115
148
|
<CategoryUI
|
|
116
149
|
data={slice.data}
|
|
@@ -125,6 +158,8 @@ export function Category({
|
|
|
125
158
|
maxOverride={effectiveMaxOverride}
|
|
126
159
|
size={size}
|
|
127
160
|
stacked={stacked}
|
|
161
|
+
hiddenSeries={hiddenSeries}
|
|
162
|
+
onHiddenSeriesChange={onHiddenSeriesChange}
|
|
128
163
|
/>
|
|
129
164
|
)
|
|
130
165
|
}
|
|
@@ -2,6 +2,7 @@ import type { CssColor } from '../../../components/types'
|
|
|
2
2
|
import { Box, Typography } from '@mui/material'
|
|
3
3
|
import type { CategorySeriesConfig } from '../types'
|
|
4
4
|
import { styles } from '../style'
|
|
5
|
+
import { NO_HIDDEN_SERIES } from '../helpers'
|
|
5
6
|
|
|
6
7
|
export interface CategoryLegendProps {
|
|
7
8
|
/** Series metadata. Empty array → renders nothing. */
|
|
@@ -30,8 +31,6 @@ export interface CategoryLegendProps {
|
|
|
30
31
|
onToggle?: (index: number) => void
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const NO_HIDDEN_SERIES: ReadonlySet<number> = new Set()
|
|
34
|
-
|
|
35
34
|
/**
|
|
36
35
|
* Sticky color legend rendered below a multi-series Category list.
|
|
37
36
|
* Per-series `color` overrides the matching `colorAt(i)` result. Clicking an
|
|
@@ -60,7 +60,9 @@ describe('createCategoryDownloadConfig', () => {
|
|
|
60
60
|
filename: 'c',
|
|
61
61
|
getData: () => data,
|
|
62
62
|
})
|
|
63
|
-
const handle = await items
|
|
63
|
+
const handle = await items
|
|
64
|
+
.find((i) => i.id === 'csv')!
|
|
65
|
+
.resolve({ id: null })
|
|
64
66
|
expect(handle.filename).toBe('c.csv')
|
|
65
67
|
expect(csvText).toBe(
|
|
66
68
|
'series,name,value\nseries_1,a,1\nseries_1,b,2\nseries_2,a,10',
|
|
@@ -73,7 +75,7 @@ describe('createCategoryDownloadConfig', () => {
|
|
|
73
75
|
getData: () => data,
|
|
74
76
|
seriesNames: ['baseline', 'override'],
|
|
75
77
|
})
|
|
76
|
-
await items.find((i) => i.id === 'csv')!.resolve()
|
|
78
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
77
79
|
expect(csvText.split('\n')[1]).toBe('baseline,a,1')
|
|
78
80
|
expect(csvText.split('\n')[3]).toBe('override,a,10')
|
|
79
81
|
})
|
|
@@ -1,40 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildCsvDownloadItem,
|
|
3
|
-
|
|
3
|
+
withPngItem,
|
|
4
4
|
type DownloadItem,
|
|
5
|
+
type WidgetDownloadArgs,
|
|
5
6
|
} from '../actions/download'
|
|
6
7
|
import type { CategoryWidgetData } from './types'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Download menu items for the Category widget. Always includes a CSV item
|
|
10
|
-
* with `series, name, value` columns (one row per item). When
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* with `series, name, value` columns (one row per item). When `getCaptureEl`
|
|
12
|
+
* is given, prepends a PNG item that rasterises the captured element via
|
|
13
|
+
* `html2canvas`.
|
|
13
14
|
*/
|
|
14
|
-
export function createCategoryDownloadConfig(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}): DownloadItem[] {
|
|
22
|
-
const items: DownloadItem[] = []
|
|
23
|
-
if (args.getCaptureEl) {
|
|
24
|
-
items.push(
|
|
25
|
-
buildPngDownloadItem({
|
|
26
|
-
filename: args.filename,
|
|
27
|
-
getCaptureEl: args.getCaptureEl,
|
|
28
|
-
pixelRatio: args.pngPixelRatio,
|
|
29
|
-
backgroundColor: args.pngBackgroundColor,
|
|
30
|
-
}),
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
items.push(
|
|
15
|
+
export function createCategoryDownloadConfig(
|
|
16
|
+
args: WidgetDownloadArgs<CategoryWidgetData> & {
|
|
17
|
+
seriesNames?: readonly string[]
|
|
18
|
+
},
|
|
19
|
+
): DownloadItem[] {
|
|
20
|
+
return withPngItem(
|
|
21
|
+
args,
|
|
34
22
|
buildCsvDownloadItem({
|
|
35
23
|
filename: args.filename,
|
|
36
|
-
getRows: () => {
|
|
37
|
-
const data = args.getData()
|
|
24
|
+
getRows: (ctx) => {
|
|
25
|
+
const data = args.getData(ctx)
|
|
38
26
|
const rows: unknown[][] = [['series', 'name', 'value']]
|
|
39
27
|
for (const [i, series] of data.entries()) {
|
|
40
28
|
const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
|
|
@@ -46,5 +34,4 @@ export function createCategoryDownloadConfig(args: {
|
|
|
46
34
|
},
|
|
47
35
|
}),
|
|
48
36
|
)
|
|
49
|
-
return items
|
|
50
37
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { categorySeriesKey } from './helpers'
|
|
3
|
+
|
|
4
|
+
describe('categorySeriesKey', () => {
|
|
5
|
+
it('depends on the series names only', () => {
|
|
6
|
+
expect(categorySeriesKey([{ name: 'A' }, { name: 'B' }])).toBe(
|
|
7
|
+
categorySeriesKey([{ name: 'A', color: 'red' }, { name: 'B' }]),
|
|
8
|
+
)
|
|
9
|
+
expect(categorySeriesKey([{ name: 'A' }])).not.toBe(
|
|
10
|
+
categorySeriesKey([{ name: 'B' }]),
|
|
11
|
+
)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('treats no series as an empty breakdown', () => {
|
|
15
|
+
expect(categorySeriesKey(undefined)).toBe(categorySeriesKey([]))
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CategorySeriesConfig } from './types'
|
|
2
|
+
|
|
3
|
+
/** Shared empty set: "no series hidden" without a new identity per render. */
|
|
4
|
+
export const NO_HIDDEN_SERIES: ReadonlySet<number> = new Set()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Identifies a series breakdown by its names. Hidden-series indices are only
|
|
8
|
+
* valid for the breakdown they were made for, and both the store copy (in
|
|
9
|
+
* `Category`) and the local one (in `CategoryUI`) are tagged with this key —
|
|
10
|
+
* so they must compute it the same way.
|
|
11
|
+
*/
|
|
12
|
+
export function categorySeriesKey(
|
|
13
|
+
series: readonly CategorySeriesConfig[] | undefined,
|
|
14
|
+
): string {
|
|
15
|
+
return JSON.stringify(series?.map((s) => s.name) ?? [])
|
|
16
|
+
}
|
|
@@ -13,5 +13,11 @@ export const styles = {
|
|
|
13
13
|
// with `<Widget.Echart sx={{ minHeight }} />`.
|
|
14
14
|
flex: 1,
|
|
15
15
|
minHeight: 304,
|
|
16
|
+
// ECharts puts its tooltip inside this box at `z-index: 9999999`. With
|
|
17
|
+
// no stacking context of its own, that number competes page-wide and
|
|
18
|
+
// beats MUI's modal and popover layers, so a tooltip left showing
|
|
19
|
+
// paints over any dialog or menu opened above the chart. Isolating
|
|
20
|
+
// keeps the tooltip above its own chart only.
|
|
21
|
+
isolation: 'isolate',
|
|
16
22
|
},
|
|
17
23
|
} satisfies Record<string, SxProps<Theme>>
|
|
@@ -56,7 +56,9 @@ describe('createFormulaDownloadConfig', () => {
|
|
|
56
56
|
filename: 'f',
|
|
57
57
|
getData: () => data,
|
|
58
58
|
})
|
|
59
|
-
const handle = await items
|
|
59
|
+
const handle = await items
|
|
60
|
+
.find((i) => i.id === 'csv')!
|
|
61
|
+
.resolve({ id: null })
|
|
60
62
|
expect(handle.filename).toBe('f.csv')
|
|
61
63
|
expect(csvText).toBe(
|
|
62
64
|
'series,prefix,value,suffix,note,delta\nRev,$,42,k,q1,12\n,,10,,,',
|
|
@@ -1,47 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildCsvDownloadItem,
|
|
3
|
-
|
|
3
|
+
withPngItem,
|
|
4
4
|
type DownloadItem,
|
|
5
|
+
type WidgetDownloadArgs,
|
|
5
6
|
} from '../actions/download'
|
|
6
7
|
import type { FormulaWidgetData } from './types'
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
filename: string
|
|
10
|
-
getData: () => FormulaWidgetData
|
|
11
|
-
/**
|
|
12
|
-
* Optional getter for the widget's capture element (registered by
|
|
13
|
-
* `Widget.State`). When provided, a PNG item is prepended.
|
|
14
|
-
*/
|
|
15
|
-
getCaptureEl?: () => HTMLElement | null
|
|
16
|
-
pngPixelRatio?: number
|
|
17
|
-
pngBackgroundColor?: string | null
|
|
18
|
-
}
|
|
9
|
+
export type FormulaDownloadConfigArgs = WidgetDownloadArgs<FormulaWidgetData>
|
|
19
10
|
|
|
20
11
|
/**
|
|
21
12
|
* Builds download items for the Formula widget. Always includes a CSV item
|
|
22
|
-
* with one row per KPI (`prefix`, `value`, `suffix`, `note`, `delta`).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
13
|
+
* with one row per KPI (`prefix`, `value`, `suffix`, `note`, `delta`). When
|
|
14
|
+
* `getCaptureEl` is given, prepends a PNG item that rasterises the captured
|
|
15
|
+
* element via `html2canvas`.
|
|
25
16
|
*/
|
|
26
17
|
export function createFormulaDownloadConfig(
|
|
27
18
|
args: FormulaDownloadConfigArgs,
|
|
28
19
|
): DownloadItem[] {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
items.push(
|
|
32
|
-
buildPngDownloadItem({
|
|
33
|
-
filename: args.filename,
|
|
34
|
-
getCaptureEl: args.getCaptureEl,
|
|
35
|
-
pixelRatio: args.pngPixelRatio,
|
|
36
|
-
backgroundColor: args.pngBackgroundColor,
|
|
37
|
-
}),
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
items.push(
|
|
20
|
+
return withPngItem(
|
|
21
|
+
args,
|
|
41
22
|
buildCsvDownloadItem({
|
|
42
23
|
filename: args.filename,
|
|
43
|
-
getRows: () => {
|
|
44
|
-
const data = args.getData()
|
|
24
|
+
getRows: (ctx) => {
|
|
25
|
+
const data = args.getData(ctx)
|
|
45
26
|
const rows: unknown[][] = [
|
|
46
27
|
['series', 'prefix', 'value', 'suffix', 'note', 'delta'],
|
|
47
28
|
]
|
|
@@ -59,5 +40,4 @@ export function createFormulaDownloadConfig(
|
|
|
59
40
|
},
|
|
60
41
|
}),
|
|
61
42
|
)
|
|
62
|
-
return items
|
|
63
43
|
}
|
|
@@ -52,7 +52,9 @@ describe('createHistogramDownloadConfig', () => {
|
|
|
52
52
|
getData: () => data,
|
|
53
53
|
getTicks: () => ticks,
|
|
54
54
|
})
|
|
55
|
-
const handle = await items
|
|
55
|
+
const handle = await items
|
|
56
|
+
.find((i) => i.id === 'csv')!
|
|
57
|
+
.resolve({ id: null })
|
|
56
58
|
expect(handle.filename).toBe('h.csv')
|
|
57
59
|
expect(csvText).toBe(
|
|
58
60
|
'bin_low,bin_high,series_1,series_2\n0,10,1,4\n10,20,2,5\n20,30,3,6',
|
|
@@ -66,7 +68,7 @@ describe('createHistogramDownloadConfig', () => {
|
|
|
66
68
|
getTicks: () => ticks,
|
|
67
69
|
seriesNames: ['lo', 'hi'],
|
|
68
70
|
})
|
|
69
|
-
await items.find((i) => i.id === 'csv')!.resolve()
|
|
71
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
70
72
|
expect(csvText.startsWith('bin_low,bin_high,lo,hi')).toBe(true)
|
|
71
73
|
})
|
|
72
74
|
|
|
@@ -76,7 +78,7 @@ describe('createHistogramDownloadConfig', () => {
|
|
|
76
78
|
getData: () => data,
|
|
77
79
|
getTicks: () => [0],
|
|
78
80
|
})
|
|
79
|
-
await items.find((i) => i.id === 'csv')!.resolve()
|
|
81
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
80
82
|
expect(csvText).toBe('bin_low,bin_high,series_1,series_2')
|
|
81
83
|
})
|
|
82
84
|
|
|
@@ -87,7 +89,7 @@ describe('createHistogramDownloadConfig', () => {
|
|
|
87
89
|
getData: () => sparse,
|
|
88
90
|
getTicks: () => ticks,
|
|
89
91
|
})
|
|
90
|
-
await items.find((i) => i.id === 'csv')!.resolve()
|
|
92
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
91
93
|
expect(csvText).toContain('0,10,1,0')
|
|
92
94
|
expect(csvText).toContain('10,20,2,0')
|
|
93
95
|
})
|
|
@@ -1,41 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildCsvDownloadItem,
|
|
3
|
-
|
|
3
|
+
withPngItem,
|
|
4
4
|
type DownloadItem,
|
|
5
|
+
type WidgetDownloadArgs,
|
|
5
6
|
} from '../actions/download'
|
|
6
7
|
import type { HistogramWidgetData } from './types'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Download menu items for histograms. Always includes a CSV item with
|
|
10
11
|
* `bin_low, bin_high, series_1_count, series_2_count, …` columns (one row
|
|
11
|
-
* per bin). When `getCaptureEl` is
|
|
12
|
+
* per bin). When `getCaptureEl` is given, prepends a PNG item that
|
|
12
13
|
* rasterises the captured element via `html2canvas`.
|
|
13
14
|
*/
|
|
14
|
-
export function createHistogramDownloadConfig(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}): DownloadItem[] {
|
|
23
|
-
const items: DownloadItem[] = []
|
|
24
|
-
if (args.getCaptureEl) {
|
|
25
|
-
items.push(
|
|
26
|
-
buildPngDownloadItem({
|
|
27
|
-
filename: args.filename,
|
|
28
|
-
getCaptureEl: args.getCaptureEl,
|
|
29
|
-
pixelRatio: args.pngPixelRatio,
|
|
30
|
-
backgroundColor: args.pngBackgroundColor,
|
|
31
|
-
}),
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
items.push(
|
|
15
|
+
export function createHistogramDownloadConfig(
|
|
16
|
+
args: WidgetDownloadArgs<HistogramWidgetData> & {
|
|
17
|
+
getTicks: () => readonly number[]
|
|
18
|
+
seriesNames?: readonly string[]
|
|
19
|
+
},
|
|
20
|
+
): DownloadItem[] {
|
|
21
|
+
return withPngItem(
|
|
22
|
+
args,
|
|
35
23
|
buildCsvDownloadItem({
|
|
36
24
|
filename: args.filename,
|
|
37
|
-
getRows: () => {
|
|
38
|
-
const data = args.getData()
|
|
25
|
+
getRows: (ctx) => {
|
|
26
|
+
const data = args.getData(ctx)
|
|
39
27
|
const ticks = args.getTicks()
|
|
40
28
|
const seriesCount = data.length
|
|
41
29
|
const header: unknown[] = ['bin_low', 'bin_high']
|
|
@@ -52,5 +40,4 @@ export function createHistogramDownloadConfig(args: {
|
|
|
52
40
|
},
|
|
53
41
|
}),
|
|
54
42
|
)
|
|
55
|
-
return items
|
|
56
43
|
}
|
package/src/widgets/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ describe('createMarkdownDownloadConfig', () => {
|
|
|
46
46
|
filename: 'doc',
|
|
47
47
|
getData: () => ({ content: '# title\n\nbody' }),
|
|
48
48
|
})
|
|
49
|
-
const handle = await items.find((i) => i.id === 'md')!.resolve()
|
|
49
|
+
const handle = await items.find((i) => i.id === 'md')!.resolve({ id: null })
|
|
50
50
|
expect(handle.url).toBe('blob:mock')
|
|
51
51
|
expect(handle.filename).toBe('doc.md')
|
|
52
52
|
expect(mdText).toBe('# title\n\nbody')
|
|
@@ -60,7 +60,7 @@ describe('createMarkdownDownloadConfig', () => {
|
|
|
60
60
|
// missing content field — exercise the `?? ''` branch
|
|
61
61
|
getData: () => ({}) as { content: string },
|
|
62
62
|
})
|
|
63
|
-
await items.find((i) => i.id === 'md')!.resolve()
|
|
63
|
+
await items.find((i) => i.id === 'md')!.resolve({ id: null })
|
|
64
64
|
expect(mdText).toBe('')
|
|
65
65
|
})
|
|
66
66
|
})
|
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DOWNLOAD_ITEM_IDS,
|
|
3
3
|
MarkdownIcon,
|
|
4
|
-
|
|
4
|
+
withPngItem,
|
|
5
5
|
toCsvString,
|
|
6
6
|
triggerLinkDownload,
|
|
7
7
|
type DownloadItem,
|
|
8
|
+
type WidgetDownloadArgs,
|
|
8
9
|
} from '../actions/download'
|
|
9
10
|
import type { MarkdownWidgetData } from './types'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Download menu items for the Markdown widget. Always includes a `.md`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Download menu items for the Markdown widget. Always includes a `.md` item
|
|
14
|
+
* that saves the markdown source verbatim. When `getCaptureEl` is given,
|
|
15
|
+
* prepends a PNG item that rasterises the captured element via
|
|
15
16
|
* `html2canvas`.
|
|
16
17
|
*/
|
|
17
|
-
export function createMarkdownDownloadConfig(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
pngPixelRatio?: number
|
|
22
|
-
pngBackgroundColor?: string | null
|
|
23
|
-
}): DownloadItem[] {
|
|
24
|
-
const items: DownloadItem[] = []
|
|
25
|
-
if (args.getCaptureEl) {
|
|
26
|
-
items.push(
|
|
27
|
-
buildPngDownloadItem({
|
|
28
|
-
filename: args.filename,
|
|
29
|
-
getCaptureEl: args.getCaptureEl,
|
|
30
|
-
pixelRatio: args.pngPixelRatio,
|
|
31
|
-
backgroundColor: args.pngBackgroundColor,
|
|
32
|
-
}),
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
items.push({
|
|
18
|
+
export function createMarkdownDownloadConfig(
|
|
19
|
+
args: WidgetDownloadArgs<MarkdownWidgetData>,
|
|
20
|
+
): DownloadItem[] {
|
|
21
|
+
return withPngItem(args, {
|
|
36
22
|
id: DOWNLOAD_ITEM_IDS.markdown,
|
|
37
23
|
label: 'Markdown',
|
|
38
24
|
icon: <MarkdownIcon fontSize='small' />,
|
|
39
|
-
resolve: () => {
|
|
40
|
-
const data = args.getData()
|
|
25
|
+
resolve: (ctx) => {
|
|
26
|
+
const data = args.getData(ctx)
|
|
41
27
|
const blob = new Blob([data.content ?? ''], {
|
|
42
28
|
type: 'text/markdown;charset=utf-8;',
|
|
43
29
|
})
|
|
@@ -49,7 +35,6 @@ export function createMarkdownDownloadConfig(args: {
|
|
|
49
35
|
})
|
|
50
36
|
},
|
|
51
37
|
})
|
|
52
|
-
return items
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
// Re-export pure helpers consumers may want when building a custom Download item.
|
|
@@ -51,7 +51,9 @@ describe('createPieDownloadConfig', () => {
|
|
|
51
51
|
getCaptureEl: () => null,
|
|
52
52
|
})
|
|
53
53
|
expect(items.map((i) => i.id)).toEqual(['png', 'csv'])
|
|
54
|
-
await expect(items[0]!.resolve()).rejects.toThrow(
|
|
54
|
+
await expect(items[0]!.resolve({ id: null })).rejects.toThrow(
|
|
55
|
+
/No PNG capture element/,
|
|
56
|
+
)
|
|
55
57
|
})
|
|
56
58
|
|
|
57
59
|
it('CSV resolve uses default series_<n> when seriesNames is absent', async () => {
|
|
@@ -59,7 +61,9 @@ describe('createPieDownloadConfig', () => {
|
|
|
59
61
|
filename: 'p',
|
|
60
62
|
getData: () => multi,
|
|
61
63
|
})
|
|
62
|
-
const handle = await items
|
|
64
|
+
const handle = await items
|
|
65
|
+
.find((i) => i.id === 'csv')!
|
|
66
|
+
.resolve({ id: null })
|
|
63
67
|
expect(handle.filename).toBe('p.csv')
|
|
64
68
|
expect(csvText).toBe('series,name,value\nseries_1,a,1\nseries_2,c,3')
|
|
65
69
|
handle.revoke?.()
|
|
@@ -72,7 +76,7 @@ describe('createPieDownloadConfig', () => {
|
|
|
72
76
|
getData: () => multi,
|
|
73
77
|
seriesNames: ['alpha', 'beta'],
|
|
74
78
|
})
|
|
75
|
-
await items.find((i) => i.id === 'csv')!.resolve()
|
|
79
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
76
80
|
expect(csvText).toBe('series,name,value\nalpha,a,1\nbeta,c,3')
|
|
77
81
|
})
|
|
78
82
|
})
|