@fundar/data-chart-telling 0.0.26 → 0.0.28
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/charts/BaseChart.svelte +18 -10
- package/dist/charts/BaseChart.svelte.d.ts +4 -2
- package/dist/charts/heatmap/Chart.svelte +2 -2
- package/dist/charts/pyramid/Chart.svelte +2 -2
- package/dist/configuration/config.svelte.js +26 -0
- package/dist/configuration/themes/dark.js +4 -0
- package/dist/configuration/themes/index.d.ts +64 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -1
- package/dist/layout/facet/FacetIndicator.svelte +31 -0
- package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
- package/dist/layout/facet/FacetLayout.svelte +249 -24
- package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
- package/dist/layout/legend/ContinuousSection.svelte +1 -1
- package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
- package/dist/layout/legend/DiscreteSection.svelte +1 -1
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
- package/dist/layout/legend/LegendLayout.svelte +1 -1
- package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
- package/dist/layout/legend/interaction.svelte.d.ts +9 -1
- package/dist/layout/legend/interaction.svelte.js +9 -1
- package/dist/layout/timeline/Timeline.svelte +1 -1
- package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte +1 -1
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
- package/dist/layout/tooltip/controller.svelte.js +16 -2
- package/dist/markers/HoverMarker.svelte +2 -2
- package/dist/plots/bar/Plot.svelte +43 -4
- package/dist/plots/bar/ValueLabels.svelte +69 -23
- package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/heatmap/Plot.svelte +4 -4
- package/dist/plots/heatmap/ValueLabels.svelte +3 -3
- package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
- package/dist/plots/line/Plot.svelte +34 -29
- package/dist/plots/line/ValueLabels.svelte +73 -51
- package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
- package/dist/plots/pyramid/Plot.svelte +41 -6
- package/dist/plots/pyramid/ValueLabels.svelte +70 -24
- package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
- package/dist/plots/scatter/Plot.svelte +40 -4
- package/dist/plots/scatter/ValueLabels.svelte +90 -47
- package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
- package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
- package/dist/plots/utils/labelOverflow.svelte.js +116 -0
- package/dist/plots/utils/legendDisabled.d.ts +1 -1
- package/dist/types/charts/common.d.ts +3 -50
- package/dist/types/charts/props.d.ts +3 -2
- package/dist/types/configuration/styling.d.ts +19 -0
- package/dist/types/layout/facet.d.ts +41 -0
- package/dist/types/layout/legend.d.ts +65 -1
- package/dist/types/layout/styles.d.ts +1 -12
- package/dist/types/layout/timeline.d.ts +32 -0
- package/dist/types/layout/timeline.js +1 -0
- package/dist/utils/interpolate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/types/charts/legend.d.ts +0 -60
- /package/dist/types/{charts/legend.js → layout/facet.js} +0 -0
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
6
6
|
import { paletteColor } from '../../utils/color';
|
|
7
7
|
import { declutter1D, laneAssignment } from '../utils/declutter';
|
|
8
|
+
import { disabledFillOverride, disabledStrokeOverride } from '../utils/legendDisabled';
|
|
9
|
+
import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
|
|
8
10
|
import type { Series } from '../../types/plots/data/common';
|
|
9
11
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
10
12
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
13
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
14
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* End-point value labels for `LinePlot` — one label per series at its last
|
|
@@ -21,11 +25,19 @@
|
|
|
21
25
|
lastPoints,
|
|
22
26
|
values,
|
|
23
27
|
onOverflow,
|
|
28
|
+
disabledFor,
|
|
29
|
+
onHoverSeries,
|
|
30
|
+
onHoverEnd,
|
|
24
31
|
}: {
|
|
25
32
|
lastPoints: { series: Series<TData>; lastRow: TData; seriesIndex: number }[];
|
|
26
33
|
values: ValuesStyle | undefined;
|
|
27
34
|
/** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
28
|
-
onOverflow?: (overflow:
|
|
35
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
36
|
+
/** Resolved legend/hover-isolate dimming style for a series, if any. */
|
|
37
|
+
disabledFor?: (name: string) => LegendDisabledStyle | undefined;
|
|
38
|
+
/** Fired when the pointer enters a label — only wired when hover-isolate is enabled. */
|
|
39
|
+
onHoverSeries?: (name: string) => void;
|
|
40
|
+
onHoverEnd?: () => void;
|
|
29
41
|
} = $props();
|
|
30
42
|
|
|
31
43
|
const cfg = $derived(getConfiguration());
|
|
@@ -49,7 +61,7 @@
|
|
|
49
61
|
|
|
50
62
|
const font = $derived(values?.fontStyle);
|
|
51
63
|
const connector = $derived(values?.strokeStyle);
|
|
52
|
-
const
|
|
64
|
+
const formatValue = $derived(values?.format ?? ((v: number, name: string) => `${name} - ${v}`));
|
|
53
65
|
const valAnchor = $derived<ValueAnchor>(values?.anchor ?? 'start');
|
|
54
66
|
// 'outside' is a per-point escape hatch resolved by HoverMarker; line's own
|
|
55
67
|
// value labels already have their own `anchor: 'outside'` concept above,
|
|
@@ -69,36 +81,33 @@
|
|
|
69
81
|
// ── Overflow measurement ─────────────────────────────────────────────────
|
|
70
82
|
// Measures how far the rendered labels extend past the plot's own content
|
|
71
83
|
// box and reports it to the caller (`Plot.svelte`), which grows its own
|
|
72
|
-
// local margin state to fit —
|
|
73
|
-
//
|
|
74
|
-
// margin-auto-sizing
|
|
75
|
-
// keeps this entirely local to one `<Plot>` instance: nothing here
|
|
76
|
-
// shared across instances, which is what made
|
|
84
|
+
// local margin state to fit — see `labelOverflow.svelte.ts` for the actual
|
|
85
|
+
// `getBBox()` measurement. Reporting through a plain callback instead of
|
|
86
|
+
// registering directly into svelteplot's *shared* margin-auto-sizing
|
|
87
|
+
// context keeps this entirely local to one `<Plot>` instance: nothing here
|
|
88
|
+
// is shared across instances, which is what made that old approach spike
|
|
77
89
|
// CPU/memory into unresponsiveness when many instances mounted/unmounted
|
|
78
90
|
// together (e.g. a multi-story docs page → single story).
|
|
79
91
|
let groupEl: SVGGElement | undefined = $state();
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
onOverflow(
|
|
99
|
-
left: Math.ceil(Math.max(0, plotLeft - box.x)),
|
|
100
|
-
right: Math.ceil(Math.max(0, box.x + box.width - plotRight)),
|
|
101
|
-
});
|
|
93
|
+
watchLabelOverflow({
|
|
94
|
+
el: () => groupEl,
|
|
95
|
+
bounds: () => {
|
|
96
|
+
// getBBox() inside isn't itself tracked — depend on whatever actually
|
|
97
|
+
// changes the rendered label layout so this re-measures.
|
|
98
|
+
void lastPoints;
|
|
99
|
+
void values;
|
|
100
|
+
const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
|
|
101
|
+
const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
|
|
102
|
+
if (!xRange || !yRange) return undefined;
|
|
103
|
+
return {
|
|
104
|
+
left: Math.min(xRange[0], xRange[1]),
|
|
105
|
+
right: Math.max(xRange[0], xRange[1]),
|
|
106
|
+
top: Math.min(yRange[0], yRange[1]),
|
|
107
|
+
bottom: Math.max(yRange[0], yRange[1]),
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
onOverflow: (overflow) => onOverflow?.(overflow),
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
/**
|
|
@@ -166,9 +175,12 @@
|
|
|
166
175
|
{@const labelDx = offset?.inGroup
|
|
167
176
|
? dir * (elbowGap + (numLanes - 1) * laneStep + labelGap)
|
|
168
177
|
: baseDx}
|
|
169
|
-
{@const text =
|
|
178
|
+
{@const text = formatValue(Number(yFn(lastRow)), s.name)}
|
|
170
179
|
{@const preferredTA = resolvedTextAnchor ?? defaultTA}
|
|
171
180
|
{@const preferredLA = font?.lineAnchor ?? defaultLA}
|
|
181
|
+
{@const disabled = disabledFor?.(s.name)}
|
|
182
|
+
{@const disabledStroke = disabledStrokeOverride(disabled)}
|
|
183
|
+
{@const disabledFill = disabledFillOverride(disabled)}
|
|
172
184
|
{#if offset?.inGroup && plot.scales.x?.fn && plot.scales.y?.fn}
|
|
173
185
|
{@const px = Number(plot.scales.x.fn(xFn(lastRow)))}
|
|
174
186
|
{@const py = Number(plot.scales.y.fn(Number(yFn(lastRow))))}
|
|
@@ -179,32 +191,42 @@
|
|
|
179
191
|
fill="none"
|
|
180
192
|
stroke={connector?.stroke ?? defaultConnectorColor}
|
|
181
193
|
stroke-width={connector?.strokeWidth ?? defaultConnectorWidth}
|
|
182
|
-
stroke-opacity={connector?.strokeOpacity ?? 1}
|
|
194
|
+
stroke-opacity={connector?.strokeOpacity ?? disabledStroke?.strokeOpacity ?? 1}
|
|
183
195
|
stroke-dasharray={connector?.strokeDasharray}
|
|
184
196
|
stroke-linecap={connector?.strokeLinecap ?? 'square'}
|
|
185
197
|
stroke-linejoin={connector?.strokeLinejoin ?? 'miter'}
|
|
186
198
|
/>
|
|
187
199
|
{/if}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
<!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
|
|
201
|
+
despite declaring them in its types — a wrapping <g> catches the
|
|
202
|
+
boundary crossing instead (pointerenter/leave fire on every ancestor). -->
|
|
203
|
+
<g
|
|
204
|
+
role="presentation"
|
|
205
|
+
onpointerenter={onHoverSeries && (() => onHoverSeries(s.name))}
|
|
206
|
+
onpointerleave={onHoverEnd}
|
|
207
|
+
>
|
|
208
|
+
<Text
|
|
209
|
+
data={[lastRow]}
|
|
210
|
+
x={xFn}
|
|
211
|
+
y={yFn}
|
|
212
|
+
text={() => text}
|
|
213
|
+
dx={labelDx}
|
|
214
|
+
dy={(font?.dy ?? defaultDy) + extraDy}
|
|
215
|
+
textAnchor={preferredTA}
|
|
216
|
+
lineAnchor={preferredLA}
|
|
217
|
+
lineHeight={font?.lineHeight}
|
|
218
|
+
rotate={font?.rotate}
|
|
219
|
+
class={font?.class}
|
|
220
|
+
textClass={font?.textClass}
|
|
221
|
+
fill={font?.fill ?? seriesColor}
|
|
222
|
+
fillOpacity={disabledFill?.fillOpacity}
|
|
223
|
+
{fontSize}
|
|
224
|
+
fontWeight={font?.fontWeight}
|
|
225
|
+
fontStyle={font?.fontStyle}
|
|
226
|
+
stroke={font?.stroke}
|
|
227
|
+
strokeWidth={font?.strokeWidth}
|
|
228
|
+
paintOrder={font?.paintOrder}
|
|
229
|
+
/>
|
|
230
|
+
</g>
|
|
209
231
|
{/each}
|
|
210
232
|
</g>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Series } from '../../types/plots/data/common';
|
|
2
2
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
3
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
4
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
3
5
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
4
6
|
props: {
|
|
5
7
|
lastPoints: {
|
|
@@ -9,10 +11,12 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
9
11
|
}[];
|
|
10
12
|
values: ValuesStyle | undefined;
|
|
11
13
|
/** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
12
|
-
onOverflow?: (overflow:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
15
|
+
/** Resolved legend/hover-isolate dimming style for a series, if any. */
|
|
16
|
+
disabledFor?: (name: string) => LegendDisabledStyle | undefined;
|
|
17
|
+
/** Fired when the pointer enters a label — only wired when hover-isolate is enabled. */
|
|
18
|
+
onHoverSeries?: (name: string) => void;
|
|
19
|
+
onHoverEnd?: () => void;
|
|
16
20
|
};
|
|
17
21
|
exports: {};
|
|
18
22
|
bindings: "";
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getLegendInteraction } from '../../layout/legend/interaction.svelte';
|
|
7
7
|
import { disabledFillOverride } from '../utils/legendDisabled';
|
|
8
8
|
import { buildPyramidBarMarkers } from './buildPyramidBarMarkers';
|
|
9
|
+
import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
|
|
9
10
|
import { buildGroupedSeries, validateSegments, buildScopedMarkerGroups } from '../utils/segments';
|
|
10
11
|
import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
|
|
11
12
|
import ValueLabels from './ValueLabels.svelte';
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
18
19
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
19
20
|
import type { AxisBasedMarkersConfig } from '../../types/markers/common';
|
|
21
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* A population-pyramid-style diverging bar chart: each series is mirrored
|
|
@@ -63,10 +65,27 @@
|
|
|
63
65
|
|
|
64
66
|
const cfg = $derived(getConfiguration());
|
|
65
67
|
const legendInteraction = $derived(getLegendInteraction());
|
|
68
|
+
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
69
|
+
|
|
70
|
+
// A value label hovered while `styles.values.hoverIsolate` is on dims
|
|
71
|
+
// every other series exactly like a legend toggle would — local to this
|
|
72
|
+
// one `<Plot>` instance (not the shared HoverStore) so the gesture can't
|
|
73
|
+
// leak into unrelated tooltip/crosshair behavior.
|
|
74
|
+
let hoverIsolatedSeries = $state<string | null>(null);
|
|
75
|
+
|
|
76
|
+
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
77
|
+
return (
|
|
78
|
+
legendInteraction?.disabledSeries.get(name) ??
|
|
79
|
+
(hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
|
|
80
|
+
? { opacity: cfg.legend.disabledOpacity }
|
|
81
|
+
: undefined)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
const leftColor = $derived(styles.colors?.left ?? paletteColor(0, cfg.palette));
|
|
67
86
|
const rightColor = $derived(styles.colors?.right ?? paletteColor(1, cfg.palette));
|
|
68
87
|
const showVals = $derived(styles.values?.show ?? false);
|
|
69
|
-
const
|
|
88
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
70
89
|
const valAnchor = $derived<ValueAnchor>(styles.values?.anchor ?? 'outside');
|
|
71
90
|
|
|
72
91
|
// Sort series names alphabetically; even index → left, odd → right.
|
|
@@ -117,7 +136,7 @@
|
|
|
117
136
|
__side: s.side,
|
|
118
137
|
__x: s.side === 'left' ? -v : v,
|
|
119
138
|
__y: getCategory(d) as AxisValue,
|
|
120
|
-
__label:
|
|
139
|
+
__label: formatValue(v, s.name),
|
|
121
140
|
__series: s.name,
|
|
122
141
|
} as TaggedRow;
|
|
123
142
|
});
|
|
@@ -169,7 +188,7 @@
|
|
|
169
188
|
...scales,
|
|
170
189
|
x: {
|
|
171
190
|
domain: [-maxMagnitude, maxMagnitude],
|
|
172
|
-
tickFormat: (v: unknown) =>
|
|
191
|
+
tickFormat: (v: unknown) => formatValue(Math.abs(Number(v)), ''),
|
|
173
192
|
...(scales.x ?? {}),
|
|
174
193
|
},
|
|
175
194
|
y: { type: 'band', padding: 0.2, domain: yDomain, ...(scales.y ?? {}) },
|
|
@@ -189,7 +208,7 @@
|
|
|
189
208
|
groupedSeries,
|
|
190
209
|
valueFor: signedValue,
|
|
191
210
|
colorFor: (s) => (s.side === 'left' ? leftColor : rightColor),
|
|
192
|
-
disabledFillFor: (name) => disabledFillOverride(
|
|
211
|
+
disabledFillFor: (name) => disabledFillOverride(effectiveDisabledFor(name)),
|
|
193
212
|
}),
|
|
194
213
|
);
|
|
195
214
|
|
|
@@ -201,6 +220,12 @@
|
|
|
201
220
|
segmentColorFor: (style, defaultColor) => style?.fill ?? defaultColor,
|
|
202
221
|
}),
|
|
203
222
|
);
|
|
223
|
+
|
|
224
|
+
// A pyramid bar's value label can grow past the plot's own content box —
|
|
225
|
+
// reserving real space there is what each label's own overflow
|
|
226
|
+
// measurement feeds into here, via a tracker local to this one `<Plot>`
|
|
227
|
+
// instance. See `labelOverflow.svelte.ts`.
|
|
228
|
+
const labelMargin = createLabelMarginTracker(() => margins);
|
|
204
229
|
</script>
|
|
205
230
|
|
|
206
231
|
<BasePlotLayout
|
|
@@ -212,7 +237,7 @@
|
|
|
212
237
|
{getY}
|
|
213
238
|
{hover}
|
|
214
239
|
scales={resolvedScales}
|
|
215
|
-
{
|
|
240
|
+
margins={labelMargin.resolvedMargins}
|
|
216
241
|
xTickRotate={resolvedScales?.x?.tickRotate}
|
|
217
242
|
markers={[...pyramidMarkers, ...markers]}
|
|
218
243
|
seriesData={resolvedSeries}
|
|
@@ -220,7 +245,17 @@
|
|
|
220
245
|
>
|
|
221
246
|
{#if showVals}
|
|
222
247
|
{#each groupedSeries as group (group.series.name)}
|
|
223
|
-
<ValueLabels
|
|
248
|
+
<ValueLabels
|
|
249
|
+
{group}
|
|
250
|
+
{valAnchor}
|
|
251
|
+
{formatValue}
|
|
252
|
+
values={styles.values}
|
|
253
|
+
axisColor={cfg.axis.color}
|
|
254
|
+
disabled={effectiveDisabledFor(group.series.name)}
|
|
255
|
+
onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
|
|
256
|
+
onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
|
|
257
|
+
onOverflow={labelMargin.report}
|
|
258
|
+
/>
|
|
224
259
|
{/each}
|
|
225
260
|
{/if}
|
|
226
261
|
</BasePlotLayout>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
-
import { Text } from 'svelteplot';
|
|
2
|
+
import { Text, usePlot } from 'svelteplot';
|
|
3
3
|
import { resolveAccessor } from '../utils/accessors';
|
|
4
|
+
import { disabledFillOverride } from '../utils/legendDisabled';
|
|
5
|
+
import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
|
|
4
6
|
import type { Series } from '../../types/plots/data/common';
|
|
5
7
|
import type { ValuesStyle } from '../../types/layout/styles';
|
|
6
8
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
7
9
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
8
10
|
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
11
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
12
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Renders one series' value label on a pyramid bar — anchor-driven default
|
|
@@ -16,17 +20,53 @@
|
|
|
16
20
|
let {
|
|
17
21
|
group,
|
|
18
22
|
valAnchor,
|
|
19
|
-
|
|
23
|
+
formatValue,
|
|
20
24
|
values,
|
|
21
25
|
axisColor,
|
|
26
|
+
disabled,
|
|
27
|
+
onHoverSeries,
|
|
28
|
+
onHoverEnd,
|
|
29
|
+
onOverflow,
|
|
22
30
|
}: {
|
|
23
31
|
group: VisualGroup<Series<TData> & { side: 'left' | 'right' }, BarSegmentStyle>;
|
|
24
32
|
valAnchor: ValueAnchor;
|
|
25
|
-
|
|
33
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
26
34
|
values: ValuesStyle | undefined;
|
|
27
35
|
axisColor: string;
|
|
36
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
37
|
+
disabled?: LegendDisabledStyle;
|
|
38
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
39
|
+
onHoverSeries?: () => void;
|
|
40
|
+
onHoverEnd?: () => void;
|
|
41
|
+
/** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
42
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
28
43
|
} = $props();
|
|
29
44
|
|
|
45
|
+
const fillOverride = $derived(disabledFillOverride(disabled));
|
|
46
|
+
const plot = usePlot();
|
|
47
|
+
let groupEl: SVGGElement | undefined = $state();
|
|
48
|
+
|
|
49
|
+
watchLabelOverflow({
|
|
50
|
+
el: () => groupEl,
|
|
51
|
+
bounds: () => {
|
|
52
|
+
// getBBox() inside isn't itself tracked — depend on whatever actually
|
|
53
|
+
// changes the rendered label layout so this re-measures.
|
|
54
|
+
void group;
|
|
55
|
+
void values;
|
|
56
|
+
void valAnchor;
|
|
57
|
+
const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
|
|
58
|
+
const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
|
|
59
|
+
if (!xRange || !yRange) return undefined;
|
|
60
|
+
return {
|
|
61
|
+
left: Math.min(xRange[0], xRange[1]),
|
|
62
|
+
right: Math.max(xRange[0], xRange[1]),
|
|
63
|
+
top: Math.min(yRange[0], yRange[1]),
|
|
64
|
+
bottom: Math.max(yRange[0], yRange[1]),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
onOverflow: (overflow) => onOverflow?.(overflow),
|
|
68
|
+
});
|
|
69
|
+
|
|
30
70
|
const isLeft = $derived(group.series.side === 'left');
|
|
31
71
|
const getCategory = $derived(resolveAccessor(group.series.x));
|
|
32
72
|
const getMagnitude = $derived(resolveAccessor(group.series.y));
|
|
@@ -57,24 +97,30 @@
|
|
|
57
97
|
const resolvedTextAnchor = $derived(font?.textAnchor === 'outside' ? undefined : font?.textAnchor);
|
|
58
98
|
</script>
|
|
59
99
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
<!-- svelteplot's Text mark never wires pointer handlers to its DOM node,
|
|
101
|
+
despite declaring them in its types — a wrapping <g> catches the
|
|
102
|
+
boundary crossing instead (pointerenter/leave fire on every ancestor). -->
|
|
103
|
+
<g bind:this={groupEl} role="presentation" onpointerenter={onHoverSeries} onpointerleave={onHoverEnd}>
|
|
104
|
+
<Text
|
|
105
|
+
data={group.series.data}
|
|
106
|
+
x={xFn}
|
|
107
|
+
y={getCategory}
|
|
108
|
+
text={(d: TData) => formatValue(Math.abs(Number(getMagnitude(d))), group.series.name)}
|
|
109
|
+
dx={font?.dx ?? defaultDx}
|
|
110
|
+
dy={font?.dy ?? 0}
|
|
111
|
+
textAnchor={resolvedTextAnchor ?? defaultTextAnchor}
|
|
112
|
+
lineAnchor={font?.lineAnchor ?? 'middle'}
|
|
113
|
+
lineHeight={font?.lineHeight}
|
|
114
|
+
rotate={font?.rotate}
|
|
115
|
+
class={font?.class}
|
|
116
|
+
textClass={font?.textClass}
|
|
117
|
+
fill={font?.fill ?? axisColor}
|
|
118
|
+
fillOpacity={fillOverride?.fillOpacity}
|
|
119
|
+
fontSize={font?.fontSize ?? 12}
|
|
120
|
+
fontWeight={font?.fontWeight}
|
|
121
|
+
fontStyle={font?.fontStyle}
|
|
122
|
+
stroke={font?.stroke}
|
|
123
|
+
strokeWidth={font?.strokeWidth}
|
|
124
|
+
paintOrder={font?.paintOrder}
|
|
125
|
+
/>
|
|
126
|
+
</g>
|
|
@@ -3,15 +3,24 @@ import type { ValuesStyle } from '../../types/layout/styles';
|
|
|
3
3
|
import type { ValueAnchor } from '../../types/plots/constants';
|
|
4
4
|
import type { BarSegmentStyle } from '../../types/plots/segments/common';
|
|
5
5
|
import type { VisualGroup } from '../../types/plots/segments/config';
|
|
6
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
7
|
+
import type { MarginOverflow } from '../utils/labelOverflow.svelte';
|
|
6
8
|
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
7
9
|
props: {
|
|
8
10
|
group: VisualGroup<Series<TData> & {
|
|
9
11
|
side: "left" | "right";
|
|
10
12
|
}, BarSegmentStyle>;
|
|
11
13
|
valAnchor: ValueAnchor;
|
|
12
|
-
|
|
14
|
+
formatValue: (value: number, seriesName: string) => string;
|
|
13
15
|
values: ValuesStyle | undefined;
|
|
14
16
|
axisColor: string;
|
|
17
|
+
/** Resolved legend/hover-isolate dimming style for this group's series, if any. */
|
|
18
|
+
disabled?: LegendDisabledStyle;
|
|
19
|
+
/** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
|
|
20
|
+
onHoverSeries?: () => void;
|
|
21
|
+
onHoverEnd?: () => void;
|
|
22
|
+
/** Reports how far this label overflows the plot's own content box, in pixels, on each side (0 = no overflow). */
|
|
23
|
+
onOverflow?: (overflow: MarginOverflow) => void;
|
|
15
24
|
};
|
|
16
25
|
exports: {};
|
|
17
26
|
bindings: "";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { disabledDotsOverride } from '../utils/legendDisabled';
|
|
11
11
|
import { buildScatterMarkers } from './buildScatterMarkers';
|
|
12
12
|
import { resolveRadiusPaddedDomain } from './resolveRadiusPaddedDomain';
|
|
13
|
+
import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
|
|
13
14
|
import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
|
|
14
15
|
import ValueLabels from './ValueLabels.svelte';
|
|
15
16
|
import { hasHoverMarker, resolveHoverStrategy, resolveHoverSync } from '../../layout/tooltip/utils';
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
import type { BasePlotStylesConfig } from '../../types/layout/styles';
|
|
23
24
|
import type { ScatterSegmentStyle } from '../../types/plots/segments/common';
|
|
24
25
|
import type { AxisBasedMarkersConfig } from '../../types/markers/common';
|
|
26
|
+
import type { LegendDisabledStyle } from '../../types/layout/legend';
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* A point cloud on continuous x/y axes. Accepts either a pre-built `series`
|
|
@@ -71,7 +73,23 @@
|
|
|
71
73
|
|
|
72
74
|
const cfg = $derived(getConfiguration());
|
|
73
75
|
const showVals = $derived(styles.values?.show ?? false);
|
|
74
|
-
const
|
|
76
|
+
const formatValue = $derived(styles.values?.format ?? ((v: number) => `${v}`));
|
|
77
|
+
const hoverIsolate = $derived(styles.values?.hoverIsolate ?? false);
|
|
78
|
+
|
|
79
|
+
// A value label hovered while `styles.values.hoverIsolate` is on dims
|
|
80
|
+
// every other series exactly like a legend toggle would — local to this
|
|
81
|
+
// one `<Plot>` instance (not the shared HoverStore) so the gesture can't
|
|
82
|
+
// leak into unrelated tooltip/crosshair behavior.
|
|
83
|
+
let hoverIsolatedSeries = $state<string | null>(null);
|
|
84
|
+
|
|
85
|
+
function effectiveDisabledFor(name: string): LegendDisabledStyle | undefined {
|
|
86
|
+
return (
|
|
87
|
+
legendInteraction?.disabledSeries.get(name) ??
|
|
88
|
+
(hoverIsolate && hoverIsolatedSeries && hoverIsolatedSeries !== name
|
|
89
|
+
? { opacity: cfg.legend.disabledOpacity }
|
|
90
|
+
: undefined)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
75
93
|
|
|
76
94
|
const xAcc = $derived(resolveAccessor(resolvedSeries[0]?.x ?? ((d: TData) => d)));
|
|
77
95
|
const yAcc = $derived(resolveAccessor(resolvedSeries[0]?.y ?? ((d: TData) => d)));
|
|
@@ -110,7 +128,7 @@
|
|
|
110
128
|
buildScatterMarkers({
|
|
111
129
|
groupedSeries,
|
|
112
130
|
r,
|
|
113
|
-
disabledDotsFor: (name) => disabledDotsOverride(
|
|
131
|
+
disabledDotsFor: (name) => disabledDotsOverride(effectiveDisabledFor(name)),
|
|
114
132
|
}),
|
|
115
133
|
);
|
|
116
134
|
|
|
@@ -184,6 +202,13 @@
|
|
|
184
202
|
y: paddedY ? { ...scales.y, domain: paddedY } : scales.y,
|
|
185
203
|
};
|
|
186
204
|
});
|
|
205
|
+
|
|
206
|
+
// A scatter point's value label can grow past the plot's own content box
|
|
207
|
+
// (e.g. a point volcado toward a corner, past what its own edge-aware flip
|
|
208
|
+
// can fit) — reserving real space there is what each label's own overflow
|
|
209
|
+
// measurement feeds into here, via a tracker local to this one `<Plot>`
|
|
210
|
+
// instance. See `labelOverflow.svelte.ts`.
|
|
211
|
+
const labelMargin = createLabelMarginTracker(() => margins);
|
|
187
212
|
</script>
|
|
188
213
|
|
|
189
214
|
<BasePlotLayout
|
|
@@ -195,12 +220,23 @@
|
|
|
195
220
|
getY={(d) => Number(yAcc(d))}
|
|
196
221
|
{hover}
|
|
197
222
|
scales={resolvedScales}
|
|
198
|
-
{
|
|
223
|
+
margins={labelMargin.resolvedMargins}
|
|
199
224
|
markers={[...scatterMarkers, ...markers]}
|
|
200
225
|
seriesData={resolvedSeries}
|
|
201
226
|
{scopedGroups}
|
|
202
227
|
>
|
|
203
228
|
{#if showVals}
|
|
204
|
-
|
|
229
|
+
{#each groupedSeries as group (group.series.name)}
|
|
230
|
+
<ValueLabels
|
|
231
|
+
{group}
|
|
232
|
+
{formatValue}
|
|
233
|
+
values={styles.values}
|
|
234
|
+
defaultColor={cfg.axis.color}
|
|
235
|
+
disabled={effectiveDisabledFor(group.series.name)}
|
|
236
|
+
onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
|
|
237
|
+
onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
|
|
238
|
+
onOverflow={labelMargin.report}
|
|
239
|
+
/>
|
|
240
|
+
{/each}
|
|
205
241
|
{/if}
|
|
206
242
|
</BasePlotLayout>
|