@fundar/data-chart-telling 0.0.46 → 0.0.47

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.
@@ -11,7 +11,10 @@
11
11
  buildScopedMarkerGroups
12
12
  } from '../utils/segments';
13
13
  import { createBarLayout } from './layout.svelte';
14
- import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
14
+ import {
15
+ createLabelMarginTracker,
16
+ halfDimensionOverflowCap
17
+ } from '../utils/labelOverflow.svelte';
15
18
  import { estimateValueLabelMargin } from '../utils/valueLabelMargin';
16
19
  import { buildHoverPoints } from './hoverPoints';
17
20
  import { buildBarMarkers } from './buildBarMarkers';
@@ -258,6 +261,9 @@
258
261
  return { left: 0, right: 0, top: Math.ceil(offset + fontSize * 1.2), bottom: 0 };
259
262
  });
260
263
 
264
+ // See `halfDimensionOverflowCap`'s own doc comment.
265
+ const maxLabelOverflow = $derived(halfDimensionOverflowCap(width, height));
266
+
261
267
  // A bar's value label can grow past the plot's own content box (e.g. a
262
268
  // tall bar's `anchor: 'outside'` label pushing past the top margin) —
263
269
  // `valueLabelFloor` covers the common case up front; each label's own
@@ -266,7 +272,8 @@
266
272
  const labelMargin = createLabelMarginTracker(
267
273
  () => margins,
268
274
  () => resolvedSeries,
269
- () => valueLabelFloor ?? { left: 0, right: 0, top: 0, bottom: 0 }
275
+ () => valueLabelFloor ?? { left: 0, right: 0, top: 0, bottom: 0 },
276
+ () => maxLabelOverflow
270
277
  );
271
278
  </script>
272
279
 
@@ -8,7 +8,10 @@
8
8
  import { buildLineMarkers } from './buildLineMarkers';
9
9
  import { buildGapMarkers } from './buildGapMarkers';
10
10
  import { isGapValue } from '../utils/gaps';
11
- import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
11
+ import {
12
+ createLabelMarginTracker,
13
+ halfDimensionOverflowCap
14
+ } from '../utils/labelOverflow.svelte';
12
15
  import { estimateLineValueLabelMargin } from './valueLabelMargin';
13
16
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
14
17
  import ValueLabels from './ValueLabels.svelte';
@@ -185,17 +188,8 @@
185
188
  });
186
189
  });
187
190
 
188
- // A facet cramped enough that its own end-of-line label can never fully
189
- // fit would otherwise grow `measured` without bound (see
190
- // `createLabelMarginTracker`'s own doc comment) — capped at half of each
191
- // dimension, so the plot's own content box never shrinks past the other
192
- // half no matter how little room a label has to work with.
193
- const maxLabelOverflow = $derived({
194
- left: Math.max(0, width * 0.5),
195
- right: Math.max(0, width * 0.5),
196
- top: Math.max(0, height * 0.5),
197
- bottom: Math.max(0, height * 0.5)
198
- });
191
+ // See `halfDimensionOverflowCap`'s own doc comment.
192
+ const maxLabelOverflow = $derived(halfDimensionOverflowCap(width, height));
199
193
 
200
194
  // Grows the plot's margin to fit value labels that overflow the content
201
195
  // box. `valueLabelFloor` covers the common rightward case up front; each
@@ -6,7 +6,10 @@
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
+ import {
10
+ createLabelMarginTracker,
11
+ halfDimensionOverflowCap
12
+ } from '../utils/labelOverflow.svelte';
10
13
  import { estimateValueLabelMargin } from '../utils/valueLabelMargin';
11
14
  import {
12
15
  buildGroupedSeries,
@@ -301,6 +304,9 @@
301
304
  return { left, right, top: 0, bottom: 0 };
302
305
  });
303
306
 
307
+ // See `halfDimensionOverflowCap`'s own doc comment.
308
+ const maxLabelOverflow = $derived(halfDimensionOverflowCap(width, height));
309
+
304
310
  // A pyramid bar's value label can grow past the plot's own content box —
305
311
  // `valueLabelFloor` covers the common case up front; each label's own
306
312
  // rendered overflow still feeds `labelMargin.report` for whatever that
@@ -308,7 +314,8 @@
308
314
  const labelMargin = createLabelMarginTracker(
309
315
  () => margins,
310
316
  () => resolvedSeries,
311
- () => valueLabelFloor ?? { left: 0, right: 0, top: 0, bottom: 0 }
317
+ () => valueLabelFloor ?? { left: 0, right: 0, top: 0, bottom: 0 },
318
+ () => maxLabelOverflow
312
319
  );
313
320
  </script>
314
321
 
@@ -7,7 +7,10 @@
7
7
  import { disabledDotsOverride } from '../utils/legendDisabled';
8
8
  import { buildScatterMarkers } from './buildScatterMarkers';
9
9
  import { resolveRadiusPaddedDomain } from './resolveRadiusPaddedDomain';
