@bicharts/chart-host 0.6.112 → 0.6.115

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.
@@ -4,11 +4,11 @@ import {
4
4
  claimParkedLasso,
5
5
  installLasso,
6
6
  lassoEffect
7
- } from "./chunk-IL5M7K6Z.mjs";
7
+ } from "./chunk-CVTCR7HQ.mjs";
8
8
  import {
9
9
  censusHitBands,
10
10
  ensureCrossfilterHitTargets
11
- } from "./chunk-AEO5SFEQ.mjs";
11
+ } from "./chunk-YOCYWON4.mjs";
12
12
  import {
13
13
  clearPress,
14
14
  clearSelectionGlyphs,
@@ -24,26 +24,26 @@ import {
24
24
  selectionRuleOf,
25
25
  trackPress,
26
26
  validRowSet
27
- } from "./chunk-KGVNZR23.mjs";
27
+ } from "./chunk-PQKLWYKK.mjs";
28
28
  import {
29
29
  clipWindowOf,
30
30
  fitRenderedChart,
31
31
  rememberScroll,
32
32
  takeScroll,
33
33
  unpinScrolledAxis
34
- } from "./chunk-7XT5OJ6M.mjs";
34
+ } from "./chunk-AQO4ROMU.mjs";
35
35
  import {
36
36
  geoFromCache
37
37
  } from "./chunk-5YSZABCS.mjs";
38
38
  import {
39
39
  Fn,
40
+ Or,
40
41
  Q,
41
42
  Rt,
42
43
  Yn,
43
- _r,
44
44
  tt,
45
45
  xt
46
- } from "./chunk-FIG5DIW7.mjs";
46
+ } from "./chunk-VPFP35XI.mjs";
47
47
  import {
48
48
  ACTIVE_TICK_CLASS,
49
49
  ANIM_LOOP_DELAY_DEFAULT,
@@ -88,7 +88,7 @@ import {
88
88
  XFILTER_REFRESH_EVENT,
89
89
  chartOwnsTimeline,
90
90
  periodTickSuppressesFeedback
91
- } from "./chunk-KY4FI2HA.mjs";
91
+ } from "./chunk-GXR7T6G5.mjs";
92
92
 
93
93
  // src/labelContrast.ts
