@fundar/data-chart-telling 0.0.31 → 0.0.32

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;
@@ -178,14 +178,19 @@
178
178
  seriesData={resolvedSeries}
179
179
  {scopedGroups}
180
180
  >
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}
181
+ {#snippet children({ numericMargins })}
182
+ {#if showVals}
183
+ <ValueLabels
184
+ {lastPoints}
185
+ values={styles.values}
186
+ {width}
187
+ {height}
188
+ {numericMargins}
189
+ onOverflow={labelMargin.report}
190
+ disabledFor={effectiveDisabledFor}
191
+ onHoverSeries={hoverIsolate ? (name) => { hoverIsolatedSeries = name; } : undefined}
192
+ onHoverEnd={hoverIsolate ? () => { hoverIsolatedSeries = null; } : undefined}
193
+ />
194
+ {/if}
195
+ {/snippet}
191
196
  </BasePlotLayout>
@@ -1,5 +1,4 @@
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';
@@ -25,6 +24,9 @@
25
24
  let {
26
25
  lastPoints,
27
26
  values,
27
+ width,
28
+ height,
29
+ numericMargins,
28
30
  onOverflow,
29
31
  disabledFor,
30
32
  onHoverSeries,
@@ -32,6 +34,11 @@
32
34
  }: {
33
35
  lastPoints: { series: Series<TData>; lastRow: TData; seriesIndex: number }[];
34
36
  values: ValuesStyle | undefined;
37
+ /** 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). */
38
+ width: number;
39
+ height: number;
40
+ /** `BasePlotLayout`'s resolved margins, already numeric on every side. */
41
+ numericMargins: { top: number; right: number; bottom: number; left: number };
35
42
  /** Reports how far the rendered labels overflow the plot's own content box, in pixels, on each side (0 = no overflow). */
36
43
  onOverflow?: (overflow: MarginOverflow) => void;
37
44
  /** Resolved legend/hover-isolate dimming style for a series, if any. */
@@ -93,33 +100,31 @@
93
100
 
94
101
  watchLabelOverflow({
95
102
  el: () => groupEl,
103
+ // Deliberately built from our own `width`/`height`/`numericMargins`
104
+ // (all already correctly sized off the `width`/`height` this `<Plot>`
105
+ // was actually mounted with — see `BasePlotLayout`'s own `numericMargins`
106
+ // doc comment), not from `usePlot().scales.x/y.fn.range()`: svelteplot's
107
+ // `<Plot>` accepts an explicit `width`/`height` (our `fixedWidth`) for
108
+ // its own real layout, but the scale object it exposes through context
109
+ // is computed off its *internal* auto-detected size instead — normally
110
+ // the same number, but never reconciled with `fixedWidth` when one is
111
+ // given. That left `.range()` reporting a stale/placeholder boundary
112
+ // (svelteplot's own internal default, unrelated to this plot's actual
113
+ // pixel size) wherever a caller fixes `width`/`height` explicitly — which
114
+ // every plot kind here does — so this watcher never saw genuine
115
+ // right/bottom overflow past the *real* edge and never grew the margin
116
+ // to fit it, leaving end-point labels to render past the plot's true
117
+ // right edge with no space reserved for them.
96
118
  bounds: () => {
97
119
  // getBBox() inside isn't itself tracked — depend on whatever actually
98
120
  // changes the rendered label layout so this re-measures.
99
121
  void lastPoints;
100
122
  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
123
  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]),
124
+ left: numericMargins.left,
125
+ right: width - numericMargins.right,
126
+ top: numericMargins.top,
127
+ bottom: height - numericMargins.bottom,
123
128
  };
124
129
  },
125
130
  onOverflow: (overflow) => onOverflow?.(overflow),
@@ -10,6 +10,16 @@ declare function $$render<TData extends Record<string, unknown>>(): {
10
10
  seriesIndex: number;
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.32",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"