@bicharts/chart-host 0.6.89 → 0.6.90

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/README.md CHANGED
@@ -420,6 +420,20 @@ only while `rememberAcrossSessions()` is true. It is keyed by the chart's code a
420
420
  somewhere arbitrary. A chart with several zoomable panels is left alone. In React, pass the hook as
421
421
  the `zoomMemory` prop of `<BicChart>`.
422
422
 
423
+ **A scrolled chart** keeps its place with no setup. A chart taller than its frame scrolls, and every
424
+ redraw empties the container, which used to send the reader back to the top. The host now parks the
425
+ scroll offset on the container before it clears it, and the fit puts it back once the new layout has
426
+ decided whether it still scrolls. The offset is clamped to what the new layout can scroll, and only
427
+ restored on an axis that still scrolls. That covers a resize, a filter, a restyle, and a new host
428
+ created on the same element. The memory is keyed by the chart (its `code`, or the `renderFn`'s
429
+ source text), so a different chart opens at the top, while a `renderFn` closure rebuilt on every draw
430
+ is still the same chart. It lives on the element for the session
431
+ only, and nothing is persisted. A page that manages the container's scrolling itself passes
432
+ `restoreScroll: false` (the `restoreScroll` prop of `<BicChart>`), and then nothing is remembered or
433
+ restored. A host that clears and fits its container on its own can use the same pieces:
434
+ `rememberScroll` before the clear, `takeScroll` after the render, and `fitRenderedChart`'s
435
+ `restoreScroll` option (or the pure `planScrollRestore`).
436
+
423
437
  ## Keeping a drawn chart readable inside its frame
424
438
 
425
439
  A generated chart cannot know how wide its own legend text will come out, where its tick labels will
package/dist/adapters.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  censusHitBands,
4
4
  ensureCrossfilterHitTargets,
5
5
  hitBandFlag
6
- } from "./chunk-XNS34AMM.mjs";
6
+ } from "./chunk-PDVEIW42.mjs";
7
7
  import {
8
8
  clickWasDrag,
9
9
  collectD3GlyphCenters,
@@ -12,13 +12,13 @@ import {
12
12
  markKeyOf,
13
13
  rowIdxsFromMark,
14
14
  selectionRuleOf
15
- } from "./chunk-DJ5IJV57.mjs";
15
+ } from "./chunk-VEVBXK44.mjs";
16
16
  import {
17
17
  AXIS_FILTER_CLASS,
18
18
  LEGEND_MARK_CLASS,
19
19
  MARK_CLASS,
20
20
  ROW_IDX_ATTR
21
- } from "./chunk-YDHXOIAI.mjs";
21
+ } from "./chunk-45VILLJT.mjs";
22
22
  import "./chunk-GHODWOAL.mjs";
23
23
 
24
24
  // src/adapters/types.ts
@@ -19,6 +19,7 @@ var CONTAINER_SLOT_ANIM_STOP = "__llmAnimStop";
19
19
  var CONTAINER_SLOT_XF_CLEAR = "__llmXfClear";
20
20
  var CONTAINER_SLOT_INITIAL_XF_MARK = "__llmInitialXfMark";
21
21
  var CONTAINER_SLOT_UI_STATE = "__lchUiState";
22
+ var CONTAINER_SLOT_SCROLL = "__lchScroll";
22
23
  var CONTAINER_SLOT_CLICKED_MARKS = "__lchClickedMarks";
23
24
  var VIEW_ONLY_UI_STATE_KEYS = Object.freeze(["camera", "llmZoom"]);
24
25
  function withoutViewOnlyKeys(bag) {
@@ -89,6 +90,7 @@ export {
89
90
  CONTAINER_SLOT_XF_CLEAR,
90
91
  CONTAINER_SLOT_INITIAL_XF_MARK,
91
92
  CONTAINER_SLOT_UI_STATE,
93
+ CONTAINER_SLOT_SCROLL,
92
94
  CONTAINER_SLOT_CLICKED_MARKS,
93
95
  VIEW_ONLY_UI_STATE_KEYS,
94
96
  withoutViewOnlyKeys,
@@ -1,7 +1,7 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
3
  LASSO_KNOB_KEY
4
- } from "./chunk-OG6RR2AJ.mjs";
4
+ } from "./chunk-QHMLMF4L.mjs";
5
5
 
6
6
  // src/filterScope.ts
