@bicharts/chart-host 0.6.41 → 0.6.43

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.
@@ -2030,6 +2030,30 @@ function stripJsComments(code) {
2030
2030
  }
2031
2031
 
2032
2032
  // src/selection.ts
2033
+ function rowIdxsFromMark(el) {
2034
+ if (!el) return [];
2035
+ const raw = el.getAttribute("data-row-idx");
2036
+ if (!raw) return [];
2037
+ const out = [];
2038
+ for (const tok of raw.split(",")) {
2039
+ const n = parseInt(tok.trim(), 10);
2040
+ if (!isNaN(n) && n >= 0) out.push(n);
2041
+ }
2042
+ return out;
2043
+ }
2044
+ function nextSelection(current, rowIdxs, mods = {}) {
2045
+ const cur = Array.from(current ?? []);
2046
+ if (mods.ctrl || mods.shift) {
2047
+ const next = new Set(cur);
2048
+ for (const r2 of rowIdxs) {
2049
+ if (next.has(r2)) next.delete(r2);
2050
+ else next.add(r2);
2051
+ }
2052
+ return Array.from(next);
2053
+ }
2054
+ const same = cur.length === rowIdxs.length && rowIdxs.every((r2) => cur.includes(r2));
2055
+ return same ? [] : rowIdxs.slice();
2056
+ }
2033
2057
  function isInsideControl(target, container) {
2034
2058
  let el = target;
2035
2059
  if (el && el.nodeType !== 1) el = el.parentElement ?? el.parentNode ?? null;
@@ -2121,17 +2145,6 @@ function createMarkResolver(env) {
2121
2145
  if (m2 && ev) return refineHitCorridor(m2, ev.clientX, ev.clientY, sel);
2122
2146
  return m2;
2123
2147
  };
2124
- const rowIdxsFromMark = (el) => {
2125
- if (!el) return [];
2126
- const raw = el.getAttribute("data-row-idx");
2127
- if (!raw) return [];
2128
- const out = [];
2129
- for (const tok of raw.split(",")) {
2130
- const n = parseInt(tok.trim(), 10);
2131
- if (!isNaN(n) && n >= 0) out.push(n);
2132
- }
2133
- return out;
2134
- };
2135
2148
  const penetrateOverlayAt = (x3, y4, sel) => {
2136
2149
  try {
2137
2150
  const stack = doc.elementsFromPoint(x3, y4);
@@ -2186,11 +2199,149 @@ function createMarkResolver(env) {
2186
2199
  };
2187
2200
  return { isInvisibleStrokePath, distToPathCenterline, refineHitCorridor, findMark, rowIdxsFromMark, penetrateOverlayAt, resolveByGeometry };
2188
2201
  }
2202
+ function collectD3GlyphCenters(container, baseRect, out) {
2203
+ const Point = container.ownerDocument?.defaultView?.DOMPoint ?? globalThis.DOMPoint;
2204
+ const marks = container.querySelectorAll(`.${MARK_CLASS}.${MARK_SELECTED_CLASS}`);
2205
+ const rects = [];
2206
+ for (let i = 0; i < marks.length; i++) {
2207
+ const r2 = marks[i].getBoundingClientRect();
2208
+ rects.push(r2);
2209
+ }
2210
+ const CONTAINMENT_CAP = 300;
2211
+ const EPS2 = 1;
2212
+ const filterContainers = marks.length <= CONTAINMENT_CAP;
2213
+ const enclosesAnother = (ci) => {
2214
+ const c2 = rects[ci];
2215
+ const cArea = c2.width * c2.height;
2216
+ if (cArea <= 0) return false;
2217
+ for (let mj = 0; mj < rects.length; mj++) {
2218
+ if (mj === ci) continue;
2219
+ const m2 = rects[mj];
2220
+ const mArea = m2.width * m2.height;
2221
+ if (mArea <= 0 || mArea >= cArea) continue;
2222
+ if (m2.left >= c2.left - EPS2 && m2.right <= c2.right + EPS2 && m2.top >= c2.top - EPS2 && m2.bottom <= c2.bottom + EPS2) return true;
2223
+ }
2224
+ return false;
2225
+ };
2226
+ for (let i = 0; i < marks.length; i++) {
2227
+ const el = marks[i];
2228
+ const r2 = rects[i];
2229
+ if (r2.width === 0 && r2.height === 0) continue;
2230
+ if (filterContainers && enclosesAnother(i)) continue;
2231
+ const tag = el.tagName.toLowerCase();
2232
+ let sx;
2233
+ let sy;
2234
+ const cxA = el.getAttribute("data-cx"), cyA = el.getAttribute("data-cy");
2235
+ if (cxA != null && cyA != null && typeof el.getScreenCTM === "function") {
2236
+ const ctm = el.getScreenCTM();
2237
+ const cxN = parseFloat(cxA), cyN = parseFloat(cyA);
2238
+ if (ctm && isFinite(cxN) && isFinite(cyN)) {
2239
+ const p2 = new Point(cxN, cyN).matrixTransform(ctm);
2240
+ sx = p2.x;
2241
+ sy = p2.y;
2242
+ }
2243
+ }
2244
+ if (sx === void 0 && tag === "path" && typeof el.getPointAtLength === "function") {
2245
+ try {
2246
+ const len = el.getTotalLength();
2247
+ const ctm = el.getScreenCTM();
2248
+ if (len > 0 && ctm) {
2249
+ const dAttr = el.getAttribute("d") || "";
2250
+ const isArcSector = /[Aa]/.test(dAttr) && !/[CcSsQqTt]/.test(dAttr);
2251
+ let local;
2252
+ if (isArcSector) {
2253
+ const N2 = 24;
2254
+ let ax = 0, ay = 0;
2255
+ for (let k3 = 0; k3 < N2; k3++) {
2256
+ const sp = el.getPointAtLength(len * k3 / N2);
2257
+ ax += sp.x;
2258
+ ay += sp.y;
2259
+ }
2260
+ const cand = new Point(ax / N2, ay / N2);
2261
+ let inside = true;
2262
+ try {
2263
+ if (typeof el.isPointInFill === "function") {
2264
+ inside = el.isPointInFill(cand);
2265
+ }
2266
+ } catch {
2267
+ inside = true;
2268
+ }
2269
+ if (inside) local = cand;
2270
+ }
2271
+ if (!local) local = el.getPointAtLength(len / 2);
2272
+ const p2 = local.matrixTransform(ctm);
2273
+ sx = p2.x;
2274
+ sy = p2.y;
2275
+ }
2276
+ } catch {
2277
+ }
2278
+ }
2279
+ if (sx === void 0 || sy === void 0) {
2280
+ const isRect = tag === "rect";
2281
+ sx = isRect ? r2.left + r2.width * 0.8 : r2.left + r2.width / 2;
2282
+ sy = isRect ? r2.top + r2.height * 0.2 : r2.top + r2.height / 2;
2283
+ }
2284
+ out.push({ x: sx - baseRect.left, y: sy - baseRect.top });
2285
+ }
2286
+ }
2287
+ var SELECTION_GLYPH_CLASS = "lch-sel-glyph";
2288
+ function clearSelectionGlyphs(overlay) {
2289
+ if (overlay) {
2290
+ while (overlay.firstChild) overlay.removeChild(overlay.firstChild);
2291
+ }
2292
+ }
2293
+ function paintSelectionGlyphs(overlay, centers, style) {
2294
+ const doc = overlay.ownerDocument;
2295
+ const frag = doc.createDocumentFragment();
2296
+ const count = Math.min(centers.length, style.max ?? 600);
2297
+ for (let i = 0; i < count; i++) {
2298
+ const c2 = centers[i];
2299
+ const g3 = doc.createElement("span");
2300
+ g3.className = SELECTION_GLYPH_CLASS;
2301
+ g3.textContent = style.symbol;
2302
+ g3.style.position = "absolute";
2303
+ g3.style.left = c2.x + "px";
2304
+ g3.style.top = c2.y + "px";
2305
+ g3.style.transform = "translate(-50%, -50%)";
2306
+ g3.style.font = "700 11px/1 system-ui, -apple-system, Segoe UI, sans-serif";
2307
+ g3.style.color = style.color;
2308
+ const halo = style.halo;
2309
+ g3.style.textShadow = `-1px -1px 1px ${halo}, 1px -1px 1px ${halo}, -1px 1px 1px ${halo}, 1px 1px 1px ${halo}, 0 0 2px ${halo}`;
2310
+ g3.style.pointerEvents = "none";
2311
+ frag.appendChild(g3);
2312
+ }
2313
+ overlay.appendChild(frag);
2314
+ return count;
2315
+ }
2189
2316
 
2190
2317
  // src/hitTargets.ts
2191
2318
  var SVG_NS = "http://www.w3.org/2000/svg";
2192
2319
  var HIT_ATTR = "data-lch-hit";
2193
2320
  var ELEMENT_CAP2 = 5e3;
2321
+ var mapsPresentationAttributes = /* @__PURE__ */ new WeakMap();
2322
+ function engineMapsPresentationAttributes(ownerDoc, view) {
2323
+ if (!ownerDoc || typeof ownerDoc !== "object") return false;
2324
+ const known = mapsPresentationAttributes.get(ownerDoc);
2325
+ if (known !== void 0) return known;
2326
+ let maps = false;
2327
+ try {
2328
+ const host = ownerDoc.documentElement;
2329
+ const probe = ownerDoc.createElementNS(SVG_NS, "svg");
2330
+ const shape = ownerDoc.createElementNS(SVG_NS, "rect");
2331
+ shape.setAttribute("fill", "none");
2332
+ probe.appendChild(shape);
2333
+ host.appendChild(probe);
2334
+ try {
2335
+ maps = String(view.getComputedStyle(shape).fill ?? "").trim().toLowerCase() === "none";
2336
+ } finally {
2337
+ host.removeChild(probe);
2338
+ }
2339
+ } catch {
2340
+ maps = false;
2341
+ }
2342
+ mapsPresentationAttributes.set(ownerDoc, maps);
2343
+ return maps;
2344
+ }
2194
2345
  function ensureCrossfilterHitTargets(container, doc) {
2195
2346
  const report = { tagged: 0, hitRects: 0, peFlips: 0, peStrokeOnly: 0 };
2196
2347
  try {
@@ -2253,7 +2404,7 @@ function ensureCrossfilterHitTargets(container, doc) {
2253
2404
  } else {
2254
2405
  const cs = computed(el);
2255
2406
  if (cs && cs.pointerEvents === "none") {
2256
- const fill = String(el.getAttribute?.("fill") ?? cs.fill ?? "").trim().toLowerCase();
2407
+ const fill = String((engineMapsPresentationAttributes(ownerDoc, view) ? cs.fill : el.getAttribute?.("fill") ?? cs.fill) ?? "").trim().toLowerCase();
2257
2408
  const unfilled = fill === "none" || fill === "transparent" || fill === "rgba(0, 0, 0, 0)";
2258
2409
  if (el.style) el.style.pointerEvents = unfilled ? "stroke" : "all";
2259
2410
  report.peFlips++;
@@ -3625,17 +3776,10 @@ function createChartHost(container, config) {
3625
3776
  const rows = parseRowIdxs(el.getAttribute(ROW_IDX_ATTR));
3626
3777
  if (!rows.length) return;
3627
3778
  const tick = el.classList?.contains?.(AXIS_FILTER_CLASS) ? el : null;
3628
- if (e?.ctrlKey || e?.metaKey || e?.shiftKey) {
3629
- const next = new Set(current ?? []);
3630
- for (const r2 of rows) {
3631
- if (next.has(r2)) next.delete(r2);
3632
- else next.add(r2);
3633
- }
3634
- notify(Array.from(next), "user", tick);
3635
- return;
3636
- }
3637
- const same = !!current && current.length === rows.length && rows.every((r2) => current.includes(r2));
3638
- notify(same ? [] : rows, "user", same ? null : tick);
3779
+ const multi = !!(e?.ctrlKey || e?.metaKey || e?.shiftKey);
3780
+ const next = nextSelection(current, rows, { ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey });
3781
+ const toggledOff = !multi && next.length === 0;
3782
+ notify(next, "user", toggledOff ? null : tick);
3639
3783
  };
3640
3784
  container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
3641
3785
  container.addEventListener("click", onClick);
@@ -4058,8 +4202,14 @@ export {
4058
4202
  Bn,
4059
4203
  buildRenderPayload,
4060
4204
  stripJsComments,
4205
+ rowIdxsFromMark,
4206
+ nextSelection,
4061
4207
  isInsideControl,
4062
4208
  createMarkResolver,
4209
+ collectD3GlyphCenters,
4210
+ SELECTION_GLYPH_CLASS,
4211
+ clearSelectionGlyphs,
4212
+ paintSelectionGlyphs,
4063
4213
  ensureCrossfilterHitTargets,
4064
4214
  censusMarks,
4065
4215
  isBlankRender,
package/dist/index.mjs CHANGED
@@ -65,6 +65,7 @@ import {
65
65
  SCROLL_SLACK_PX,
66
66
  SEASONAL_MARKERS_DEFAULT,
67
67
  SELECTION_ACTIVE_CLASS,
68
+ SELECTION_GLYPH_CLASS,
68
69
  VALUE_AXIS_BASELINE_DEFAULT,
69
70
  VALUE_AXIS_MIN_FIT,
70
71
  VIEW_ONLY_UI_STATE_KEYS,
@@ -86,6 +87,8 @@ import {
86
87
  censusValuePlacement,
87
88
  chartOwnsTimeline,
88
89
  clearGeoCache,
90
+ clearSelectionGlyphs,
91
+ collectD3GlyphCenters,
89
92
  colourSpreadFlag,
90
93
  compileRenderFn,
91
94
  compositeOver,
@@ -117,7 +120,9 @@ import {
117
120
  loadGeo,
118
121
  measureContainerBoxes,
119
122
  needsScroll,
123
+ nextSelection,
120
124
  noopViewStateProvider,
125
+ paintSelectionGlyphs,
121
126
  periodTickSuppressesFeedback,
122
127
  pillBacksGlyph,
123
128
  pinScrolledAxis,
@@ -129,6 +134,7 @@ import {
129
134
  requiredD3Plugins,
130
135
  resolveAutoColorScale,
131
136
  resolveOptions,
137
+ rowIdxsFromMark,
132
138
  scrollFitFor,
133
139
  sessionViewStateProvider,
134
140
  stripEsmExports,
@@ -140,7 +146,7 @@ import {
140
146
  unpinScrolledAxis,
141
147
  valuePlacementFlag,
142
148
  withoutViewOnlyKeys
143
- } from "./chunk-NMEXSUOR.mjs";
149
+ } from "./chunk-MVM2R7K7.mjs";
144
150
  import "./chunk-A2GMXZP7.mjs";
145
151
  import "./chunk-GHODWOAL.mjs";
146
152
 
@@ -2629,6 +2635,7 @@ export {
2629
2635
  SCROLL_SLACK_PX,
2630
2636
  SEASONAL_MARKERS_DEFAULT,
2631
2637
  SELECTION_ACTIVE_CLASS,
2638
+ SELECTION_GLYPH_CLASS,
2632
2639
  TRANSPORT_LOST_CHECKING_MESSAGE,
2633
2640
  VALUE_AXIS_BASELINE_DEFAULT,
2634
2641
  VALUE_AXIS_MIN_FIT,
@@ -2662,7 +2669,9 @@ export {
2662
2669
  chartOwnsTimeline,
2663
2670
  chooserFitsViewport,
2664
2671
  clearGeoCache,
2672
+ clearSelectionGlyphs,
2665
2673
  codeReadsDatesInLocalTime,
2674
+ collectD3GlyphCenters,
2666
2675
  colourSpreadFlag,
2667
2676
  compileRenderFn,
2668
2677
  compileTrivialSource,
@@ -2725,12 +2734,14 @@ export {
2725
2734
  needsScroll,
2726
2735
  newQualifyGroupState,
2727
2736
  newQualifyRefusalGroupState,
2737
+ nextSelection,
2728
2738
  noopViewStateProvider,
2729
2739
  normaliseAggregation,
2730
2740
  normaliseProductFeedbackText,
2731
2741
  normalizeFilterTerm,
2732
2742
  orderMostDistinct,
2733
2743
  orderRefusalsForDisplay,
2744
+ paintSelectionGlyphs,
2734
2745
  parsePendingGenerate,
2735
2746
  pendingRecoveryNextDelayMs,
2736
2747
  pendingRecoveryRemainingMs,
@@ -2768,6 +2779,7 @@ export {
2768
2779
  requiredD3Plugins,
2769
2780
  resolveAutoColorScale,
2770
2781
  resolveOptions,
2782
+ rowIdxsFromMark,
2771
2783
  sameMarker,
2772
2784
  scrollFitFor,
2773
2785
  servedCorrelationProves,
package/dist/react.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  loadGeo,
8
8
  syncMemberSelection,
9
9
  toSourceRows
10
- } from "./chunk-NMEXSUOR.mjs";
10
+ } from "./chunk-MVM2R7K7.mjs";
11
11
  import "./chunk-A2GMXZP7.mjs";
12
12
  import "./chunk-GHODWOAL.mjs";
13
13
 
@@ -9,7 +9,7 @@ export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./
9
9
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
10
10
  export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
11
11
  export { createChartGroup, toSourceRows, syncMemberSelection, type ChartGroup, type ChartGroupMember, type ChartGroupSelection, type ChartGroupChange, type ChartGroupMemberOptions, type ChartGroupSourceOptions, type ChartGroupPayload } from "./group";
12
- export { createMarkResolver, isInsideControl, type MarkResolver, type MarkResolverEnv } from "./selection";
12
+ export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
13
13
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
14
14
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, type SnapshotOptions } from "./snapshot";
15
15
  export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
@@ -1,3 +1,30 @@
1
+ /**
2
+ * The rows a mark names: its data-row-idx read as a comma list of non-negative whole
3
+ * numbers. A token that is not one names no row and is skipped; no attribute, no rows.
4
+ */
5
+ export declare function rowIdxsFromMark(el: Element | null): number[];
6
+ /** The keys held during a click. Cmd counts as Ctrl. */
7
+ export interface SelectionModifiers {
8
+ ctrl?: boolean;
9
+ shift?: boolean;
10
+ }
11
+ /**
12
+ * THE SELECTION A CLICK LEAVES - the one rule every host applies to a click on a mark that names
13
+ * `rowIdxs`, from the selection it holds (`current`).
14
+ *
15
+ * With Ctrl/Cmd or Shift the semantics are TOGGLE-PER-ROW, not union: each clicked row that is
16
+ * selected is removed and each that is not is added, so Ctrl-clicking a selected mark takes it
17
+ * off, and a mark spanning several rows (a legend swatch) toggles each of them - a naive union
18
+ * would leave a mark lit after the reader Ctrl-clicked it off. Toggling the last row off is a
19
+ * legitimate way to reach empty.
20
+ *
21
+ * Without a modifier a click REPLACES the selection, and a click on exactly the current selection
22
+ * - the same rows, compared with the selection the host holds now, never with a remembered click -
23
+ * clears it: the other half of the gesture. So a plain click that returns [] is a toggle-off.
24
+ * Comparing with a remembered click instead went stale whenever the selection changed under it (a
25
+ * clear from elsewhere, a re-derived selection), and the next click on the mark then did nothing.
26
+ */
27
+ export declare function nextSelection(current: Iterable<number> | null | undefined, rowIdxs: readonly number[], mods?: SelectionModifiers): number[];
1
28
  /**
2
29
  * True when a click's target sits inside a reader-operated control the chart drew (CONTROL_CLASS
3
30
  * on the target or an ancestor), looking no further out than `container` - a page that wraps the
@@ -20,3 +47,37 @@ export interface MarkResolver {
20
47
  resolveByGeometry(x: number, y: number, sel: string): HTMLElement | null;
21
48
  }
22
49
  export declare function createMarkResolver(env: MarkResolverEnv): MarkResolver;
50
+ /** A glyph's centre, in pixels from the overlay's own top-left corner. */
51
+ export interface GlyphCenter {
52
+ x: number;
53
+ y: number;
54
+ }
55
+ /**
56
+ * Where each SELECTED mark in `container` gets its glyph (every `.d3-mark` that also carries the
57
+ * selected class), pushed onto `out` in document order, relative to `baseRect` (the overlay's own
58
+ * client rect). Throws what the platform throws; a host that must not fail its paint catches it.
59
+ */
60
+ export declare function collectD3GlyphCenters(container: Element, baseRect: {
61
+ left: number;
62
+ top: number;
63
+ }, out: GlyphCenter[]): void;
64
+ /** The class every painted selection glyph carries. */
65
+ export declare const SELECTION_GLYPH_CLASS = "lch-sel-glyph";
66
+ /** Removes every glyph (every child) from the overlay. */
67
+ export declare function clearSelectionGlyphs(overlay: Element | null | undefined): void;
68
+ export interface SelectionGlyphStyle {
69
+ /** The character drawn at each centre. */
70
+ symbol: string;
71
+ /** The glyph's colour. */
72
+ color: string;
73
+ /** The halo drawn on every side of it, so it reads on a dark and a light mark alike. */
74
+ halo: string;
75
+ /** At most this many are drawn. Default 600, which keeps the DOM light on a huge selection. */
76
+ max?: number;
77
+ }
78
+ /**
79
+ * Draws one glyph per centre (up to `max`) into `overlay`, positioned from the overlay's own
80
+ * top-left corner and never hit-tested. Returns how many it drew. It does not clear the overlay
81
+ * first: a host clears it (clearSelectionGlyphs) before deciding whether to paint at all.
82
+ */
83
+ export declare function paintSelectionGlyphs(overlay: Element, centers: readonly GlyphCenter[], style: SelectionGlyphStyle): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.41",
3
+ "version": "0.6.43",
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",