94
94
  function parseRGBA(c) {
@@ -554,6 +554,252 @@ function resolveAutoColorScale(p) {
554
554
  return memoValue;
555
555
  }
556
556
 
557
+ // src/selectionMarker.ts
558
+ var SELECTION_MARKER_DEFAULT = "auto";
559
+ var MAP_SELECTION_MARKER_DEFAULT = "outline";
560
+ function readSelectionMarker(raw) {
561
+ const v = typeof raw === "string" ? raw.trim().toLowerCase() : "";
562
+ return v === "none" || v === "dot" || v === "check" || v === "outline" || v === "auto" ? v : SELECTION_MARKER_DEFAULT;
563
+ }
564
+ function readMapSelectionMarker(raw) {
565
+ const v = typeof raw === "string" ? raw.trim().toLowerCase() : "";
566
+ return v === "none" || v === "dot" || v === "check" || v === "outline" ? v : MAP_SELECTION_MARKER_DEFAULT;
567
+ }
568
+ var REGION_JOIN_COLUMN = "__geoIso__";
569
+ function isRegionMapData(data) {
570
+ const cols = data?.columns;
571
+ if (!Array.isArray(cols)) return false;
572
+ return cols.some((c) => !!c && c.name === REGION_JOIN_COLUMN);
573
+ }
574
+ var NEUTRAL_CENTRE_RAMP_SELECTOR = '.llm-value-ramp[data-kind="diverging"]';
575
+ function hasNeutralCentreRamp(container) {
576
+ try {
577
+ return !!container?.querySelector?.(NEUTRAL_CENTRE_RAMP_SELECTOR);
578
+ } catch {
579
+ return false;
580
+ }
581
+ }
582
+ function selectionMarkerFor(facts) {
583
+ if (facts.regionMap) return readMapSelectionMarker(facts.mapChoice);
584
+ const c = readSelectionMarker(facts.choice);
585
+ if (c !== "auto") return c;
586
+ return facts.neutralCentreRamp ? "outline" : "none";
587
+ }
588
+ var SELECTION_OUTLINE_CLASS = "lch-sel-outline";
589
+ var SVG_NS = "http://www.w3.org/2000/svg";
590
+ var AREA_TAGS = /* @__PURE__ */ new Set(["path", "rect", "polygon"]);
591
+ var UNFILLED = /^(none|transparent|rgba\([^)]*,\s*0(\.0+)?\s*\))$/i;
592
+ function isAreaMark(el) {
593
+ const tag = (el.localName || el.tagName || "").toLowerCase();
594
+ if (!AREA_TAGS.has(tag)) return false;
595
+ let fill = "", fillOpacity = "";
596
+ try {
597
+ const cs = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
598
+ fill = (cs?.fill || "").trim();
599
+ fillOpacity = (cs?.fillOpacity || "").trim();
600
+ } catch {
601
+ }
602
+ const style = el.style;
603
+ if (!fill) fill = (style?.fill || el.getAttribute("fill") || "").trim();
604
+ if (!fillOpacity) fillOpacity = (style?.fillOpacity || el.getAttribute("fill-opacity") || "").trim();
605
+ if (fill && UNFILLED.test(fill)) return false;
606
+ if (fillOpacity !== "" && parseFloat(fillOpacity) === 0) return false;
607
+ return true;
608
+ }
609
+ function collectOutlineMarks(container, filter) {
610
+ const picked = [];
611
+ const seen = /* @__PURE__ */ new Set();
612
+ const add = (el) => {
613
+ if (seen.has(el)) return;
614
+ seen.add(el);
615
+ if (isAreaMark(el)) picked.push(el);
616
+ };
617
+ for (const m of Array.from(container.querySelectorAll(`.${MARK_CLASS}.${MARK_SELECTED_CLASS}`))) {
618
+ if (filter?.shown && !filter.shown(m)) continue;
619
+ if ((m.localName || "").toLowerCase() === "g") {
620
+ for (const s of Array.from(m.querySelectorAll("path, rect, polygon"))) add(s);
621
+ } else add(m);
622
+ }
623
+ if (picked.length < 2 || picked.length > 300) return picked;
624
+ const EPS2 = 1;
625
+ const rects = picked.map((m) => {
626
+ try {
627
+ return m.getBoundingClientRect();
628
+ } catch {
629
+ return null;
630
+ }
631
+ });
632
+ return picked.filter((_, i) => {
633
+ const c = rects[i];
634
+ if (!c) return true;
635
+ const cArea = c.width * c.height;
636
+ if (!(cArea > 0)) return true;
637
+ for (let j = 0; j < rects.length; j++) {
638
+ const r = rects[j];
639
+ if (j === i || !r) continue;
640
+ const a = r.width * r.height;
641
+ if (!(a > 0) || a >= cArea) continue;
642
+ if (r.left >= c.left - EPS2 && r.right <= c.right + EPS2 && r.top >= c.top - EPS2 && r.bottom <= c.bottom + EPS2) return false;
643
+ }
644
+ return true;
645
+ });
646
+ }
647
+ function outlineWindowOf(mark, stop) {
648
+ let acc = null;
649
+ const narrow = (w) => {
650
+ if (!w) return;
651
+ acc = acc ? {
652
+ left: Math.max(acc.left, w.left),
653
+ top: Math.max(acc.top, w.top),
654
+ right: Math.min(acc.right, w.right),
655
+ bottom: Math.min(acc.bottom, w.bottom)
656
+ } : { ...w };
657
+ };
658
+ for (let el = mark; el && el !== stop; el = el.parentElement) {
659
+ try {
660
+ if ((el.localName || "").toLowerCase() === "svg") {
661
+ const r = el.getBoundingClientRect();
662
+ if (r.width > 0 && r.height > 0) narrow({ left: r.left, top: r.top, right: r.right, bottom: r.bottom });
663
+ } else if (el.hasAttribute?.("clip-path")) narrow(clipWindowOf(el));
664
+ } catch {
665
+ }
666
+ }
667
+ return acc;
668
+ }
669
+ function controlBoxesIn(root, opts = {}) {
670
+ const pad = opts.pad ?? 2, join = opts.join ?? 4;
671
+ let boxes = [];
672
+ try {
673
+ for (const el of Array.from(root?.querySelectorAll?.(`.${CONTROL_CLASS}`) ?? [])) {
674
+ const r = el.getBoundingClientRect();
675
+ if (!(r.width > 0) || !(r.height > 0)) continue;
676
+ boxes.push({ left: r.left - pad, top: r.top - pad, right: r.right + pad, bottom: r.bottom + pad });
677
+ }
678
+ } catch {
679
+ return [];
680
+ }
681
+ const near = (a, b) => a.left - join <= b.right && b.left - join <= a.right && a.top - join <= b.bottom && b.top - join <= a.bottom;
682
+ let merged = true;
683
+ while (merged) {
684
+ merged = false;
685
+ const out = [];
686
+ for (const b of boxes) {
687
+ const hit = out.find((o) => near(o, b));
688
+ if (hit) {
689
+ hit.left = Math.min(hit.left, b.left);
690
+ hit.top = Math.min(hit.top, b.top);
691
+ hit.right = Math.max(hit.right, b.right);
692
+ hit.bottom = Math.max(hit.bottom, b.bottom);
693
+ merged = true;
694
+ } else out.push({ ...b });
695
+ }
696
+ boxes = out;
697
+ }
698
+ return boxes;
699
+ }
700
+ function copyGeometry(doc, el) {
701
+ const tag = (el.localName || "").toLowerCase();
702
+ const keep = tag === "path" ? ["d"] : tag === "rect" ? ["x", "y", "width", "height", "rx", "ry"] : tag === "polygon" ? ["points"] : null;
703
+ if (!keep) return null;
704
+ const out = doc.createElementNS(SVG_NS, tag);
705
+ let any = false;
706
+ for (const a of keep) {
707
+ const v = el.getAttribute(a);
708
+ if (v != null && v !== "") {
709
+ out.setAttribute(a, v);
710
+ any = true;
711
+ }
712
+ }
713
+ return any ? out : null;
714
+ }
715
+ var num = (n) => String(Math.round(n * 1e3) / 1e3);
716
+ var maskSeq = 0;
717
+ function paintSelectionOutlines(overlay, marks, style) {
718
+ const doc = overlay.ownerDocument;
719
+ const base = overlay.getBoundingClientRect();
720
+ const width = style.width ?? 2;
721
+ const windowOf = style.windowOf ?? ((m) => outlineWindowOf(m));
722
+ const root = doc.createElementNS(SVG_NS, "svg");
723
+ root.setAttribute("class", SELECTION_OUTLINE_CLASS);
724
+ root.setAttribute("aria-hidden", "true");
725
+ root.setAttribute("focusable", "false");
726
+ root.setAttribute("style", "position:absolute;left:0;top:0;width:1px;height:1px;overflow:visible;pointer-events:none");
727
+ let host = root;
728
+ const holes = (style.avoid ?? []).filter((b) => b.right > b.left && b.bottom > b.top);
729
+ if (holes.length) {
730
+ const id = `${SELECTION_OUTLINE_CLASS}-mask-${++maskSeq}`;
731
+ const defs = doc.createElementNS(SVG_NS, "defs");
732
+ const mask = doc.createElementNS(SVG_NS, "mask");
733
+ mask.setAttribute("id", id);
734
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
735
+ const BIG = 1e5;
736
+ for (const [k, v] of [["x", -BIG], ["y", -BIG], ["width", 2 * BIG], ["height", 2 * BIG]]) mask.setAttribute(k, String(v));
737
+ const open = doc.createElementNS(SVG_NS, "rect");
738
+ for (const [k, v] of [["x", -BIG], ["y", -BIG], ["width", 2 * BIG], ["height", 2 * BIG]]) open.setAttribute(k, String(v));
739
+ open.setAttribute("fill", "#fff");
740
+ mask.appendChild(open);
741
+ for (const b of holes) {
742
+ const r = doc.createElementNS(SVG_NS, "rect");
743
+ r.setAttribute("class", `${SELECTION_OUTLINE_CLASS}-hole`);
744
+ r.setAttribute("x", num(b.left - base.left));
745
+ r.setAttribute("y", num(b.top - base.top));
746
+ r.setAttribute("width", num(b.right - b.left));
747
+ r.setAttribute("height", num(b.bottom - b.top));
748
+ r.setAttribute("fill", "#000");
749
+ mask.appendChild(r);
750
+ }
751
+ defs.appendChild(mask);
752
+ root.appendChild(defs);
753
+ host = doc.createElementNS(SVG_NS, "g");
754
+ host.setAttribute("mask", `url(#${id})`);
755
+ root.appendChild(host);
756
+ }
757
+ let drawn = 0;
758
+ const max = style.max ?? 600;
759
+ for (const m of marks) {
760
+ if (drawn >= max) break;
761
+ let ctm = null;
762
+ try {
763
+ ctm = m.getScreenCTM?.() ?? null;
764
+ } catch {
765
+ ctm = null;
766
+ }
767
+ if (!ctm) continue;
768
+ const shape = copyGeometry(doc, m);
769
+ if (!shape) continue;
770
+ let parent = host;
771
+ let ox = base.left, oy = base.top;
772
+ const w = windowOf(m);
773
+ if (w) {
774
+ if (!(w.right > w.left) || !(w.bottom > w.top)) continue;
775
+ const v = doc.createElementNS(SVG_NS, "svg");
776
+ v.setAttribute("x", num(w.left - base.left));
777
+ v.setAttribute("y", num(w.top - base.top));
778
+ v.setAttribute("width", num(w.right - w.left));
779
+ v.setAttribute("height", num(w.bottom - w.top));
780
+ v.setAttribute("overflow", "hidden");
781
+ host.appendChild(v);
782
+ parent = v;
783
+ ox = w.left;
784
+ oy = w.top;
785
+ }
786
+ const transform = `matrix(${num(ctm.a)} ${num(ctm.b)} ${num(ctm.c)} ${num(ctm.d)} ${num(ctm.e - ox)} ${num(ctm.f - oy)})`;
787
+ for (const [stroke, sw] of [[style.halo, width + 2], [style.color, width]]) {
788
+ const p = shape.cloneNode(false);
789
+ p.setAttribute("transform", transform);
790
+ p.setAttribute("fill", "none");
791
+ p.setAttribute("stroke", stroke);
792
+ p.setAttribute("stroke-width", String(sw));
793
+ p.setAttribute("stroke-linejoin", "round");
794
+ p.setAttribute("vector-effect", "non-scaling-stroke");
795
+ parent.appendChild(p);
796
+ }
797
+ drawn++;
798
+ }
799
+ if (drawn > 0) overlay.appendChild(root);
800
+ return drawn;
801
+ }
802
+
557
803
  // src/exportTable.ts
