@bicharts/chart-host 0.6.87 → 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
@@ -267,6 +267,51 @@ would show its selected lines at 38%, which the eye reads as dimmed. Such a char
267
267
  painted at full opacity. Undeclared charts are unchanged, so alpha used as an encoding (a depth
268
268
  cue, a hierarchy level) is never flattened by a fix meant for another chart.
269
269
 
270
+ **How far the rest dims, per chart.** `options.dimOpacity` (a share of full opacity, `0.01`-`1`;
271
+ `DIM_OPACITY_MIN` is the floor) sets the level for one host without writing CSS, and it is live
272
+ through `setOptions`. The host writes it as `--lch-dim-opacity` on its own container and removes
273
+ only what it wrote, so with the option absent nothing is written and the CSS above decides: the
274
+ stylesheet's `0.25`, or whatever your page sets on the variable.
275
+
276
+ ```js
277
+ createChartHost(el, { code, data, options: { dimOpacity: 0.15 } });
278
+ ```
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
+
270
315
  **Which marks count as selected.** By default a mark is selected when *any* of the rows in its
271
316
  `data-row-idx` list is selected, which is what lights a legend swatch together with its series. A
272
317
  chart whose marks share rows declares the other rule by stamping
package/dist/adapters.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  censusHitBands,
4
4
  ensureCrossfilterHitTargets,
5
5
  hitBandFlag
6
- } from "./chunk-7HKVV5T5.mjs";
6
+ } from "./chunk-XNS34AMM.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-IURROJT7.mjs";
15
+ } from "./chunk-DJ5IJV57.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-MR6TL7QZ.mjs";
21
+ } from "./chunk-YDHXOIAI.mjs";
22
22
  import "./chunk-GHODWOAL.mjs";
23
23
 
24
24
  // src/adapters/types.ts
