@bicharts/chart-host 0.6.88 → 0.6.89

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
@@ -277,6 +277,41 @@ stylesheet's `0.25`, or whatever your page sets on the variable.
277
277
  createChartHost(el, { code, data, options: { dimOpacity: 0.15 } });
278
278
  ```
279
279
 
280
+ **A glyph on each selected mark.** `options.selectionGlyph` (`"dot"` or `"check"`; absent or
281
+ `"none"` draws nothing, the default) puts a small glyph on every selected mark, in a layer the host
282
+ owns inside the container (`.bic-selection-glyphs`, each glyph `.lch-sel-glyph`, never hit-tested).
283
+ It is painted after every selection change and, on a redraw, after the layout pass and the fit, so
284
+ nothing measures it as part of the chart. Only a mark the reader can see gets one: a mark hidden by
285
+ `visibility` or `opacity` (a legend toggle) and a mark outside its svg's frame or its plot's
286
+ `clip-path` (a point panned into the axis margin) get none. While a selection is lit, a mark that
287
+ moves or hides without a redraw (a pan or zoom, a transition, an animation frame, a legend toggle)
288
+ takes its glyph with it on the next frame; a write that holds no mark (a hover dot, a tooltip)
289
+ repaints nothing, and that repaint is not reported again. Where the browser has a `ResizeObserver`,
290
+ a container resized without a redraw (a responsive chart, a hidden tab shown) is repainted too, and
291
+ reported when it drew a different count. The layer clips at the container's scroll area, so a
292
+ glyph never widens what the container scrolls, and it sits under the note badges, under a chart's
293
+ own tooltip or control that sets a `z-index` above 1, and under a pinned axis, so a glyph on a row
294
+ scrolled beneath the axis is covered with its row. When the page left the container
295
+ `position: static`, the host makes it `relative` so the layer scrolls and clips with the marks, and
296
+ `destroy()` puts back what the page had. A period picked on a chart's own timeline dims nothing and
297
+ marks nothing.
298
+
299
+ Some charts never take a glyph: on a flow or relationship chart a selected series would get one on
300
+ every point. The generate answer says so (`suppressSelectionGlyph`, read by shape-core's
301
+ `parseGenerateResponse`); pass it as `options.suppressSelectionGlyph`, or leave it in the payload's
302
+ `meta` (`data.meta.suppressSelectionGlyph`) and the host promotes it, the way it promotes
303
+ `geoPoint`. Keep it with the chart's code, so a redraw from a cache honors it too.
304
+ `onSelectionGlyphs` reports what each paint did (`{ mode, drawn, total, suppressed }`), and is
305
+ called only when a glyph mode is set:
306
+
307
+ ```js
308
+ createChartHost(el, {
309
+ code, data,
310
+ options: { selectionGlyph: "dot", suppressSelectionGlyph: answer.suppressSelectionGlyph },
311
+ onSelectionGlyphs: r => console.debug("glyphs", r),
312
+ });
313
+ ```
314
+
280
315
  **Which marks count as selected.** By default a mark is selected when *any* of the rows in its
281
316
  `data-row-idx` list is selected, which is what lights a legend swatch together with its series. A
282
317
  chart whose marks share rows declares the other rule by stamping
package/dist/adapters.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  markKeyOf,
13
13
  rowIdxsFromMark,
14
14
  selectionRuleOf
15
- } from "./chunk-IUXNCX7D.mjs";
15
+ } from "./chunk-DJ5IJV57.mjs";
16
16
  import {
17
17
  AXIS_FILTER_CLASS,
18
18
  LEGEND_MARK_CLASS,
@@ -2,25 +2,28 @@
2
2
  import {
3
3
  LASSO_OUTLINE_CLASS,
4
4
  installLasso
5
- } from "./chunk-IZXVV74H.mjs";
5
+ } from "./chunk-OG6RR2AJ.mjs";
6
6
  import {
7
7
  censusHitBands,
8
8
  ensureCrossfilterHitTargets
9
9
  } from "./chunk-XNS34AMM.mjs";
10
10
  import {
11
11
  clearPress,
12
+ clearSelectionGlyphs,
12
13
  clickWasDrag,
14
+ collectGlyphCentersWhere,
13
15
  createMarkResolver,
14
16
  isInsideControl,
15
17
  markKeyOf,
16
18
  nextClickedMarks,
17
19
  nextSelection,
20
+ paintSelectionGlyphs,
18
21
  planSelectionPaint,
19
22
  sameRowSet,
20
23
  selectionRuleOf,
21
24
  trackPress,
22
25
  validRowSet
23
- } from "./chunk-IUXNCX7D.mjs";
26
+ } from "./chunk-DJ5IJV57.mjs";
24
27
  import {
25
28
  clipWindowOf,
26
29
  fitRenderedChart,
@@ -615,6 +618,14 @@ function resolveOptions(p) {
615
618
  const n = numberOr(p.dimOpacity, NaN);
616
619
  return Number.isFinite(n) ? clamp(n, DIM_OPACITY_MIN, 1) : void 0;
617
620
  })(),
621
+ // A recognised glyph or none: an unknown string must never reach the painter as a live mode, and "none" is
622
+ // undefined rather than a value, so a host that passes nothing and one that passes "none" resolve alike.
623
+ selectionGlyph: (() => {
624
+ const v = (p.selectionGlyph == null ? "" : String(p.selectionGlyph)).toLowerCase();
625
+ return v === "dot" || v === "check" ? v : void 0;
626
+ })(),
627
+ // A host FACT from the generate answer, true only for a literal true: silence never suppresses anything.
628
+ suppressSelectionGlyph: p.suppressSelectionGlyph === true ? true : void 0,
618
629
  onSelect: p.onSelect,
619
630
  // Control gestures (1.10.0). Passed straight through like onSelect: the host owns what a gesture MEANS,
620
631
  // and a host that supplies nothing leaves every control behaving exactly as before.
@@ -2208,12 +2219,209 @@ function createChartHost(container, config) {
2208
2219
  } catch {
2209
2220
  }
2210
2221
  };
2222
+ const GLYPH_LAYER_CLASS = "bic-selection-glyphs";
2223
+ let glyphPositionWas = null;
2224
+ let glyphObserver = null;
2225
+ let glyphResize = null;
2226
+ let glyphSize = "";
2227
+ let glyphQueued = false;
2228
+ let glyphQueuedReport = false;
2229
+ let glyphReported = "";
2230
+ const sizeOfContainer = () => `${container.offsetWidth ?? 0}x${container.offsetHeight ?? 0}`;
2231
+ const glyphBlind = `.${GLYPH_LAYER_CLASS}, .${LASSO_OUTLINE_CLASS}`;
2232
+ const movesAMark = (r) => {
2233
+ const t = r.target;
2234
+ if (!t || t.closest?.(glyphBlind)) return false;
2235
+ return !!(t.classList?.contains(MARK_CLASS) || t.querySelector?.(`.${MARK_CLASS}`));
2236
+ };
2237
+ const queueGlyphs = (reportIfChanged) => {
2238
+ if (reportIfChanged) glyphQueuedReport = true;
2239
+ if (glyphQueued) return;
2240
+ glyphQueued = true;
2241
+ const run = () => {
2242
+ const r = glyphQueuedReport;
2243
+ glyphQueued = false;
2244
+ glyphQueuedReport = false;
2245
+ if (glyphObserver || glyphResize) paintGlyphLayer(r ? "if-changed" : false);
2246
+ };
2247
+ if (win?.requestAnimationFrame) win.requestAnimationFrame(run);
2248
+ else setTimeout(run, 16);
2249
+ };
2250
+ const followGlyphs = (on) => {
2251
+ if (!on) {
2252
+ if (glyphObserver) {
2253
+ glyphObserver.disconnect();
2254
+ glyphObserver = null;
2255
+ }
2256
+ if (glyphResize) {
2257
+ glyphResize.disconnect();
2258
+ glyphResize = null;
2259
+ }
2260
+ return;
2261
+ }
2262
+ const MO = win?.MutationObserver;
2263
+ if (!glyphObserver && MO) {
2264
+ glyphObserver = new MO((records) => {
2265
+ if (!glyphQueued && records.some(movesAMark)) queueGlyphs(false);
2266
+ });
2267
+ glyphObserver.observe(container, {
2268
+ subtree: true,
2269
+ attributes: true,
2270
+ attributeFilter: [
2271
+ "transform",
2272
+ "d",
2273
+ "x",
2274
+ "y",
2275
+ "cx",
2276
+ "cy",
2277
+ "points",
2278
+ "width",
2279
+ "height",
2280
+ "visibility",
2281
+ "opacity",
2282
+ "style",
2283
+ "class"
2284
+ ]
2285
+ });
2286
+ }
2287
+ const RO = win?.ResizeObserver;
2288
+ if (!glyphResize && RO) {
2289
+ glyphResize = new RO(() => {
2290
+ if (sizeOfContainer() !== glyphSize) queueGlyphs(true);
2291
+ });
2292
+ glyphResize.observe(container);
2293
+ }
2294
+ };
2295
+ const markShown = (m) => {
2296
+ try {
2297
+ const cv = m.checkVisibility;
2298
+ if (typeof cv === "function") return cv.call(m, { visibilityProperty: true, opacityProperty: true }) !== false;
2299
+ const cs = win?.getComputedStyle;
2300
+ if (typeof cs !== "function") return true;
2301
+ const own = cs.call(win, m);
2302
+ if (own.visibility === "hidden" || own.visibility === "collapse") return false;
2303
+ for (let el = m; el && el !== container; el = el.parentElement) {
2304
+ const s = el === m ? own : cs.call(win, el);
2305
+ if (s.display === "none" || s.opacity !== "" && parseFloat(s.opacity) === 0) return false;
2306
+ }
2307
+ } catch {
2308
+ }
2309
+ return true;
2310
+ };
2311
+ const glyphWindows = () => {
2312
+ const windowOf = /* @__PURE__ */ new Map();
2313
+ const read = (el) => {
2314
+ if (!windowOf.has(el)) {
2315
+ let w = null;
2316
+ try {
2317
+ if ((el.tagName || "").toLowerCase() === "svg") {
2318
+ const r = el.getBoundingClientRect();
2319
+ if (r.width > 0 && r.height > 0) w = r;
2320
+ } else if (el.hasAttribute?.("clip-path")) w = clipWindowOf(el);
2321
+ } catch {
2322
+ w = null;
2323
+ }
2324
+ windowOf.set(el, w);
2325
+ }
2326
+ return windowOf.get(el) ?? null;
2327
+ };
2328
+ const EPS2 = 0.5;
2329
+ return (m, x, y) => {
2330
+ for (let el = m; el && el !== container; el = el.parentElement) {
2331
+ const w = read(el);
2332
+ if (w && (x < w.left - EPS2 || x > w.right + EPS2 || y < w.top - EPS2 || y > w.bottom + EPS2)) return false;
2333
+ }
2334
+ return true;
2335
+ };
2336
+ };
2337
+ const paintGlyphLayer = (report = true) => {
2338
+ if (destroyed) return;
2339
+ let follow = false;
2340
+ try {
2341
+ let layer = container.querySelector(`:scope > .${GLYPH_LAYER_CLASS}`);
2342
+ clearSelectionGlyphs(layer);
2343
+ const mode = resolved.selectionGlyph;
2344
+ if (mode !== "dot" && mode !== "check") {
2345
+ glyphReported = "";
2346
+ return;
2347
+ }
2348
+ const active = !!current && current.length > 0 && !selectionIsPeriod;
2349
+ if (!active) {
2350
+ glyphReported = "";
2351
+ return;
2352
+ }
2353
+ if (resolved.suppressSelectionGlyph) {
2354
+ if (report === true) try {
2355
+ config.onSelectionGlyphs?.({ mode, drawn: 0, total: 0, suppressed: true });
2356
+ } catch {
2357
+ }
2358
+ glyphReported = "";
2359
+ return;
2360
+ }
2361
+ follow = true;
2362
+ if (!layer) {
2363
+ layer = doc.createElement("div");
2364
+ layer.className = GLYPH_LAYER_CLASS;
2365
+ layer.setAttribute("aria-hidden", "true");
2366
+ Object.assign(layer.style, {
2367
+ position: "absolute",
2368
+ left: "0",
2369
+ top: "0",
2370
+ width: "0",
2371
+ height: "0",
2372
+ overflow: "hidden",
2373
+ pointerEvents: "none",
2374
+ zIndex: "1"
2375
+ });
2376
+ try {
2377
+ const pos = win?.getComputedStyle?.(container)?.position || container.style.position;
2378
+ if (!pos || pos === "static") {
2379
+ const was = container.style.position;
2380
+ container.style.position = "relative";
2381
+ if (glyphPositionWas === null) glyphPositionWas = was;
2382
+ }
2383
+ } catch {
2384
+ }
2385
+ container.appendChild(layer);
2386
+ }
2387
+ layer.style.width = "0";
2388
+ layer.style.height = "0";
2389
+ const sw = container.scrollWidth || 0, sh = container.scrollHeight || 0;
2390
+ layer.style.width = `${sw}px`;
2391
+ layer.style.height = `${sh}px`;
2392
+ glyphSize = sizeOfContainer();
2393
+ const centers = [];
2394
+ collectGlyphCentersWhere(container, layer.getBoundingClientRect(), centers, { shown: markShown, at: glyphWindows() });
2395
+ const hc = !!resolved.isHighContrast;
2396
+ const drawn = centers.length ? paintSelectionGlyphs(layer, centers, {
2397
+ symbol: mode === "check" ? "\u2713" : "\u25CF",
2398
+ color: hc ? resolved.themeFg || "#111" : "#111",
2399
+ halo: hc ? resolved.themeBg || "#fff" : "#fff"
2400
+ }) : 0;
2401
+ const said = `${mode}:${drawn}:${centers.length}`;
2402
+ if (report === true || report === "if-changed" && said !== glyphReported) {
2403
+ glyphReported = said;
2404
+ try {
2405
+ config.onSelectionGlyphs?.({ mode, drawn, total: centers.length, suppressed: false });
2406
+ } catch {
2407
+ }
2408
+ }
2409
+ } catch {
2410
+ } finally {
2411
+ try {
2412
+ followGlyphs(follow);
2413
+ glyphObserver?.takeRecords();
2414
+ } catch {
2415
+ }
2416
+ }
2417
+ };
2211
2418
  const notify = (rowIdxs, source, tick, fromChart = false) => {
2212
2419
  current = rowIdxs;
2213
2420
  selectionFromChart = rowIdxs.length > 0 && fromChart;
2214
2421
  activeTickEl = rowIdxs.length ? tick ?? null : null;
2215
2422
  selectionIsPeriod = rowIdxs.length > 0 && periodTickSuppressesFeedback(!!tick, chartOwnsTimeline(container));
2216
2423
  paintSelection();
2424
+ paintGlyphLayer();
2217
2425
  for (const cb of subs) {
2218
2426
  try {
2219
2427
  cb(rowIdxs, source);
@@ -2411,6 +2619,7 @@ function createChartHost(container, config) {
2411
2619
  drawAnnotations();
2412
2620
  } catch {
2413
2621
  }
2622
+ paintGlyphLayer();
2414
2623
  try {
2415
2624
  lasso = installLasso({
2416
2625
  container,
@@ -2437,6 +2646,7 @@ function createChartHost(container, config) {
2437
2646
  if (destroyed) return;
2438
2647
  const seq2 = ++renderSeq;
2439
2648
  stopAnim();
2649
+ followGlyphs(false);
2440
2650
  disposeLasso();
2441
2651
  flushZoom();
2442
2652
  container.innerHTML = "";
@@ -2461,6 +2671,8 @@ function createChartHost(container, config) {
2461
2671
  if (resolved.geoPoint === void 0 && data.geoPoint) resolved = { ...resolved, geoPoint: data.geoPoint };
2462
2672
  if (resolved.geoPointDest === void 0 && data.geoPointDest) resolved = { ...resolved, geoPointDest: data.geoPointDest };
2463
2673
  if (resolved.dateCellsAreUtcDays === void 0 && data.dateCellsAreUtcDays) resolved = { ...resolved, dateCellsAreUtcDays: true };
2674
+ if (resolved.suppressSelectionGlyph === void 0 && data.meta?.suppressSelectionGlyph === true)
2675
+ resolved = { ...resolved, suppressSelectionGlyph: true };
2464
2676
  let outcome;
2465
2677
  zoomCap = null;
2466
2678
  if (zoomMemory && config.code && !config.renderFn) {
@@ -2606,7 +2818,15 @@ function createChartHost(container, config) {
2606
2818
  annotationLayer.destroy();
2607
2819
  unpinScrolledAxis(container);
2608
2820
  flushZoom();
2821
+ followGlyphs(false);
2609
2822
  container.innerHTML = "";
2823
+ if (glyphPositionWas !== null) {
2824
+ try {
2825
+ if (container.style.position === "relative") container.style.position = glyphPositionWas;
2826
+ } catch {
2827
+ }
2828
+ glyphPositionWas = null;
2829
+ }
2610
2830
  }
2611
2831
  };
2612
2832
  return host;
@@ -304,8 +304,12 @@ function createMarkResolver(env) {
304
304
  return { isInvisibleStrokePath, distToPathCenterline, refineHitCorridor, findMark, rowIdxsFromMark, penetrateOverlayAt, resolveByGeometry };
305
305
  }
306
306
  function collectD3GlyphCenters(container, baseRect, out) {
307
+ collectGlyphCentersWhere(container, baseRect, out);
308
+ }
309
+ function collectGlyphCentersWhere(container, baseRect, out, filter) {
307
310
  const Point = container.ownerDocument?.defaultView?.DOMPoint ?? globalThis.DOMPoint;
308
- const marks = container.querySelectorAll(`.${MARK_CLASS}.${MARK_SELECTED_CLASS}`);
311
+ let marks = container.querySelectorAll(`.${MARK_CLASS}.${MARK_SELECTED_CLASS}`);
312
+ if (filter?.shown) marks = Array.from(marks).filter((m) => filter.shown(m));
309
313
  const rects = [];
310
314
  for (let i = 0; i < marks.length; i++) {
311
315
  const r = marks[i].getBoundingClientRect();
@@ -385,6 +389,7 @@ function collectD3GlyphCenters(container, baseRect, out) {
385
389
  sx = isRect ? r.left + r.width * 0.8 : r.left + r.width / 2;
386
390
  sy = isRect ? r.top + r.height * 0.2 : r.top + r.height / 2;
387
391
  }
392
+ if (filter?.at && !filter.at(el, sx, sy)) continue;
388
393
  out.push({ x: sx - baseRect.left, y: sy - baseRect.top });
389
394
  }
390
395
  }
@@ -439,6 +444,7 @@ export {
439
444
  clickWasDrag,
440
445
  createMarkResolver,
441
446
  collectD3GlyphCenters,
447
+ collectGlyphCentersWhere,
442
448
  SELECTION_GLYPH_CLASS,
443
449
  clearSelectionGlyphs,
444
450
  paintSelectionGlyphs
@@ -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-IZXVV74H.mjs";
4
+ } from "./chunk-OG6RR2AJ.mjs";
5
5
 
6
6
  // src/filterScope.ts
7
7
  function viewToken(view) {
@@ -4,7 +4,7 @@ import {
4
4
  gestureWasDrag,
5
5
  isInsideControl,
6
6
  rowIdxsFromMark
7
- } from "./chunk-IUXNCX7D.mjs";
7
+ } from "./chunk-DJ5IJV57.mjs";
8
8
  import {
9
9
  AXIS_FILTER_CLASS,
10
10
  CONTROL_CLASS,
package/dist/index.mjs CHANGED
@@ -97,7 +97,7 @@ import {
97
97
  toRGBA,
98
98
  toSourceRows,
99
99
  valuePlacementFlag
100
- } from "./chunk-HXH6NIRV.mjs";
100
+ } from "./chunk-562LF7XM.mjs";
101
101
  import {
102
102
  LASSO_AXES_SLOT,
103
103
  LASSO_COUNT_CLASS,
@@ -122,7 +122,7 @@ import {
122
122
  lassoVerdict,
123
123
  pointInShape,
124
124
  registerLassoAction
125
- } from "./chunk-IZXVV74H.mjs";
125
+ } from "./chunk-OG6RR2AJ.mjs";
126
126
  import {
127
127
  MIN_HIT_BAND_PX,
128
128
  censusHitBands,
@@ -153,7 +153,7 @@ import {
153
153
  selectionRuleOf,
154
154
  trackPress,
155
155
  validRowSet
156
- } from "./chunk-IUXNCX7D.mjs";
156
+ } from "./chunk-DJ5IJV57.mjs";
157
157
  import {
158
158
  AXIS_PIN_BAND_PAD_PX,
159
159
  AXIS_PIN_MAX_BAND_FRACTION,
package/dist/page.mjs CHANGED
@@ -14,9 +14,9 @@ import {
14
14
  readControls,
15
15
  viewFromToken,
16
16
  viewToken
17
- } from "./chunk-56IZWXW2.mjs";
18
- import "./chunk-IZXVV74H.mjs";
19
- import "./chunk-IUXNCX7D.mjs";
17
+ } from "./chunk-GOIG756B.mjs";
18
+ import "./chunk-OG6RR2AJ.mjs";
19
+ import "./chunk-DJ5IJV57.mjs";
20
20
  import "./chunk-YDHXOIAI.mjs";
21
21
  import "./chunk-GHODWOAL.mjs";
22
22
  export {
package/dist/react.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  payloadRowReader,
14
14
  viewFromToken,
15
15
  viewToken
16
- } from "./chunk-56IZWXW2.mjs";
16
+ } from "./chunk-GOIG756B.mjs";
17
17
  import {
18
18
  assembleD3,
19
19
  createChartGroup,
@@ -22,10 +22,10 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-HXH6NIRV.mjs";
26
- import "./chunk-IZXVV74H.mjs";
25
+ } from "./chunk-562LF7XM.mjs";
26
+ import "./chunk-OG6RR2AJ.mjs";
27
27
  import "./chunk-XNS34AMM.mjs";
28
- import "./chunk-IUXNCX7D.mjs";
28
+ import "./chunk-DJ5IJV57.mjs";
29
29
  import "./chunk-QHIPRAZK.mjs";
30
30
  import "./chunk-KQN5C7SZ.mjs";
31
31
  import "./chunk-A2GMXZP7.mjs";
@@ -70,6 +70,7 @@ function BicChart(props) {
70
70
  zoomMemory,
71
71
  labelContrast,
72
72
  onLabelContrast,
73
+ onSelectionGlyphs,
73
74
  layoutPass,
74
75
  onInvalidSentinel,
75
76
  id,
@@ -116,6 +117,8 @@ function BicChart(props) {
116
117
  onSelectRef.current = onSelect;
117
118
  const onLabelContrastRef = useRef(onLabelContrast);
118
119
  onLabelContrastRef.current = onLabelContrast;
120
+ const onSelectionGlyphsRef = useRef(onSelectionGlyphs);
121
+ onSelectionGlyphsRef.current = onSelectionGlyphs;
119
122
  const onInvalidSentinelRef = useRef(onInvalidSentinel);
120
123
  onInvalidSentinelRef.current = onInvalidSentinel;
121
124
  const onAnnotationClickRef = useRef(onAnnotationClick);
@@ -155,6 +158,7 @@ function BicChart(props) {
155
158
  labelContrast,
156
159
  layoutPass,
157
160
  onLabelContrast: (r) => onLabelContrastRef.current?.(r),
161
+ onSelectionGlyphs: (r) => onSelectionGlyphsRef.current?.(r),
158
162
  // A GETTER, not a wrapper: the host decides whether to
159
163
  // swallow the sentinel throw by whether this is SET, so
160
164
  // an always-present wrapper would hide the throw from a
@@ -163,6 +163,10 @@ export interface RenderOptions {
163
163
  /** How far a selection dims the marks outside it, as a share of full opacity (DIM_OPACITY_MIN-1). Absent: the
164
164
  * stylesheet decides - DIM_OPACITY_DEFAULT, or whatever the page sets on --lch-dim-opacity. */
165
165
  dimOpacity?: number;
166
+ /** A small glyph on each selected mark: "dot" or "check". Absent or "none": no glyph (the default). */
167
+ selectionGlyph?: "none" | "dot" | "check";
168
+ /** A host FACT from the generate answer: this chart's family never takes a selection glyph. */
169
+ suppressSelectionGlyph?: boolean;
166
170
  animAutoPlay?: boolean;
167
171
  animPlaySpeedMs?: number;
168
172
  animLoopDelaySec?: number;
@@ -62,6 +62,9 @@ export interface ChartHostConfig {
62
62
  meta?: {
63
63
  hostContract?: string;
64
64
  chart?: string;
65
+ /** The generate answer's fact that this chart's family never takes a selection glyph. Promoted into
66
+ * options.suppressSelectionGlyph, so a host that hands over the whole payload honors it unasked. */
67
+ suppressSelectionGlyph?: boolean;
65
68
  };
66
69
  };
67
70
  /** Generated render() source — compiled exactly like the visual/preview/exec-gate. */
@@ -199,6 +202,18 @@ export interface ChartHostConfig {
199
202
  labelContrast?: boolean | LabelContrastOptions;
200
203
  /** What the label-contrast pass did after each render - counts, so a host can log them. */
201
204
  onLabelContrast?: (report: LabelContrastReport) => void;
205
+ /**
206
+ * What the selection-glyph pass did after each paint of an active selection - counts, so a host can log them.
207
+ * Called only when options.selectionGlyph is "dot" or "check"; `suppressed` is true when the chart's family never
208
+ * takes a glyph (options.suppressSelectionGlyph, or the same fact in data.meta). A repaint that only follows marks
209
+ * moving without a render (a pan, a transition) is not reported again.
210
+ */
211
+ onSelectionGlyphs?: (report: {
212
+ mode: "dot" | "check";
213
+ drawn: number;
214
+ total: number;
215
+ suppressed: boolean;
216
+ }) => void;
202
217
  /**
203
218
  * A LAYOUT PASS the host brings, run after every render (both the synchronous and the promise lane, and every
204
219
  * setOptions / setData re-render) AFTER the label-contrast pass and BEFORE the hit-target heal, the selection
@@ -48,6 +48,9 @@ export interface BicChartProps {
48
48
  labelContrast?: ChartHostConfig["labelContrast"];
49
49
  /** What the label-contrast pass did after each render - counts, for a page that logs. */
50
50
  onLabelContrast?: ChartHostConfig["onLabelContrast"];
51
+ /** What the selection-glyph pass did after each paint, when `options.selectionGlyph` is set - counts, for a page
52
+ * that logs. The glyph itself, the dim level and the suppression fact ride the `options` prop. */
53
+ onSelectionGlyphs?: ChartHostConfig["onSelectionGlyphs"];
51
54
  /**
52
55
  * A LAYOUT PASS run after every render, after label contrast and before the fit - e.g.
53
56
  * `createLegendFitPass()` from "@bicharts/chart-host/legend-fit", which slides a legend clear of
@@ -169,6 +169,18 @@ export declare function collectD3GlyphCenters(container: Element, baseRect: {
169
169
  left: number;
170
170
  top: number;
171
171
  }, out: GlyphCenter[]): void;
172
+ /** Which selected marks get a glyph, for a host that draws its own layer: `shown` drops a mark before anything is
173
+ * measured against it (a mark the reader can't see), `at` drops one by where its glyph would go, in client
174
+ * coordinates (a mark panned or zoomed out of its plot). Either absent keeps every mark. */
175
+ export interface GlyphCenterFilter {
176
+ shown?(mark: Element): boolean;
177
+ at?(mark: Element, clientX: number, clientY: number): boolean;
178
+ }
179
+ /** collectD3GlyphCenters with a filter. Package-internal: the exported function keeps every mark, as it always has. */
180
+ export declare function collectGlyphCentersWhere(container: Element, baseRect: {
181
+ left: number;
182
+ top: number;
183
+ }, out: GlyphCenter[], filter?: GlyphCenterFilter): void;
172
184
  /** The class every painted selection glyph carries. */
173
185
  export declare const SELECTION_GLYPH_CLASS = "lch-sel-glyph";
174
186
  /** Removes every glyph (every child) from the overlay. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.88",
3
+ "version": "0.6.89",
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",