@carto/ps-react-ui 6.1.1 → 6.3.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 → actions-CkxKe8-J.js} +57 -47
- package/dist/actions-CkxKe8-J.js.map +1 -0
- package/dist/category-DULnoxvk.js +933 -0
- package/dist/category-DULnoxvk.js.map +1 -0
- package/dist/chat.js +1 -1
- package/dist/chat.js.map +1 -1
- package/dist/components-CTUpdQuj.js.map +1 -1
- package/dist/download-B4sykOFm.js.map +1 -1
- package/dist/{echart-CV8F50gn.js → echart-ysYFLa_T.js} +2 -2
- package/dist/echart-ysYFLa_T.js.map +1 -0
- package/dist/formula-DJS-JLpk.js.map +1 -1
- package/dist/{option-builders-DQivvvbb.js → item-color-B7xkIpDZ.js} +26 -9
- package/dist/item-color-B7xkIpDZ.js.map +1 -0
- package/dist/legend.js.map +1 -1
- package/dist/{markdown-Bn2nKCbn.js → markdown-DmvLi3vx.js} +2 -2
- package/dist/markdown-DmvLi3vx.js.map +1 -0
- package/dist/{markdown-content-kYNLOX0T.js → markdown-content-BpAH_V6S.js} +5 -4
- package/dist/markdown-content-BpAH_V6S.js.map +1 -0
- package/dist/range-BpU2zBmq.js.map +1 -1
- package/dist/smart-tooltip-Dc_rHhyI.js.map +1 -1
- package/dist/spread-CQwFIwuz.js.map +1 -1
- package/dist/stores-DcBOZlU3.js.map +1 -1
- package/dist/stores-mad-9yFn.js.map +1 -1
- package/dist/styles-BCEBm7xh.js.map +1 -1
- package/dist/table-DEdW_fDj.js.map +1 -1
- package/dist/types/widgets/bar/types.d.ts +6 -0
- package/dist/types/widgets/category/category-ui.d.ts +10 -2
- package/dist/types/widgets/category/category.d.ts +7 -1
- package/dist/types/widgets/category/components/category-bar-stacked.d.ts +8 -1
- package/dist/types/widgets/category/components/category-bar.d.ts +24 -2
- package/dist/types/widgets/category/components/category-row-multi.d.ts +3 -1
- package/dist/types/widgets/category/components/category-row-single.d.ts +3 -1
- package/dist/types/widgets/category/components/category-row-stacked.d.ts +10 -1
- package/dist/types/widgets/category/style.d.ts +33 -6
- package/dist/types/widgets/histogram/bins.d.ts +7 -0
- package/dist/types/widgets/histogram/download.d.ts +2 -2
- package/dist/types/widgets/histogram/index.d.ts +2 -1
- package/dist/types/widgets/histogram/options.d.ts +1 -1
- package/dist/types/widgets/histogram/transforms.d.ts +5 -4
- package/dist/types/widgets/histogram/types.d.ts +22 -5
- package/dist/types/widgets/index.d.ts +1 -1
- package/dist/types/widgets/pie/types.d.ts +6 -0
- package/dist/types/widgets/scatterplot/download.d.ts +2 -2
- package/dist/types/widgets/scatterplot/index.d.ts +2 -1
- package/dist/types/widgets/scatterplot/points.d.ts +7 -0
- package/dist/types/widgets/scatterplot/transforms.d.ts +4 -3
- package/dist/types/widgets/scatterplot/types.d.ts +21 -2
- package/dist/types/widgets/timeseries/types.d.ts +6 -0
- package/dist/types/widgets/utils/item-color.d.ts +26 -0
- package/dist/utils-C-eWwOhE.js.map +1 -1
- package/dist/widgets/actions.js +1 -1
- package/dist/widgets/bar.js +78 -70
- 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/histogram.js +116 -85
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +2 -2
- package/dist/widgets/pie.js +109 -90
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js +131 -92
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/timeseries.js +82 -74
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets.js +5 -5
- package/dist/widgets.js.map +1 -1
- package/dist/zoom-toggle-MmLjMTEw.js.map +1 -1
- package/package.json +2 -2
- package/src/widgets/actions/fullscreen/fullscreen-store.test.tsx +77 -13
- package/src/widgets/actions/fullscreen/fullscreen.tsx +48 -5
- package/src/widgets/bar/options.test.ts +106 -0
- package/src/widgets/bar/options.ts +25 -14
- package/src/widgets/bar/types.ts +6 -0
- package/src/widgets/category/category-ui.test.tsx +139 -0
- package/src/widgets/category/category-ui.tsx +64 -11
- package/src/widgets/category/category.test.tsx +67 -0
- package/src/widgets/category/category.tsx +32 -15
- package/src/widgets/category/components/category-bar-stacked.test.tsx +18 -0
- package/src/widgets/category/components/category-bar-stacked.tsx +42 -29
- package/src/widgets/category/components/category-bar.test.tsx +66 -0
- package/src/widgets/category/components/category-bar.tsx +65 -9
- package/src/widgets/category/components/category-row-multi.tsx +21 -0
- package/src/widgets/category/components/category-row-single.tsx +4 -0
- package/src/widgets/category/components/category-row-stacked.tsx +24 -1
- package/src/widgets/category/style.ts +58 -14
- package/src/widgets/histogram/bins.ts +22 -0
- package/src/widgets/histogram/download.test.ts +10 -0
- package/src/widgets/histogram/download.ts +7 -3
- package/src/widgets/histogram/index.ts +3 -0
- package/src/widgets/histogram/options.test.ts +62 -0
- package/src/widgets/histogram/options.ts +38 -17
- package/src/widgets/histogram/transforms.test.ts +21 -7
- package/src/widgets/histogram/transforms.ts +26 -12
- package/src/widgets/histogram/types.ts +25 -5
- package/src/widgets/index.ts +6 -1
- package/src/widgets/pie/options.test.ts +91 -0
- package/src/widgets/pie/options.ts +50 -20
- package/src/widgets/pie/types.ts +6 -0
- package/src/widgets/scatterplot/download.test.ts +9 -0
- package/src/widgets/scatterplot/download.ts +5 -3
- package/src/widgets/scatterplot/index.ts +3 -0
- package/src/widgets/scatterplot/options.test.ts +78 -0
- package/src/widgets/scatterplot/options.ts +35 -11
- package/src/widgets/scatterplot/points.ts +35 -0
- package/src/widgets/scatterplot/transforms.test.ts +19 -0
- package/src/widgets/scatterplot/transforms.ts +23 -18
- package/src/widgets/scatterplot/types.ts +25 -2
- package/src/widgets/timeseries/options.test.ts +65 -0
- package/src/widgets/timeseries/options.ts +31 -14
- package/src/widgets/timeseries/types.ts +6 -0
- package/src/widgets/utils/item-color.test.ts +74 -0
- package/src/widgets/utils/item-color.ts +55 -0
- package/dist/actions-BWlS5tpP.js.map +0 -1
- package/dist/category-alzjKzvQ.js +0 -839
- package/dist/category-alzjKzvQ.js.map +0 -1
- package/dist/echart-CV8F50gn.js.map +0 -1
- package/dist/markdown-Bn2nKCbn.js.map +0 -1
- package/dist/markdown-content-kYNLOX0T.js.map +0 -1
- package/dist/option-builders-DQivvvbb.js.map +0 -1
|
@@ -26,6 +26,11 @@ export interface CategoryRowStackedProps {
|
|
|
26
26
|
onToggle: (name: CategoryKey) => void
|
|
27
27
|
/** Bar visual density. Forwarded to the inner {@link CategoryBarStacked}. */
|
|
28
28
|
size?: CategorySize
|
|
29
|
+
/**
|
|
30
|
+
* Where the list's zero sits (% of the track), shared by every row so the
|
|
31
|
+
* zero lines up: `0` positive-only, `50` signed, `100` negative-only.
|
|
32
|
+
*/
|
|
33
|
+
zeroPct?: number
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
@@ -41,6 +46,10 @@ export interface CategoryRowStackedProps {
|
|
|
41
46
|
* total relative to other rows. The total numeric value is still shown in
|
|
42
47
|
* the header so the per-row magnitude isn't lost.
|
|
43
48
|
*
|
|
49
|
+
* Signed data diverges from `zeroPct`: positives stack rightwards and
|
|
50
|
+
* negatives leftwards, and whichever side is longer reaches its edge of the
|
|
51
|
+
* track. The header total is the signed sum.
|
|
52
|
+
*
|
|
44
53
|
* Visual selection / dim signal lives entirely in the segment colors
|
|
45
54
|
* (`colors` prop) — every series in a dimmed row receives
|
|
46
55
|
* `theme.palette.action.disabled`. The bar visually collapses to a single
|
|
@@ -57,6 +66,7 @@ export function CategoryRowStacked({
|
|
|
57
66
|
selected,
|
|
58
67
|
onToggle,
|
|
59
68
|
size,
|
|
69
|
+
zeroPct = 0,
|
|
60
70
|
}: CategoryRowStackedProps) {
|
|
61
71
|
const handleKey = (e: KeyboardEvent<HTMLDivElement>): void => {
|
|
62
72
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -65,6 +75,18 @@ export function CategoryRowStacked({
|
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
const total = values.reduce((a, b) => a + b, 0)
|
|
78
|
+
let pos = 0
|
|
79
|
+
let neg = 0
|
|
80
|
+
for (const v of values) {
|
|
81
|
+
if (v > 0) pos += v
|
|
82
|
+
else neg -= v
|
|
83
|
+
}
|
|
84
|
+
// Magnitude that would fill the whole track, picked so the longer side
|
|
85
|
+
// just reaches its edge: `pos` spans `100 - zeroPct`, `neg` spans `zeroPct`.
|
|
86
|
+
const scale = Math.max(
|
|
87
|
+
zeroPct < 100 ? (pos * 100) / (100 - zeroPct) : 0,
|
|
88
|
+
zeroPct > 0 ? (neg * 100) / zeroPct : 0,
|
|
89
|
+
)
|
|
68
90
|
return (
|
|
69
91
|
<Box
|
|
70
92
|
role='button'
|
|
@@ -85,7 +107,8 @@ export function CategoryRowStacked({
|
|
|
85
107
|
<CategoryBarStacked
|
|
86
108
|
values={values}
|
|
87
109
|
colors={colors}
|
|
88
|
-
maxValue={
|
|
110
|
+
maxValue={scale}
|
|
111
|
+
zeroPct={zeroPct}
|
|
89
112
|
size={size}
|
|
90
113
|
/>
|
|
91
114
|
<Box sx={styles.stackedBreakdown}>
|
|
@@ -28,17 +28,19 @@ const bar: Record<CategorySize, SxProps<Theme>> = {
|
|
|
28
28
|
const barFill: Record<CategorySize, SxProps<Theme>> = {
|
|
29
29
|
small: {
|
|
30
30
|
position: 'absolute',
|
|
31
|
-
|
|
31
|
+
top: 0,
|
|
32
|
+
bottom: 0,
|
|
32
33
|
borderRadius: 1,
|
|
33
34
|
transition:
|
|
34
|
-
'width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease',
|
|
35
|
+
'left 300ms ease-in-out, width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease',
|
|
35
36
|
},
|
|
36
37
|
medium: {
|
|
37
38
|
position: 'absolute',
|
|
38
|
-
|
|
39
|
+
top: 0,
|
|
40
|
+
bottom: 0,
|
|
39
41
|
borderRadius: 0.25,
|
|
40
42
|
transition:
|
|
41
|
-
'width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease',
|
|
43
|
+
'left 300ms ease-in-out, width 300ms ease-in-out, background-color 150ms ease, filter 150ms ease',
|
|
42
44
|
},
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -71,13 +73,47 @@ export const styles = {
|
|
|
71
73
|
position: 'relative',
|
|
72
74
|
width: '100%',
|
|
73
75
|
},
|
|
74
|
-
// Flat list of category rows.
|
|
76
|
+
// Flat list of category rows. A grid rather than a flex column so the
|
|
77
|
+
// multi-series rows can share one value column through `subgrid`: with
|
|
78
|
+
// every bar track the same width, the zero ticks line up across rows.
|
|
75
79
|
list: {
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
position: 'relative',
|
|
81
|
+
display: 'grid',
|
|
82
|
+
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
78
83
|
gap: 1,
|
|
79
84
|
py: 0.5,
|
|
80
85
|
},
|
|
86
|
+
// Layered on `list` for unstacked multi-series: bars | values.
|
|
87
|
+
listMulti: {
|
|
88
|
+
gridTemplateColumns: 'minmax(0, 1fr) max-content',
|
|
89
|
+
},
|
|
90
|
+
// Zero tick for signed data: one per bar track, drawn outside the track's
|
|
91
|
+
// `overflow: hidden` so it can stand 6px proud of it on both sides.
|
|
92
|
+
// Tracks share a width (see `listMulti`), so the ticks line up by rows.
|
|
93
|
+
barWrap: {
|
|
94
|
+
position: 'relative',
|
|
95
|
+
},
|
|
96
|
+
// Multi-series rows draw one zero line through every series bar instead
|
|
97
|
+
// of a tick per bar: this box is the row's bar column (grid area set
|
|
98
|
+
// inline) and holds a `zeroTick`, so the line stays out of the label.
|
|
99
|
+
multiZeroArea: {
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
inset: 0,
|
|
102
|
+
gridColumn: '1 / 2',
|
|
103
|
+
pointerEvents: 'none',
|
|
104
|
+
},
|
|
105
|
+
zeroTick: {
|
|
106
|
+
position: 'absolute',
|
|
107
|
+
top: -6,
|
|
108
|
+
bottom: -6,
|
|
109
|
+
width: '1px',
|
|
110
|
+
// Centred on the zero so both halves of the bar meet it evenly. No
|
|
111
|
+
// z-index: it comes before the track in the DOM, so the bar paints over
|
|
112
|
+
// it and only the overhang shows.
|
|
113
|
+
transform: 'translateX(-50%)',
|
|
114
|
+
bgcolor: 'text.disabled',
|
|
115
|
+
pointerEvents: 'none',
|
|
116
|
+
},
|
|
81
117
|
// Layered on `list` only in scroll mode (`maxItems === 0`): right padding
|
|
82
118
|
// so the bars + right-aligned values clear the scrollbar. Right side only
|
|
83
119
|
// — the left edge stays at 0 so content stays aligned with the capped
|
|
@@ -91,6 +127,7 @@ export const styles = {
|
|
|
91
127
|
// bar fills the row width below. Mirrors `rowMulti` for visual
|
|
92
128
|
// consistency.
|
|
93
129
|
rowSingle: {
|
|
130
|
+
gridColumn: '1 / -1',
|
|
94
131
|
display: 'flex',
|
|
95
132
|
flexDirection: 'column',
|
|
96
133
|
gap: 0.5,
|
|
@@ -124,10 +161,14 @@ export const styles = {
|
|
|
124
161
|
// Flex column: label fills the top row; bar+value pairs stack below.
|
|
125
162
|
// Bars get full track width — better legibility for N>=2 series than a
|
|
126
163
|
// shared grid would give.
|
|
164
|
+
// Spans the list and adopts its columns, so every row's value column is
|
|
165
|
+
// as wide as the widest value in the list.
|
|
127
166
|
rowMulti: {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
position: 'relative',
|
|
168
|
+
gridColumn: '1 / -1',
|
|
169
|
+
display: 'grid',
|
|
170
|
+
gridTemplateColumns: 'subgrid',
|
|
171
|
+
rowGap: 0.5,
|
|
131
172
|
// py: 0.5,
|
|
132
173
|
// px: 0.5,
|
|
133
174
|
cursor: 'pointer',
|
|
@@ -155,6 +196,7 @@ export const styles = {
|
|
|
155
196
|
fontWeight: 'medium',
|
|
156
197
|
},
|
|
157
198
|
multiName: {
|
|
199
|
+
gridColumn: '1 / -1',
|
|
158
200
|
overflow: 'hidden',
|
|
159
201
|
whiteSpace: 'nowrap',
|
|
160
202
|
textOverflow: 'ellipsis',
|
|
@@ -171,6 +213,7 @@ export const styles = {
|
|
|
171
213
|
// minWidth here is also harmless in that context.
|
|
172
214
|
minWidth: 48,
|
|
173
215
|
textAlign: 'right',
|
|
216
|
+
alignSelf: 'center',
|
|
174
217
|
},
|
|
175
218
|
|
|
176
219
|
// ── Bar (track + fill) ────────────────────────────────────────────
|
|
@@ -184,13 +227,12 @@ export const styles = {
|
|
|
184
227
|
|
|
185
228
|
// ── Multi-series internals ────────────────────────────────────────
|
|
186
229
|
// One [bar | value] flex row per series, inside `rowMulti`'s column.
|
|
230
|
+
// `contents` hands the bar and the value to `rowMulti`'s subgrid cells.
|
|
187
231
|
multiBarRow: {
|
|
188
|
-
display: '
|
|
189
|
-
alignItems: 'center',
|
|
190
|
-
gap: 1,
|
|
232
|
+
display: 'contents',
|
|
191
233
|
},
|
|
192
234
|
multiBarContainer: {
|
|
193
|
-
|
|
235
|
+
alignSelf: 'center',
|
|
194
236
|
},
|
|
195
237
|
|
|
196
238
|
// ── Stacked-mode row ──────────────────────────────────────────────
|
|
@@ -198,6 +240,7 @@ export const styles = {
|
|
|
198
240
|
// focus ring) but stack three children vertically: header line, bar,
|
|
199
241
|
// breakdown line.
|
|
200
242
|
rowStacked: {
|
|
243
|
+
gridColumn: '1 / -1',
|
|
201
244
|
display: 'flex',
|
|
202
245
|
flexDirection: 'column',
|
|
203
246
|
gap: 0.5,
|
|
@@ -285,6 +328,7 @@ export const styles = {
|
|
|
285
328
|
// (no parentheses). Only the label is the click target when interactive;
|
|
286
329
|
// the count is always static text beside it.
|
|
287
330
|
otherRow: {
|
|
331
|
+
gridColumn: '1 / -1',
|
|
288
332
|
display: 'flex',
|
|
289
333
|
alignItems: 'center',
|
|
290
334
|
gap: 1,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HistogramBin } from './types'
|
|
2
|
+
|
|
3
|
+
/** Count of a {@link HistogramBin}, whichever form it takes. */
|
|
4
|
+
export function getHistogramBinValue(bin: HistogramBin): number {
|
|
5
|
+
return typeof bin === 'number' ? bin : bin.value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Whether `v` is a valid {@link HistogramBin} with a finite count. */
|
|
9
|
+
export function isHistogramBin(v: unknown): v is HistogramBin {
|
|
10
|
+
if (typeof v === 'number') return Number.isFinite(v)
|
|
11
|
+
if (v == null || typeof v !== 'object') return false
|
|
12
|
+
const value = (v as { value?: unknown }).value
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Same bin with its count replaced; keeps the object form (and `color`). */
|
|
17
|
+
export function withHistogramBinValue(
|
|
18
|
+
bin: HistogramBin,
|
|
19
|
+
value: number,
|
|
20
|
+
): HistogramBin {
|
|
21
|
+
return typeof bin === 'number' ? value : { ...bin, value }
|
|
22
|
+
}
|
|
@@ -61,6 +61,16 @@ describe('createHistogramDownloadConfig', () => {
|
|
|
61
61
|
)
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
+
it('CSV writes the count of object bins (colour is not exported)', async () => {
|
|
65
|
+
const items = createHistogramDownloadConfig({
|
|
66
|
+
filename: 'h',
|
|
67
|
+
getData: () => [[{ value: 1, color: '#f00' }, 2, { value: 3 }]],
|
|
68
|
+
getTicks: () => ticks,
|
|
69
|
+
})
|
|
70
|
+
await items.find((i) => i.id === 'csv')!.resolve({ id: null })
|
|
71
|
+
expect(csvText).toBe('bin_low,bin_high,series_1\n0,10,1\n10,20,2\n20,30,3')
|
|
72
|
+
})
|
|
73
|
+
|
|
64
74
|
it('CSV honours supplied seriesNames', async () => {
|
|
65
75
|
const items = createHistogramDownloadConfig({
|
|
66
76
|
filename: 'h',
|
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
type WidgetDownloadArgs,
|
|
6
6
|
} from '../actions/download'
|
|
7
|
-
import
|
|
7
|
+
import { getHistogramBinValue } from './bins'
|
|
8
|
+
import type { HistogramBin, HistogramWidgetData } from './types'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Download menu items for histograms. Always includes a CSV item with
|
|
@@ -13,7 +14,7 @@ import type { HistogramWidgetData } from './types'
|
|
|
13
14
|
* rasterises the captured element via `html2canvas`.
|
|
14
15
|
*/
|
|
15
16
|
export function createHistogramDownloadConfig(
|
|
16
|
-
args: WidgetDownloadArgs<HistogramWidgetData
|
|
17
|
+
args: WidgetDownloadArgs<HistogramWidgetData<HistogramBin>> & {
|
|
17
18
|
getTicks: () => readonly number[]
|
|
18
19
|
seriesNames?: readonly string[]
|
|
19
20
|
},
|
|
@@ -33,7 +34,10 @@ export function createHistogramDownloadConfig(
|
|
|
33
34
|
const rows: unknown[][] = [header]
|
|
34
35
|
for (let bin = 0; bin < Math.max(0, ticks.length - 1); bin++) {
|
|
35
36
|
const row: unknown[] = [ticks[bin], ticks[bin + 1]]
|
|
36
|
-
for (let s = 0; s < seriesCount; s++)
|
|
37
|
+
for (let s = 0; s < seriesCount; s++) {
|
|
38
|
+
const value = data[s]?.[bin]
|
|
39
|
+
row.push(value == null ? 0 : getHistogramBinValue(value))
|
|
40
|
+
}
|
|
37
41
|
rows.push(row)
|
|
38
42
|
}
|
|
39
43
|
return rows
|
|
@@ -5,7 +5,10 @@ export {
|
|
|
5
5
|
toRelativeHistogramData,
|
|
6
6
|
filterHistogramByLockedBins,
|
|
7
7
|
} from './transforms'
|
|
8
|
+
export { getHistogramBinValue } from './bins'
|
|
8
9
|
export type {
|
|
10
|
+
HistogramBin,
|
|
11
|
+
HistogramBinObject,
|
|
9
12
|
HistogramWidgetData,
|
|
10
13
|
HistogramOptionsInput,
|
|
11
14
|
HistogramOptionFactoryInput,
|
|
@@ -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 { createHistogramOptionFactory, histogramOptions } from './options'
|
|
4
5
|
|
|
@@ -409,3 +410,64 @@ describe('createHistogramOptionFactory (data → dataset merger)', () => {
|
|
|
409
410
|
})
|
|
410
411
|
})
|
|
411
412
|
})
|
|
413
|
+
|
|
414
|
+
describe('createHistogramOptionFactory — per-item colour', () => {
|
|
415
|
+
interface Out {
|
|
416
|
+
dataset: { source: (string | number)[][] }[]
|
|
417
|
+
series: { itemStyle: { color: (p: unknown) => string } }[]
|
|
418
|
+
}
|
|
419
|
+
const ticks = [0, 10, 20, 30]
|
|
420
|
+
const dim = (c: string) => echarts.color.modifyAlpha(c, 0.15)
|
|
421
|
+
|
|
422
|
+
it('accepts object bins: dataset carries the counts', () => {
|
|
423
|
+
const merge = createHistogramOptionFactory({ theme, ticks })
|
|
424
|
+
const out = merge({}, [[{ value: 1, color: '#00ff00' }, 2, { value: 3 }]])
|
|
425
|
+
expect((out as Out).dataset[0]!.source.map(([, v]) => v)).toEqual([1, 2, 3])
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('bin colour wins over the series colour; template next', () => {
|
|
429
|
+
const merge = createHistogramOptionFactory({
|
|
430
|
+
theme,
|
|
431
|
+
ticks,
|
|
432
|
+
series: [{ name: 'S', color: '#0000ff' }],
|
|
433
|
+
selection: [0, 1],
|
|
434
|
+
})
|
|
435
|
+
const out = merge(
|
|
436
|
+
{ series: [{ type: 'bar', itemStyle: { color: '#123456' } }] },
|
|
437
|
+
[[{ value: 1, color: '#00ff00' }, 2, { value: 3 }]],
|
|
438
|
+
) as Out
|
|
439
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
440
|
+
expect(colorFn({ dataIndex: 0, color: '#0000ff' })).toBe('#00ff00')
|
|
441
|
+
expect(colorFn({ dataIndex: 1, color: '#0000ff' })).toBe('#123456')
|
|
442
|
+
expect(colorFn({ dataIndex: 2, color: '#0000ff' })).toBe(dim('#123456'))
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('calls a function template itemStyle.color via optionsOverride', () => {
|
|
446
|
+
const factory = createHistogramOptionFactory({
|
|
447
|
+
theme,
|
|
448
|
+
ticks,
|
|
449
|
+
optionsOverride: {
|
|
450
|
+
series: [
|
|
451
|
+
{
|
|
452
|
+
type: 'bar',
|
|
453
|
+
itemStyle: {
|
|
454
|
+
color: (p: { dataIndex: number }) => `#c${p.dataIndex}`,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
const out = factory(factory(undefined, undefined), [[1, 2, 3]]) as Out
|
|
461
|
+
expect(out.series[0]!.itemStyle.color({ dataIndex: 2, color: '#x' })).toBe(
|
|
462
|
+
'#c2',
|
|
463
|
+
)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('keeps the series / palette colour for plain number bins', () => {
|
|
467
|
+
const merge = createHistogramOptionFactory({ theme, ticks, selection: [1] })
|
|
468
|
+
const out = merge({}, [[1, 2, 3]]) as Out
|
|
469
|
+
const colorFn = out.series[0]!.itemStyle.color
|
|
470
|
+
expect(colorFn({ dataIndex: 0, color: '#abcdef' })).toBe(dim('#abcdef'))
|
|
471
|
+
expect(colorFn({ dataIndex: 1, color: '#abcdef' })).toBe('#abcdef')
|
|
472
|
+
})
|
|
473
|
+
})
|
|
@@ -12,11 +12,14 @@ 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 { getHistogramBinValue } from './bins'
|
|
15
17
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
16
18
|
import type {
|
|
17
19
|
HistogramEChartsOption,
|
|
18
20
|
HistogramOptionFactoryInput,
|
|
19
21
|
HistogramOptionsInput,
|
|
22
|
+
HistogramBin,
|
|
20
23
|
HistogramWidgetData,
|
|
21
24
|
} from './types'
|
|
22
25
|
|
|
@@ -147,7 +150,7 @@ export function histogramOptions({
|
|
|
147
150
|
* the consumer-supplied `optionsOverride`. Called once by Provider to
|
|
148
151
|
* seed `rawOptions` in the store.
|
|
149
152
|
* - **Merge phase** (`option != null`) — fuses post-pipeline `state.data`
|
|
150
|
-
* (`HistogramWidgetData` — `
|
|
153
|
+
* (`HistogramWidgetData` — `HistogramBin[][]`) into the option via the
|
|
151
154
|
* dataset API. Each series gets its own dataset of `[binLabel, count]`
|
|
152
155
|
* pairs derived from `ticks`; series reference their dataset by
|
|
153
156
|
* `datasetIndex` and are encoded by column position. Reactive
|
|
@@ -199,8 +202,8 @@ export function createHistogramOptionFactory(
|
|
|
199
202
|
: structural
|
|
200
203
|
}
|
|
201
204
|
|
|
202
|
-
const seriesArr: HistogramWidgetData = Array.isArray(data)
|
|
203
|
-
? (data as HistogramWidgetData)
|
|
205
|
+
const seriesArr: HistogramWidgetData<HistogramBin> = Array.isArray(data)
|
|
206
|
+
? (data as HistogramWidgetData<HistogramBin>)
|
|
204
207
|
: []
|
|
205
208
|
if (seriesArr.length === 0 || binLabels.length === 0) {
|
|
206
209
|
return { ...option, dataset: [], series: [] }
|
|
@@ -261,14 +264,28 @@ export function createHistogramOptionFactory(
|
|
|
261
264
|
// and bins would stay dimmed forever after an external clear. Always
|
|
262
265
|
// emitting lets normal merge swap the callback in place, no
|
|
263
266
|
// `replaceMerge` and no entry-animation flash on selection on/off.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
//
|
|
268
|
+
// The base colour is the bin's own `color` (object bins), else the
|
|
269
|
+
// template's `itemStyle.color`, else the series colour / palette.
|
|
270
|
+
const makeDimItemStyle = (
|
|
271
|
+
datums: readonly unknown[],
|
|
272
|
+
template: unknown,
|
|
273
|
+
) => {
|
|
274
|
+
const baseColor = createItemColorResolver({
|
|
275
|
+
theme,
|
|
276
|
+
datums,
|
|
277
|
+
template,
|
|
278
|
+
fallback: (params) => params.color as string,
|
|
279
|
+
})
|
|
280
|
+
return {
|
|
281
|
+
color: (params: CallbackDataParams) => {
|
|
282
|
+
const base = baseColor(params)
|
|
283
|
+
if (!selectionSet) return base
|
|
284
|
+
return selectionSet.has(params.dataIndex)
|
|
285
|
+
? base
|
|
286
|
+
: echarts.color.modifyAlpha(base, 0.15)
|
|
287
|
+
},
|
|
288
|
+
}
|
|
272
289
|
}
|
|
273
290
|
|
|
274
291
|
// A consumer's `optionsOverride.xAxis` may legitimately be an array
|
|
@@ -306,12 +323,16 @@ export function createHistogramOptionFactory(
|
|
|
306
323
|
return {
|
|
307
324
|
...option,
|
|
308
325
|
xAxis: nextXAxis,
|
|
309
|
-
dataset: seriesArr.map((
|
|
310
|
-
source: binLabels.map(
|
|
311
|
-
|
|
312
|
-
|
|
326
|
+
dataset: seriesArr.map((bins) => ({
|
|
327
|
+
source: binLabels.map((label, i) => {
|
|
328
|
+
const bin = bins[i]
|
|
329
|
+
return [label, bin == null ? 0 : getHistogramBinValue(bin)] as [
|
|
330
|
+
string,
|
|
331
|
+
number,
|
|
332
|
+
]
|
|
333
|
+
}),
|
|
313
334
|
})),
|
|
314
|
-
series: seriesArr.map((
|
|
335
|
+
series: seriesArr.map((bins, i) => {
|
|
315
336
|
const template =
|
|
316
337
|
(seriesTemplates[i] as object | undefined) ??
|
|
317
338
|
(broadcastTemplate as object)
|
|
@@ -324,7 +345,7 @@ export function createHistogramOptionFactory(
|
|
|
324
345
|
encode: { x: 0, y: 1 },
|
|
325
346
|
barCategoryGap: '0%',
|
|
326
347
|
emphasis: { focus: 'series' },
|
|
327
|
-
itemStyle:
|
|
348
|
+
itemStyle: makeDimItemStyle(bins, template),
|
|
328
349
|
...(overrideColor ? { color: overrideColor } : {}),
|
|
329
350
|
}
|
|
330
351
|
}),
|
|
@@ -36,15 +36,20 @@ describe('toRelativeHistogramData', () => {
|
|
|
36
36
|
expect(out[0]?.[1]).toBeCloseTo(-49.74874, 4)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it('returns the input unchanged when shape does not match
|
|
39
|
+
it('returns the input unchanged when shape does not match HistogramBin[][]', () => {
|
|
40
40
|
expect(toRelativeHistogramData(null)).toBe(null)
|
|
41
41
|
expect(toRelativeHistogramData('not an array')).toBe('not an array')
|
|
42
|
-
// Series that isn't a
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
expect(toRelativeHistogramData([[
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
// Series that isn't a list of bins (e.g. strings or tuples) is returned
|
|
43
|
+
// as-is — keeps the transform safe to attach to a widget whose shape
|
|
44
|
+
// changes through other actions.
|
|
45
|
+
expect(toRelativeHistogramData([['a', 'b']])).toEqual([['a', 'b']])
|
|
46
|
+
expect(toRelativeHistogramData([[[1, 2]]])).toEqual([[[1, 2]]])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('relativizes object bins, keeping their colour', () => {
|
|
50
|
+
expect(
|
|
51
|
+
toRelativeHistogramData([[{ value: 5, color: '#f00' }, 15]]),
|
|
52
|
+
).toEqual([[{ value: 25, color: '#f00' }, 75]])
|
|
48
53
|
})
|
|
49
54
|
})
|
|
50
55
|
|
|
@@ -75,6 +80,15 @@ describe('filterHistogramByLockedBins', () => {
|
|
|
75
80
|
])
|
|
76
81
|
})
|
|
77
82
|
|
|
83
|
+
it('zeroes non-locked object bins, keeping their colour', () => {
|
|
84
|
+
expect(
|
|
85
|
+
filterHistogramByLockedBins(
|
|
86
|
+
[[{ value: 10, color: '#f00' }, { value: 20 }, 30]],
|
|
87
|
+
[1],
|
|
88
|
+
),
|
|
89
|
+
).toEqual([[{ value: 0, color: '#f00' }, { value: 20 }, 0]])
|
|
90
|
+
})
|
|
91
|
+
|
|
78
92
|
it('returns input unchanged when input is not array-shaped', () => {
|
|
79
93
|
expect(filterHistogramByLockedBins('nope', [0])).toBe('nope')
|
|
80
94
|
})
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getHistogramBinValue,
|
|
3
|
+
isHistogramBin,
|
|
4
|
+
withHistogramBinValue,
|
|
5
|
+
} from './bins'
|
|
6
|
+
import type { HistogramBin } from './types'
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* Histogram-specific `RelativeData` transform. Histogram data is
|
|
3
|
-
* `
|
|
10
|
+
* `HistogramBin[][]` — each series is a flat list of bin counts (plain
|
|
11
|
+
* numbers or `{ value, color? }`, whose shape is kept). This
|
|
4
12
|
* rewrites each count to its share of the series total (0–100), so
|
|
5
13
|
* pairing with `createPercentFormatter` renders the y-axis as
|
|
6
14
|
* percentages.
|
|
@@ -11,27 +19,31 @@
|
|
|
11
19
|
* inputs produce sane signed shares-of-magnitude. A series whose total
|
|
12
20
|
* magnitude is zero (all-zero or empty input) is returned unchanged so
|
|
13
21
|
* a stalled or empty data set doesn't show misleading 0% values. Inputs
|
|
14
|
-
* whose shape isn't `
|
|
22
|
+
* whose shape isn't `HistogramBin[][]` fall through untouched (same defensive
|
|
15
23
|
* shape-check pattern as the default transform).
|
|
16
24
|
*/
|
|
17
25
|
export const toRelativeHistogramData = (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 (!isBinArray(series)) return series
|
|
29
|
+
const total = series.reduce<number>(
|
|
30
|
+
(acc, b) => acc + Math.abs(getHistogramBinValue(b)),
|
|
31
|
+
0,
|
|
32
|
+
)
|
|
22
33
|
if (total <= 0) return series
|
|
23
|
-
return series.map((
|
|
34
|
+
return series.map((b) =>
|
|
35
|
+
withHistogramBinValue(b, (getHistogramBinValue(b) / total) * 100),
|
|
36
|
+
)
|
|
24
37
|
})
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
return v.every((item) => typeof item === 'number' && Number.isFinite(item))
|
|
40
|
+
function isBinArray(v: unknown): v is HistogramBin[] {
|
|
41
|
+
return Array.isArray(v) && v.every(isHistogramBin)
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
|
-
* `LockSelection` filter for Histogram. Data is `
|
|
34
|
-
* of bin counts per series, positionally paired with `ticks` (bin `i` covers
|
|
45
|
+
* `LockSelection` filter for Histogram. Data is `HistogramBin[][]` — a flat
|
|
46
|
+
* list of bin counts per series, positionally paired with `ticks` (bin `i` covers
|
|
35
47
|
* `[ticks[i], ticks[i+1])`). Unlike name-keyed widgets, locked-out bins can't
|
|
36
48
|
* be REMOVED — that would shift every later bin's index and desync it from
|
|
37
49
|
* its `ticks` range. Zeroing them out instead preserves array length/position,
|
|
@@ -47,7 +59,9 @@ export function filterHistogramByLockedBins(
|
|
|
47
59
|
if (!Array.isArray(input)) return input
|
|
48
60
|
const locked = new Set<number>(lockedItems.map(Number))
|
|
49
61
|
return input.map((series: unknown): unknown => {
|
|
50
|
-
if (!
|
|
51
|
-
return series.map((
|
|
62
|
+
if (!isBinArray(series)) return series
|
|
63
|
+
return series.map((bin, i) =>
|
|
64
|
+
locked.has(i) ? bin : withHistogramBinValue(bin, 0),
|
|
65
|
+
)
|
|
52
66
|
})
|
|
53
67
|
}
|
|
@@ -3,12 +3,32 @@ import type { EChartsOption } from 'echarts'
|
|
|
3
3
|
import type { WidgetSeries } from '../types'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* One histogram bin: either its count, or `{ value, color? }` to give the
|
|
7
|
+
* bar its own colour (CSS colour or theme palette path such as
|
|
8
|
+
* `'primary.main'`). A bin `color` wins over the template's
|
|
9
|
+
* `itemStyle.color` and the series colour; selection dimming still applies.
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
11
|
+
export type HistogramBin = number | HistogramBinObject
|
|
12
|
+
|
|
13
|
+
/** Object form of a {@link HistogramBin}. */
|
|
14
|
+
export interface HistogramBinObject {
|
|
15
|
+
value: number
|
|
16
|
+
color?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Histogram widget data — array of series, each a flat array of bins. The
|
|
21
|
+
* option factory's merge phase (see {@link createHistogramOptionFactory})
|
|
22
|
+
* combines this with `ticks` to produce a 2-column dataset
|
|
23
|
+
* (`[binLabel, count]`) for ECharts.
|
|
24
|
+
*
|
|
25
|
+
* Defaults to plain counts (`number[][]`) so existing code that reads bins
|
|
26
|
+
* as numbers keeps type-checking; use `HistogramWidgetData<HistogramBin>`
|
|
27
|
+
* to also accept `{ value, color? }` bins. Every histogram API (option
|
|
28
|
+
* factory, transforms, download) accepts both forms.
|
|
29
|
+
*/
|
|
30
|
+
export type HistogramWidgetData<B extends HistogramBin = number> =
|
|
31
|
+
readonly (readonly B[])[]
|
|
12
32
|
|
|
13
33
|
/** Inputs to the structural-only {@link histogramOptions} builder. */
|
|
14
34
|
export interface HistogramOptionsInput {
|
package/src/widgets/index.ts
CHANGED
|
@@ -175,7 +175,12 @@ export type {
|
|
|
175
175
|
export type { MarkdownUIProps, MarkdownWidgetData } from './markdown'
|
|
176
176
|
export type { PieDatum, PieWidgetData } from './pie'
|
|
177
177
|
export type { TimeseriesDatum, TimeseriesWidgetData } from './timeseries'
|
|
178
|
-
export type {
|
|
178
|
+
export type {
|
|
179
|
+
ScatterplotDatum,
|
|
180
|
+
ScatterplotPoint,
|
|
181
|
+
ScatterplotItem,
|
|
182
|
+
ScatterplotWidgetData,
|
|
183
|
+
} from './scatterplot'
|
|
179
184
|
export type {
|
|
180
185
|
SpreadUIProps,
|
|
181
186
|
SpreadDataItem,
|