@@ -2,30 +2,33 @@
2
2
  import {
3
3
  LASSO_OUTLINE_CLASS,
4
4
  installLasso
5
- } from "./chunk-QIRLWJBI.mjs";
5
+ } from "./chunk-OG6RR2AJ.mjs";
6
6
  import {
7
7
  censusHitBands,
8
8
  ensureCrossfilterHitTargets
9
- } from "./chunk-7HKVV5T5.mjs";
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-IURROJT7.mjs";
26
+ } from "./chunk-DJ5IJV57.mjs";
24
27
  import {
25
28
  clipWindowOf,
26
29
  fitRenderedChart,
27
30
  unpinScrolledAxis
28
- } from "./chunk-BQFG5HWZ.mjs";
31
+ } from "./chunk-QHIPRAZK.mjs";
29
32
  import {
30
33
  Fn,
31
34
  J3,
@@ -60,6 +63,7 @@ import {
60
63
  CONTAINER_SLOT_XF_CLEAR,
61
64
  CONTROL_CLASS,
62
65
  DIM_OPACITY_DEFAULT,
66
+ DIM_OPACITY_MIN,
63
67
  DIM_OPACITY_VAR,
64
68
  FLIP_DIRECTION_DEFAULT,
65
69
  FLIP_MODE_DEFAULT,
@@ -78,7 +82,7 @@ import {
78
82
  XFILTER_REFRESH_EVENT,
79
83
  chartOwnsTimeline,
80
84
  periodTickSuppressesFeedback
81
- } from "./chunk-MR6TL7QZ.mjs";
85
+ } from "./chunk-YDHXOIAI.mjs";
82
86
 
83
87
  // src/labelContrast.ts
84
88
  function parseRGBA(c) {
@@ -608,6 +612,20 @@ function resolveOptions(p) {
608
612
  cultureCode: p.cultureCode,
609
613
  allowTooltips: p.allowTooltips,
610
614
  noDataText: p.noDataText,
615
+ // A finite number is clamped to DIM_OPACITY_MIN-1; absent, blank or junk stays undefined so the stylesheet - and a
616
+ // page's own CSS override - still decides. Never defaulted here: that would override every page's CSS.
617
+ dimOpacity: (() => {
618
+ const n = numberOr(p.dimOpacity, NaN);
619
+ return Number.isFinite(n) ? clamp(n, DIM_OPACITY_MIN, 1) : void 0;
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,
611
629
  onSelect: p.onSelect,
612
630
  // Control gestures (1.10.0). Passed straight through like onSelect: the host owns what a gesture MEANS,
613
631
  // and a host that supplies nothing leaves every control behaving exactly as before.
@@ -2077,6 +2095,7 @@ function createChartHost(container, config) {
2077
2095
  p.catch(() => {
2078
2096
  });
2079
2097
  };
2098
+ let dimVarSet = false;
2080
2099
  let current = null;
2081
2100
  let activeTickEl = null;
2082
2101
  let selectionIsPeriod = false;
@@ -2153,6 +2172,14 @@ function createChartHost(container, config) {
2153
2172
  const active = !!sel && sel.length > 0;
2154
2173
  const dim = active && !selectionIsPeriod;
2155
2174
  container.classList?.[dim ? "add" : "remove"](SELECTION_ACTIVE_CLASS);
2175
+ const want = resolved.dimOpacity;
2176
+ if (typeof want === "number") {
2177
+ container.style?.setProperty(DIM_OPACITY_VAR, String(want));
2178
+ dimVarSet = true;
2179
+ } else if (dimVarSet) {
2180
+ container.style?.removeProperty(DIM_OPACITY_VAR);
2181
+ dimVarSet = false;
2182
+ }
2156
2183
  const lit = container.querySelectorAll(`.${ACTIVE_TICK_CLASS}`);
2157
2184
  for (const t of Array.from(lit)) t.classList?.remove(ACTIVE_TICK_CLASS);
2158
2185
  const tickStillDrawn = !!activeTickEl && (typeof container.contains === "function" ? container.contains(activeTickEl) : activeTickEl.isConnected !== false);
@@ -2192,12 +2219,209 @@ function createChartHost(container, config) {
2192
2219
  } catch {
2193
2220
  }
2194
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
+ };
2195
2418
  const notify = (rowIdxs, source, tick, fromChart = false) => {
2196
2419
  current = rowIdxs;
2197
2420
  selectionFromChart = rowIdxs.length > 0 && fromChart;
2198
2421
  activeTickEl = rowIdxs.length ? tick ?? null : null;
2199
2422
  selectionIsPeriod = rowIdxs.length > 0 && periodTickSuppressesFeedback(!!tick, chartOwnsTimeline(container));
2200
2423
  paintSelection();
2424
+ paintGlyphLayer();
2201
2425
  for (const cb of subs) {
2202
2426
  try {
2203
2427
  cb(rowIdxs, source);
@@ -2395,6 +2619,7 @@ function createChartHost(container, config) {
2395
2619
  drawAnnotations();
2396
2620
  } catch {
2397
2621
  }
2622
+ paintGlyphLayer();
2398
2623
  try {
2399
2624
  lasso = installLasso({
2400
2625
  container,
@@ -2421,6 +2646,7 @@ function createChartHost(container, config) {
2421
2646
  if (destroyed) return;
2422
2647
  const seq2 = ++renderSeq;
2423
2648
  stopAnim();
2649
+ followGlyphs(false);
2424
2650
  disposeLasso();
2425
2651
  flushZoom();
2426
2652
  container.innerHTML = "";
@@ -2445,6 +2671,8 @@ function createChartHost(container, config) {
2445
2671
  if (resolved.geoPoint === void 0 && data.geoPoint) resolved = { ...resolved, geoPoint: data.geoPoint };
2446
2672
  if (resolved.geoPointDest === void 0 && data.geoPointDest) resolved = { ...resolved, geoPointDest: data.geoPointDest };
2447
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 };
2448
2676
  let outcome;
2449
2677
  zoomCap = null;
2450
2678
  if (zoomMemory && config.code && !config.renderFn) {
@@ -2580,10 +2808,25 @@ function createChartHost(container, config) {
2580
2808
  clearPress(container);
2581
2809
  disposeLasso();
2582
2810
  subs.clear();
2811
+ if (dimVarSet) {
2812
+ try {
2813
+ container.style?.removeProperty(DIM_OPACITY_VAR);
2814
+ } catch {
2815
+ }
2816
+ dimVarSet = false;
2817
+ }
2583
2818
  annotationLayer.destroy();
2584
2819
  unpinScrolledAxis(container);
2585
2820
  flushZoom();
2821
+ followGlyphs(false);
2586
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
+ }
2587
2830
  }
2588
2831
  };
2589
2832
  return host;
@@ -8,7 +8,7 @@ import {
8
8
  SELECTION_RULE_ATTR,
9
9
  SELECTION_RULE_CLICKED_MARKS,
10
10
  XFILTER_REFRESH_EVENT
11
- } from "./chunk-MR6TL7QZ.mjs";
11
+ } from "./chunk-YDHXOIAI.mjs";
12
12
 
13
13
  // src/selection.ts
14
14
  function rowIdxsFromMark(el) {
@@ -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-QIRLWJBI.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-IURROJT7.mjs";
7
+ } from "./chunk-DJ5IJV57.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-MR6TL7QZ.mjs";
15
+ } from "./chunk-YDHXOIAI.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-MR6TL7QZ.mjs";
4
+ } from "./chunk-YDHXOIAI.mjs";
5
5
 
6
6
  // src/fit.ts
7
7
  var SCROLL_SLACK_PX = 8;
@@ -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-MR6TL7QZ.mjs";
4
+ } from "./chunk-YDHXOIAI.mjs";
5
5
 
6
6
  // src/snapshot.ts
7
7
  var DEFAULT_TIMEOUT_MS = 2e3;
@@ -4,7 +4,7 @@ import {
4
4
  LEGEND_MARK_CLASS,
5
5
  MARK_CLASS,
6
6
  ROW_IDX_ATTR
7
- } from "./chunk-MR6TL7QZ.mjs";
7
+ } from "./chunk-YDHXOIAI.mjs";
8
8
 
9
9
  // src/hitTargets.ts
10
10
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -34,6 +34,7 @@ var MARK_SELECTED_CLASS = "lch-mark-selected";
34
34
  var ACTIVE_TICK_CLASS = "lch-tick-active";
35
35
  var DIM_OPACITY_VAR = "--lch-dim-opacity";
36
36
  var DIM_OPACITY_DEFAULT = 0.25;
37
+ var DIM_OPACITY_MIN = 0.01;
37
38
  function chartOwnsTimeline(container) {
38
39
  return !!container && (!!container[CONTAINER_SLOT_XF_CLEAR] || !!container[CONTAINER_SLOT_ANIM_STOP]);
39
40
  }
@@ -97,6 +98,7 @@ export {
97
98
  ACTIVE_TICK_CLASS,
98
99
  DIM_OPACITY_VAR,
99
100
  DIM_OPACITY_DEFAULT,
101
+ DIM_OPACITY_MIN,
100
102
  chartOwnsTimeline,
101
103
  periodTickSuppressesFeedback,
102
104
  ANIM_PLAY_SPEED_DEFAULT,
package/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  svgNaturalSize,
16
16
  svgToDataUrl,
17
17
  tableHostOf
18
- } from "./chunk-ZGQURBRJ.mjs";
18
+ } from "./chunk-UBB5LNH4.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-HZNMMRQ3.mjs";
100
+ } from "./chunk-562LF7XM.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-QIRLWJBI.mjs";
125
+ } from "./chunk-OG6RR2AJ.mjs";
126
126
  import {
127
127
  MIN_HIT_BAND_PX,
128
128
  censusHitBands,
129
129
  ensureCrossfilterHitTargets,
130
130
  hitBandFlag
131
- } from "./chunk-7HKVV5T5.mjs";
131
+ } from "./chunk-XNS34AMM.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-IURROJT7.mjs";
156
+ } from "./chunk-DJ5IJV57.mjs";
157
157
  import {
158
158
  AXIS_PIN_BAND_PAD_PX,
159
159
  AXIS_PIN_MAX_BAND_FRACTION,
@@ -179,7 +179,7 @@ import {
179
179
  scrollFitFor,
180
180
  svgInkReach,
181
181
  unpinScrolledAxis
182
- } from "./chunk-BQFG5HWZ.mjs";
182
+ } from "./chunk-QHIPRAZK.mjs";
183
183
  import {
184
184
  _3,
185
185
  l,
@@ -209,6 +209,7 @@ import {
209
209
  CONTAINER_SLOT_XF_CLEAR,
210
210
  CONTROL_CLASS,
211
211
  DIM_OPACITY_DEFAULT,
212
+ DIM_OPACITY_MIN,
212
213
  DIM_OPACITY_VAR,
213
214
  FLIP_DIRECTION_DEFAULT,
214
215
  FLIP_INTERVAL_MAX_MS,
@@ -236,7 +237,7 @@ import {
236
237
  chartOwnsTimeline,
237
238
  periodTickSuppressesFeedback,
238
239
  withoutViewOnlyKeys
239
- } from "./chunk-MR6TL7QZ.mjs";
240
+ } from "./chunk-YDHXOIAI.mjs";
240
241
  import "./chunk-GHODWOAL.mjs";
241
242
 
242
243
  // src/optionsVocabulary.ts
@@ -2161,6 +2162,7 @@ export {
2161
2162
  DEFAULT_FILTER_MATCH_STYLE,
2162
2163
  DEFAULT_MODEL_SUFFIX,
2163
2164
  DIM_OPACITY_DEFAULT,
2165
+ DIM_OPACITY_MIN,
2164
2166
  DIM_OPACITY_VAR,
2165
2167
  DRAG_NOT_CLICK_PX,
2166
2168
  DUPLICATE_GENERATE_WINDOW_MS,
@@ -2,13 +2,13 @@
2
2
  import {
3
3
  chartSvgOf,
4
4
  tableHostOf
5
- } from "./chunk-ZGQURBRJ.mjs";
5
+ } from "./chunk-UBB5LNH4.mjs";
6
6
  import {
7
7
  FIT_CONTENT_SELECTOR,
8
8
  ctmScaleOf,
9
9
  isPhantomBox
10
- } from "./chunk-BQFG5HWZ.mjs";
11
- import "./chunk-MR6TL7QZ.mjs";
10
+ } from "./chunk-QHIPRAZK.mjs";
11
+ import "./chunk-YDHXOIAI.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-DTOGTKAJ.mjs";
18
- import "./chunk-QIRLWJBI.mjs";
19
- import "./chunk-IURROJT7.mjs";
20
- import "./chunk-MR6TL7QZ.mjs";
17
+ } from "./chunk-GOIG756B.mjs";
18
+ import "./chunk-OG6RR2AJ.mjs";
19
+ import "./chunk-DJ5IJV57.mjs";
20
+ import "./chunk-YDHXOIAI.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-DTOGTKAJ.mjs";
16
+ } from "./chunk-GOIG756B.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-HZNMMRQ3.mjs";
26
- import "./chunk-QIRLWJBI.mjs";
27
- import "./chunk-7HKVV5T5.mjs";
28
- import "./chunk-IURROJT7.mjs";
29
- import "./chunk-BQFG5HWZ.mjs";
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";
30
30
  import "./chunk-KQN5C7SZ.mjs";