7
7
  function viewToken(view) {
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  LASSO_OUTLINE_CLASS,
4
4
  installLasso
5
- } from "./chunk-OG6RR2AJ.mjs";
5
+ } from "./chunk-QHMLMF4L.mjs";
6
6
  import {
7
7
  censusHitBands,
8
8
  ensureCrossfilterHitTargets
9
- } from "./chunk-XNS34AMM.mjs";
9
+ } from "./chunk-PDVEIW42.mjs";
10
10
  import {
11
11
  clearPress,
12
12
  clearSelectionGlyphs,
@@ -23,12 +23,14 @@ import {
23
23
  selectionRuleOf,
24
24
  trackPress,
25
25
  validRowSet
26
- } from "./chunk-DJ5IJV57.mjs";
26
+ } from "./chunk-VEVBXK44.mjs";
27
27
  import {
28
28
  clipWindowOf,
29
29
  fitRenderedChart,
30
+ rememberScroll,
31
+ takeScroll,
30
32
  unpinScrolledAxis
31
- } from "./chunk-QHIPRAZK.mjs";
33
+ } from "./chunk-NNLBFIWU.mjs";
32
34
  import {
33
35
  Fn,
34
36
  J3,
@@ -82,7 +84,7 @@ import {
82
84
  XFILTER_REFRESH_EVENT,
83
85
  chartOwnsTimeline,
84
86
  periodTickSuppressesFeedback
85
- } from "./chunk-YDHXOIAI.mjs";
87
+ } from "./chunk-45VILLJT.mjs";
86
88
 
87
89
  // src/labelContrast.ts
88
90
  function parseRGBA(c) {
@@ -2521,6 +2523,20 @@ function createChartHost(container, config) {
2521
2523
  }
2522
2524
  }
2523
2525
  };
