@fundar/data-chart-telling 0.0.26 → 0.0.27

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.
Files changed (58) hide show
  1. package/dist/charts/BaseChart.svelte +18 -10
  2. package/dist/charts/BaseChart.svelte.d.ts +4 -2
  3. package/dist/charts/heatmap/Chart.svelte +2 -2
  4. package/dist/charts/pyramid/Chart.svelte +2 -2
  5. package/dist/configuration/config.svelte.js +26 -0
  6. package/dist/configuration/themes/dark.js +4 -0
  7. package/dist/configuration/themes/index.d.ts +64 -0
  8. package/dist/index.d.ts +6 -2
  9. package/dist/index.js +2 -0
  10. package/dist/layout/coordinates/CoordinatesLayout.svelte +1 -3
  11. package/dist/layout/facet/FacetIndicator.svelte +31 -0
  12. package/dist/layout/facet/FacetIndicator.svelte.d.ts +8 -0
  13. package/dist/layout/facet/FacetLayout.svelte +249 -24
  14. package/dist/layout/facet/FacetLayout.svelte.d.ts +7 -1
  15. package/dist/layout/legend/ContinuousSection.svelte +1 -1
  16. package/dist/layout/legend/ContinuousSection.svelte.d.ts +1 -1
  17. package/dist/layout/legend/DiscreteSection.svelte +1 -1
  18. package/dist/layout/legend/DiscreteSection.svelte.d.ts +1 -1
  19. package/dist/layout/legend/LegendLayout.svelte +1 -1
  20. package/dist/layout/legend/LegendLayout.svelte.d.ts +1 -1
  21. package/dist/layout/legend/interaction.svelte.d.ts +9 -1
  22. package/dist/layout/legend/interaction.svelte.js +9 -1
  23. package/dist/layout/timeline/Timeline.svelte +1 -1
  24. package/dist/layout/timeline/Timeline.svelte.d.ts +1 -1
  25. package/dist/layout/timeline/TimelineLayout.svelte +1 -1
  26. package/dist/layout/timeline/TimelineLayout.svelte.d.ts +1 -1
  27. package/dist/layout/tooltip/controller.svelte.js +16 -2
  28. package/dist/markers/HoverMarker.svelte +2 -2
  29. package/dist/plots/bar/Plot.svelte +43 -4
  30. package/dist/plots/bar/ValueLabels.svelte +69 -23
  31. package/dist/plots/bar/ValueLabels.svelte.d.ts +10 -1
  32. package/dist/plots/heatmap/Plot.svelte +4 -4
  33. package/dist/plots/heatmap/ValueLabels.svelte +3 -3
  34. package/dist/plots/heatmap/ValueLabels.svelte.d.ts +1 -1
  35. package/dist/plots/line/Plot.svelte +34 -29
  36. package/dist/plots/line/ValueLabels.svelte +73 -51
  37. package/dist/plots/line/ValueLabels.svelte.d.ts +8 -4
  38. package/dist/plots/pyramid/Plot.svelte +41 -6
  39. package/dist/plots/pyramid/ValueLabels.svelte +70 -24
  40. package/dist/plots/pyramid/ValueLabels.svelte.d.ts +10 -1
  41. package/dist/plots/scatter/Plot.svelte +40 -4
  42. package/dist/plots/scatter/ValueLabels.svelte +90 -47
  43. package/dist/plots/scatter/ValueLabels.svelte.d.ts +14 -5
  44. package/dist/plots/utils/labelOverflow.svelte.d.ts +51 -0
  45. package/dist/plots/utils/labelOverflow.svelte.js +116 -0
  46. package/dist/plots/utils/legendDisabled.d.ts +1 -1
  47. package/dist/types/charts/common.d.ts +3 -50
  48. package/dist/types/charts/props.d.ts +3 -2
  49. package/dist/types/configuration/styling.d.ts +19 -0
  50. package/dist/types/layout/facet.d.ts +41 -0
  51. package/dist/types/layout/legend.d.ts +65 -1
  52. package/dist/types/layout/styles.d.ts +1 -12
  53. package/dist/types/layout/timeline.d.ts +32 -0
  54. package/dist/types/layout/timeline.js +1 -0
  55. package/dist/utils/interpolate.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/dist/types/charts/legend.d.ts +0 -60
  58. /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: { left: number; right: number }) => void;
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 fmtVal = $derived(values?.format ?? ((v: number, name: string) => `${name} - ${v}`));
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 — real `getBBox()` measurement, same as the
73
- // approach this used to register directly into svelteplot's *shared*
74
- // margin-auto-sizing context. Reporting through a plain callback instead
75
- // keeps this entirely local to one `<Plot>` instance: nothing here is
76
- // shared across instances, which is what made the old approach spike
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
- $effect(() => {
82
- // getBBox() below is a plain DOM read, not itself tracked — depend on
83
- // whatever actually changes the rendered label layout so this re-runs.
84
- void lastPoints;
85
- void values;
86
- const el = groupEl;
87
- if (!el || !onOverflow) return;
88
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
89
- if (!xRange) return;
90
- const plotLeft = Math.min(xRange[0], xRange[1]);
91
- const plotRight = Math.max(xRange[0], xRange[1]);
92
- let box: { x: number; width: number };
93
- try {
94
- box = el.getBBox();
95
- } catch {
96
- return;
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 = fmtVal(Number(yFn(lastRow)), s.name)}
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
- <Text
189
- data={[lastRow]}
190
- x={xFn}
191
- y={yFn}
192
- text={() => text}
193
- dx={labelDx}
194
- dy={(font?.dy ?? defaultDy) + extraDy}
195
- textAnchor={preferredTA}
196
- lineAnchor={preferredLA}
197
- lineHeight={font?.lineHeight}
198
- rotate={font?.rotate}
199
- class={font?.class}
200
- textClass={font?.textClass}
201
- fill={font?.fill ?? seriesColor}
202
- {fontSize}
203
- fontWeight={font?.fontWeight}
204
- fontStyle={font?.fontStyle}
205
- stroke={font?.stroke}
206
- strokeWidth={font?.strokeWidth}
207
- paintOrder={font?.paintOrder}
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
- left: number;
14
- right: number;
15
- }) => void;
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 fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
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: fmtVal(v, s.name),
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) => fmtVal(Math.abs(Number(v)), ''),
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(legendInteraction?.disabledSeries.get(name)),
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
- {margins}
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 {group} {valAnchor} {fmtVal} values={styles.values} axisColor={cfg.axis.color} />
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
- fmtVal,
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
- fmtVal: (value: number, seriesName: string) => string;
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
- <Text
61
- data={group.series.data}
62
- x={xFn}
63
- y={getCategory}
64
- text={(d: TData) => fmtVal(Math.abs(Number(getMagnitude(d))), group.series.name)}
65
- dx={font?.dx ?? defaultDx}
66
- dy={font?.dy ?? 0}
67
- textAnchor={resolvedTextAnchor ?? defaultTextAnchor}
68
- lineAnchor={font?.lineAnchor ?? 'middle'}
69
- lineHeight={font?.lineHeight}
70
- rotate={font?.rotate}
71
- class={font?.class}
72
- textClass={font?.textClass}
73
- fill={font?.fill ?? axisColor}
74
- fontSize={font?.fontSize ?? 12}
75
- fontWeight={font?.fontWeight}
76
- fontStyle={font?.fontStyle}
77
- stroke={font?.stroke}
78
- strokeWidth={font?.strokeWidth}
79
- paintOrder={font?.paintOrder}
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
- fmtVal: (value: number, seriesName: string) => string;
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 fmtVal = $derived(styles.values?.format ?? ((v: number) => `${v}`));
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(legendInteraction?.disabledSeries.get(name)),
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
- {margins}
223
+ margins={labelMargin.resolvedMargins}
199
224
  markers={[...scatterMarkers, ...markers]}
200
225
  seriesData={resolvedSeries}
201
226
  {scopedGroups}
202
227
  >
203
228
  {#if showVals}
204
- <ValueLabels data={flat} getX={xAcc} getY={(d) => Number(yAcc(d))} {fmtVal} values={styles.values} defaultColor={cfg.axis.color} />
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>