558
804
  var EXPORT_FORMATS = Object.freeze(["xlsx", "csv"]);
559
805
  var XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
@@ -1169,12 +1415,20 @@ function resolveOptions(p) {
1169
1415
  const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
1170
1416
  return Number.isFinite(n) ? clamp(n, DIM_OPACITY_MIN, 1) : void 0;
1171
1417
  })(),
1172
- // A recognised glyph or none: an unknown string must never reach the painter as a live mode, and "none" is
1173
- // undefined rather than a value, so a host that passes nothing and one that passes "none" resolve alike.
1418
+ // A recognised marker or nothing: an unknown string must never reach the painter as a live mode. "none" and
1419
+ // "auto" read undefined here, as a host that passes nothing does; what tells them apart is selectionMarker.
1174
1420
  selectionGlyph: (() => {
1175
1421
  const v = (p.selectionGlyph == null ? "" : String(p.selectionGlyph)).toLowerCase();
1176
- return v === "dot" || v === "check" ? v : void 0;
1422
+ return v === "dot" || v === "check" || v === "outline" ? v : void 0;
1423
+ })(),
1424
+ // THE WHOLE CHOICE: absent, unknown and "auto" are the default ("auto"), and "none" is a reader's None. The map's
1425
+ // choice beside it, its default "outline" when it made none.
1426
+ selectionMarker: readSelectionMarker(p.selectionGlyph),
1427
+ mapSelectionGlyph: (() => {
1428
+ const v = (p.mapSelectionGlyph == null ? "" : String(p.mapSelectionGlyph)).toLowerCase();
1429
+ return v === "none" || v === "dot" || v === "check" || v === "outline" ? v : void 0;
1177
1430
  })(),