2526
+ const keepScroll = config.restoreScroll !== false;
2527
+ let chartSource;
2528
+ const scrollKey = () => {
2529
+ if (config.code) return config.code;
2530
+ if (chartSource === void 0 && typeof config.renderFn === "function") {
2531
+ try {
2532
+ chartSource = Function.prototype.toString.call(config.renderFn);
2533
+ } catch {
2534
+ return config.renderFn;
2535
+ }
2536
+ }
2537
+ return chartSource;
2538
+ };
2539
+ let hasRendered = false;
2524
2540
  const runPostRenderPasses = () => {
2525
2541
  if (zoomMemory && zoomCap) {
2526
2542
  try {
@@ -2608,9 +2624,11 @@ function createChartHost(container, config) {
2608
2624
  } catch {
2609
2625
  }
2610
2626
  }
2627
+ const scrollMem = takeScroll(container);
2611
2628
  if (fitOpts !== false) {
2612
2629
  try {
2613
- const r = fitRenderedChart(container, fitOpts === true ? {} : fitOpts);
2630
+ const base = fitOpts === true ? {} : fitOpts;
2631
+ const r = fitRenderedChart(container, keepScroll ? { ...base, restoreScroll: { memory: scrollMem, key: scrollKey() } } : base);
2614
2632
  config.onFit?.(r);
2615
2633
  } catch {
2616
2634
  }
@@ -2649,7 +2667,9 @@ function createChartHost(container, config) {
2649
2667
  followGlyphs(false);
2650
2668
  disposeLasso();
2651
2669
  flushZoom();
2670
+ if (hasRendered && keepScroll) rememberScroll(container, scrollKey());
2652
2671
  container.innerHTML = "";
2672
+ hasRendered = true;
2653
2673
  if (!renderFn) {
2654
2674
  if (!config.code) throw new Error("createChartHost: provide code or renderFn");
2655
2675
  renderFn = compileRenderFn(config.code, win, doc, chartD3);
@@ -2816,6 +2836,7 @@ function createChartHost(container, config) {
2816
2836
  dimVarSet = false;
2817
2837
  }
2818
2838
  annotationLayer.destroy();
2839
+ if (hasRendered && keepScroll) rememberScroll(container, scrollKey());
2819
2840
  unpinScrolledAxis(container);
2820
2841
  flushZoom();
2821
2842
  followGlyphs(false);
@@ -1,7 +1,8 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
+ CONTAINER_SLOT_SCROLL,
3
4
  SNAPSHOT_SVG_ATTR
4
- } from "./chunk-YDHXOIAI.mjs";
5
+ } from "./chunk-45VILLJT.mjs";
5
6
 
6
7
  // src/fit.ts
7
8
  var SCROLL_SLACK_PX = 8;
@@ -104,6 +105,15 @@ function axisPinPlacement(band, scrollTop, viewportH) {
104
105
  if (band.bottom > scrollTop + viewportH) return "bottom";
105
106
  return null;
106
107
  }
108
+ var nonNegative = (n) => typeof n === "number" && isFinite(n) && n > 0 ? n : 0;
109
+ function planScrollRestore(saved, key, m) {
110
+ if (!saved || key === void 0 || key === null || saved.key !== key) return { x: 0, y: 0 };
111
+ const room = (content, view) => Math.max(0, nonNegative(content) - nonNegative(view));
112
+ return {
113
+ x: m.overflowX === "auto" ? Math.min(nonNegative(saved.x), room(m.scrollWidth, m.clientWidth)) : 0,
114
+ y: m.overflowY === "auto" ? Math.min(nonNegative(saved.y), room(m.scrollHeight, m.clientHeight)) : 0
115
+ };
116
+ }
107
117
 
108
118
  // src/fitDom.ts
109
119
  var FIT_WALK_CAP = 8e3;
@@ -256,6 +266,28 @@ function unpinScrolledAxis(container) {
256
266
  } catch {
257
267
  }
258
268
  }
269
+ function rememberScroll(container, key) {
270
+ if (!container) return;
271
+ try {
272
+ const slot = container;
273
+ if (slot[CONTAINER_SLOT_SCROLL]) return;
274
+ if (container.querySelector(".llm-mm-scroll")) return;
275
+ const x = container.scrollLeft, y = container.scrollTop;
276
+ if (x > 0 || y > 0) slot[CONTAINER_SLOT_SCROLL] = { key, x, y };
277
+ } catch {
278
+ }
279
+ }
280
+ function takeScroll(container) {
281
+ if (!container) return null;
282
+ try {
283
+ const slot = container;
284
+ const m = slot[CONTAINER_SLOT_SCROLL] ?? null;
285
+ delete slot[CONTAINER_SLOT_SCROLL];
286
+ return m;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
259
291
  function textHidden(t, view) {
260
292
  if (t.getAttribute("display") === "none" || t.getAttribute("visibility") === "hidden") return true;
261
293
  try {
@@ -476,7 +508,8 @@ function fitRenderedChart(container, opts = {}) {
476
508
  overflowX: "hidden",
477
509
  overflowY: "hidden",
478
510
  reading: null,
479
- axisPin: noPin("no-container")
511
+ axisPin: noPin("no-container"),
512
+ scrolledTo: null
480
513
  };
481
514
  if (!container) return result;
482
515
  try {
@@ -537,6 +570,19 @@ function fitRenderedChart(container, opts = {}) {
537
570
  if (opts.applyOverflow !== false) {
538
571
  container.style.overflowX = fit.overflowX;
539
572
  container.style.overflowY = fit.overflowY;
573
+ if (opts.restoreScroll) {
574
+ const to = planScrollRestore(opts.restoreScroll.memory, opts.restoreScroll.key, {
575
+ scrollWidth: container.scrollWidth,
576
+ scrollHeight: container.scrollHeight,
577
+ clientWidth: container.clientWidth,
578
+ clientHeight: container.clientHeight,
579
+ overflowX: fit.overflowX,
580
+ overflowY: fit.overflowY
581
+ });
582
+ if (container.scrollLeft !== to.x) container.scrollLeft = to.x;
583
+ if (container.scrollTop !== to.y) container.scrollTop = to.y;
584
+ result.scrolledTo = to;
585
+ }
540
586
  if (fit.overflowY !== "auto") {
541
587
  unpinScrolledAxis(container);
542
588
  result.axisPin = noPin("not-scrolling");
@@ -571,12 +617,15 @@ export {
571
617
  labelBand,
572
618
  planAxisPin,
573
619
  axisPinPlacement,
620
+ planScrollRestore,
574
621
  ctmScaleOf,
575
622
  svgInkReach,
576
623
  clipWindowOf,
577
624
  measureContainerBoxes,
578
625
  fitReadingFor,
579
626
  unpinScrolledAxis,
627
+ rememberScroll,
628
+ takeScroll,
580
629
  pinScrolledAxis,
581
630
  fitRenderedChart
582
631
  };
@@ -4,7 +4,7 @@ import {
4
4
  LEGEND_MARK_CLASS,
5
5
  MARK_CLASS,
6
6
  ROW_IDX_ATTR
7
- } from "./chunk-YDHXOIAI.mjs";
7
+ } from "./chunk-45VILLJT.mjs";
8
8
 
9
9
  // src/hitTargets.ts
10
10
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -4,7 +4,7 @@ import {
4
4
  gestureWasDrag,
5
5
  isInsideControl,
6
6
  rowIdxsFromMark
7
- } from "./chunk-DJ5IJV57.mjs";
7
+ } from "./chunk-VEVBXK44.mjs";
8
8
  import {
9
9
  AXIS_FILTER_CLASS,
10
10
  CONTROL_CLASS,
@@ -12,7 +12,7 @@ import {
12
12
  MARK_CLASS,
13
13
  ROW_IDX_ATTR,
14
14
  SNAPSHOT_SVG_ATTR
15
- } from "./chunk-YDHXOIAI.mjs";
15
+ } from "./chunk-45VILLJT.mjs";
16
16
 
17
17
  // src/lasso.ts
18
18
  var LASSO_ROW_CAP = 5e3;
@@ -1,7 +1,7 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
3
  SNAPSHOT_SVG_ATTR
4
- } from "./chunk-YDHXOIAI.mjs";
4
+ } from "./chunk-45VILLJT.mjs";
5
5
 
6
6
  // src/snapshot.ts
7
7
  var DEFAULT_TIMEOUT_MS = 2e3;
@@ -8,7 +8,7 @@ import {
8
8
  SELECTION_RULE_ATTR,
9
9
  SELECTION_RULE_CLICKED_MARKS,
10
10
  XFILTER_REFRESH_EVENT
11
- } from "./chunk-YDHXOIAI.mjs";
11
+ } from "./chunk-45VILLJT.mjs";
12
12
 
13
13
  // src/selection.ts
14
14
  function rowIdxsFromMark(el) {
package/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  svgNaturalSize,
16
16
  svgToDataUrl,
17
17
  tableHostOf
18
- } from "./chunk-UBB5LNH4.mjs";
18
+ } from "./chunk-VC3Z5CH7.mjs";
19
19
  import {
20
20
  ANNOTATION_CLASS,
21
21
  ANNOTATION_LAYER_CLASS,
@@ -97,7 +97,7 @@ import {
97
97
  toRGBA,
98
98
  toSourceRows,
99
99
  valuePlacementFlag
100
- } from "./chunk-562LF7XM.mjs";
100
+ } from "./chunk-FHHT7DBM.mjs";
101
101
  import {
102
102
  LASSO_AXES_SLOT,
103
103
  LASSO_COUNT_CLASS,
@@ -122,13 +122,13 @@ import {
122
122
  lassoVerdict,
123
123
  pointInShape,
124
124
  registerLassoAction
125
- } from "./chunk-OG6RR2AJ.mjs";
125
+ } from "./chunk-QHMLMF4L.mjs";
126
126
  import {
127
127
  MIN_HIT_BAND_PX,
128
128
  censusHitBands,
129
129
  ensureCrossfilterHitTargets,
130
130
  hitBandFlag
131
- } from "./chunk-XNS34AMM.mjs";
131
+ } from "./chunk-PDVEIW42.mjs";
132
132
  import {
133
133
  CONTAINER_SLOT_PRESS_AT,
134
134
  DRAG_NOT_CLICK_PX,
@@ -153,7 +153,7 @@ import {
153
153
  selectionRuleOf,
154
154
  trackPress,
155
155
  validRowSet
156
- } from "./chunk-DJ5IJV57.mjs";
156
+ } from "./chunk-VEVBXK44.mjs";
157
157
  import {
158
158
  AXIS_PIN_BAND_PAD_PX,
159
159
  AXIS_PIN_MAX_BAND_FRACTION,
@@ -176,10 +176,13 @@ import {
176
176
  pinScrolledAxis,
177
177
  planAxisPin,
178
178
  planFrameGrow,
179
+ planScrollRestore,
180
+ rememberScroll,
179
181
  scrollFitFor,
180
182
  svgInkReach,
183
+ takeScroll,
181
184
  unpinScrolledAxis
182
- } from "./chunk-QHIPRAZK.mjs";
185
+ } from "./chunk-NNLBFIWU.mjs";
183
186
  import {
184
187
  _3,
185
188
  l,
@@ -205,6 +208,7 @@ import {
205
208
  CONTAINER_SLOT_ANIM_STOP,
206
209
  CONTAINER_SLOT_CLICKED_MARKS,
207
210
  CONTAINER_SLOT_INITIAL_XF_MARK,
211
+ CONTAINER_SLOT_SCROLL,
208
212
  CONTAINER_SLOT_UI_STATE,
209
213
  CONTAINER_SLOT_XF_CLEAR,
210
214
  CONTROL_CLASS,
@@ -237,7 +241,7 @@ import {
237
241
  chartOwnsTimeline,
238
242
  periodTickSuppressesFeedback,
239
243
  withoutViewOnlyKeys
240
- } from "./chunk-YDHXOIAI.mjs";
244
+ } from "./chunk-45VILLJT.mjs";
241
245
  import "./chunk-GHODWOAL.mjs";
242
246
 
243
247
  // src/optionsVocabulary.ts
@@ -2154,6 +2158,7 @@ export {
2154
2158
  CONTAINER_SLOT_CLICKED_MARKS,
2155
2159
  CONTAINER_SLOT_INITIAL_XF_MARK,
2156
2160
  CONTAINER_SLOT_PRESS_AT,
2161
+ CONTAINER_SLOT_SCROLL,
2157
2162
  CONTAINER_SLOT_UI_STATE,
2158
2163
  CONTAINER_SLOT_XF_CLEAR,
2159
2164
  CONTROL_CLASS,
@@ -2393,6 +2398,7 @@ export {
2393
2398
  pinScrolledAxis,
2394
2399
  planAxisPin,
2395
2400
  planFrameGrow,
2401
+ planScrollRestore,
2396
2402
  planSelectionPaint,
2397
2403
  planTrivialChart,
2398
2404
  pointInShape,
@@ -2429,6 +2435,7 @@ export {
2429
2435
  registerGeoAsset,
2430
2436
  registerLassoAction,
2431
2437
  relativeLuminance,
2438
+ rememberScroll,
2432
2439
  requiredD3Plugins,
2433
2440
  resolveAutoColorScale,
2434
2441
  resolveOptions,
@@ -2455,6 +2462,7 @@ export {
2455
2462
  svgToDataUrl,
2456
2463
  syncMemberSelection,
2457
2464
  tableHostOf,
2465
+ takeScroll,
2458
2466
  toRGBA,
2459
2467
  toSourceRows,
2460
2468
  trackPress,
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  chartSvgOf,
4
4
  tableHostOf
5
- } from "./chunk-UBB5LNH4.mjs";
5
+ } from "./chunk-VC3Z5CH7.mjs";
6
6
  import {
7
7
  FIT_CONTENT_SELECTOR,
8
8
  ctmScaleOf,
9
9
  isPhantomBox
10
- } from "./chunk-QHIPRAZK.mjs";
11
- import "./chunk-YDHXOIAI.mjs";
10
+ } from "./chunk-NNLBFIWU.mjs";
11
+ import "./chunk-45VILLJT.mjs";
12
12
  import "./chunk-GHODWOAL.mjs";
13
13
 
14
14
  // src/legendFit/legendReconcile.ts
package/dist/page.mjs CHANGED
@@ -14,10 +14,10 @@ import {
14
14
  readControls,
15
15
  viewFromToken,
16
16
  viewToken
17
- } from "./chunk-GOIG756B.mjs";
18
- import "./chunk-OG6RR2AJ.mjs";
19
- import "./chunk-DJ5IJV57.mjs";
20
- import "./chunk-YDHXOIAI.mjs";
17
+ } from "./chunk-CR7DSJVG.mjs";
18
+ import "./chunk-QHMLMF4L.mjs";
19
+ import "./chunk-VEVBXK44.mjs";
20
+ import "./chunk-45VILLJT.mjs";
21
21
  import "./chunk-GHODWOAL.mjs";
22
22
  export {
23
23
  LINKED_HOVER_CLASS,
package/dist/react.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  payloadRowReader,
14
14
  viewFromToken,
15
15
  viewToken
16
- } from "./chunk-GOIG756B.mjs";
16
+ } from "./chunk-CR7DSJVG.mjs";
17
17
  import {
18
18
  assembleD3,
19
19
  createChartGroup,
@@ -22,14 +22,14 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-562LF7XM.mjs";
26
- import "./chunk-OG6RR2AJ.mjs";
27
- import "./chunk-XNS34AMM.mjs";
28
- import "./chunk-DJ5IJV57.mjs";
29
- import "./chunk-QHIPRAZK.mjs";
25
+ } from "./chunk-FHHT7DBM.mjs";
26
+ import "./chunk-QHMLMF4L.mjs";
27
+ import "./chunk-PDVEIW42.mjs";
28
+ import "./chunk-VEVBXK44.mjs";
29
+ import "./chunk-NNLBFIWU.mjs";
30
30
  import "./chunk-KQN5C7SZ.mjs";
31
31
  import "./chunk-A2GMXZP7.mjs";
32
- import "./chunk-YDHXOIAI.mjs";
32
+ import "./chunk-45VILLJT.mjs";
33
33
  import "./chunk-GHODWOAL.mjs";
34
34
 
35
35
  // src/react.tsx
@@ -69,6 +69,7 @@ function BicChart(props) {
69
69
  viewState,
70
70
  zoomMemory,
71
71
  labelContrast,
72
+ restoreScroll,
72
73
  onLabelContrast,
73
74
  onSelectionGlyphs,
74
75
  layoutPass,
@@ -157,6 +158,7 @@ function BicChart(props) {
157
158
  zoomMemory,
158
159
  labelContrast,
159
160
  layoutPass,
161
+ restoreScroll,
160
162
  onLabelContrast: (r) => onLabelContrastRef.current?.(r),
161
163
  onSelectionGlyphs: (r) => onSelectionGlyphsRef.current?.(r),
162
164
  // A GETTER, not a wrapper: the host decides whether to
@@ -189,7 +191,7 @@ function BicChart(props) {
189
191
  host.destroy();
190
192
  hostRef.current = null;
191
193
  };
192
- }, [code, renderFn, d3, geoKind, labelContrast, layoutPass, controls]);
194
+ }, [code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll, controls]);
193
195
  useLayoutEffect(() => {
194
196
  const host = hostRef.current;
195
197
  if (!host || !selects) return;
@@ -215,7 +217,7 @@ function BicChart(props) {
215
217
  b.detach();
216
218
  if (bindingRef.current === b) bindingRef.current = null;
217
219
  };
218
- }, [selects, code, renderFn, d3, geoKind, labelContrast, layoutPass]);
220
+ }, [selects, code, renderFn, d3, geoKind, labelContrast, layoutPass, restoreScroll]);
219
221
  useEffect(() => {
220
222
  if (!hostRef.current || !options) return;
221
223
  if (builtWithRef.current?.optKey === optKey) return;
package/dist/testing.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  MARK_CLASS,
5
5
  MARK_SELECTED_CLASS,
6
6
  ROW_IDX_ATTR
7
- } from "./chunk-YDHXOIAI.mjs";
7
+ } from "./chunk-45VILLJT.mjs";
8
8
  import "./chunk-GHODWOAL.mjs";