10
- import { createLabelMarginTracker } from '../utils/labelOverflow.svelte';
10
+ import {
11
+ createLabelMarginTracker,
12
+ halfDimensionOverflowCap
13
+ } from '../utils/labelOverflow.svelte';
11
14
  import BasePlotLayout from '../../layout/plot/BasePlotLayout.svelte';
12
15
  import ValueLabels from './ValueLabels.svelte';
13
16
  import {
@@ -214,6 +217,11 @@
214
217
  };
215
218
  });
216
219
 
220
+ // See `halfDimensionOverflowCap`'s own doc comment — relevant here mainly
221
+ // for a corner point with no room in any direction even after
222
+ // `resolveEdgeAwareAnchor`'s own flip.
223
+ const maxLabelOverflow = $derived(halfDimensionOverflowCap(width, height));
224
+
217
225
  // A scatter point's value label can grow past the plot's own content box
218
226
  // (e.g. a point volcado toward a corner, past what its own edge-aware flip
219
227
  // can fit) — reserving real space there is what each label's own overflow
@@ -223,7 +231,9 @@
223
231
  // that frame is gone.
224
232
  const labelMargin = createLabelMarginTracker(
225
233
  () => margins,
226
- () => resolvedSeries
234
+ () => resolvedSeries,
235
+ undefined,
236
+ () => maxLabelOverflow
227
237
  );
228
238
  </script>
229
239
 
@@ -6,6 +6,16 @@ export type MarginOverflow = {
6
6
  top: number;
7
7
  bottom: number;
8
8
  };
