@carto/ps-react-ui 6.3.0 → 6.5.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-D_PVFZaM.js +1272 -0
- package/dist/actions-D_PVFZaM.js.map +1 -0
- package/dist/{category-DULnoxvk.js → category-UIGvFN3D.js} +263 -246
- package/dist/category-UIGvFN3D.js.map +1 -0
- package/dist/chat.js +472 -470
- package/dist/chat.js.map +1 -1
- package/dist/{components-CTUpdQuj.js → components-DDJU4m0t.js} +8 -8
- package/dist/components-DDJU4m0t.js.map +1 -0
- package/dist/components.js +2 -2
- package/dist/{download-B4sykOFm.js → download-DIj1Iz0t.js} +2 -2
- package/dist/{download-B4sykOFm.js.map → download-DIj1Iz0t.js.map} +1 -1
- package/dist/{formula-DJS-JLpk.js → formula-CSHhAoyW.js} +4 -4
- package/dist/{formula-DJS-JLpk.js.map → formula-CSHhAoyW.js.map} +1 -1
- package/dist/{item-color-B7xkIpDZ.js → item-color-7o7pFBR2.js} +26 -18
- package/dist/item-color-7o7pFBR2.js.map +1 -0
- package/dist/markdown-B8lLR-NY.js +106 -0
- package/dist/markdown-B8lLR-NY.js.map +1 -0
- package/dist/scale-Bhx8HKEB.js +29 -0
- package/dist/scale-Bhx8HKEB.js.map +1 -0
- package/dist/{spread-CQwFIwuz.js → spread-BRfPPIep.js} +3 -3
- package/dist/{spread-CQwFIwuz.js.map → spread-BRfPPIep.js.map} +1 -1
- package/dist/style-DTLuIo84.js +87 -0
- package/dist/style-DTLuIo84.js.map +1 -0
- package/dist/{styles-BCEBm7xh.js → styles-yKcpfyNV.js} +5 -5
- package/dist/styles-yKcpfyNV.js.map +1 -0
- package/dist/{table-DEdW_fDj.js → table-Col_FRvb.js} +2 -2
- package/dist/{table-DEdW_fDj.js.map → table-Col_FRvb.js.map} +1 -1
- package/dist/theme/tailwind.css +373 -0
- package/dist/theme.js +123 -0
- package/dist/theme.js.map +1 -0
- package/dist/types/components/lasso-tool/styles.d.ts +4 -4
- package/dist/types/theme/create-carto-theme.d.ts +7 -0
- package/dist/types/theme/deep-merge.d.ts +6 -0
- package/dist/types/theme/generate-tailwind-css.d.ts +8 -0
- package/dist/types/theme/index.d.ts +2 -0
- package/dist/types/widgets/actions/index.d.ts +1 -1
- package/dist/types/widgets/actions/relative-data/index.d.ts +1 -0
- package/dist/types/widgets/actions/relative-data/labels.d.ts +3 -0
- package/dist/types/widgets/actions/relative-data/relative-data.d.ts +16 -2
- package/dist/types/widgets/actions/relative-data/scale.d.ts +26 -0
- package/dist/types/widgets/actions/relative-data/style.d.ts +12 -0
- package/dist/types/widgets/category/category.d.ts +2 -1
- package/dist/types/widgets/index.d.ts +1 -1
- package/dist/types/widgets/markdown/style.d.ts +74 -0
- package/dist/types/widgets/utils/spacing-px.d.ts +13 -0
- package/dist/{utils-C-eWwOhE.js → utils-Dw4y8tiq.js} +2 -2
- package/dist/{utils-C-eWwOhE.js.map → utils-Dw4y8tiq.js.map} +1 -1
- package/dist/widgets/actions.js +3 -3
- package/dist/widgets/bar.js +85 -84
- package/dist/widgets/bar.js.map +1 -1
- package/dist/widgets/category.js +1 -1
- package/dist/widgets/formula.js +1 -1
- package/dist/widgets/histogram.js +109 -108
- package/dist/widgets/histogram.js.map +1 -1
- package/dist/widgets/markdown.js +1 -1
- package/dist/widgets/pie.js +78 -78
- package/dist/widgets/pie.js.map +1 -1
- package/dist/widgets/scatterplot.js +121 -120
- package/dist/widgets/scatterplot.js.map +1 -1
- package/dist/widgets/spread.js +1 -1
- package/dist/widgets/table.js +1 -1
- package/dist/widgets/timeseries.js +91 -90
- package/dist/widgets/timeseries.js.map +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets.js +283 -279
- package/dist/widgets.js.map +1 -1
- package/dist/{zoom-toggle-MmLjMTEw.js → zoom-toggle-DqrV_0Ji.js} +2 -2
- package/dist/{zoom-toggle-MmLjMTEw.js.map → zoom-toggle-DqrV_0Ji.js.map} +1 -1
- package/package.json +8 -1
- package/src/chat/bubbles/styles.ts +2 -0
- package/src/components/lasso-tool/lasso-tool-inline.test.tsx +4 -4
- package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -6
- package/src/components/lasso-tool/styles.ts +4 -4
- package/src/theme/create-carto-theme.test.ts +53 -0
- package/src/theme/create-carto-theme.ts +21 -0
- package/src/theme/deep-merge.test.ts +25 -0
- package/src/theme/deep-merge.ts +22 -0
- package/src/theme/generate-tailwind-css.test.ts +87 -0
- package/src/theme/generate-tailwind-css.ts +178 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/tailwind-compile.test.ts +71 -0
- package/src/theme/tailwind.css +373 -0
- package/src/widgets/actions/index.ts +1 -0
- package/src/widgets/actions/relative-data/index.ts +1 -0
- package/src/widgets/actions/relative-data/labels.ts +6 -0
- package/src/widgets/actions/relative-data/relative-data.test.tsx +78 -1
- package/src/widgets/actions/relative-data/relative-data.tsx +137 -20
- package/src/widgets/actions/relative-data/scale.ts +48 -0
- package/src/widgets/actions/relative-data/style.ts +10 -0
- package/src/widgets/bar/options.test.ts +16 -0
- package/src/widgets/bar/options.ts +13 -8
- package/src/widgets/category/category.relative-data.test.tsx +51 -63
- package/src/widgets/category/category.tsx +25 -11
- package/src/widgets/histogram/options.ts +13 -8
- package/src/widgets/index.ts +1 -0
- package/src/widgets/markdown/style.ts +40 -0
- package/src/widgets/note/widget-note.tsx +4 -1
- package/src/widgets/pie/options.ts +5 -4
- package/src/widgets/scatterplot/options.test.ts +12 -0
- package/src/widgets/scatterplot/options.ts +25 -10
- package/src/widgets/timeseries/options.ts +13 -8
- package/src/widgets/utils/css-variables-theme.test.ts +32 -0
- package/src/widgets/utils/option-builders.ts +4 -6
- package/src/widgets/utils/spacing-px.test.ts +42 -0
- package/src/widgets/utils/spacing-px.ts +24 -0
- package/dist/actions-CkxKe8-J.js +0 -1160
- package/dist/actions-CkxKe8-J.js.map +0 -1
- package/dist/category-DULnoxvk.js.map +0 -1
- package/dist/components-CTUpdQuj.js.map +0 -1
- package/dist/item-color-B7xkIpDZ.js.map +0 -1
- package/dist/markdown-DmvLi3vx.js +0 -159
- package/dist/markdown-DmvLi3vx.js.map +0 -1
- package/dist/styles-BCEBm7xh.js.map +0 -1
|
@@ -1,25 +1,56 @@
|
|
|
1
|
-
import { useEffect, useMemo, type ComponentType } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { useEffect, useMemo, useState, type ComponentType } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
IconButton,
|
|
5
|
+
ListItemText,
|
|
6
|
+
Menu,
|
|
7
|
+
MenuItem,
|
|
8
|
+
type SvgIconProps,
|
|
9
|
+
} from '@mui/material'
|
|
10
|
+
import {
|
|
11
|
+
ArrowDropDown as ArrowDropDownIcon,
|
|
12
|
+
Percent as PercentIcon,
|
|
13
|
+
} from '@mui/icons-material'
|
|
4
14
|
import { Tooltip } from '../../../components'
|
|
5
15
|
import {
|
|
6
16
|
getWidgetStore,
|
|
7
|
-
|
|
17
|
+
useTransform,
|
|
8
18
|
useWidget,
|
|
9
19
|
useWidgetId,
|
|
20
|
+
type TransformPair,
|
|
10
21
|
} from '../../stores'
|
|
11
22
|
import { createPercentFormatter, toRelativeData } from './transforms'
|
|
23
|
+
import {
|
|
24
|
+
RELATIVE_DATA_ID,
|
|
25
|
+
toRelativeTotalAxis,
|
|
26
|
+
type RelativeScale,
|
|
27
|
+
} from './scale'
|
|
12
28
|
import { DEFAULT_RELATIVE_DATA_LABELS, type RelativeDataLabels } from './labels'
|
|
13
29
|
import { styles } from './style'
|
|
14
30
|
|
|
15
31
|
const DATA_DESCRIPTOR = {
|
|
16
|
-
id:
|
|
32
|
+
id: RELATIVE_DATA_ID,
|
|
17
33
|
type: 'data' as const,
|
|
18
34
|
order: 15,
|
|
19
35
|
}
|
|
20
36
|
|
|
37
|
+
const AXIS_DESCRIPTOR = {
|
|
38
|
+
id: 'relative-data-axis',
|
|
39
|
+
type: 'config' as const,
|
|
40
|
+
order: 15,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const identity = (option: unknown) => option
|
|
44
|
+
|
|
21
45
|
export interface RelativeDataProps {
|
|
22
46
|
initialEnabled?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Initial bar scale: `'max'` (the largest item fills its bar) or
|
|
49
|
+
* `'total'` (bars end at 100%). Values are the share of the total
|
|
50
|
+
* either way. Only seeds the store — the menu, or a write to
|
|
51
|
+
* `transformStates['relative-data'].scale`, changes it afterwards.
|
|
52
|
+
*/
|
|
53
|
+
scale?: RelativeScale
|
|
23
54
|
/**
|
|
24
55
|
* BCP-47 locale tag forwarded to `Intl.NumberFormat`. Defaults to the
|
|
25
56
|
* runtime locale.
|
|
@@ -43,7 +74,8 @@ export interface RelativeDataProps {
|
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
/**
|
|
46
|
-
* Toggle between absolute and relative (percentage) values
|
|
77
|
+
* Toggle between absolute and relative (percentage) values, as a split
|
|
78
|
+
* button: the toggle plus an arrow that opens the scale menu.
|
|
47
79
|
*
|
|
48
80
|
* - **Data side**: a single data transform ({@link toRelativeData}) registered
|
|
49
81
|
* through `useSingleTransform`. When disabled, the pipeline skips it and the
|
|
@@ -53,9 +85,15 @@ export interface RelativeDataProps {
|
|
|
53
85
|
* reads `state.rawFormatter` (the consumer-supplied original, always live)
|
|
54
86
|
* and writes it back to `formatter`. No "original" snapshot is captured —
|
|
55
87
|
* `rawFormatter` is the original.
|
|
88
|
+
* - **Scale**: lives in `transformStates['relative-data'].scale` next to
|
|
89
|
+
* `enabled`, so duplicate mounts (Toolbox inline + overflow) and
|
|
90
|
+
* `keepAlive` remounts agree on it. It never changes the values; under
|
|
91
|
+
* `'total'` a config transform pins the ECharts value axis to 100%, and
|
|
92
|
+
* Category reads it through `selectRelativeScale`.
|
|
56
93
|
*/
|
|
57
94
|
export function RelativeData({
|
|
58
95
|
initialEnabled = false,
|
|
96
|
+
scale: initialScale = 'max',
|
|
59
97
|
locale,
|
|
60
98
|
labels,
|
|
61
99
|
icon: Icon = PercentIcon,
|
|
@@ -65,12 +103,39 @@ export function RelativeData({
|
|
|
65
103
|
const id = useWidgetId()
|
|
66
104
|
const _labels = { ...DEFAULT_RELATIVE_DATA_LABELS, ...labels }
|
|
67
105
|
|
|
68
|
-
const
|
|
106
|
+
const persistedScale = useWidget(
|
|
69
107
|
id,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ initialEnabled },
|
|
108
|
+
(s) =>
|
|
109
|
+
s.transformStates[DATA_DESCRIPTOR.id]?.scale as RelativeScale | undefined,
|
|
73
110
|
)
|
|
111
|
+
const scale = persistedScale ?? initialScale
|
|
112
|
+
|
|
113
|
+
const pairs = useMemo<readonly TransformPair[]>(
|
|
114
|
+
() => [
|
|
115
|
+
{ descriptor: DATA_DESCRIPTOR, fn: transform ?? toRelativeData },
|
|
116
|
+
{
|
|
117
|
+
descriptor: AXIS_DESCRIPTOR,
|
|
118
|
+
fn: scale === 'total' ? toRelativeTotalAxis : identity,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
[transform, scale],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const { enabled, setEnabled, toggle } = useTransform(id, pairs, {
|
|
125
|
+
initialEnabled,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (persistedScale === undefined)
|
|
130
|
+
writeScale(id, initialScale, initialEnabled)
|
|
131
|
+
}, [id, persistedScale, initialScale, initialEnabled])
|
|
132
|
+
|
|
133
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
134
|
+
const selectScale = (next: RelativeScale) => {
|
|
135
|
+
writeScale(id, next, initialEnabled)
|
|
136
|
+
setEnabled(true)
|
|
137
|
+
setAnchorEl(null)
|
|
138
|
+
}
|
|
74
139
|
|
|
75
140
|
const percentFormatter = useMemo(
|
|
76
141
|
() => createPercentFormatter(locale),
|
|
@@ -92,16 +157,68 @@ export function RelativeData({
|
|
|
92
157
|
const tooltip = enabled ? _labels.on : _labels.off
|
|
93
158
|
|
|
94
159
|
return (
|
|
95
|
-
<
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
160
|
+
<Box sx={styles.group}>
|
|
161
|
+
<Tooltip title={tooltip}>
|
|
162
|
+
<IconButton
|
|
163
|
+
size='small'
|
|
164
|
+
aria-label={tooltip}
|
|
165
|
+
aria-pressed={enabled}
|
|
166
|
+
onClick={toggle}
|
|
167
|
+
sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}
|
|
168
|
+
>
|
|
169
|
+
<Icon fontSize='small' {...iconProps} />
|
|
170
|
+
</IconButton>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
<Tooltip title={_labels.scaleMenu}>
|
|
173
|
+
<IconButton
|
|
174
|
+
size='small'
|
|
175
|
+
aria-label={_labels.scaleMenu}
|
|
176
|
+
aria-haspopup='true'
|
|
177
|
+
aria-expanded={anchorEl ? 'true' : undefined}
|
|
178
|
+
onClick={(e) => setAnchorEl(e.currentTarget)}
|
|
179
|
+
sx={styles.arrow}
|
|
180
|
+
>
|
|
181
|
+
<ArrowDropDownIcon />
|
|
182
|
+
</IconButton>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
<Menu
|
|
185
|
+
anchorEl={anchorEl}
|
|
186
|
+
open={Boolean(anchorEl)}
|
|
187
|
+
onClose={() => setAnchorEl(null)}
|
|
188
|
+
variant='menu'
|
|
189
|
+
elevation={8}
|
|
190
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
191
|
+
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
102
192
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
193
|
+
{SCALES.map((option) => (
|
|
194
|
+
<MenuItem
|
|
195
|
+
key={option}
|
|
196
|
+
selected={option === scale}
|
|
197
|
+
onClick={() => selectScale(option)}
|
|
198
|
+
>
|
|
199
|
+
<ListItemText>
|
|
200
|
+
{option === 'max' ? _labels.scaleMax : _labels.scaleTotal}
|
|
201
|
+
</ListItemText>
|
|
202
|
+
</MenuItem>
|
|
203
|
+
))}
|
|
204
|
+
</Menu>
|
|
205
|
+
</Box>
|
|
106
206
|
)
|
|
107
207
|
}
|
|
208
|
+
|
|
209
|
+
const SCALES: readonly RelativeScale[] = ['max', 'total']
|
|
210
|
+
|
|
211
|
+
function writeScale(id: string, scale: RelativeScale, initialEnabled: boolean) {
|
|
212
|
+
getWidgetStore(id).setState((s) => ({
|
|
213
|
+
transformStates: {
|
|
214
|
+
...s.transformStates,
|
|
215
|
+
[DATA_DESCRIPTOR.id]: {
|
|
216
|
+
// `useSingleTransform` seeds `enabled` itself; this fallback only
|
|
217
|
+
// keeps the slot well-typed if the scale lands first.
|
|
218
|
+
enabled: initialEnabled,
|
|
219
|
+
...s.transformStates[DATA_DESCRIPTOR.id],
|
|
220
|
+
scale,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { WidgetState } from '../../stores/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How far a bar reaches while `RelativeData` is on. The value is always
|
|
5
|
+
* the share of the series total; the scale only moves the end of the
|
|
6
|
+
* value axis: `'max'` ends it at the largest share so the top item fills
|
|
7
|
+
* the bar, `'total'` ends it at 100%.
|
|
8
|
+
*/
|
|
9
|
+
export type RelativeScale = 'max' | 'total'
|
|
10
|
+
|
|
11
|
+
export const RELATIVE_DATA_ID = 'relative-data'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The scale the value axis should use right now, or `null` when relative
|
|
15
|
+
* values are off. For widgets that size bars themselves (Category); the
|
|
16
|
+
* ECharts widgets get it through the config transform below.
|
|
17
|
+
*/
|
|
18
|
+
export function selectRelativeScale(s: WidgetState): RelativeScale | null {
|
|
19
|
+
const slot = s.transformStates[RELATIVE_DATA_ID]
|
|
20
|
+
if (slot?.enabled !== true) return null
|
|
21
|
+
return (slot.scale as RelativeScale | undefined) ?? 'max'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Config transform for `'total'`: pins the value axis to ±100. The widget
|
|
26
|
+
* mergers read numeric `yAxis.min` / `yAxis.max` back through
|
|
27
|
+
* {@link fixedAxisBounds} and apply each only on the side the data reaches.
|
|
28
|
+
*/
|
|
29
|
+
export const toRelativeTotalAxis = (option: unknown): unknown => {
|
|
30
|
+
if (option == null || typeof option !== 'object') return option
|
|
31
|
+
const cfg = option as Record<string, unknown>
|
|
32
|
+
const yAxis = cfg.yAxis
|
|
33
|
+
if (yAxis == null || typeof yAxis !== 'object' || Array.isArray(yAxis))
|
|
34
|
+
return option
|
|
35
|
+
return { ...cfg, yAxis: { ...yAxis, min: -100, max: 100 } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Numeric bounds a config transform set on the structural `yAxis`. */
|
|
39
|
+
export function fixedAxisBounds(yAxis: object): {
|
|
40
|
+
min?: number
|
|
41
|
+
max?: number
|
|
42
|
+
} {
|
|
43
|
+
const { min, max } = yAxis as { min?: unknown; max?: unknown }
|
|
44
|
+
return {
|
|
45
|
+
min: typeof min === 'number' ? min : undefined,
|
|
46
|
+
max: typeof max === 'number' ? max : undefined,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SxProps, Theme } from '@mui/material'
|
|
2
2
|
|
|
3
3
|
export const styles = {
|
|
4
|
+
group: {
|
|
5
|
+
display: 'inline-flex',
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
},
|
|
4
8
|
toggle: {
|
|
5
9
|
p: 0.5,
|
|
6
10
|
'& .MuiSvgIcon-root': { fontSize: 20 },
|
|
@@ -8,4 +12,10 @@ export const styles = {
|
|
|
8
12
|
toggleActive: {
|
|
9
13
|
background: (theme: Theme) => theme.palette.primary.relatedLight,
|
|
10
14
|
},
|
|
15
|
+
arrow: {
|
|
16
|
+
p: 0,
|
|
17
|
+
width: 16,
|
|
18
|
+
borderRadius: 0.5,
|
|
19
|
+
'& .MuiSvgIcon-root': { fontSize: 18 },
|
|
20
|
+
},
|
|
11
21
|
} satisfies Record<string, SxProps<Theme>>
|
|
@@ -237,6 +237,22 @@ describe('createBarOptionFactory', () => {
|
|
|
237
237
|
expect(out.yAxis.axisLabel.formatter(0)).toBe('')
|
|
238
238
|
})
|
|
239
239
|
|
|
240
|
+
it("pins the axis ends a config transform set (RelativeData 'total')", () => {
|
|
241
|
+
const merge = createBarOptionFactory({ theme })
|
|
242
|
+
const out = merge({ yAxis: { type: 'value', min: -100, max: 100 } }, [
|
|
243
|
+
[{ name: 'A', value: 57 }],
|
|
244
|
+
]) as {
|
|
245
|
+
yAxis: {
|
|
246
|
+
min: (extent: { min: number }) => number
|
|
247
|
+
max: (extent: { min: number; max: number }) => number
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
expect(out.yAxis.max({ min: 0, max: 57 })).toBe(100)
|
|
251
|
+
// The data never goes below zero, so the -100 end does not apply.
|
|
252
|
+
expect(out.yAxis.min({ min: 0 })).toBe(0)
|
|
253
|
+
expect(out.yAxis.min({ min: -20 })).toBe(-100)
|
|
254
|
+
})
|
|
255
|
+
|
|
240
256
|
it('rounds the ECharts-supplied (stacked) extent so stacked bars cannot overflow', () => {
|
|
241
257
|
// Regression: previously the merge phase precomputed the y-axis max from
|
|
242
258
|
// the largest *single* series value, so stacking (StackToggle →
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { ZOOM_LAYOUT } from '../actions/zoom-toggle'
|
|
13
13
|
import type { OptionFactory } from '../echart'
|
|
14
14
|
import { mergeOptions, resolveThemeColor } from '../utils'
|
|
15
|
+
import { spacingPx } from '../utils/spacing-px'
|
|
15
16
|
import { createItemColorResolver } from '../utils/item-color'
|
|
16
17
|
import { positionDataZoomForLegend } from '../utils/data-zoom-layout'
|
|
17
18
|
import type {
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
BarOptionsInput,
|
|
21
22
|
BarWidgetData,
|
|
22
23
|
} from './types'
|
|
24
|
+
import { fixedAxisBounds } from '../actions/relative-data/scale'
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Builds the **structural** ECharts option for a bar widget — axes, grid,
|
|
@@ -49,9 +51,9 @@ export function barOptions({
|
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
53
|
grid: {
|
|
52
|
-
left:
|
|
53
|
-
top:
|
|
54
|
-
right:
|
|
54
|
+
left: spacingPx(theme, 1),
|
|
55
|
+
top: spacingPx(theme, 3),
|
|
56
|
+
right: spacingPx(theme, 1),
|
|
55
57
|
// Default: no legend. Merger bumps this when there are >1 series.
|
|
56
58
|
...buildGridConfig(false, theme),
|
|
57
59
|
containLabel: true,
|
|
@@ -60,7 +62,7 @@ export function barOptions({
|
|
|
60
62
|
trigger: 'axis',
|
|
61
63
|
backgroundColor: theme.palette.grey[900],
|
|
62
64
|
borderWidth: 0,
|
|
63
|
-
padding: [
|
|
65
|
+
padding: [spacingPx(theme, 1), spacingPx(theme, 1)],
|
|
64
66
|
textStyle: {
|
|
65
67
|
color: theme.palette.common.white,
|
|
66
68
|
fontSize: 11,
|
|
@@ -89,7 +91,7 @@ export function barOptions({
|
|
|
89
91
|
axisTick: { show: false },
|
|
90
92
|
axisLabel: {
|
|
91
93
|
...buildAxisLabelStyle(theme),
|
|
92
|
-
padding: [
|
|
94
|
+
padding: [spacingPx(theme, 0.5), 0, 0, 0],
|
|
93
95
|
margin: 0,
|
|
94
96
|
// `interval: 0` makes every category label a candidate so `hideOverlap`
|
|
95
97
|
// can pack the maximum number that physically fit; the category-axis
|
|
@@ -119,7 +121,7 @@ export function barOptions({
|
|
|
119
121
|
},
|
|
120
122
|
axisLabel: {
|
|
121
123
|
...buildAxisLabelStyle(theme),
|
|
122
|
-
margin:
|
|
124
|
+
margin: spacingPx(theme, 1),
|
|
123
125
|
show: true,
|
|
124
126
|
showMaxLabel: true,
|
|
125
127
|
showMinLabel: true,
|
|
@@ -202,6 +204,7 @@ export function createBarOptionFactory(
|
|
|
202
204
|
typeof option.yAxis === 'object' && !Array.isArray(option.yAxis)
|
|
203
205
|
? option.yAxis
|
|
204
206
|
: {}
|
|
207
|
+
const fixedY = fixedAxisBounds(baseYAxis)
|
|
205
208
|
|
|
206
209
|
const seriesTemplates = Array.isArray(option.series) ? option.series : []
|
|
207
210
|
const broadcastTemplate = seriesTemplates[0] ?? {}
|
|
@@ -300,12 +303,14 @@ export function createBarOptionFactory(
|
|
|
300
303
|
...(dataZoomLayout ? { dataZoom: dataZoomLayout } : {}),
|
|
301
304
|
yAxis: {
|
|
302
305
|
...baseYAxis,
|
|
306
|
+
// A config transform (RelativeData's `'total'` scale) may pin the
|
|
307
|
+
// ends; each applies only on the side the data reaches.
|
|
303
308
|
min: (extent: { min: number }) => {
|
|
304
|
-
niceMin = extent.min < 0 ? niceNum(extent.min) : 0
|
|
309
|
+
niceMin = extent.min < 0 ? (fixedY.min ?? niceNum(extent.min)) : 0
|
|
305
310
|
return niceMin
|
|
306
311
|
},
|
|
307
312
|
max: (extent: { min: number; max: number }) => {
|
|
308
|
-
niceMax = extent.max <= 0 ? 1 : niceNum(extent.max)
|
|
313
|
+
niceMax = extent.max <= 0 ? 1 : (fixedY.max ?? niceNum(extent.max))
|
|
309
314
|
return niceMax
|
|
310
315
|
},
|
|
311
316
|
axisLabel: {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
3
3
|
import { Provider } from '../provider/widget-provider'
|
|
4
|
-
import {
|
|
5
|
-
clearAllWidgetStores,
|
|
6
|
-
useTransformEnabled,
|
|
7
|
-
useWidgetId,
|
|
8
|
-
} from '../stores'
|
|
4
|
+
import { clearAllWidgetStores, getWidgetStore } from '../stores'
|
|
9
5
|
import { Category } from './category'
|
|
10
6
|
import { RelativeData } from '../actions/relative-data/relative-data'
|
|
11
7
|
import type { CategoryWidgetData } from './types'
|
|
@@ -13,21 +9,7 @@ import type { CategoryWidgetData } from './types'
|
|
|
13
9
|
beforeEach(() => clearAllWidgetStores())
|
|
14
10
|
afterEach(() => clearAllWidgetStores())
|
|
15
11
|
|
|
16
|
-
//
|
|
17
|
-
// (apps/storybook/stories/modules/widgets/category/category.tsx). The
|
|
18
|
-
// composer-level wiring isn't testable in `apps/storybook` (no vitest
|
|
19
|
-
// infra), so we re-implement just the relative-data branch here and
|
|
20
|
-
// assert the bar-width math end-to-end through Provider → action →
|
|
21
|
-
// bridge → UI.
|
|
22
|
-
function Wired({ maxOverride }: { maxOverride?: number }) {
|
|
23
|
-
const id = useWidgetId()
|
|
24
|
-
const relativeDataActive = useTransformEnabled(id, 'relative-data')
|
|
25
|
-
const effectiveMaxOverride = relativeDataActive ? 100 : maxOverride
|
|
26
|
-
return <Category maxOverride={effectiveMaxOverride} />
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Single-series dataset where the max value is 1000 so percent values
|
|
30
|
-
// map to round numbers we can match exactly against bar widths.
|
|
12
|
+
// Single series: shares of the 1750 total are ≈57.14 / 28.57 / 14.29.
|
|
31
13
|
const DATA: CategoryWidgetData = [
|
|
32
14
|
[
|
|
33
15
|
{ name: 'A', value: 1000 },
|
|
@@ -36,72 +18,78 @@ const DATA: CategoryWidgetData = [
|
|
|
36
18
|
],
|
|
37
19
|
]
|
|
38
20
|
|
|
39
|
-
function
|
|
21
|
+
function widths(): number[] {
|
|
40
22
|
return Array.from(
|
|
41
23
|
document.querySelectorAll<HTMLElement>('[data-bar-fill="true"]'),
|
|
42
|
-
)
|
|
24
|
+
).map((el) => parseFloat(el.style.width))
|
|
43
25
|
}
|
|
44
26
|
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
const toggle = () =>
|
|
28
|
+
fireEvent.click(screen.getByRole('button', { name: /show relative values/i }))
|
|
29
|
+
|
|
30
|
+
describe('Category + RelativeData', () => {
|
|
31
|
+
it('off → bars scale to the data max', () => {
|
|
47
32
|
render(
|
|
48
33
|
<Provider id='cat-rd-off' data={DATA}>
|
|
49
34
|
<RelativeData />
|
|
50
|
-
<
|
|
35
|
+
<Category />
|
|
51
36
|
</Provider>,
|
|
52
37
|
)
|
|
53
|
-
|
|
54
|
-
expect(fills).toHaveLength(3)
|
|
55
|
-
// 1000/1000, 500/1000, 250/1000
|
|
56
|
-
expect(fills[0]!.style.width).toBe('100%')
|
|
57
|
-
expect(fills[1]!.style.width).toBe('50%')
|
|
58
|
-
expect(fills[2]!.style.width).toBe('25%')
|
|
38
|
+
expect(widths()).toEqual([100, 50, 25])
|
|
59
39
|
})
|
|
60
40
|
|
|
61
|
-
it('
|
|
41
|
+
it("scale 'max' (default): values are shares of the total, the top bar fills", () => {
|
|
62
42
|
render(
|
|
63
|
-
<Provider id='cat-rd-
|
|
43
|
+
<Provider id='cat-rd-max' data={DATA}>
|
|
64
44
|
<RelativeData />
|
|
65
|
-
<
|
|
45
|
+
<Category />
|
|
46
|
+
</Provider>,
|
|
47
|
+
)
|
|
48
|
+
toggle()
|
|
49
|
+
const values = (
|
|
50
|
+
getWidgetStore('cat-rd-max').getState().data as CategoryWidgetData
|
|
51
|
+
)[0]!.map((d) => d.value)
|
|
52
|
+
expect(values[0]).toBeCloseTo(57.14, 2)
|
|
53
|
+
const [a, b, c] = widths()
|
|
54
|
+
expect(a).toBeCloseTo(100)
|
|
55
|
+
expect(b).toBeCloseTo(50)
|
|
56
|
+
expect(c).toBeCloseTo(25)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("scale 'total': bars end at 100%", () => {
|
|
60
|
+
render(
|
|
61
|
+
<Provider id='cat-rd-total' data={DATA}>
|
|
62
|
+
<RelativeData scale='total' />
|
|
63
|
+
<Category />
|
|
66
64
|
</Provider>,
|
|
67
65
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
expect(b).toBeLessThan(30)
|
|
84
|
-
expect(c).toBeGreaterThan(13)
|
|
85
|
-
expect(c).toBeLessThan(16)
|
|
86
|
-
// The largest bar must be NOT 100% — that's the whole point of the
|
|
87
|
-
// fix. Without the wiring, A would have been 100% (largest of the
|
|
88
|
-
// percent series).
|
|
89
|
-
expect(a).toBeLessThan(100)
|
|
66
|
+
toggle()
|
|
67
|
+
const [a, b, c] = widths()
|
|
68
|
+
expect(a).toBeCloseTo(57.14, 1)
|
|
69
|
+
expect(b).toBeCloseTo(28.57, 1)
|
|
70
|
+
expect(c).toBeCloseTo(14.29, 1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('ignores maxOverride while relative is on (legacy ±100 wiring)', () => {
|
|
74
|
+
render(
|
|
75
|
+
<Provider id='cat-rd-legacy' data={DATA}>
|
|
76
|
+
<RelativeData initialEnabled />
|
|
77
|
+
<Category maxOverride={100} minOverride={-100} />
|
|
78
|
+
</Provider>,
|
|
79
|
+
)
|
|
80
|
+
expect(widths()[0]).toBeCloseTo(100)
|
|
90
81
|
})
|
|
91
82
|
|
|
92
83
|
it('toggling off again restores data-max scaling', () => {
|
|
93
84
|
render(
|
|
94
85
|
<Provider id='cat-rd-restore' data={DATA}>
|
|
95
|
-
<RelativeData />
|
|
96
|
-
<
|
|
86
|
+
<RelativeData scale='total' />
|
|
87
|
+
<Category />
|
|
97
88
|
</Provider>,
|
|
98
89
|
)
|
|
99
|
-
const button = screen.getByRole('button', { name: /relative
|
|
90
|
+
const button = screen.getByRole('button', { name: /show relative values/i })
|
|
100
91
|
fireEvent.click(button) // on
|
|
101
92
|
fireEvent.click(button) // off
|
|
102
|
-
|
|
103
|
-
expect(fills[0]!.style.width).toBe('100%')
|
|
104
|
-
expect(fills[1]!.style.width).toBe('50%')
|
|
105
|
-
expect(fills[2]!.style.width).toBe('25%')
|
|
93
|
+
expect(widths()).toEqual([100, 50, 25])
|
|
106
94
|
})
|
|
107
95
|
})
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useWidgetShallow,
|
|
7
7
|
type WidgetState,
|
|
8
8
|
} from '../stores'
|
|
9
|
+
import { selectRelativeScale } from '../actions/relative-data/scale'
|
|
9
10
|
import { CategoryUI } from './category-ui'
|
|
10
11
|
import { categorySeriesKey, NO_HIDDEN_SERIES } from './helpers'
|
|
11
12
|
import type {
|
|
@@ -86,7 +87,8 @@ export interface CategoryProps {
|
|
|
86
87
|
* stay coherent across data transforms (e.g., the Searcher filtering
|
|
87
88
|
* rows in and out — bars don't rescale just because the larger rows
|
|
88
89
|
* got hidden). Pass an explicit number to fix the denominator
|
|
89
|
-
* regardless of the data.
|
|
90
|
+
* regardless of the data. Ignored while `RelativeData` is on, whose
|
|
91
|
+
* `scale` decides the denominator instead.
|
|
90
92
|
*/
|
|
91
93
|
maxOverride?: number
|
|
92
94
|
/**
|
|
@@ -133,18 +135,30 @@ export function Category({
|
|
|
133
135
|
// Auto-fill both ends of the scale from `rawData` so bar widths and the
|
|
134
136
|
// zero stay coherent across data transforms (e.g., the Searcher filtering
|
|
135
137
|
// rows in/out). Consumer's explicit value wins, but each end only applies
|
|
136
|
-
// when
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
138
|
+
// when the data reaches past zero on that side, so positive-only data
|
|
139
|
+
// keeps its zero at the left edge, negative-only at the right. With no
|
|
140
|
+
// data on a side we pass nothing and CategoryUI falls back to the live
|
|
141
|
+
// `data`.
|
|
142
|
+
// With RelativeData on, the values are percentages: `'total'` ends the
|
|
143
|
+
// scale at ±100, `'max'` at the largest share so the top row fills.
|
|
144
|
+
const relativeScale = useWidget(id, selectRelativeScale)
|
|
145
|
+
const [naturalMin, naturalMax] = useMemo(() => {
|
|
146
|
+
const [min, max] = extentFromCategoryData(slice.rawData)
|
|
147
|
+
if (relativeScale === 'total')
|
|
148
|
+
return [min < 0 ? -100 : 0, max > 0 ? 100 : 0]
|
|
149
|
+
if (relativeScale === 'max') return extentFromCategoryData(slice.data)
|
|
150
|
+
return [min, max]
|
|
151
|
+
}, [slice.rawData, slice.data, relativeScale])
|
|
152
|
+
// The overrides are in absolute units, so they sit out while relative
|
|
153
|
+
// values are on.
|
|
144
154
|
const effectiveMaxOverride =
|
|
145
|
-
naturalMax > 0
|
|
155
|
+
naturalMax > 0
|
|
156
|
+
? ((relativeScale ? undefined : maxOverride) ?? naturalMax)
|
|
157
|
+
: undefined
|
|
146
158
|
const effectiveMinOverride =
|
|
147
|
-
naturalMin < 0
|
|
159
|
+
naturalMin < 0
|
|
160
|
+
? ((relativeScale ? undefined : minOverride) ?? naturalMin)
|
|
161
|
+
: undefined
|
|
148
162
|
|
|
149
163
|
const seriesKey = categorySeriesKey(series)
|
|
150
164
|
const stored = useWidget<
|