@carto/ps-react-ui 4.12.1 → 4.13.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/components.js +706 -658
- package/dist/components.js.map +1 -1
- package/dist/legend/stores.js +20 -0
- package/dist/legend/stores.js.map +1 -0
- package/dist/legend.js +1536 -0
- package/dist/legend.js.map +1 -0
- package/dist/selectors-DwHj6mE2.js +388 -0
- package/dist/selectors-DwHj6mE2.js.map +1 -0
- package/dist/types/components/index.d.ts +1 -0
- package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +35 -3
- package/dist/types/legend/components/contexts.d.ts +20 -0
- package/dist/types/legend/components/icons.d.ts +12 -0
- package/dist/types/legend/components/index.d.ts +26 -0
- package/dist/types/legend/components/legend-actions/legend-actions.d.ts +24 -0
- package/dist/types/legend/components/legend-actions/styles.d.ts +7 -0
- package/dist/types/legend/components/legend-category/legend-category-ui.d.ts +13 -0
- package/dist/types/legend/components/legend-category/styles.d.ts +52 -0
- package/dist/types/legend/components/legend-config-select/legend-config-select.d.ts +14 -0
- package/dist/types/legend/components/legend-config-select/styles.d.ts +44 -0
- package/dist/types/legend/components/legend-group/legend-group.d.ts +21 -0
- package/dist/types/legend/components/legend-group/styles.d.ts +63 -0
- package/dist/types/legend/components/legend-icon/legend-icon-ui.d.ts +10 -0
- package/dist/types/legend/components/legend-icon/styles.d.ts +52 -0
- package/dist/types/legend/components/legend-item/legend-item-ui.d.ts +14 -0
- package/dist/types/legend/components/legend-item/legend-item.d.ts +10 -0
- package/dist/types/legend/components/legend-item/styles.d.ts +7 -0
- package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +32 -0
- package/dist/types/legend/components/legend-opacity/styles.d.ts +23 -0
- package/dist/types/legend/components/legend-proportion/legend-proportion-ui.d.ts +16 -0
- package/dist/types/legend/components/legend-proportion/styles.d.ts +37 -0
- package/dist/types/legend/components/legend-ramp/legend-ramp-ui.d.ts +20 -0
- package/dist/types/legend/components/legend-ramp/styles.d.ts +53 -0
- package/dist/types/legend/components/legend-row/legend-row.d.ts +23 -0
- package/dist/types/legend/components/legend-row/styles.d.ts +77 -0
- package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +18 -0
- package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +15 -0
- package/dist/types/legend/components/legend-swatch/legend-swatch.d.ts +26 -0
- package/dist/types/legend/components/legend-visibility-toggle/legend-visibility-toggle.d.ts +35 -0
- package/dist/types/legend/index.d.ts +88 -0
- package/dist/types/legend/provider/index.d.ts +6 -0
- package/dist/types/legend/provider/labels.d.ts +27 -0
- package/dist/types/legend/provider/legend-config-context.d.ts +7 -0
- package/dist/types/legend/provider/legend-provider.d.ts +38 -0
- package/dist/types/legend/stores/index.d.ts +5 -0
- package/dist/types/legend/stores/legend-context.d.ts +4 -0
- package/dist/types/legend/stores/legend-store-registry.d.ts +39 -0
- package/dist/types/legend/stores/legend-store.test.d.ts +1 -0
- package/dist/types/legend/stores/selectors.d.ts +33 -0
- package/dist/types/legend/stores/types.d.ts +275 -0
- package/dist/widgets-v2/bar.js +7 -3
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/histogram.js +9 -4
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/package.json +11 -3
- package/src/components/geolocation-controls/geolocation-controls.test.tsx +15 -0
- package/src/components/geolocation-controls/geolocation-controls.tsx +19 -3
- package/src/components/index.ts +2 -0
- package/src/components/responsive-drawer/responsive-drawer.test.tsx +129 -25
- package/src/components/responsive-drawer/responsive-drawer.tsx +149 -16
- package/src/legend/components/contexts.ts +32 -0
- package/src/legend/components/icons.tsx +22 -0
- package/src/legend/components/index.ts +39 -0
- package/src/legend/components/legend-actions/legend-actions.tsx +57 -0
- package/src/legend/components/legend-actions/styles.ts +9 -0
- package/src/legend/components/legend-category/legend-category-ui.tsx +91 -0
- package/src/legend/components/legend-category/styles.ts +61 -0
- package/src/legend/components/legend-config-select/legend-config-select.tsx +96 -0
- package/src/legend/components/legend-config-select/styles.ts +48 -0
- package/src/legend/components/legend-group/legend-group.test.tsx +115 -0
- package/src/legend/components/legend-group/legend-group.tsx +88 -0
- package/src/legend/components/legend-group/styles.ts +82 -0
- package/src/legend/components/legend-icon/legend-icon-ui.tsx +56 -0
- package/src/legend/components/legend-icon/styles.ts +56 -0
- package/src/legend/components/legend-item/legend-item-ui.test.tsx +362 -0
- package/src/legend/components/legend-item/legend-item-ui.tsx +43 -0
- package/src/legend/components/legend-item/legend-item.tsx +34 -0
- package/src/legend/components/legend-item/styles.ts +11 -0
- package/src/legend/components/legend-opacity/legend-opacity.tsx +179 -0
- package/src/legend/components/legend-opacity/styles.ts +28 -0
- package/src/legend/components/legend-proportion/legend-proportion-ui.tsx +122 -0
- package/src/legend/components/legend-proportion/styles.ts +43 -0
- package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +159 -0
- package/src/legend/components/legend-ramp/styles.ts +62 -0
- package/src/legend/components/legend-row/legend-row.test.tsx +498 -0
- package/src/legend/components/legend-row/legend-row.tsx +129 -0
- package/src/legend/components/legend-row/styles.ts +111 -0
- package/src/legend/components/legend-row-menu/legend-row-menu.tsx +68 -0
- package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +35 -0
- package/src/legend/components/legend-swatch/legend-swatch.test.tsx +109 -0
- package/src/legend/components/legend-swatch/legend-swatch.tsx +237 -0
- package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +127 -0
- package/src/legend/index.ts +121 -0
- package/src/legend/provider/index.ts +6 -0
- package/src/legend/provider/labels.ts +59 -0
- package/src/legend/provider/legend-config-context.ts +15 -0
- package/src/legend/provider/legend-provider.tsx +102 -0
- package/src/legend/stores/index.ts +46 -0
- package/src/legend/stores/legend-context.ts +17 -0
- package/src/legend/stores/legend-store-registry.ts +639 -0
- package/src/legend/stores/legend-store.test.ts +457 -0
- package/src/legend/stores/selectors.ts +65 -0
- package/src/legend/stores/types.ts +347 -0
- package/src/widgets-v2/bar/options.ts +4 -0
- package/src/widgets-v2/histogram/options.ts +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
|
|
3
|
+
export const styles = {
|
|
4
|
+
// Tooltip wrapper around the (possibly disabled) trigger button. A plain
|
|
5
|
+
// inline span would be sized by the line box (baseline + descender) and
|
|
6
|
+
// end up ~1px taller than the 24px button inside.
|
|
7
|
+
triggerWrap: {
|
|
8
|
+
display: 'inline-flex',
|
|
9
|
+
},
|
|
10
|
+
popover: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
gap: 1.5,
|
|
14
|
+
padding: ({ spacing }) => spacing(1, 1.5),
|
|
15
|
+
width: 280,
|
|
16
|
+
},
|
|
17
|
+
slider: {
|
|
18
|
+
flex: 1,
|
|
19
|
+
},
|
|
20
|
+
// Wide enough for "100 %" — the value must never ellipsize.
|
|
21
|
+
input: {
|
|
22
|
+
width: 76,
|
|
23
|
+
flexShrink: 0,
|
|
24
|
+
'& input': {
|
|
25
|
+
textAlign: 'right',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
} satisfies Record<string, SxProps<Theme>>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Box, Typography } from '@mui/material'
|
|
3
|
+
import { styles } from './styles'
|
|
4
|
+
import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels'
|
|
5
|
+
import type { LegendProportionVariable } from '../../stores'
|
|
6
|
+
|
|
7
|
+
export interface LegendProportionUIProps {
|
|
8
|
+
data: LegendProportionVariable
|
|
9
|
+
/** Override the renderer's user-facing strings. */
|
|
10
|
+
labels?: Pick<LegendLabels, 'radiusRangeBy' | 'maxPrefix' | 'minPrefix'>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MAX_DIAMETER = 72
|
|
14
|
+
const RATIO_FLOOR = 0.3
|
|
15
|
+
const MIN_STEPS = 2
|
|
16
|
+
const MAX_STEPS = 4
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Value breaks, largest→smallest. Explicit `stops` (≥2 entries) win, capped at
|
|
20
|
+
* the 4 largest; otherwise breaks are derived evenly from `min`/`max` over
|
|
21
|
+
* `steps` clamped to 2–4. Circles map 1:1 to the breaks.
|
|
22
|
+
*/
|
|
23
|
+
function resolveBreaks(data: LegendProportionVariable): number[] {
|
|
24
|
+
const explicit = (data.stops ?? []).filter((n) => Number.isFinite(n))
|
|
25
|
+
if (explicit.length >= MIN_STEPS) {
|
|
26
|
+
return explicit.sort((a, b) => b - a).slice(0, MAX_STEPS)
|
|
27
|
+
}
|
|
28
|
+
const { min, max } = data
|
|
29
|
+
if (min === max) return [max]
|
|
30
|
+
const steps = Math.min(
|
|
31
|
+
MAX_STEPS,
|
|
32
|
+
Math.max(MIN_STEPS, data.steps ?? MAX_STEPS),
|
|
33
|
+
)
|
|
34
|
+
return Array.from(
|
|
35
|
+
{ length: steps },
|
|
36
|
+
(_, i) => max - ((max - min) * i) / (steps - 1),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Decorative nested-circle diameter for circle `i` of `n` (i=0 outermost). */
|
|
41
|
+
function diameterFor(i: number, n: number): number {
|
|
42
|
+
if (n <= 1) return MAX_DIAMETER
|
|
43
|
+
const ratio = 1 - (1 - RATIO_FLOOR) * (i / (n - 1))
|
|
44
|
+
return MAX_DIAMETER * ratio
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Proportional-symbol legend — a stack of decorative nested circles with the
|
|
49
|
+
* value breaks labelled beside them (largest = MAX, smallest = MIN). Circle
|
|
50
|
+
* sizes are fixed/decorative, not value-derived, so the legend stays readable
|
|
51
|
+
* across very wide ranges.
|
|
52
|
+
*
|
|
53
|
+
* @experimental This API is new and may change in a future release.
|
|
54
|
+
*/
|
|
55
|
+
export function LegendProportionUI({
|
|
56
|
+
data,
|
|
57
|
+
labels = DEFAULT_LEGEND_LABELS,
|
|
58
|
+
}: LegendProportionUIProps) {
|
|
59
|
+
const { attribute, formatValue = String } = data
|
|
60
|
+
const breaks = resolveBreaks(data)
|
|
61
|
+
const n = breaks.length
|
|
62
|
+
|
|
63
|
+
const labelFor = (value: number, i: number): ReactNode => {
|
|
64
|
+
const formatted = formatValue(value)
|
|
65
|
+
if (n <= 1) return formatted
|
|
66
|
+
if (i === 0)
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{labels.maxPrefix}
|
|
70
|
+
{formatted}
|
|
71
|
+
</>
|
|
72
|
+
)
|
|
73
|
+
if (i === n - 1)
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
{labels.minPrefix}
|
|
77
|
+
{formatted}
|
|
78
|
+
</>
|
|
79
|
+
)
|
|
80
|
+
return formatted
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Box sx={styles.root}>
|
|
85
|
+
{attribute && (
|
|
86
|
+
<Box sx={styles.attribute}>
|
|
87
|
+
<Typography variant='overline' color='text.secondary'>
|
|
88
|
+
{labels.radiusRangeBy}
|
|
89
|
+
</Typography>
|
|
90
|
+
<Typography variant='caption' color='text.primary'>
|
|
91
|
+
{attribute}
|
|
92
|
+
</Typography>
|
|
93
|
+
</Box>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<Box sx={styles.body}>
|
|
97
|
+
<Box
|
|
98
|
+
sx={{ ...styles.circles, width: MAX_DIAMETER, height: MAX_DIAMETER }}
|
|
99
|
+
>
|
|
100
|
+
{breaks.map((_, i) => {
|
|
101
|
+
const diameter = diameterFor(i, n)
|
|
102
|
+
return (
|
|
103
|
+
<Box
|
|
104
|
+
key={i}
|
|
105
|
+
aria-hidden
|
|
106
|
+
sx={{ ...styles.circle, width: diameter, height: diameter }}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
})}
|
|
110
|
+
</Box>
|
|
111
|
+
|
|
112
|
+
<Box sx={{ ...styles.labels, height: MAX_DIAMETER }}>
|
|
113
|
+
{breaks.map((value, i) => (
|
|
114
|
+
<Typography key={i} variant='caption' color='text.secondary'>
|
|
115
|
+
{labelFor(value, i)}
|
|
116
|
+
</Typography>
|
|
117
|
+
))}
|
|
118
|
+
</Box>
|
|
119
|
+
</Box>
|
|
120
|
+
</Box>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
|
|
3
|
+
export const styles = {
|
|
4
|
+
root: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
flexDirection: 'column',
|
|
7
|
+
gap: 1,
|
|
8
|
+
},
|
|
9
|
+
attribute: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
},
|
|
13
|
+
body: {
|
|
14
|
+
display: 'flex',
|
|
15
|
+
alignItems: 'flex-end',
|
|
16
|
+
gap: 2,
|
|
17
|
+
paddingTop: 0.5,
|
|
18
|
+
},
|
|
19
|
+
circles: {
|
|
20
|
+
position: 'relative',
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
},
|
|
23
|
+
// All circles share the bottom and right edges (anchored bottom-right), so
|
|
24
|
+
// they nest along the diagonal — largest fanning up-left, smallest in the
|
|
25
|
+
// bottom-right. Every circle has the SAME opaque fill, so overlaps don't
|
|
26
|
+
// compound (uniform colour) while the borders still read as nested rings.
|
|
27
|
+
circle: {
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
right: 0,
|
|
30
|
+
bottom: 0,
|
|
31
|
+
borderRadius: '50%',
|
|
32
|
+
backgroundColor: 'grey.50',
|
|
33
|
+
border: '1px solid',
|
|
34
|
+
borderColor: 'grey.100',
|
|
35
|
+
boxSizing: 'border-box',
|
|
36
|
+
},
|
|
37
|
+
labels: {
|
|
38
|
+
flex: 1,
|
|
39
|
+
display: 'flex',
|
|
40
|
+
flexDirection: 'column',
|
|
41
|
+
justifyContent: 'space-between',
|
|
42
|
+
},
|
|
43
|
+
} satisfies Record<string, SxProps<Theme>>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Box, Tooltip, Typography } from '@mui/material'
|
|
2
|
+
import { ErrorOutline as ErrorIcon } from '@mui/icons-material'
|
|
3
|
+
import { styles } from './styles'
|
|
4
|
+
import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels'
|
|
5
|
+
import type { LegendRampVariable } from '../../stores'
|
|
6
|
+
|
|
7
|
+
export interface LegendRampUIProps {
|
|
8
|
+
data: LegendRampVariable
|
|
9
|
+
/** Override the renderer's user-facing strings. */
|
|
10
|
+
labels?: Pick<LegendLabels, 'colorBasedOn' | 'colorGradient'>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Horizontal position (0–100) of `avg` on the bar — linear between the first
|
|
15
|
+
* and last stop values, clamped to the edges. `null` when there's no scale
|
|
16
|
+
* (fewer than 2 stops or a zero-width range).
|
|
17
|
+
*/
|
|
18
|
+
function avgPercent(data: LegendRampVariable): number | null {
|
|
19
|
+
const { avg, stops } = data
|
|
20
|
+
if (avg == null || stops.length < 2) return null
|
|
21
|
+
const v0 = stops[0]!.value
|
|
22
|
+
const vn = stops[stops.length - 1]!.value
|
|
23
|
+
if (v0 === vn) return null
|
|
24
|
+
const pct = ((avg - v0) / (vn - v0)) * 100
|
|
25
|
+
return Math.min(100, Math.max(0, pct))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Color-ramp legend. `colors` and `stops` are both ordered low→high and are
|
|
30
|
+
* never reversed, so step colors and their labels stay aligned.
|
|
31
|
+
*
|
|
32
|
+
* Continuous mode renders a gradient with the endpoint labels. Discrete mode
|
|
33
|
+
* **requires `stops.length === colors.length + 1`** — stops are the boundaries
|
|
34
|
+
* between color steps (including both outer edges) and each label is centered
|
|
35
|
+
* on its boundary line. A discrete ramp with any other stop count is an
|
|
36
|
+
* incoherent state and renders a compact inline error instead of the bar.
|
|
37
|
+
*
|
|
38
|
+
* @experimental This API is new and may change in a future release.
|
|
39
|
+
*/
|
|
40
|
+
export function LegendRampUI({
|
|
41
|
+
data,
|
|
42
|
+
labels = DEFAULT_LEGEND_LABELS,
|
|
43
|
+
}: LegendRampUIProps) {
|
|
44
|
+
const { colors, stops, isContinuous, attribute, formatValue = String } = data
|
|
45
|
+
const avgPct = avgPercent(data)
|
|
46
|
+
|
|
47
|
+
const first = stops[0]
|
|
48
|
+
const last = stops[stops.length - 1]
|
|
49
|
+
const gradient = `linear-gradient(90deg, ${colors.join(', ')})`
|
|
50
|
+
|
|
51
|
+
// Discrete ramps need one stop per color boundary (colors.length + 1).
|
|
52
|
+
// `isBoundary` is therefore equivalent to "valid discrete".
|
|
53
|
+
const isBoundary = !isContinuous && stops.length === colors.length + 1
|
|
54
|
+
const invalidDiscrete = !isContinuous && !isBoundary
|
|
55
|
+
|
|
56
|
+
// Boundary range for assistive tech (valid discrete only reaches here).
|
|
57
|
+
const stepAriaLabel = (index: number): string =>
|
|
58
|
+
`${stops[index]?.label ?? ''} – ${stops[index + 1]?.label ?? ''}`
|
|
59
|
+
|
|
60
|
+
const attributeHeader = attribute && (
|
|
61
|
+
<Box sx={styles.attribute}>
|
|
62
|
+
<Typography variant='overline' color='text.secondary'>
|
|
63
|
+
{labels.colorBasedOn}
|
|
64
|
+
</Typography>
|
|
65
|
+
<Typography variant='caption' color='text.primary'>
|
|
66
|
+
{attribute}
|
|
67
|
+
</Typography>
|
|
68
|
+
</Box>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (invalidDiscrete) {
|
|
72
|
+
return (
|
|
73
|
+
<Box sx={styles.container}>
|
|
74
|
+
{attributeHeader}
|
|
75
|
+
<Box sx={styles.error}>
|
|
76
|
+
<ErrorIcon fontSize='small' />
|
|
77
|
+
<Typography variant='caption' color='error'>
|
|
78
|
+
{`Discrete ramp needs ${colors.length + 1} stops (one per color boundary) but got ${stops.length}.`}
|
|
79
|
+
</Typography>
|
|
80
|
+
</Box>
|
|
81
|
+
</Box>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Box sx={styles.container}>
|
|
87
|
+
{attributeHeader}
|
|
88
|
+
|
|
89
|
+
<Box sx={styles.barWrap}>
|
|
90
|
+
<Box sx={styles.bar}>
|
|
91
|
+
{isContinuous ? (
|
|
92
|
+
<Box
|
|
93
|
+
role='img'
|
|
94
|
+
aria-label={labels.colorGradient}
|
|
95
|
+
sx={{ width: '100%', height: '100%', background: gradient }}
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
colors.map((color, index) => (
|
|
99
|
+
<Box
|
|
100
|
+
key={index}
|
|
101
|
+
role='img'
|
|
102
|
+
aria-label={stepAriaLabel(index)}
|
|
103
|
+
sx={{ ...styles.step, backgroundColor: color }}
|
|
104
|
+
/>
|
|
105
|
+
))
|
|
106
|
+
)}
|
|
107
|
+
</Box>
|
|
108
|
+
|
|
109
|
+
{avgPct != null && data.avg != null && (
|
|
110
|
+
<Tooltip title={formatValue(data.avg)} placement='top' arrow>
|
|
111
|
+
{/* Inline style: per-instance dynamic value, no emotion class churn. */}
|
|
112
|
+
<Box sx={styles.avgLine} style={{ left: `${avgPct}%` }} />
|
|
113
|
+
</Tooltip>
|
|
114
|
+
)}
|
|
115
|
+
</Box>
|
|
116
|
+
|
|
117
|
+
{isBoundary ? (
|
|
118
|
+
<Box sx={styles.boundaryLabels}>
|
|
119
|
+
{stops.map((stop, index) => {
|
|
120
|
+
// Interior labels center on their boundary line; the outer two
|
|
121
|
+
// clamp inside the bar (first left-aligned, last right-aligned)
|
|
122
|
+
// so the row never overflows.
|
|
123
|
+
const isLast = index === stops.length - 1
|
|
124
|
+
const transform =
|
|
125
|
+
index === 0
|
|
126
|
+
? undefined
|
|
127
|
+
: isLast
|
|
128
|
+
? 'translateX(-100%)'
|
|
129
|
+
: 'translateX(-50%)'
|
|
130
|
+
return (
|
|
131
|
+
<Typography
|
|
132
|
+
key={`${stop.label}-${index}`}
|
|
133
|
+
variant='overline'
|
|
134
|
+
color='text.secondary'
|
|
135
|
+
sx={styles.boundaryLabel}
|
|
136
|
+
style={{
|
|
137
|
+
left: `${(index / colors.length) * 100}%`,
|
|
138
|
+
transform,
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
{stop.label}
|
|
142
|
+
</Typography>
|
|
143
|
+
)
|
|
144
|
+
})}
|
|
145
|
+
</Box>
|
|
146
|
+
) : (
|
|
147
|
+
// Continuous gradients show only the range endpoints.
|
|
148
|
+
<Box sx={styles.labels}>
|
|
149
|
+
<Typography variant='overline' color='text.secondary'>
|
|
150
|
+
{first?.label}
|
|
151
|
+
</Typography>
|
|
152
|
+
<Typography variant='overline' color='text.secondary'>
|
|
153
|
+
{last?.label}
|
|
154
|
+
</Typography>
|
|
155
|
+
</Box>
|
|
156
|
+
)}
|
|
157
|
+
</Box>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SxProps, Theme } from '@mui/material'
|
|
2
|
+
|
|
3
|
+
export const styles = {
|
|
4
|
+
container: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
flexDirection: 'column',
|
|
7
|
+
gap: 0.5,
|
|
8
|
+
},
|
|
9
|
+
attribute: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
},
|
|
13
|
+
// Inline error shown when a discrete ramp's stop count is incoherent.
|
|
14
|
+
error: {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
gap: 0.5,
|
|
18
|
+
color: 'error.main',
|
|
19
|
+
},
|
|
20
|
+
// Positioning context for the avg marker; the inner bar keeps its rounded
|
|
21
|
+
// clip while the line is free to overflow vertically.
|
|
22
|
+
barWrap: {
|
|
23
|
+
position: 'relative',
|
|
24
|
+
},
|
|
25
|
+
bar: {
|
|
26
|
+
height: 8,
|
|
27
|
+
borderRadius: '2px',
|
|
28
|
+
overflow: 'hidden',
|
|
29
|
+
display: 'flex',
|
|
30
|
+
},
|
|
31
|
+
avgLine: {
|
|
32
|
+
position: 'absolute',
|
|
33
|
+
top: -2,
|
|
34
|
+
bottom: -2,
|
|
35
|
+
width: 0,
|
|
36
|
+
borderLeft: '1.5px dashed',
|
|
37
|
+
borderColor: 'text.primary',
|
|
38
|
+
transform: 'translateX(-50%)',
|
|
39
|
+
},
|
|
40
|
+
step: {
|
|
41
|
+
flex: 1,
|
|
42
|
+
height: '100%',
|
|
43
|
+
},
|
|
44
|
+
labels: {
|
|
45
|
+
display: 'flex',
|
|
46
|
+
justifyContent: 'space-between',
|
|
47
|
+
gap: 1,
|
|
48
|
+
},
|
|
49
|
+
// Boundary mode: labels absolutely positioned so each centers on the line
|
|
50
|
+
// between adjacent color steps. The outer two clamp inside the bar's width
|
|
51
|
+
// (first left-aligned, last right-aligned) so nothing overflows the row.
|
|
52
|
+
// `left`/`transform` are applied inline per label in the renderer.
|
|
53
|
+
boundaryLabels: {
|
|
54
|
+
position: 'relative',
|
|
55
|
+
height: 16,
|
|
56
|
+
},
|
|
57
|
+
boundaryLabel: {
|
|
58
|
+
position: 'absolute',
|
|
59
|
+
top: 0,
|
|
60
|
+
whiteSpace: 'nowrap',
|
|
61
|
+
},
|
|
62
|
+
} satisfies Record<string, SxProps<Theme>>
|