31
31
  import "./chunk-A2GMXZP7.mjs";
32
- import "./chunk-MR6TL7QZ.mjs";
32
+ import "./chunk-YDHXOIAI.mjs";
33
33
  import "./chunk-GHODWOAL.mjs";
34
34
 
35
35
  // src/react.tsx
@@ -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
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-MR6TL7QZ.mjs";
7
+ } from "./chunk-YDHXOIAI.mjs";
8
8
  import "./chunk-GHODWOAL.mjs";
9
9
 
10
10
  // src/testing/interact.ts
@@ -72,6 +72,7 @@ export declare const MARK_SELECTED_CLASS = "lch-mark-selected";
72
72
  export declare const ACTIVE_TICK_CLASS = "lch-tick-active";
73
73
  export declare const DIM_OPACITY_VAR = "--lch-dim-opacity";
74
74
  export declare const DIM_OPACITY_DEFAULT = 0.25;
75
+ export declare const DIM_OPACITY_MIN = 0.01;
75
76
  export declare function chartOwnsTimeline(container: any): boolean;
76
77
  export declare function periodTickSuppressesFeedback(isAxisTick: boolean, ownsTimeline: boolean): boolean;
77
78
  export declare const ANIM_PLAY_SPEED_DEFAULT = 1000;
@@ -159,6 +160,13 @@ export interface RenderOptions {
159
160
  cultureCode?: string;
160
161
  allowTooltips?: boolean;
161
162
  noDataText?: string;
163
+ /** How far a selection dims the marks outside it, as a share of full opacity (DIM_OPACITY_MIN-1). Absent: the
164
+ * stylesheet decides - DIM_OPACITY_DEFAULT, or whatever the page sets on --lch-dim-opacity. */
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;
162
170
  animAutoPlay?: boolean;
163
171
  animPlaySpeedMs?: number;
164
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
@@ -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, 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, 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";
@@ -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.87",
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",