@fundar/data-chart-telling 0.0.31 → 0.0.33

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.
@@ -225,7 +225,7 @@
225
225
  getY={plotGetY}
226
226
  {hover}
227
227
  scales={resolvedScales}
228
- margins={labelMargin.resolvedMargins}
228
+ {...labelMargin.plotProps}
229
229
  xTickRotate={resolvedScales?.x?.tickRotate}
230
230
  markers={({ setHoverMatch, clearHoverMatch }) => [
231
231
  ...buildBarMarkers({
@@ -245,22 +245,27 @@
245
245
  seriesData={resolvedSeries}
246
246
  {scopedGroups}
247
247
  >
248
- {#if showVals}
249
- {#each groupedSeries as group, seriesIndex (group.series.name)}
250
- <ValueLabels
251
- {group}
252
- {seriesIndex}
253
- {barLayout}
254
- {isHorizontal}
255
- {valAnchor}
256
- {formatValue}
257
- values={styles.values}
258
- axisColor={cfg.axis.color}
259
- disabled={effectiveDisabledFor(group.series.name)}
260
- onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
261
- onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
262
- onOverflow={labelMargin.report}
263
- />
264
- {/each}
265
- {/if}
248
+ {#snippet children({ numericMargins })}
249
+ {#if showVals}
250
+ {#each groupedSeries as group, seriesIndex (group.series.name)}
251
+ <ValueLabels
252
+ {group}
253
+ {seriesIndex}
254
+ {barLayout}
255
+ {isHorizontal}
256
+ {valAnchor}
257
+ {formatValue}
258
+ values={styles.values}
259
+ axisColor={cfg.axis.color}
260
+ disabled={effectiveDisabledFor(group.series.name)}
261
+ {width}
262
+ {height}
263
+ {numericMargins}
264
+ onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
265
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
266
+ onOverflow={labelMargin.report}
267
+ />
268
+ {/each}
269
+ {/if}
270
+ {/snippet}
266
271
  </BasePlotLayout>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { Text, usePlot } from 'svelteplot';
2
+ import { Text } from 'svelteplot';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
4
  import { disabledFillOverride } from '../utils/legendDisabled';
5
5
  import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
@@ -30,6 +30,9 @@
30
30
  values,
31
31
  axisColor,
32
32
  disabled,
33
+ width,
34
+ height,
35
+ numericMargins,
33
36
  onHoverSeries,
34
37
  onHoverEnd,
35
38
  onOverflow,
@@ -44,6 +47,11 @@
44
47
  axisColor: string;
45
48
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
46
49
  disabled?: LegendDisabledStyle;
50
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
51
+ width: number;
52
+ height: number;
53
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
54
+ numericMargins: { top: number; right: number; bottom: number; left: number };
47
55
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
48
56
  onHoverSeries?: () => void;
49
57
  onHoverEnd?: () => void;
@@ -52,25 +60,26 @@
52
60
  } = $props();
53
61
 
54
62
  const fillOverride = $derived(disabledFillOverride(disabled));
55
- const plot = usePlot();
56
63
  let groupEl: SVGGElement | undefined = $state();
57
64
 
58
65
  watchLabelOverflow({
59
66
  el: () => groupEl,
67
+ // Built from our own `width`/`height`/`numericMargins` rather than
68
+ // `usePlot().scales.x/y.fn.range()` — see line/ValueLabels.svelte's
69
+ // matching comment for why that scale's exposed range can't be trusted
70
+ // once a caller fixes `width`/`height` explicitly (every plot kind here
71
+ // does).
60
72
  bounds: () => {
61
73
  // getBBox() inside isn't itself tracked — depend on whatever actually
62
74
  // changes the rendered label layout so this re-measures.
63
75
  void group;
64
76
  void values;
65
77
  void valAnchor;
66
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
67
- const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
68
- if (!xRange || !yRange) return undefined;
69
78
  return {
70
- left: Math.min(xRange[0], xRange[1]),
71
- right: Math.max(xRange[0], xRange[1]),
72
- top: Math.min(yRange[0], yRange[1]),
73
- bottom: Math.max(yRange[0], yRange[1]),
79
+ left: numericMargins.left,
80
+ right: width - numericMargins.right,
81
+ top: numericMargins.top,
82
+ bottom: height - numericMargins.bottom,
74
83
  };
75
84
  },
76
85
  onOverflow: (overflow) => onOverflow?.(overflow),
@@ -18,6 +18,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
18
18
  axisColor: string;
19
19
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
20
20
  disabled?: LegendDisabledStyle;
21
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
22
+ width: number;
23
+ height: number;
24
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
25
+ numericMargins: {
26
+ top: number;
27
+ right: number;
28
+ bottom: number;
29
+ left: number;
30
+ };
21
31
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
22
32
  onHoverSeries?: () => void;
23
33
  onHoverEnd?: () => void;
@@ -107,18 +107,6 @@
107
107
  }),
108
108
  );
109
109
 
110
- // Last *real* data point per series (skipping any trailing gap row), with
111
- // its palette index for colour.
112
- const lastPoints = $derived(
113
- resolvedSeries
114
- .map((s, idx) => {
115
- const sy = resolveAccessor(s.y);
116
- const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
117
- return { series: s, lastRow, seriesIndex: idx };
118
- })
119
- .filter((p): p is { series: Series<TData>; lastRow: TData; seriesIndex: number } => p.lastRow !== undefined),
120
- );
121
-
122
110
  const hover = $derived({
123
111
  active: tooltip != null || hasHoverMarker(markers),
124
112
  points: hoverPoints,
@@ -153,6 +141,21 @@
153
141
  }),
154
142
  );
155
143
 
144
+ const lastPoints = $derived(
145
+ resolvedSeries
146
+ .map((s, idx) => {
147
+ const sy = resolveAccessor(s.y);
148
+ const lastRow = [...s.data].reverse().find((d) => !isGapValue(sy(d)));
149
+ if (lastRow === undefined) return undefined;
150
+ const group = scopedGroups.find((g) => g.name === s.name);
151
+ const matchedSeg = group?.segments.find((seg) => seg.match(lastRow));
152
+ const color =
153
+ matchedSeg?.color ?? group?.segments[0]?.color ?? group?.color ?? paletteColor(idx, cfg.palette);
154
+ return { series: s, lastRow, color };
155
+ })
156
+ .filter((p): p is { series: Series<TData>; lastRow: TData; color: string } => p !== undefined),
157
+ );
158
+
156
159
  // End-of-line labels can grow past the plot's own content box, into its
157
160
  // margin — reserving real space there (rather than padding the domain,
158
161
  // which would falsely imply data exists past the last real point) is
@@ -178,14 +181,19 @@
178
181
  seriesData={resolvedSeries}
179
182
  {scopedGroups}
180
183
  >
181
- {#if showVals}
182
- <ValueLabels
183
- {lastPoints}
184
- values={styles.values}
185
- onOverflow={labelMargin.report}
186
- disabledFor={effectiveDisabledFor}
187
- onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
188
- onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
189
- />
190
- {/if}
184
+ {#snippet children({ numericMargins })}
185
+ {#if showVals}
186
+ <ValueLabels
187
+ {lastPoints}
188
+ values={styles.values}
189
+ {width}
190
+ {height}
191
+ {numericMargins}
192
+ onOverflow={labelMargin.report}
193
+ disabledFor={effectiveDisabledFor}
194
+ onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
195
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
196
+ />
197
+ {/if}
198
+ {/snippet}
191
199
  </BasePlotLayout>
@@ -1,10 +1,8 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { untrack } from 'svelte';
3
2
  import { SvelteMap } from 'svelte/reactivity';
4
3
  import { Text, usePlot } from 'svelteplot';
5
4
  import { resolveAccessor } from '../utils/accessors';
6
5
  import { getConfiguration } from '../../configuration/config.svelte';
7
- import { paletteColor } from '../../utils/color';
8
6
  import { declutter1D, laneAssignment } from '../utils/declutter';
9
7
  import { disabledFillOverride, disabledStrokeOverride } from '../utils/legendDisabled';
10
8
  import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
@@ -25,13 +23,21 @@
25
23
  let {
26
24
  lastPoints,
27
25
  values,
26
+ width,
27
+ height,
28
+ numericMargins,
28
29
  onOverflow,
29
30
  disabledFor,
30
31
  onHoverSeries,
31
32
  onHoverEnd,
32
33
  }: {
33
- lastPoints: { series: Series<TData>; lastRow: TData; seriesIndex: number }[];
34
+ lastPoints: { series: Series<TData>; lastRow: TData; color: string }[];
34
35
  values: ValuesStyle | undefined;
36
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below for why `usePlot()`'s own scale range can't be trusted for this). */
37
+ width: number;
38
+ height: number;
39
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
40
+ numericMargins: { top: number; right: number; bottom: number; left: number };
35
41
  /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
36
42
  onOverflow?: (overflow: MarginOverflow) => void;
37
43
  /** Resolved legend/hover-isolate dimming style for a series, if any. */
@@ -93,33 +99,31 @@
93
99
 
94
100
  watchLabelOverflow({
95
101
  el: () => groupEl,
102
+ // Deliberately built from our own `width`/`height`/`numericMargins`
103
+ // (all already correctly sized off the `width`/`height` this `<Plot>`
104
+ // was actually mounted with — see `BasePlotLayout`'s own `numericMargins`
105
+ // doc comment), not from `usePlot().scales.x/y.fn.range()`: svelteplot's
106
+ // `<Plot>` accepts an explicit `width`/`height` (our `fixedWidth`) for
107
+ // its own real layout, but the scale object it exposes through context
108
+ // is computed off its *internal* auto-detected size instead — normally
109
+ // the same number, but never reconciled with `fixedWidth` when one is
110
+ // given. That left `.range()` reporting a stale/placeholder boundary
111
+ // (svelteplot's own internal default, unrelated to this plot's actual
112
+ // pixel size) wherever a caller fixes `width`/`height` explicitly — which
113
+ // every plot kind here does — so this watcher never saw genuine
114
+ // right/bottom overflow past the *real* edge and never grew the margin
115
+ // to fit it, leaving end-point labels to render past the plot's true
116
+ // right edge with no space reserved for them.
96
117
  bounds: () => {
97
118
  // getBBox() inside isn't itself tracked — depend on whatever actually
98
119
  // changes the rendered label layout so this re-measures.
99
120
  void lastPoints;
100
121
  void values;
101
- // The scale range itself is read `untrack`ed on purpose: this margin
102
- // watcher's own report (see labelOverflow.svelte.ts) grows the plot's
103
- // margin to fit the labels, which feeds back into svelteplot's own
104
- // auto-margin sizing and nudges this same range — occasionally by a
105
- // permanent, never-settling ±1px each render. Treating the range as a
106
- // tracked dependency here reran the whole watcher every single time it
107
- // twitched, which (when it never settles) never let the watcher's own
108
- // "poll until stable" loop finish — an infinite effect-restart loop
109
- // that tripped Svelte's effect_update_depth_exceeded guard. Reading it
110
- // untracked still measures against the *current* range on every
111
- // re-run triggered by `lastPoints`/`values` above; it just stops the
112
- // range's own fluctuations from being what triggers those re-runs.
113
- const { xRange, yRange } = untrack(() => ({
114
- xRange: plot.scales.x?.fn?.range?.()?.map(Number),
115
- yRange: plot.scales.y?.fn?.range?.()?.map(Number),
116
- }));
117
- if (!xRange || !yRange) return undefined;
118
122
  return {
119
- left: Math.min(xRange[0], xRange[1]),
120
- right: Math.max(xRange[0], xRange[1]),
121
- top: Math.min(yRange[0], yRange[1]),
122
- bottom: Math.max(yRange[0], yRange[1]),
123
+ left: numericMargins.left,
124
+ right: width - numericMargins.right,
125
+ top: numericMargins.top,
126
+ bottom: height - numericMargins.bottom,
123
127
  };
124
128
  },
125
129
  onOverflow: (overflow) => onOverflow?.(overflow),
@@ -178,10 +182,9 @@
178
182
  </script>
179
183
 
180
184
  <g bind:this={groupEl}>
181
- {#each lastPoints as { series: s, lastRow, seriesIndex } (s.name)}
185
+ {#each lastPoints as { series: s, lastRow, color: seriesColor } (s.name)}
182
186
  {@const xFn = resolveAccessor(s.x)}
183
187
  {@const yFn = resolveAccessor(s.y)}
184
- {@const seriesColor = paletteColor(seriesIndex, cfg.palette)}
185
188
  {@const offset = declutteredOffsets.get(s.name)}
186
189
  {@const extraDy = offset?.inGroup ? offset.extraDy : 0}
187
190
  {@const baseDx = font?.dx ?? defaultDx}
@@ -7,9 +7,19 @@ declare function $$render<TData extends Record<string, unknown>>(): {
7
7
  lastPoints: {
8
8
  series: Series<TData>;
9
9
  lastRow: TData;
10
- seriesIndex: number;
10
+ color: string;
11
11
  }[];
12
12
  values: ValuesStyle | undefined;
13
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below for why `usePlot()`'s own scale range can't be trusted for this). */
14
+ width: number;
15
+ height: number;
16
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
17
+ numericMargins: {
18
+ top: number;
19
+ right: number;
20
+ bottom: number;
21
+ left: number;
22
+ };
13
23
  /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
14
24
  onOverflow?: (overflow: MarginOverflow) => void;
15
25
  /** Resolved legend/hover-isolate dimming style for a series, if any. */
@@ -262,25 +262,30 @@
262
262
  {getY}
263
263
  {hover}
264
264
  scales={resolvedScales}
265
- margins={labelMargin.resolvedMargins}
265
+ {...labelMargin.plotProps}
266
266
  xTickRotate={resolvedScales?.x?.tickRotate}
267
267
  markers={[...pyramidMarkers, ...markers]}
268
268
  seriesData={resolvedSeries}
269
269
  {scopedGroups}
270
270
  >
271
- {#if showVals}
272
- {#each groupedSeries as group (group.series.name)}
273
- <ValueLabels
274
- {group}
275
- {valAnchor}
276
- {formatValue}
277
- values={styles.values}
278
- axisColor={cfg.axis.color}
279
- disabled={effectiveDisabledFor(group.series.name)}
280
- onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
281
- onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
282
- onOverflow={labelMargin.report}
283
- />
284
- {/each}
285
- {/if}
271
+ {#snippet children({ numericMargins })}
272
+ {#if showVals}
273
+ {#each groupedSeries as group (group.series.name)}
274
+ <ValueLabels
275
+ {group}
276
+ {valAnchor}
277
+ {formatValue}
278
+ values={styles.values}
279
+ axisColor={cfg.axis.color}
280
+ disabled={effectiveDisabledFor(group.series.name)}
281
+ {width}
282
+ {height}
283
+ {numericMargins}
284
+ onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
285
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
286
+ onOverflow={labelMargin.report}
287
+ />
288
+ {/each}
289
+ {/if}
290
+ {/snippet}
286
291
  </BasePlotLayout>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" generics="TData extends Record<string, unknown>">
2
- import { Text, usePlot } from 'svelteplot';
2
+ import { Text } from 'svelteplot';
3
3
  import { resolveAccessor } from '../utils/accessors';
4
4
  import { disabledFillOverride } from '../utils/legendDisabled';
5
5
  import { watchLabelOverflow } from '../utils/labelOverflow.svelte';
@@ -25,6 +25,9 @@
25
25
  values,
26
26
  axisColor,
27
27
  disabled,
28
+ width,
29
+ height,
30
+ numericMargins,
28
31
  onHoverSeries,
29
32
  onHoverEnd,
30
33
  onOverflow,
@@ -36,6 +39,11 @@
36
39
  axisColor: string;
37
40
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
38
41
  disabled?: LegendDisabledStyle;
42
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
43
+ width: number;
44
+ height: number;
45
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
46
+ numericMargins: { top: number; right: number; bottom: number; left: number };
39
47
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
40
48
  onHoverSeries?: () => void;
41
49
  onHoverEnd?: () => void;
@@ -44,25 +52,26 @@
44
52
  } = $props();
45
53
 
46
54
  const fillOverride = $derived(disabledFillOverride(disabled));
47
- const plot = usePlot();
48
55
  let groupEl: SVGGElement | undefined = $state();
49
56
 
50
57
  watchLabelOverflow({
51
58
  el: () => groupEl,
59
+ // Built from our own `width`/`height`/`numericMargins` rather than
60
+ // `usePlot().scales.x/y.fn.range()` — see line/ValueLabels.svelte's
61
+ // matching comment for why that scale's exposed range can't be trusted
62
+ // once a caller fixes `width`/`height` explicitly (every plot kind here
63
+ // does).
52
64
  bounds: () => {
53
65
  // getBBox() inside isn't itself tracked — depend on whatever actually
54
66
  // changes the rendered label layout so this re-measures.
55
67
  void group;
56
68
  void values;
57
69
  void valAnchor;
58
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
59
- const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
60
- if (!xRange || !yRange) return undefined;
61
70
  return {
62
- left: Math.min(xRange[0], xRange[1]),
63
- right: Math.max(xRange[0], xRange[1]),
64
- top: Math.min(yRange[0], yRange[1]),
65
- bottom: Math.max(yRange[0], yRange[1]),
71
+ left: numericMargins.left,
72
+ right: width - numericMargins.right,
73
+ top: numericMargins.top,
74
+ bottom: height - numericMargins.bottom,
66
75
  };
67
76
  },
68
77
  onOverflow: (overflow) => onOverflow?.(overflow),
@@ -16,6 +16,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
16
16
  axisColor: string;
17
17
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
18
18
  disabled?: LegendDisabledStyle;
19
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
20
+ width: number;
21
+ height: number;
22
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
23
+ numericMargins: {
24
+ top: number;
25
+ right: number;
26
+ bottom: number;
27
+ left: number;
28
+ };
19
29
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
20
30
  onHoverSeries?: () => void;
21
31
  onHoverEnd?: () => void;
@@ -220,23 +220,28 @@
220
220
  getY={(d) => Number(yAcc(d))}
221
221
  {hover}
222
222
  scales={resolvedScales}
223
- margins={labelMargin.resolvedMargins}
223
+ {...labelMargin.plotProps}
224
224
  markers={[...scatterMarkers, ...markers]}
225
225
  seriesData={resolvedSeries}
226
226
  {scopedGroups}
227
227
  >
228
- {#if showVals}
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}
241
- {/if}
228
+ {#snippet children({ numericMargins })}
229
+ {#if showVals}
230
+ {#each groupedSeries as group (group.series.name)}
231
+ <ValueLabels
232
+ {group}
233
+ {formatValue}
234
+ values={styles.values}
235
+ defaultColor={cfg.axis.color}
236
+ disabled={effectiveDisabledFor(group.series.name)}
237
+ {width}
238
+ {height}
239
+ {numericMargins}
240
+ onHoverSeries={hoverIsolate ? () => { hoverIsolatedSeries = group.series.name; } : undefined}
241
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
242
+ onOverflow={labelMargin.report}
243
+ />
244
+ {/each}
245
+ {/if}
246
+ {/snippet}
242
247
  </BasePlotLayout>
@@ -29,6 +29,9 @@
29
29
  values,
30
30
  defaultColor,
31
31
  disabled,
32
+ width,
33
+ height,
34
+ numericMargins,
32
35
  onHoverSeries,
33
36
  onHoverEnd,
34
37
  onOverflow,
@@ -39,6 +42,11 @@
39
42
  defaultColor: string;
40
43
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
41
44
  disabled?: LegendDisabledStyle;
45
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
46
+ width: number;
47
+ height: number;
48
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
49
+ numericMargins: { top: number; right: number; bottom: number; left: number };
42
50
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
43
51
  onHoverSeries?: () => void;
44
52
  onHoverEnd?: () => void;
@@ -54,19 +62,22 @@
54
62
 
55
63
  watchLabelOverflow({
56
64
  el: () => groupEl,
65
+ // Built from our own `width`/`height`/`numericMargins` rather than
66
+ // `usePlot().scales.x/y.fn.range()` — see line/ValueLabels.svelte's
67
+ // matching comment for why that scale's exposed range can't be trusted
68
+ // once a caller fixes `width`/`height` explicitly (every plot kind here
69
+ // does). `plot.plotWidth`/`plotHeight` (used below for edge-aware
70
+ // placement) don't have this problem — only the scale's own `.range()`.
57
71
  bounds: () => {
58
72
  // getBBox() inside isn't itself tracked — depend on whatever actually
59
73
  // changes the rendered label layout so this re-measures.
60
74
  void data;
61
75
  void values;
62
- const xRange = plot.scales.x?.fn?.range?.()?.map(Number);
63
- const yRange = plot.scales.y?.fn?.range?.()?.map(Number);
64
- if (!xRange || !yRange) return undefined;
65
76
  return {
66
- left: Math.min(xRange[0], xRange[1]),
67
- right: Math.max(xRange[0], xRange[1]),
68
- top: Math.min(yRange[0], yRange[1]),
69
- bottom: Math.max(yRange[0], yRange[1]),
77
+ left: numericMargins.left,
78
+ right: width - numericMargins.right,
79
+ top: numericMargins.top,
80
+ bottom: height - numericMargins.bottom,
70
81
  };
71
82
  },
72
83
  onOverflow: (overflow) => onOverflow?.(overflow),
@@ -12,6 +12,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
12
12
  defaultColor: string;
13
13
  /** Resolved legend/hover-isolate dimming style for this group's series, if any. */
14
14
  disabled?: LegendDisabledStyle;
15
+ /** The plot's own pixel size — together with `numericMargins`, the ground truth for its content box (see `bounds()` below). */
16
+ width: number;
17
+ height: number;
18
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
19
+ numericMargins: {
20
+ top: number;
21
+ right: number;
22
+ bottom: number;
23
+ left: number;
24
+ };
15
25
  /** Fired when the pointer enters this series' label — only wired when hover-isolate is enabled. */
16
26
  onHoverSeries?: () => void;
17
27
  onHoverEnd?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"