9
+ /**
10
+ * A generic `maxOverflow` cap for {@link createLabelMarginTracker}: half of
11
+ * each dimension, so a facet cramped enough that its own value label can
12
+ * never fully fit still leaves the plot's own content box the other half,
13
+ * rather than `measured` growing without bound (see that function's own
14
+ * `maxOverflow` doc comment). The same backstop applies regardless of *what*
15
+ * overflows or *why* — every value-label-drawing plot (line/bar/pyramid/
16
+ * scatter) passes this, sized off its own `width`/`height`.
17
+ */
18
+ export declare function halfDimensionOverflowCap(width: number, height: number): MarginOverflow;
9
19
  /**
10
20
  * Watches an SVG group's rendered bounding box against the plot's own
11
21
  * content box (`bounds`) and reports how far it overflows on each side.
@@ -1,5 +1,22 @@
1
1
  import { AUTO_MARGIN_ESTIMATE } from '../../layout/plot/margins';
2
2
  const NO_OVERFLOW = { left: 0, right: 0, top: 0, bottom: 0 };
3
+ /**
4
+ * A generic `maxOverflow` cap for {@link createLabelMarginTracker}: half of
5
+ * each dimension, so a facet cramped enough that its own value label can
6
+ * never fully fit still leaves the plot's own content box the other half,
7
+ * rather than `measured` growing without bound (see that function's own
8
+ * `maxOverflow` doc comment). The same backstop applies regardless of *what*
9
+ * overflows or *why* — every value-label-drawing plot (line/bar/pyramid/
10
+ * scatter) passes this, sized off its own `width`/`height`.
11
+ */
12
+ export function halfDimensionOverflowCap(width, height) {
13
+ return {
14
+ left: Math.max(0, width * 0.5),
15
+ right: Math.max(0, width * 0.5),
16
+ top: Math.max(0, height * 0.5),
17
+ bottom: Math.max(0, height * 0.5)
18
+ };
19
+ }
3
20
  /**
4
21
  * Watches an SVG group's rendered bounding box against the plot's own
5
22
  * content box (`bounds`) and reports how far it overflows on each side.
@@ -145,18 +162,23 @@ maxOverflow) {
145
162
  measured = floorValue();
146
163
  }
147
164
  });
165
+ // Pure `Math.max` — no `maxOverflow` cap here. `measured` is a *tracked*
166
+ // value, not the applied margin, and must stay strictly monotonic
167
+ // regardless of anything else changing (see this function's own doc
168
+ // comment on why shrinking it back oscillates). `maxOverflow` is
169
+ // typically sized off the plot's own live `width`/`height` (see
170
+ // `halfDimensionOverflowCap`), which moves continuously during a resize
171
+ // — capping *here* would let a shrinking cap force `measured` down mid-
172
+ // resize, immediately re-exposing the overflow that grew it in the first
173
+ // place and re-triggering growth, over and over, fast enough to trip
174
+ // Svelte's `effect_update_depth_exceeded` guard. The cap is applied only
175
+ // where it can't feed back into `measured` itself — see `resolvedMargins`.
148
176
  function report(overflow) {
149
- const cap = maxOverflow?.() ?? {
150
- left: Infinity,
151
- right: Infinity,
152
- top: Infinity,
153
- bottom: Infinity
154
- };
155
177
  const next = {
156
- left: Math.min(cap.left, Math.max(measured.left, overflow.left)),
157
- right: Math.min(cap.right, Math.max(measured.right, overflow.right)),
158
- top: Math.min(cap.top, Math.max(measured.top, overflow.top)),
159
- bottom: Math.min(cap.bottom, Math.max(measured.bottom, overflow.bottom))
178
+ left: Math.max(measured.left, overflow.left),
179
+ right: Math.max(measured.right, overflow.right),
180
+ top: Math.max(measured.top, overflow.top),
181
+ bottom: Math.max(measured.bottom, overflow.bottom)
160
182
  };
161
183
  if (next.left !== measured.left ||
162
184
  next.right !== measured.right ||
@@ -173,33 +195,55 @@ maxOverflow) {
173
195
  // number that only accounts for the value label. `AUTO_MARGIN_ESTIMATE` is
174
196
  // a fixed constant, not `numericMargins`' own already-grown value, so this
175
197
  // can't compound into unbounded growth across repeated reports.
198
+ //
199
+ // `maxOverflow`, if given, clamps `measured` down to a live cap *here*,
200
+ // read-only — every render re-reads the current cap and applies it fresh
201
+ // to the unclamped, monotonically-tracked `measured`, without ever
202
+ // writing the clamped value back into `measured` itself (see `report`'s
203
+ // own doc comment for why that distinction matters).
176
204
  const resolvedMargins = $derived.by(() => {
177
205
  const base = margins();
178
- if (measured.left <= 0 && measured.right <= 0 && measured.top <= 0 && measured.bottom <= 0)
206
+ const cap = maxOverflow?.() ?? {
207
+ left: Infinity,
208
+ right: Infinity,
209
+ top: Infinity,
210
+ bottom: Infinity
211
+ };
212
+ const clamped = {
213
+ left: Math.min(measured.left, cap.left),
214
+ right: Math.min(measured.right, cap.right),
215
+ top: Math.min(measured.top, cap.top),
216
+ bottom: Math.min(measured.bottom, cap.bottom)
217
+ };
218
+ if (clamped.left <= 0 && clamped.right <= 0 && clamped.top <= 0 && clamped.bottom <= 0)
179
219
  return base;
180
220
  const out = { ...base };
181
- if (measured.right > 0 && base?.right === undefined)
182
- out.right = AUTO_MARGIN_ESTIMATE.right + measured.right;
183
- if (measured.left > 0 && base?.left === undefined)
184
- out.left = AUTO_MARGIN_ESTIMATE.left + measured.left;
185
- if (measured.top > 0 && base?.top === undefined)
186
- out.top = AUTO_MARGIN_ESTIMATE.top + measured.top;
187
- if (measured.bottom > 0 && base?.bottom === undefined)
188
- out.bottom = AUTO_MARGIN_ESTIMATE.bottom + measured.bottom;
221
+ if (clamped.right > 0 && base?.right === undefined)
222
+ out.right = AUTO_MARGIN_ESTIMATE.right + clamped.right;
223
+ if (clamped.left > 0 && base?.left === undefined)
224
+ out.left = AUTO_MARGIN_ESTIMATE.left + clamped.left;
225
+ if (clamped.top > 0 && base?.top === undefined)
226
+ out.top = AUTO_MARGIN_ESTIMATE.top + clamped.top;
227
+ if (clamped.bottom > 0 && base?.bottom === undefined)
228
+ out.bottom = AUTO_MARGIN_ESTIMATE.bottom + clamped.bottom;
189
229
  return out;
190
230
  });
191
231
  // Bundles `resolvedMargins` with a remount key, ready to spread straight
192
232
  // onto `<BasePlotLayout>` as its `margins`/`marginRemountKey` props (see
193
233
  // that prop's doc comment for *why* a remount key is needed here at all)
194
- // — keeps the caller from re-deriving the same key by hand. Tracks both
195
- // `measured` (this tracker's own auto-grown overflow) and the caller's
196
- // own `margins()` — a caller that changes its explicit margins prop
197
- // after mount (e.g. locking in a previously-measured value) needs that
198
- // picked up just as cleanly as internal growth does; keying only on
199
- // `measured` would silently miss it.
234
+ // — keeps the caller from re-deriving the same key by hand. Keyed off
235
+ // `resolvedMargins` itself (the actually-applied value) rather than raw
236
+ // `measured` — with a `maxOverflow` cap in play, `measured` can keep
237
+ // climbing past the point where it's clamped without the applied margin
238
+ // changing at all, and remounting for that would be pure waste. Also
239
+ // tracks the caller's own `margins()` a caller that changes its
240
+ // explicit margins prop after mount (e.g. locking in a previously-
241
+ // measured value) needs that picked up just as cleanly as internal
242
+ // growth does; `resolvedMargins` alone already reflects that too, but
243
+ // spelling it out keeps the key's intent obvious.
200
244
  const plotProps = $derived.by(() => ({
201
245
  margins: resolvedMargins,
202
- marginRemountKey: JSON.stringify({ base: margins(), measured })
246
+ marginRemountKey: JSON.stringify({ base: margins(), resolvedMargins })
203
247
  }));
204
248
  return {
205
249
  report,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fundar/data-chart-telling",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"