1431
+ mapSelectionMarker: readMapSelectionMarker(p.mapSelectionGlyph),
1178
1432
  // A host FACT from the generate answer, true only for a literal true: silence never suppresses anything.
1179
1433
  suppressSelectionGlyph: p.suppressSelectionGlyph === true ? true : void 0,
1180
1434
  // A host FACT (1.22.0): the chart works from the keyboard. True only for a literal true, like the one above.
@@ -1319,7 +1573,7 @@ function resolvePointChannel(cols, rowObjs, point) {
1319
1573
  let labelCol;
1320
1574
  if (bind) {
1321
1575
  const dims = cols.filter((c) => !c.isMeasure).map((c) => c.name);
1322
- const res = _r(dims, rowObjs, bind, point?.mapKind, cols.map((c) => c.name));
1576
+ const res = Or(dims, rowObjs, bind, point?.mapKind, cols.map((c) => c.name));
1323
1577
  bind = res.bind;
1324
1578
  rolesBackfilled = res.backfilled;
1325
1579
  rolesRefused = res.refused;
@@ -1634,6 +1888,93 @@ function stripJsComments(code) {
1634
1888
  return out;
1635
1889
  }
1636
1890
 
1891
+ // src/markFollower.ts
1892
+ var MARK_MOVE_ATTRIBUTES = Object.freeze([
1893
+ "transform",
1894
+ "d",
1895
+ "x",
1896
+ "y",
1897
+ "cx",
1898
+ "cy",
1899
+ "points",
1900
+ "width",
1901
+ "height",
1902
+ "visibility",
1903
+ "opacity",
1904
+ "style",
1905
+ "class"
1906
+ ]);
1907
+ function followMarks(container, opts) {
1908
+ const win = opts.win ?? container.ownerDocument?.defaultView ?? null;
1909
+ let observer = null;
1910
+ let resize = null;
1911
+ let size = "";
1912
+ let queued = false;
1913
+ let queuedResize = false;
1914
+ const sizeOf = () => `${container.offsetWidth ?? 0}x${container.offsetHeight ?? 0}`;
1915
+ const movesAMark = (r) => {
1916
+ const t = r.target;
1917
+ if (!t || opts.blind && t.closest?.(opts.blind)) return false;
1918
+ return !!(t.classList?.contains(MARK_CLASS) || t.querySelector?.(`.${MARK_CLASS}`));
1919
+ };
1920
+ const queue = (resized) => {
1921
+ if (resized) queuedResize = true;
1922
+ if (queued) return;
1923
+ queued = true;
1924
+ const run = () => {
1925
+ const r = queuedResize;
1926
+ queued = false;
1927
+ queuedResize = false;
1928
+ if (observer || resize) {
1929
+ try {
1930
+ opts.repaint(r);
1931
+ } catch {
1932
+ }
1933
+ }
1934
+ };
1935
+ if (win?.requestAnimationFrame) win.requestAnimationFrame(run);
1936
+ else setTimeout(run, 16);
1937
+ };
1938
+ return {
1939
+ follow(on) {
1940
+ if (!on) {
1941
+ if (observer) {
1942
+ observer.disconnect();
1943
+ observer = null;
1944
+ }
1945
+ if (resize) {
1946
+ resize.disconnect();
1947
+ resize = null;
1948
+ }
1949
+ return;
1950
+ }
1951
+ const MO = win?.MutationObserver;
1952
+ if (!observer && MO) {
1953
+ observer = new MO((records) => {
1954
+ if (!queued && records.some(movesAMark)) queue(false);
1955
+ });
1956
+ observer.observe(container, { subtree: true, attributes: true, attributeFilter: [...MARK_MOVE_ATTRIBUTES] });
1957
+ }
1958
+ const RO = win?.ResizeObserver;
1959
+ if (!resize && RO) {
1960
+ resize = new RO(() => {
1961
+ if (sizeOf() !== size) queue(true);
1962
+ });
1963
+ resize.observe(container);
1964
+ }
1965
+ },
1966
+ painted() {
1967
+ size = sizeOf();
1968
+ },
1969
+ takeRecords() {
1970
+ observer?.takeRecords();
1971
+ },
1972
+ get following() {
1973
+ return !!(observer || resize);
1974
+ }
1975
+ };
1976
+ }
1977
+
1637
1978
  // src/blankRender.ts
1638
1979
  var EMPTY2 = { markCount: 0, legendMarkCount: 0, containerKind: "empty", declaresNoRowMarks: false };
1639
1980
  function censusMarks(container) {
@@ -1722,7 +2063,7 @@ var FIT_SAMPLE = 90;
1722
2063
  var VALUE_AXIS_MIN_FIT = 0.8;
1723
2064
  var MIN_AXIS_SPAN_PX = 40;
1724
2065
  var EMPTY3 = { marks: 0, axis: "", column: "", fit: 0, offShare: 0, medianOffRadii: 0 };
1725
- function num(el, name) {
2066
+ function num2(el, name) {
1726
2067
  const v = el.getAttribute?.(name);
1727
2068
  if (v === null || v === void 0 || String(v).trim() === "") return NaN;
1728
2069
  return parseFloat(String(v));
@@ -1767,7 +2108,7 @@ function censusValuePlacement(container, data) {
1767
2108
  if (!/^\d+$/.test(raw)) continue;
1768
2109
  const i = +raw;
1769
2110
  const row = rows[i];
1770
- const x = num(el, "cx"), y = num(el, "cy"), r = num(el, "r");
2111
+ const x = num2(el, "cx"), y = num2(el, "cy"), r = num2(el, "r");
1771
2112
  if (!row || !Number.isFinite(x) || !Number.isFinite(y) || !(r > 0)) continue;
1772
2113
  seen.add(i);
1773
2114
  dots.push({ row, x, y, r });
@@ -1929,11 +2270,11 @@ function extendPillOver(pill, tr) {
1929
2270
  const inv = m.inverse();
1930
2271
  const toUser = (x2, y2) => ({ x: inv.a * x2 + inv.c * y2 + inv.e, y: inv.b * x2 + inv.d * y2 + inv.f });
1931
2272
  const a = toUser(tr.left, tr.top), b = toUser(tr.right, tr.bottom);
1932
- const num2 = (k) => {
2273
+ const num3 = (k) => {
1933
2274
  const v = parseFloat(pill.getAttribute(k) || "");
1934
2275
  return isFinite(v) ? v : NaN;
1935
2276
  };
1936
- const x = num2("x"), y = num2("y"), w = num2("width"), h = num2("height");
2277
+ const x = num3("x"), y = num3("y"), w = num3("width"), h = num3("height");
1937
2278
  if (![x, y, w, h].every((v) => isFinite(v)) || !(w > 0) || !(h > 0)) return false;
1938
2279
  const pad = 1 / m.a;
1939
2280
  const x0 = Math.min(x, Math.min(a.x, b.x) - pad), x1 = Math.max(x + w, Math.max(a.x, b.x) + pad);
@@ -2340,10 +2681,10 @@ function seenRight(el, right, stop) {
2340
2681
  }
2341
2682
  return right;
2342
2683
  }
2343
- var SVG_NS = "http://www.w3.org/2000/svg";
2684
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
2344
2685
  function overflowWindowOf(el) {
2345
2686
  const win = el.ownerDocument.defaultView;
2346
- if (!win || el.namespaceURI === SVG_NS) return null;
2687
+ if (!win || el.namespaceURI === SVG_NS2) return null;
2347
2688
  try {
2348
2689
  const o = win.getComputedStyle(el);
2349
2690
  const clips = (v) => !!v && v !== "visible";
@@ -2426,17 +2767,22 @@ function createAnnotationLayer(container, onClick, opts) {
2426
2767
  const names = (data.columns || []).map((c) => typeof c === "string" ? c : c?.name);
2427
2768
  const marks = Array.from(container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}]`)).filter((m) => !m.closest(`.${ANNOTATION_LAYER_CLASS}`));
2428
2769
  const marksByRow = /* @__PURE__ */ new Map();
2429
- for (const m of marks) for (const r of parseIdxs(m.getAttribute(ROW_IDX_ATTR))) {
2430
- const at = marksByRow.get(r);
2431
- if (at) at.push(m);
2432
- else marksByRow.set(r, [m]);
2770
+ const rowsOf = /* @__PURE__ */ new Map();
2771
+ for (const m of marks) {
2772
+ const own = new Set(parseIdxs(m.getAttribute(ROW_IDX_ATTR)));
2773
+ rowsOf.set(m, own);
2774
+ for (const r of own) {
2775
+ const at = marksByRow.get(r);
2776
+ if (at) at.push(m);
2777
+ else marksByRow.set(r, [m]);
2778
+ }
2433
2779
  }
2434
2780
  const origin = container.getBoundingClientRect();
2435
2781
  const open = origin.width > 0 && origin.height > 0 ? box(origin) : FREE;
2436
2782
  const clipOf = /* @__PURE__ */ new Map();
2437
2783
  const windowOf = (el) => {
2438
2784
  if (!clipOf.has(el)) {
2439
- if (el.namespaceURI === SVG_NS) {
2785
+ if (el.namespaceURI === SVG_NS2) {
2440
2786
  const w = clipWindowOf(el);
2441
2787
  clipOf.set(el, w && box(w));
2442
2788
  } else clipOf.set(el, overflowWindowOf(el));
@@ -2451,7 +2797,7 @@ function createAnnotationLayer(container, onClick, opts) {
2451
2797
  const viewOf = (m) => {
2452
2798
  let v = inView;
2453
2799
  for (let el = m.parentElement; v && el && el !== container; el = el.parentElement) {
2454
- if (el.namespaceURI === SVG_NS && el.localName === "svg") {
2800
+ if (el.namespaceURI === SVG_NS2 && el.localName === "svg") {
2455
2801
  const s = el.getBoundingClientRect();
2456
2802
  if (s.width > 0 && s.height > 0) v = cut(v, box(s));
2457
2803
  }
@@ -2546,16 +2892,31 @@ function createAnnotationLayer(container, onClick, opts) {
2546
2892
  for (const a of list) {
2547
2893
  const pairs = (a.where ? Object.entries(a.where) : a.column != null ? [[a.column, a.value]] : []).map(([c, v]) => [names.indexOf(c), text(v)]);
2548
2894
  const want = pairs.map((p) => p[1]).join(" / ");
2549
- const hits2 = /* @__PURE__ */ new Set();
2895
+ const hits2 = /* @__PURE__ */ new Set(), keyRows = /* @__PURE__ */ new Set();
2550
2896
  if (pairs.length && pairs.every((p) => p[0] >= 0)) (data.rows || []).forEach((row, r) => {
2551
- if (pairs.every(([ci, v]) => text(row?.[ci]) === v)) for (const m of marksByRow.get(r) ?? []) hits2.add(m);
2897
+ if (!pairs.every(([ci, v]) => text(row?.[ci]) === v)) return;
2898
+ keyRows.add(r);
2899
+ for (const m of marksByRow.get(r) ?? []) hits2.add(m);
2552
2900
  });
2553
2901
  if (!hits2.size) continue;
2554
- let best = null, bestWhole = null, bestMark = null, laidOut = false;
2902
+ const rank = (m) => {
2903
+ const own = rowsOf.get(m);
2904
+ let out = 0;
2905
+ for (const r of own) if (!keyRows.has(r)) out++;
2906
+ const level = parseInt(m.getAttribute("aria-level") || "", 10);
2907
+ return [out, out === 0 && own.size === keyRows.size ? 0 : 1, own.size, Number.isFinite(level) ? -level : 0];
2908
+ };
2909
+ const before = (p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2] || p[3] - q[3];
2910
+ const drawn = [];
2555
2911
  for (const m of hits2) {
2556
2912
  const b = m.getBoundingClientRect();
2557
- if (!(b.width > 0 && b.height > 0)) continue;
2558
- laidOut = true;
2913
+ if (b.width > 0 && b.height > 0) drawn.push({ m, b, k: rank(m) });
2914
+ }
2915
+ const laidOut = drawn.length > 0;
2916
+ const most = drawn.reduce((k, d) => !k || before(d.k, k) < 0 ? d.k : k, null);
2917
+ let best = null, bestWhole = null, bestMark = null;
2918
+ for (const { m, b, k } of drawn) {
2919
+ if (before(k, most) !== 0) continue;
2559
2920
  const v = cut(box(b), viewOf(m));
2560
2921
  if (v && (!best || area(v) > area(best))) {
2561
2922
  best = v;
@@ -2815,16 +3176,17 @@ function installKeyboard(env) {
2815
3176
  const { container } = env;
2816
3177
  let rovingKey = null;
2817
3178
  let destroyed = false;
2818
- const marksNow = () => {
2819
- if (env.inert()) return [];
2820
- return Array.from(container.querySelectorAll(env.markSelector)).filter((m) => !m.closest(env.skipSelector) && (m.getAttribute("data-row-idx") ?? "") !== "");
2821
- };
3179
+ const ownRole = /* @__PURE__ */ new WeakSet();
3180
+ const marksNow = () => Array.from(container.querySelectorAll(env.markSelector)).filter((m) => !m.closest(env.skipSelector) && (m.getAttribute("data-row-idx") ?? "") !== "");
2822
3181
  const wired = (el) => el.hasAttribute(WIRED_ATTR);
2823
3182
  const wire = (el, tabindex, role) => {
2824
3183
  if (el.hasAttribute("tabindex") && !wired(el)) return false;
2825
3184
  el.setAttribute(WIRED_ATTR, "");
2826
3185
  el.setAttribute("tabindex", tabindex);
2827
- if (!el.hasAttribute("role")) el.setAttribute("role", role);
3186
+ if (!el.hasAttribute("role") || ownRole.has(el)) {
3187
+ el.setAttribute("role", role);
3188
+ ownRole.add(el);
3189
+ }
2828
3190
  return true;
2829
3191
  };
2830
3192
  const setRoving = (list, to) => {
@@ -2835,22 +3197,17 @@ function installKeyboard(env) {
2835
3197
  if (destroyed) return;
2836
3198
  try {
2837
3199
  container.classList?.add(KEYBOARD_CLASS);
3200
+ const inert = env.inert();
2838
3201
  const list = marksNow();
2839
3202
  for (const m of list) {
2840
- if (!wire(m, "-1", "button")) continue;
2841
- m.setAttribute("aria-pressed", env.isSelected(m) ? "true" : "false");
3203
+ if (!wire(m, "-1", inert ? "img" : "button")) continue;
3204
+ if (inert) m.removeAttribute("aria-pressed");
3205
+ else m.setAttribute("aria-pressed", env.isSelected(m) ? "true" : "false");
2842
3206
  if (!m.hasAttribute("aria-label") && !m.querySelector(":scope > title")) {
2843
3207
  const name = env.labelOf(m);
2844
3208
  if (name) m.setAttribute("aria-label", name);
2845
3209
  }
2846
3210
  }
2847
- if (!list.length) {
2848
- for (const m of Array.from(container.querySelectorAll(`[${WIRED_ATTR}]`)))
2849
- if (m.matches(env.markSelector)) {
2850
- m.removeAttribute("tabindex");
2851
- m.removeAttribute(WIRED_ATTR);
2852
- }
2853
- }
2854
3211
  const own = list.filter(wired);
2855
3212
  if (own.length) {
2856
3213
  const keep = own.find((m) => env.keyOf(m) === rovingKey) ?? own.find((m) => env.isSelected(m)) ?? own[0];
@@ -2879,7 +3236,7 @@ function installKeyboard(env) {
2879
3236
  e.preventDefault();
2880
3237
  const ME = container.ownerDocument.defaultView?.MouseEvent ?? MouseEvent;
2881
3238
  press.dispatchEvent(new ME("click", { bubbles: true, cancelable: true }));
2882
- } else if (k === "Escape" && env.escapeClears !== false) env.clear();
3239
+ } else if (k === "Escape" && env.escapeClears !== false && !env.inert()) env.clear();
2883
3240
  return;
2884
3241
  }
2885
3242
  const mark = t.closest?.(env.markSelector);
@@ -2921,9 +3278,10 @@ function installKeyboard(env) {
2921
3278
  rovingKey = env.keyOf(mark);
2922
3279
  env.select(mark, { ctrl: !!(e.ctrlKey || e.metaKey), shift: !!e.shiftKey });
2923
3280
  break;
3281
+ // An inert chart's click on empty canvas clears nothing, so neither does its Escape.
2924
3282
  case "Escape":
2925
3283
  hideTip();
2926
- if (env.escapeClears !== false) env.clear();
3284
+ if (env.escapeClears !== false && !env.inert()) env.clear();
2927
3285
  break;
2928
3286
  }
2929
3287
  };
@@ -3399,77 +3757,13 @@ function createChartHost(container, config) {
3399
3757
  };
3400
3758
  const GLYPH_LAYER_CLASS = "bic-selection-glyphs";
3401
3759
  let glyphPositionWas = null;
3402
- let glyphObserver = null;
3403
- let glyphResize = null;
3404
- let glyphSize = "";
3405
- let glyphQueued = false;
3406
- let glyphQueuedReport = false;
3760
+ const glyphFollower = followMarks(container, {
3761
+ win,
3762
+ blind: `.${GLYPH_LAYER_CLASS}, .${LASSO_OUTLINE_CLASS}`,
3763
+ repaint: (resized) => paintGlyphLayer(resized ? "if-changed" : false)
3764
+ });
3407
3765
  let glyphReported = "";
3408
- const sizeOfContainer = () => `${container.offsetWidth ?? 0}x${container.offsetHeight ?? 0}`;
3409
- const glyphBlind = `.${GLYPH_LAYER_CLASS}, .${LASSO_OUTLINE_CLASS}`;
3410
- const movesAMark = (r) => {
3411
- const t = r.target;
3412
- if (!t || t.closest?.(glyphBlind)) return false;
3413
- return !!(t.classList?.contains(MARK_CLASS) || t.querySelector?.(`.${MARK_CLASS}`));
3414
- };
3415
- const queueGlyphs = (reportIfChanged) => {
3416
- if (reportIfChanged) glyphQueuedReport = true;
3417
- if (glyphQueued) return;
3418
- glyphQueued = true;
3419
- const run = () => {
3420
- const r = glyphQueuedReport;
3421
- glyphQueued = false;
3422
- glyphQueuedReport = false;
3423
- if (glyphObserver || glyphResize) paintGlyphLayer(r ? "if-changed" : false);
3424
- };
3425
- if (win?.requestAnimationFrame) win.requestAnimationFrame(run);
3426
- else setTimeout(run, 16);
3427
- };
3428
- const followGlyphs = (on) => {
3429
- if (!on) {
3430
- if (glyphObserver) {
3431
- glyphObserver.disconnect();
3432
- glyphObserver = null;
3433
- }
3434
- if (glyphResize) {
3435
- glyphResize.disconnect();
3436
- glyphResize = null;
3437
- }
3438
- return;
3439
- }
3440
- const MO = win?.MutationObserver;
3441
- if (!glyphObserver && MO) {
3442
- glyphObserver = new MO((records) => {
3443
- if (!glyphQueued && records.some(movesAMark)) queueGlyphs(false);
3444
- });
3445
- glyphObserver.observe(container, {
3446
- subtree: true,
3447
- attributes: true,
3448
- attributeFilter: [
3449
- "transform",
3450
- "d",
3451
- "x",
3452
- "y",
3453
- "cx",
3454
- "cy",
3455
- "points",
3456
- "width",
3457
- "height",
3458
- "visibility",
3459
- "opacity",
3460
- "style",
3461
- "class"
3462
- ]
3463
- });
3464
- }
3465
- const RO = win?.ResizeObserver;
3466
- if (!glyphResize && RO) {
3467
- glyphResize = new RO(() => {
3468
- if (sizeOfContainer() !== glyphSize) queueGlyphs(true);
3469
- });
3470
- glyphResize.observe(container);
3471
- }
3472
- };
3766
+ const followGlyphs = (on) => glyphFollower.follow(on);
3473
3767
  const markShown = (m) => {
3474
3768
  try {
3475
3769
  const cv = m.checkVisibility;
@@ -3518,8 +3812,13 @@ function createChartHost(container, config) {
3518
3812
  try {
3519
3813
  let layer = container.querySelector(`:scope > .${GLYPH_LAYER_CLASS}`);
3520
3814
  clearSelectionGlyphs(layer);
3521
- const mode = resolved.selectionGlyph;
3522
- if (mode !== "dot" && mode !== "check") {
3815
+ const mode = selectionMarkerFor({
3816
+ choice: resolved.selectionMarker,
3817
+ mapChoice: resolved.mapSelectionMarker,
3818
+ regionMap: isRegionMapData(data),
3819
+ neutralCentreRamp: hasNeutralCentreRamp(container)
3820
+ });
3821
+ if (mode !== "dot" && mode !== "check" && mode !== "outline") {
3523
3822
  glyphReported = "";
3524
3823
  return;
3525
3824
  }
@@ -3567,20 +3866,41 @@ function createChartHost(container, config) {
3567
3866
  const sw = container.scrollWidth || 0, sh = container.scrollHeight || 0;
3568
3867
  layer.style.width = `${sw}px`;
3569
3868
  layer.style.height = `${sh}px`;
3570
- glyphSize = sizeOfContainer();
3571
- const centers = [];
3572
- collectGlyphCentersWhere(container, layer.getBoundingClientRect(), centers, { shown: markShown, at: glyphWindows() });
3869
+ glyphFollower.painted();
3573
3870
  const hc = !!resolved.isHighContrast;
3574
- const drawn = centers.length ? paintSelectionGlyphs(layer, centers, {
3575
- symbol: mode === "check" ? "\u2713" : "\u25CF",
3576
- color: hc ? resolved.themeFg || "#111" : "#111",
3577
- halo: hc ? resolved.themeBg || "#fff" : "#fff"
3578
- }) : 0;
3579
- const said = `${mode}:${drawn}:${centers.length}`;
3871
+ let drawn = 0, total = 0;
3872
+ if (mode === "outline") {
3873
+ const marks = collectOutlineMarks(container, { shown: markShown });
3874
+ total = marks.length;
3875
+ let ink = resolved.themeFg || "";
3876
+ if (!ink) {
3877
+ try {
3878
+ ink = win?.getComputedStyle?.(container)?.color || "";
3879
+ } catch {
3880
+ ink = "";
3881
+ }
3882
+ }
3883
+ drawn = total ? paintSelectionOutlines(layer, marks, {
3884
+ color: ink || "#111",
3885
+ halo: resolved.themeBg || (hc ? "#000" : "#fff"),
3886
+ windowOf: (m) => outlineWindowOf(m, container),
3887
+ avoid: controlBoxesIn(container)
3888
+ }) : 0;
3889
+ } else {
3890
+ const centers = [];
3891
+ collectGlyphCentersWhere(container, layer.getBoundingClientRect(), centers, { shown: markShown, at: glyphWindows() });
3892
+ total = centers.length;
3893
+ drawn = centers.length ? paintSelectionGlyphs(layer, centers, {
3894
+ symbol: mode === "check" ? "\u2713" : "\u25CF",
3895
+ color: hc ? resolved.themeFg || "#111" : "#111",
3896
+ halo: hc ? resolved.themeBg || "#fff" : "#fff"
3897
+ }) : 0;
3898
+ }
3899
+ const said = `${mode}:${drawn}:${total}`;
3580
3900
  if (report === true || report === "if-changed" && said !== glyphReported) {
3581
3901
  glyphReported = said;
3582
3902
  try {
3583
- config.onSelectionGlyphs?.({ mode, drawn, total: centers.length, suppressed: false });
3903
+ config.onSelectionGlyphs?.({ mode, drawn, total, suppressed: false });
3584
3904
  } catch {
3585
3905
  }
3586
3906
  }
@@ -3588,7 +3908,7 @@ function createChartHost(container, config) {
3588
3908
  } finally {
3589
3909
  try {
3590
3910
  followGlyphs(follow);
3591
- glyphObserver?.takeRecords();
3911
+ glyphFollower.takeRecords();
3592
3912
  } catch {
3593
3913
  }
3594
3914
  }
@@ -3716,7 +4036,15 @@ function createChartHost(container, config) {
3716
4036
  { ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey }
3717
4037
  ));
3718
4038
  const fromLabel = !!tick && !multi && !toggledOff && !chartOwnsTimeline(container);
3719
- notify(next, "user", toggledOff ? null : tick, false, true, fromLabel ? { fromLabel: true } : void 0);
4039
+ const fromMark = !tick && !multi && !toggledOff;
4040
+ notify(
4041
+ next,
4042
+ "user",
4043
+ toggledOff ? null : tick,
4044
+ false,
4045
+ true,
4046
+ fromLabel ? { fromLabel: true } : fromMark ? { fromMark: true } : void 0
4047
+ );
3720
4048
  };
3721
4049
  container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
3722
4050
  const untrackPress = trackPress(container);
@@ -4390,6 +4718,21 @@ export {
4390
4718
  autoRampMinStep,
4391
4719
  autoRampCanvas,
4392
4720
  resolveAutoColorScale,
4721
+ SELECTION_MARKER_DEFAULT,
4722
+ MAP_SELECTION_MARKER_DEFAULT,
4723
+ readSelectionMarker,
4724
+ readMapSelectionMarker,
4725
+ REGION_JOIN_COLUMN,
4726
+ isRegionMapData,
4727
+ NEUTRAL_CENTRE_RAMP_SELECTOR,
4728
+ hasNeutralCentreRamp,
4729
+ selectionMarkerFor,
4730
+ SELECTION_OUTLINE_CLASS,
4731
+ isAreaMark,
4732
+ collectOutlineMarks,
4733
+ outlineWindowOf,
4734
+ controlBoxesIn,
4735
+ paintSelectionOutlines,
4393
4736
  EXPORT_FORMATS,
4394
4737
  XLSX_MIME,
4395
4738
  CSV_MIME,
@@ -4409,6 +4752,8 @@ export {
4409
4752
  resolveOptions,
4410
4753
  buildRenderPayload,
4411
4754
  stripJsComments,
4755
+ MARK_MOVE_ATTRIBUTES,
4756
+ followMarks,
4412
4757
  censusMarks,
4413
4758
  isBlankRender,
4414
4759
  blankRenderFlag,