9
9
 
10
10
  // src/testing/interact.ts
@@ -35,6 +35,7 @@ export declare const CONTAINER_SLOT_ANIM_STOP = "__llmAnimStop";
35
35
  export declare const CONTAINER_SLOT_XF_CLEAR = "__llmXfClear";
36
36
  export declare const CONTAINER_SLOT_INITIAL_XF_MARK = "__llmInitialXfMark";
37
37
  export declare const CONTAINER_SLOT_UI_STATE = "__lchUiState";
38
+ export declare const CONTAINER_SLOT_SCROLL = "__lchScroll";
38
39
  export declare const CONTAINER_SLOT_CLICKED_MARKS = "__lchClickedMarks";
39
40
  export declare const VIEW_ONLY_UI_STATE_KEYS: readonly string[];
40
41
  /** The bag with every view-only key removed: what a host persists when the reader has not asked
@@ -90,3 +90,26 @@ export type AxisPinEdge = "top" | "bottom";
90
90
  * one drawn over it.
91
91
  */
92
92
  export declare function axisPinPlacement(band: AxisBand, scrollTop: number, viewportH: number): AxisPinEdge | null;
93
+ /** A reader's scroll offsets and the identity of the chart they were made on. */
94
+ export interface ScrollMemory {
95
+ key: unknown;
96
+ x: number;
97
+ y: number;
98
+ }
99
+ /** What the container reports once the overflow is decided. */
100
+ export interface ScrollMetrics {
101
+ scrollWidth: number;
102
+ scrollHeight: number;
103
+ clientWidth: number;
104
+ clientHeight: number;
105
+ overflowX: string;
106
+ overflowY: string;
107
+ }
108
+ /**
109
+ * Where the scroll goes after a redraw. The saved offsets when the SAME chart is back, clamped to what the new layout can scroll and
110
+ * applied per axis only where the container scrolls; otherwise the origin. A new chart opens at the top.
111
+ */
112
+ export declare function planScrollRestore(saved: ScrollMemory | null | undefined, key: unknown, m: ScrollMetrics): {
113
+ x: number;
114
+ y: number;
115
+ };
@@ -1,4 +1,4 @@
1
- import type { ContentExtent, MeasuredBox, AxisPinEdge } from "./fit";
1
+ import type { ContentExtent, MeasuredBox, AxisPinEdge, ScrollMemory } from "./fit";
2
2
  export declare function ctmScaleOf(m: {
3
3
  a: number;
4
4
  b: number;
@@ -59,6 +59,16 @@ export interface FitRenderedChartOptions {
59
59
  * Set false for a host that draws its own sticky header.
60
60
  */
61
61
  pinAxis?: boolean;
62
+ /**
63
+ * Put the reader's scroll back once the overflow is decided: `memory` (what `takeScroll` handed
64
+ * back) is restored only when its key is `key`, the chart now drawn, clamped to what the new
65
+ * layout can scroll and only on an axis that scrolls. Applied before the axis pin, which places
66
+ * its copy from the scroll it finds. Absent, the container is left where the render put it.
67
+ */
68
+ restoreScroll?: {
69
+ memory: ScrollMemory | null;
70
+ key: unknown;
71
+ };
62
72
  }
63
73
  export interface FitRenderedChartResult {
64
74
  /** The frame was grown, so content that was being clipped is now painted. */
@@ -73,6 +83,11 @@ export interface FitRenderedChartResult {
73
83
  reading: FitReading | null;
74
84
  /** The axis pin: what was pinned when the container scrolls, or why nothing was. */
75
85
  axisPin: AxisPinReport;
86
+ /** Where `restoreScroll` put the container ({0,0} for a different chart), or null when it was not asked or the overflow was not applied. */
87
+ scrolledTo: {
88
+ x: number;
89
+ y: number;
90
+ } | null;
76
91
  }
77
92
  export interface AxisPinReport {
78
93
  pinned: boolean;
@@ -94,5 +109,13 @@ export interface AxisPinReport {
94
109
  * when there is none. Hosts that clear the container themselves still need this for the
95
110
  * listener, which outlives the overlay. */
96
111
  export declare function unpinScrolledAxis(container: HTMLElement | null | undefined): void;
112
+ /**
113
+ * Remember where the container is scrolled, BEFORE the container is cleared. A memory already waiting is kept: a second render that
114
+ * starts before the first one restored sees an emptied (unscrolled) container, and must not overwrite what the reader really did.
115
+ * A diagram that scrolls inside itself remembers its own position, so it is left alone.
116
+ */
117
+ export declare function rememberScroll(container: HTMLElement | null | undefined, key: unknown): void;
118
+ /** The waiting memory, removed. The one place it is consumed, so a stale memory can never outlive a render. */
119
+ export declare function takeScroll(container: HTMLElement | null | undefined): ScrollMemory | null;
97
120
  export declare function pinScrolledAxis(container: HTMLElement | null | undefined, svg: SVGSVGElement | null | undefined): AxisPinReport;
98
121
  export declare function fitRenderedChart(container: HTMLElement | null | undefined, opts?: FitRenderedChartOptions): FitRenderedChartResult;
@@ -181,6 +181,15 @@ export interface ChartHostConfig {
181
181
  * reading without touching the chart (`grow: false`, `applyOverflow: false`).
182
182
  */
183
183
  fit?: boolean | FitRenderedChartOptions;
184
+ /**
185
+ * KEEP THE READER'S SCROLL when the SAME chart is drawn again - a resize, a filter, a theme
186
+ * switch, or a new host on the same element. Default true: the offset is parked on the
187
+ * container before each clear and put back by the fit, clamped to what the new layout can
188
+ * scroll; a different chart (other code, or a renderFn with other source) opens at the top. Pass `false`
189
+ * for a page that manages the container's scrolling itself: nothing is remembered or restored,
190
+ * and a memory an earlier host left on the element is discarded at the next render.
191
+ */
192
+ restoreScroll?: boolean;
184
193
  /** The fit result after each render - what was grown, and how the chart measured. */
185
194
  onFit?: (result: FitRenderedChartResult) => void;
186
195
  /**
@@ -1,4 +1,4 @@
1
- export { HOST_CONTRACT_VERSION, GEO_POINT_PRECISIONS, MARK_CLASS, LEGEND_MARK_CLASS, AXIS_FILTER_CLASS, ROW_IDX_ATTR, LIFT_SELECTED_CLASS, CONTROL_CLASS, NO_ROW_MARKS_ATTR, SELECTION_RULE_ATTR, SELECTION_RULE_CLICKED_MARKS, MARK_KEY_ATTR, CONTAINER_SLOT_CLICKED_MARKS, SNAPSHOT_SVG_ATTR, XFILTER_REFRESH_EVENT, CONTAINER_SLOT_ANIM_STOP, CONTAINER_SLOT_XF_CLEAR, CONTAINER_SLOT_INITIAL_XF_MARK, CONTAINER_SLOT_UI_STATE, VIEW_ONLY_UI_STATE_KEYS, withoutViewOnlyKeys, HOST_CONTAINER_CLASS, SELECTION_ACTIVE_CLASS, MARK_SELECTED_CLASS, ACTIVE_TICK_CLASS, DIM_OPACITY_VAR, DIM_OPACITY_DEFAULT, DIM_OPACITY_MIN, chartOwnsTimeline, periodTickSuppressesFeedback, ANIM_PLAY_SPEED_DEFAULT, ANIM_PLAY_SPEED_MIN, ANIM_PLAY_SPEED_MAX, ANIM_LOOP_DELAY_DEFAULT, ANIM_LOOP_DELAY_MIN, ANIM_MAX_IDEAL_FRAMES_DEFAULT, ANIM_MAX_IDEAL_FRAMES_MIN, ANIM_MAX_IDEAL_FRAMES_MAX, ANIMATION_OPTION_KEYS, COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT, COLOR_SCALE_SELF_CLAMP_PCT_MIN, COLOR_SCALE_SELF_CLAMP_PCT_MAX, FLIP_MODE_DEFAULT, FLIP_INTERVAL_MIN_MS, FLIP_INTERVAL_MAX_MS, FLIP_DIRECTION_DEFAULT, APPROXIMATE_POSITIONS_DEFAULT, VALUE_AXIS_BASELINE_DEFAULT, SEASONAL_MARKERS_DEFAULT, MAX_MAP_POINTS_DEFAULT, type GeoPointPrecision, type GeoMapKind, type TimelineStyle, type FlipMode, type FlipDirection, type ApproximatePositions, type ValueAxisBaseline, type SeasonalMarkers, type ColorScaleScope, type RenderOptions, type ViewStateProvider, type ChartControlChange, type XfilterRefreshDetail, } from "./contract";
1
+ export { HOST_CONTRACT_VERSION, GEO_POINT_PRECISIONS, MARK_CLASS, LEGEND_MARK_CLASS, AXIS_FILTER_CLASS, ROW_IDX_ATTR, LIFT_SELECTED_CLASS, CONTROL_CLASS, NO_ROW_MARKS_ATTR, SELECTION_RULE_ATTR, SELECTION_RULE_CLICKED_MARKS, MARK_KEY_ATTR, CONTAINER_SLOT_CLICKED_MARKS, SNAPSHOT_SVG_ATTR, XFILTER_REFRESH_EVENT, CONTAINER_SLOT_ANIM_STOP, CONTAINER_SLOT_XF_CLEAR, CONTAINER_SLOT_INITIAL_XF_MARK, CONTAINER_SLOT_UI_STATE, CONTAINER_SLOT_SCROLL, VIEW_ONLY_UI_STATE_KEYS, withoutViewOnlyKeys, HOST_CONTAINER_CLASS, SELECTION_ACTIVE_CLASS, MARK_SELECTED_CLASS, ACTIVE_TICK_CLASS, DIM_OPACITY_VAR, DIM_OPACITY_DEFAULT, DIM_OPACITY_MIN, chartOwnsTimeline, periodTickSuppressesFeedback, ANIM_PLAY_SPEED_DEFAULT, ANIM_PLAY_SPEED_MIN, ANIM_PLAY_SPEED_MAX, ANIM_LOOP_DELAY_DEFAULT, ANIM_LOOP_DELAY_MIN, ANIM_MAX_IDEAL_FRAMES_DEFAULT, ANIM_MAX_IDEAL_FRAMES_MIN, ANIM_MAX_IDEAL_FRAMES_MAX, ANIMATION_OPTION_KEYS, COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT, COLOR_SCALE_SELF_CLAMP_PCT_MIN, COLOR_SCALE_SELF_CLAMP_PCT_MAX, FLIP_MODE_DEFAULT, FLIP_INTERVAL_MIN_MS, FLIP_INTERVAL_MAX_MS, FLIP_DIRECTION_DEFAULT, APPROXIMATE_POSITIONS_DEFAULT, VALUE_AXIS_BASELINE_DEFAULT, SEASONAL_MARKERS_DEFAULT, MAX_MAP_POINTS_DEFAULT, type GeoPointPrecision, type GeoMapKind, type TimelineStyle, type FlipMode, type FlipDirection, type ApproximatePositions, type ValueAxisBaseline, type SeasonalMarkers, type ColorScaleScope, type RenderOptions, type ViewStateProvider, type ChartControlChange, type XfilterRefreshDetail, } from "./contract";
2
2
  export { resolveOptions, type ResolveOptionsInput } from "./defaults";
3
3
  export { OPTIONS_VOCABULARY, LIVE_OPTION_KEYS, MAX_MAP_POINTS_MIN, MAX_MAP_POINTS_MAX, LIMIT_COUNT_MAX, wireLimitCount, codeReadsOption, type OptionVocabularyEntry, type OptionTiming, type OptionWire, type OptionKey, } from "./optionsVocabulary";
4
4
  export { loadGeo, geoFromCache, registerGeo, registerGeoAsset, geoAssetFor, clearGeoCache, type GeoAssetName, } from "./geoLazy";
@@ -42,6 +42,8 @@ export { SCROLL_SLACK_PX, MAX_FRAME_GROW_FACTOR, PHANTOM_FRACTION, FIT_CONTENT_S
42
42
  export { fitRenderedChart, fitReadingFor, measureContainerBoxes, svgInkReach, ctmScaleOf, type FitReading, type InkReach, type FitRenderedChartOptions, type FitRenderedChartResult, } from "./fitDom";
43
43
  export { AXIS_PIN_MIN_LABELS, AXIS_PIN_BAND_PAD_PX, AXIS_PIN_MAX_BAND_FRACTION, AXIS_PIN_TRACK_REACH_PX, isHorizontalLabelRow, labelBand, planAxisPin, axisPinPlacement, type LabelRowBox, type AxisBand, type AxisPinCandidate, type AxisPinPlan, type AxisPinEdge, } from "./fit";
44
44
  export { pinScrolledAxis, unpinScrolledAxis, type AxisPinReport } from "./fitDom";
45
+ export { planScrollRestore, type ScrollMemory, type ScrollMetrics } from "./fit";
46
+ export { rememberScroll, takeScroll } from "./fitDom";
45
47
  export { MIN_DELTA_E, pickDistinctColorFromSeed, buildPalette, buildPaletteFromSeed, orderMostDistinct, LEGIBLE_LIGHTNESS, type PaletteResult, type SeedForSlot, type PaletteOptions, } from "./palette";
46
48
  export { deltaEHex } from "./deltaE";
47
49
  export { resolveAutoColorScale, autoRampMinStep, autoRampCanvas, AUTO_RAMP_CLASSES, AUTO_RAMP_DRAWN_OPACITY, AUTO_RAMP_MIN_STEP_DELTA_E, type AutoColorScale, type AutoColorScaleInput } from "./autoRamp";
@@ -46,6 +46,12 @@ export interface BicChartProps {
46
46
  * themed page is not white, and readability is measured against it. See createChartHost.
47
47
  */
48
48
  labelContrast?: ChartHostConfig["labelContrast"];
49
+ /**
50
+ * KEEP THE READER'S SCROLL when the same chart repaints (new data, new options, a rebuild).
51
+ * Default on; a different chart opens at the top. Pass `false` for a page that manages the
52
+ * chart's scrolling itself. Host config: changing it rebuilds the host. See createChartHost.
53
+ */
54
+ restoreScroll?: ChartHostConfig["restoreScroll"];
49
55
  /** What the label-contrast pass did after each render - counts, for a page that logs. */
50
56
  onLabelContrast?: ChartHostConfig["onLabelContrast"];
51
57
  /** What the selection-glyph pass did after each paint, when `options.selectionGlyph` is set - counts, for a page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.89",
3
+ "version": "0.6.90",
4
4
  "description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",