@bicharts/chart-host 0.6.107 → 0.6.109

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-IE426WU6.mjs";
7
+ } from "./chunk-4DOKJ3R3.mjs";
8
8
  import {
9
9
  censusHitBands,
10
10
  ensureCrossfilterHitTargets
11
- } from "./chunk-CPLWAW3Z.mjs";
11
+ } from "./chunk-GQLEU3VC.mjs";
12
12
  import {
13
13
  clearPress,
14
14
  clearSelectionGlyphs,
@@ -25,14 +25,14 @@ import {
25
25
  selectionRuleOf,
26
26
  trackPress,
27
27
  validRowSet
28
- } from "./chunk-P2DLK7WZ.mjs";
28
+ } from "./chunk-3ER4C7TG.mjs";
29
29
  import {
30
30
  clipWindowOf,
31
31
  fitRenderedChart,
32
32
  rememberScroll,
33
33
  takeScroll,
34
34
  unpinScrolledAxis
35
- } from "./chunk-IE7TM6DX.mjs";
35
+ } from "./chunk-TN5JRULV.mjs";
36
36
  import {
37
37
  geoFromCache
38
38
  } from "./chunk-5WTQN5DL.mjs";
@@ -43,7 +43,7 @@ import {
43
43
  Rr,
44
44
  Yn,
45
45
  tt
46
- } from "./chunk-SYUJ4TPX.mjs";
46
+ } from "./chunk-TH577JHI.mjs";
47
47
  import {
48
48
  ACTIVE_TICK_CLASS,
49
49
  ANIM_LOOP_DELAY_DEFAULT,
@@ -61,6 +61,7 @@ import {
61
61
  COLOR_SCALE_SELF_CLAMP_PCT_MIN,
62
62
  CONTAINER_SLOT_ANIM_STOP,
63
63
  CONTAINER_SLOT_CLICKED_MARKS,
64
+ CONTAINER_SLOT_HOST_SELECTION,
64
65
  CONTAINER_SLOT_INITIAL_XF_MARK,
65
66
  CONTAINER_SLOT_UI_STATE,
66
67
  CONTAINER_SLOT_XF_CLEAR,
@@ -74,6 +75,7 @@ import {
74
75
  HOST_CONTRACT_VERSION,
75
76
  LEGEND_MARK_CLASS,
76
77
  LIFT_SELECTED_CLASS,
78
+ MARKS_INERT_CLASS,
77
79
  MARK_CLASS,
78
80
  MARK_SELECTED_CLASS,
79
81
  MAX_MAP_POINTS_DEFAULT,
@@ -86,7 +88,7 @@ import {
86
88
  XFILTER_REFRESH_EVENT,
87
89
  chartOwnsTimeline,
88
90
  periodTickSuppressesFeedback
89
- } from "./chunk-7XRQUFVH.mjs";
91
+ } from "./chunk-USZSHKIB.mjs";
90
92
 
91
93
  // src/labelContrast.ts
92
94
  function parseRGBA(c) {
@@ -1057,6 +1059,18 @@ function themedLand(p) {
1057
1059
  const fg = toRGBA(p.themeFg) ?? [255, 255, 255, 1];
1058
1060
  return "#" + compositeOver([fg[0], fg[1], fg[2], 0.18], [bg[0], bg[1], bg[2]]).map((v) => v.toString(16).padStart(2, "0")).join("");
1059
1061
  }
1062
+ function cleanHostSelection(v) {
1063
+ if (!v || typeof v !== "object" || !Array.isArray(v.rows)) return void 0;
1064
+ const raw = v.rows;
1065
+ const rows = Array.from(new Set(raw.filter((n2) => typeof n2 === "number" && Number.isInteger(n2) && n2 >= 0))).sort((a, b) => a - b);
1066
+ const h = Number(v.hidden);
1067
+ const out = { rows, hidden: Number.isFinite(h) && h > 0 ? Math.floor(h) : 0 };
1068
+ const n = v.narrowed;
1069
+ if (n && typeof n === "object" && Number.isInteger(n.published) && Number.isInteger(n.kept) && n.kept >= 0 && n.kept < n.published) {
1070
+ out.narrowed = { published: n.published, kept: n.kept };
1071
+ }
1072
+ return out;
1073
+ }
1060
1074
  var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
1061
1075
  var numberOr = (raw, dflt) => {
1062
1076
  const n = raw == null || raw === "" ? NaN : Number(raw);
@@ -1089,6 +1103,8 @@ function resolveOptions(p) {
1089
1103
  dataComplete: p.dataComplete,
1090
1104
  rowsWithheld: p.rowsWithheld,
1091
1105
  rowsFilteredOut: p.rowsFilteredOut,
1106
+ // A host FACT about its selection (contract 1.21.0), cleaned, never invented: absent stays absent.
1107
+ hostSelection: cleanHostSelection(p.hostSelection),
1092
1108
  // Host services (contract 1.18.0): a FUNCTION or nothing, so a junk value never reaches a chart as something
1093
1109
  // it would try to call. Passed through untouched otherwise: the host owns what opening or copying means.
1094
1110
  openUrl: typeof p.openUrl === "function" ? p.openUrl : void 0,
@@ -1144,6 +1160,8 @@ function resolveOptions(p) {
1144
1160
  })(),
1145
1161
  // A host FACT from the generate answer, true only for a literal true: silence never suppresses anything.
1146
1162
  suppressSelectionGlyph: p.suppressSelectionGlyph === true ? true : void 0,
1163
+ // A host FACT (1.22.0): the chart works from the keyboard. True only for a literal true, like the one above.
1164
+ keyboardMarks: p.keyboardMarks === true ? true : void 0,
1147
1165
  onSelect: p.onSelect,
1148
1166
  // Control gestures (1.10.0). Passed straight through like onSelect: the host owns what a gesture MEANS,
1149
1167
  // and a host that supplies nothing leaves every control behaving exactly as before.
@@ -2307,7 +2325,7 @@ var DOT = 8;
2307
2325
  var RING = 1.5;
2308
2326
  var DOT_HALF = DOT / 2 + RING;
2309
2327
  var DOT_IN = 8;
2310
- function createAnnotationLayer(container, onClick) {
2328
+ function createAnnotationLayer(container, onClick, opts) {
2311
2329
  let list = [];
2312
2330
  let report = { shown: 0, notShown: 0 };
2313
2331
  let last = null;
@@ -2431,7 +2449,7 @@ function createAnnotationLayer(container, onClick) {
2431
2449
  const after = own.filter((t) => t.b.l >= run.r && t.b.b > run.t && t.b.t < run.b).map((t) => t.b.l);
2432
2450
  return { r: Math.min(run.r, seen), y: (run.t + run.b) / 2, next: after.length ? Math.min(...after) : v.r };
2433
2451
  };
2434
- const clear = (v, len, mark) => {
2452
+ const clear = (v, len, mark, whole = v) => {
2435
2453
  const hw = Math.max(16, len * 6 + 8) / 2, hh = 8, g = 2;
2436
2454
  const cx = (v.l + v.r) / 2, cy = (v.t + v.b) / 2;
2437
2455
  const at = (x, y) => ({ l: x - hw, t: y - hh, r: x + hw, b: y + hh });
@@ -2444,6 +2462,7 @@ function createAnnotationLayer(container, onClick) {
2444
2462
  const tries = [];
2445
2463
  const add = (x, y, px = x, py = y) => tries.push({ x: clampX(x, hw), y: clampY(y, hh), px, py });
2446
2464
  const label = rowLabel(v, mark);
2465
+ if (!label && whole !== v && rowLabel(whole, mark)) return null;
2447
2466
  if (label) {
2448
2467
  const x = label.r + g + hw;
2449
2468
  if (x + hw <= label.next - g || label.next >= v.r) add(x, label.y, Math.min(x, v.r - 1), label.y);
@@ -2508,7 +2527,7 @@ function createAnnotationLayer(container, onClick) {
2508
2527
  }
2509
2528
  }
2510
2529
  const dot = wantsDot(bestMark ?? hits2.values().next().value ?? null);
2511
- const pt = !best ? null : dot ? dotSpot(best, bestWhole) : clear(best, (a.label ?? "\u2022").length, bestMark);
2530
+ const pt = !best ? null : dot ? dotSpot(best, bestWhole) : clear(best, (a.label ?? "\u2022").length, bestMark, bestWhole ?? best);
2512
2531
  if (laidOut && !pt) continue;
2513
2532
  shown++;
2514
2533
  const badge = doc.createElement("div");
@@ -2549,6 +2568,15 @@ function createAnnotationLayer(container, onClick) {
2549
2568
  e.stopPropagation();
2550
2569
  onClick?.(a);
2551
2570
  });
2571
+ if (opts?.keyboard?.()) {
2572
+ badge.setAttribute("tabindex", "0");
2573
+ badge.addEventListener("keydown", (e) => {
2574
+ if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
2575
+ e.preventDefault();
2576
+ e.stopPropagation();
2577
+ onClick?.(a);
2578
+ });
2579
+ }
2552
2580
  layer.appendChild(badge);
2553
2581
  }
2554
2582
  report = { shown, notShown: list.length - shown };
@@ -2575,6 +2603,222 @@ function createAnnotationLayer(container, onClick) {
2575
2603
  };
2576
2604
  }
2577
2605
 
2606
+ // src/focusKeep.ts
2607
+ function signatureOf(el) {
2608
+ const a = (n) => el.getAttribute(n) ?? "";
2609
+ const cls = Array.from(el.classList ?? []).filter((c) => !/^lch-|^bic-/.test(c)).sort().join(" ");
2610
+ return [el.localName, a("type"), cls, a("name"), a("placeholder"), a("aria-label"), a("role")].join("|");
2611
+ }
2612
+ function alike(container, sig) {
2613
+ const tag = sig.split("|", 1)[0];
2614
+ if (!tag) return [];
2615
+ return Array.from(container.querySelectorAll(tag)).filter((e) => signatureOf(e) === sig);
2616
+ }
2617
+ function rememberFocus(container) {
2618
+ try {
2619
+ const active = container.ownerDocument?.activeElement;
2620
+ if (!active || active === container || !container.contains(active)) return null;
2621
+ const sig = signatureOf(active);
2622
+ const nth = alike(container, sig).indexOf(active);
2623
+ const t = active;
2624
+ let start = null, end = null, dir = null;
2625
+ try {
2626
+ if (typeof t.selectionStart === "number") {
2627
+ start = t.selectionStart;
2628
+ end = t.selectionEnd;
2629
+ dir = t.selectionDirection ?? null;
2630
+ }
2631
+ } catch {
2632
+ }
2633
+ return { sig, nth: Math.max(0, nth), start, end, dir };
2634
+ } catch {
2635
+ return null;
2636
+ }
2637
+ }
2638
+ function restoreFocus(container, memory) {
2639
+ if (!memory) return false;
2640
+ try {
2641
+ const active = container.ownerDocument?.activeElement;
2642
+ if (active && active !== container && container.contains(active)) return false;
2643
+ const list = alike(container, memory.sig);
2644
+ const el = list[memory.nth] ?? (list.length === 1 ? list[0] : null);
2645
+ if (!el || typeof el.focus !== "function") return false;
2646
+ el.focus({ preventScroll: true });
2647
+ const t = el;
2648
+ if (memory.start != null && typeof t.setSelectionRange === "function") {
2649
+ const len = typeof t.value === "string" ? t.value.length : 0;
2650
+ try {
2651
+ t.setSelectionRange(Math.min(memory.start, len), Math.min(memory.end ?? memory.start, len), memory.dir ?? void 0);
2652
+ } catch {
2653
+ }
2654
+ }
2655
+ return container.ownerDocument?.activeElement === el;
2656
+ } catch {
2657
+ return false;
2658
+ }
2659
+ }
2660
+
2661
+ // src/keyboardMarks.ts
2662
+ var KEYBOARD_STAMP = "// bic:keyboard-marks";
2663
+ var STAMP_RE = /^[ \t]*\/\/[ \t]*bic:keyboard-marks\b/;
2664
+ var STAMP_HEAD_LINES = 12;
2665
+ function codeHasKeyboardStamp(code) {
2666
+ if (typeof code !== "string" || !code) return false;
2667
+ let start = 0;
2668
+ for (let n = 0; n < STAMP_HEAD_LINES && start <= code.length; n++) {
2669
+ const end = code.indexOf("\n", start);
2670
+ const line = code.slice(start, end < 0 ? code.length : end);
2671
+ if (STAMP_RE.test(line)) return true;
2672
+ if (end < 0) break;
2673
+ start = end + 1;
2674
+ }
2675
+ return false;
2676
+ }
2677
+ var KEYBOARD_CLASS = "lch-keyboard";
2678
+ var WIRED_ATTR = "data-lch-kb";
2679
+ function rowNameOf(columns, row, culture) {
2680
+ if (!row) return null;
2681
+ const parts = [];
2682
+ (columns || []).forEach((c, i) => {
2683
+ const name = typeof c === "string" ? c : c?.name;
2684
+ const v = row[i];
2685
+ if (parts.length >= 4 || v == null || v === "" || typeof name === "string" && name.startsWith("__")) return;
2686
+ parts.push(typeof v === "number" ? v.toLocaleString(culture || void 0, { maximumFractionDigits: 2 }) : String(v));
2687
+ });
2688
+ return parts.length ? parts.join(", ") : null;
2689
+ }
2690
+ var isClearChord = (e) => !!e.ctrlKey && !!e.shiftKey && (e.key === "C" || e.key === "c");
2691
+ var NATIVE = /* @__PURE__ */ new Set(["input", "select", "textarea", "button", "a"]);
2692
+ var isNative = (el) => NATIVE.has((el.localName || "").toLowerCase()) || el.isContentEditable === true;
2693
+ function installKeyboard(env) {
2694
+ const { container } = env;
2695
+ let rovingKey = null;
2696
+ let destroyed = false;
2697
+ const marksNow = () => {
2698
+ if (env.inert()) return [];
2699
+ return Array.from(container.querySelectorAll(env.markSelector)).filter((m) => !m.closest(env.skipSelector) && (m.getAttribute("data-row-idx") ?? "") !== "");
2700
+ };
2701
+ const wired = (el) => el.hasAttribute(WIRED_ATTR);
2702
+ const wire = (el, tabindex, role) => {
2703
+ if (el.hasAttribute("tabindex") && !wired(el)) return false;
2704
+ el.setAttribute(WIRED_ATTR, "");
2705
+ el.setAttribute("tabindex", tabindex);
2706
+ if (!el.hasAttribute("role")) el.setAttribute("role", role);
2707
+ return true;
2708
+ };
2709
+ const setRoving = (list, to) => {
2710
+ for (const m of list) if (wired(m)) m.setAttribute("tabindex", m === to ? "0" : "-1");
2711
+ rovingKey = env.keyOf(to);
2712
+ };
2713
+ const refresh = () => {
2714
+ if (destroyed) return;
2715
+ try {
2716
+ container.classList?.add(KEYBOARD_CLASS);
2717
+ const list = marksNow();
2718
+ for (const m of list) {
2719
+ if (!wire(m, "-1", "button")) continue;
2720
+ m.setAttribute("aria-pressed", env.isSelected(m) ? "true" : "false");
2721
+ if (!m.hasAttribute("aria-label") && !m.querySelector(":scope > title")) {
2722
+ const name = env.labelOf(m);
2723
+ if (name) m.setAttribute("aria-label", name);
2724
+ }
2725
+ }
2726
+ if (!list.length) {
2727
+ for (const m of Array.from(container.querySelectorAll(`[${WIRED_ATTR}]`)))
2728
+ if (m.matches(env.markSelector)) {
2729
+ m.removeAttribute("tabindex");
2730
+ m.removeAttribute(WIRED_ATTR);
2731
+ }
2732
+ }
2733
+ const own = list.filter(wired);
2734
+ if (own.length) {
2735
+ const keep = own.find((m) => env.keyOf(m) === rovingKey) ?? own.find((m) => env.isSelected(m)) ?? own[0];
2736
+ setRoving(own, keep);
2737
+ }
2738
+ for (const c of Array.from(container.querySelectorAll(env.pressSelector))) {
2739
+ if (isNative(c)) continue;
2740
+ wire(c, "0", "button");
2741
+ }
2742
+ } catch {
2743
+ }
2744
+ };
2745
+ const onKey = (e) => {
2746
+ if (destroyed || e.defaultPrevented) return;
2747
+ const t = e.target;
2748
+ if (!t || !container.contains(t)) return;
2749
+ const k = e.key;
2750
+ if (isClearChord(e)) {
2751
+ e.preventDefault();
2752
+ env.clear();
2753
+ return;
2754
+ }
2755
+ const press = t.closest?.(env.pressSelector);
2756
+ if (press && wired(press) && container.contains(press)) {
2757
+ if (k === "Enter" || k === " " || k === "Spacebar") {
2758
+ e.preventDefault();
2759
+ const ME = container.ownerDocument.defaultView?.MouseEvent ?? MouseEvent;
2760
+ press.dispatchEvent(new ME("click", { bubbles: true, cancelable: true }));
2761
+ } else if (k === "Escape" && env.escapeClears !== false) env.clear();
2762
+ return;
2763
+ }
2764
+ const mark = t.closest?.(env.markSelector);
2765
+ if (!mark || !wired(mark) || !container.contains(mark)) return;
2766
+ const list = marksNow().filter(wired);
2767
+ const at = list.indexOf(mark);
2768
+ const go = (i) => {
2769
+ const to = list[Math.max(0, Math.min(list.length - 1, i))];
2770
+ if (!to) return;
2771
+ setRoving(list, to);
2772
+ try {
2773
+ to.focus({ preventScroll: false });
2774
+ } catch {
2775
+ }
2776
+ };
2777
+ switch (k) {
2778
+ case "ArrowRight":
2779
+ case "ArrowDown":
2780
+ e.preventDefault();
2781
+ go(at + 1);
2782
+ break;
2783
+ case "ArrowLeft":
2784
+ case "ArrowUp":
2785
+ e.preventDefault();
2786
+ go(at - 1);
2787
+ break;
2788
+ case "Home":
2789
+ e.preventDefault();
2790
+ go(0);
2791
+ break;
2792
+ case "End":
2793
+ e.preventDefault();
2794
+ go(list.length - 1);
2795
+ break;
2796
+ case "Enter":
2797
+ case " ":
2798
+ case "Spacebar":
2799
+ e.preventDefault();
2800
+ rovingKey = env.keyOf(mark);
2801
+ env.select(mark, { ctrl: !!(e.ctrlKey || e.metaKey), shift: !!e.shiftKey });
2802
+ break;
2803
+ case "Escape":
2804
+ if (env.escapeClears !== false) env.clear();
2805
+ break;
2806
+ }
2807
+ };
2808
+ container.addEventListener("keydown", onKey);
2809
+ return {
2810
+ refresh,
2811
+ destroy() {
2812
+ destroyed = true;
2813
+ container.removeEventListener("keydown", onKey);
2814
+ try {
2815
+ container.classList?.remove(KEYBOARD_CLASS);
2816
+ } catch {
2817
+ }
2818
+ }
2819
+ };
2820
+ }
2821
+
2578
2822
  // src/host.ts
2579
2823
  function sessionViewStateProvider(container) {
2580
2824
  const holder = container;
@@ -2723,6 +2967,9 @@ function createChartHost(container, config) {
2723
2967
  }
2724
2968
  }
2725
2969
  let lassoClaimed = false;
2970
+ let ownBag = null;
2971
+ const seedKey = (s) => s && typeof s === "object" ? JSON.stringify(s) : void 0;
2972
+ let lastSeed = seedKey(raw.uiState);
2726
2973
  if (typeof raw.setUiState !== "function") {
2727
2974
  const provider = config.viewState ?? sessionViewStateProvider(container);
2728
2975
  let bag;
@@ -2736,6 +2983,7 @@ function createChartHost(container, config) {
2736
2983
  if (raw.uiState && typeof raw.uiState === "object" && Object.keys(bag).length === 0)
2737
2984
  Object.assign(bag, raw.uiState);
2738
2985
  raw.uiState = bag;
2986
+ ownBag = { bag, provider };
2739
2987
  raw.setUiState = (s) => {
2740
2988
  if (!s || typeof s !== "object") return;
2741
2989
  try {
@@ -2744,6 +2992,26 @@ function createChartHost(container, config) {
2744
2992
  }
2745
2993
  };
2746
2994
  }
2995
+ const takeSeed = (partial) => {
2996
+ if (typeof partial.setUiState === "function") {
2997
+ ownBag = null;
2998
+ return partial;
2999
+ }
3000
+ if (!ownBag || !("uiState" in partial)) return partial;
3001
+ const { uiState: seed, ...rest } = partial;
3002
+ const key = seedKey(seed);
3003
+ if (key !== void 0 && key !== lastSeed) {
3004
+ lastSeed = key;
3005
+ const { bag, provider } = ownBag;
3006
+ for (const k of Object.keys(bag)) delete bag[k];
3007
+ Object.assign(bag, seed);
3008
+ try {
3009
+ provider.save({ ...bag });
3010
+ } catch {
3011
+ }
3012
+ }
3013
+ return rest;
3014
+ };
2747
3015
  const resolveForHost = (input) => {
2748
3016
  const out = resolveOptions(input);
2749
3017
  return out.allowTooltips == null ? { ...out, allowTooltips: true } : out;
@@ -2758,7 +3026,9 @@ function createChartHost(container, config) {
2758
3026
  let warnedLassoNoFilter = false;
2759
3027
  const idScope = newIdScope();
2760
3028
  let renderSeq = 0;
2761
- const annotationLayer = createAnnotationLayer(container, (a) => config.onAnnotationClick?.(a));
3029
+ const keyboardOn = codeHasKeyboardStamp(config.code);
3030
+ let keyboard = null;
3031
+ const annotationLayer = createAnnotationLayer(container, (a) => config.onAnnotationClick?.(a), { keyboard: () => keyboardOn });
2762
3032
  const drawAnnotations = () => annotationLayer.draw(
2763
3033
  { columns: data.columns, rows: data.rows },
2764
3034
  {
@@ -2802,6 +3072,21 @@ function createChartHost(container, config) {
2802
3072
  }
2803
3073
  };
2804
3074
  const subs = /* @__PURE__ */ new Set();
3075
+ let hiddenHeld = 0;
3076
+ let factFresh = true;
3077
+ const selectionFact = () => {
3078
+ const n = Array.isArray(data?.rows) ? data.rows.length : 0;
3079
+ const rows = Array.from(new Set((current ?? []).filter((r) => Number.isInteger(r) && r >= 0 && r < n))).sort((a, b) => a - b);
3080
+ return { rows, hidden: hiddenHeld };
3081
+ };
3082
+ const tellSelection = () => {
3083
+ const slot = container[CONTAINER_SLOT_HOST_SELECTION];
3084
+ if (typeof slot !== "function") return;
3085
+ try {
3086
+ slot(selectionFact());
3087
+ } catch {
3088
+ }
3089
+ };
2805
3090
  const ensureAffordanceStyles = () => {
2806
3091
  try {
2807
3092
  if (!doc || doc.getElementById("bic-chart-host-affordances")) return;
@@ -2827,7 +3112,18 @@ function createChartHost(container, config) {
2827
3112
  anomaly badge, a highlight stroke) and must keep dimming with its datum. */
2828
3113
  .${HOST_CONTAINER_CLASS} .${MARK_CLASS} { cursor: pointer; transition: opacity 120ms ease-in-out, filter 100ms ease-in-out; }
2829
3114
  .${HOST_CONTAINER_CLASS} .${MARK_CLASS}:not([${ROW_IDX_ATTR}=""]):hover { filter: brightness(1.1) drop-shadow(0 0 1.5px rgba(0,0,0,0.5)) !important; }
2830
- .${HOST_CONTAINER_CLASS}:has(.${MARK_CLASS}:not([${ROW_IDX_ATTR}=""]):hover) .${MARK_CLASS}:not(:hover):not([${ROW_IDX_ATTR}=""]) { opacity: 0.55; }
3115
+ .${HOST_CONTAINER_CLASS}:not(.${MARKS_INERT_CLASS}):has(.${MARK_CLASS}:not([${ROW_IDX_ATTR}=""]):hover) .${MARK_CLASS}:not(:hover):not([${ROW_IDX_ATTR}=""]) { opacity: 0.55; }
3116
+ /* A MARK THAT SELECTS NOTHING DOESN'T LOOK CLICKABLE: no pointer, over the inline cursor generated code
3117
+ sets on its marks, and no dimming of the others on hover (the rule above). A control the chart draws keeps its own. */
3118
+ .${HOST_CONTAINER_CLASS}.${MARKS_INERT_CLASS} .${MARK_CLASS} { cursor: default !important; }
3119
+ /* THE KEYBOARD'S FOCUS: a ring on the mark, control or badge that holds keyboard focus, in the colour the
3120
+ chart's text is drawn in, so it reads in either theme. A pointer's focus shows none. */
3121
+ .${HOST_CONTAINER_CLASS}.${KEYBOARD_CLASS} [${WIRED_ATTR}]:focus:not(:focus-visible) { outline: none; }
3122
+ .${HOST_CONTAINER_CLASS}.${KEYBOARD_CLASS} [${WIRED_ATTR}]:focus-visible,
3123
+ .${HOST_CONTAINER_CLASS} .bic-annotation:focus-visible { outline: 2px solid var(--lch-focus-ring, currentColor) !important; outline-offset: 2px; }
3124
+ /* A dimmed mark that takes keyboard focus is drawn at full strength while it has it, ring included: the dim would
3125
+ otherwise fade the ring with the mark. */
3126
+ .${HOST_CONTAINER_CLASS}.${KEYBOARD_CLASS}.${SELECTION_ACTIVE_CLASS} .${MARK_CLASS}[${WIRED_ATTR}]:focus-visible:not(.${MARK_SELECTED_CLASS}) { opacity: 1 !important; }
2831
3127
  .${HOST_CONTAINER_CLASS}.${SELECTION_ACTIVE_CLASS} .${MARK_CLASS}:not(.${MARK_SELECTED_CLASS}):not([${ROW_IDX_ATTR}=""]) {
2832
3128
  opacity: var(${DIM_OPACITY_VAR}, ${DIM_OPACITY_DEFAULT}) !important;
2833
3129
  }
@@ -2860,9 +3156,18 @@ function createChartHost(container, config) {
2860
3156
  } catch {
2861
3157
  }
2862
3158
  };
3159
+ const marksInert = () => {
3160
+ if (typeof config.marksSelect !== "function") return false;
3161
+ try {
3162
+ return config.marksSelect() === false;
3163
+ } catch {
3164
+ return false;
3165
+ }
3166
+ };
2863
3167
  const paintSelection = () => {
2864
3168
  if (destroyed) return;
2865
3169
  try {
3170
+ container.classList?.[marksInert() ? "add" : "remove"](MARKS_INERT_CLASS);
2866
3171
  const sel = current;
2867
3172
  const active = !!sel && sel.length > 0;
2868
3173
  const dim = active && !selectionIsPeriod;
@@ -3120,22 +3425,25 @@ function createChartHost(container, config) {
3120
3425
  }
3121
3426
  }
3122
3427
  };
3123
- const notify = (rowIdxs, source, tick, fromChart = false, publish = true) => {
3428
+ const notify = (rowIdxs, source, tick, fromChart = false, publish = true, meta) => {
3124
3429
  if (destroyed) return;
3125
3430
  current = rowIdxs;
3126
3431
  selectionFromChart = rowIdxs.length > 0 && fromChart;
3127
3432
  selectionPublished = publish;
3128
3433
  activeTickEl = rowIdxs.length ? tick ?? null : null;
3129
3434
  selectionIsPeriod = rowIdxs.length > 0 && periodTickSuppressesFeedback(!!tick, chartOwnsTimeline(container));
3435
+ if (!rowIdxs.length && !meta?.keepHidden) hiddenHeld = 0;
3436
+ factFresh = true;
3130
3437
  paintSelection();
3131
3438
  paintGlyphLayer();
3132
- if (!publish) return;
3133
- for (const cb of subs) {
3439
+ keyboard?.refresh();
3440
+ if (publish) for (const cb of subs) {
3134
3441
  try {
3135
- cb(rowIdxs, source);
3442
+ cb(rowIdxs, source, meta);
3136
3443
  } catch {
3137
3444
  }
3138
3445
  }
3446
+ tellSelection();
3139
3447
  };
3140
3448
  const selectFromLasso = (rows) => {
3141
3449
  if (lassoEffect(resolved.lasso) === "highlight") {
@@ -3164,8 +3472,9 @@ function createChartHost(container, config) {
3164
3472
  const d = e?.detail || {};
3165
3473
  xfAt = Date.now();
3166
3474
  if (d.clear) notify([], d.source || "chart", void 0, false, selectionPublished || !(current && current.length));
3475
+ else if (marksInert()) return;
3167
3476
  else if (d.mark) notify(parseRowIdxs(d.mark.getAttribute?.(ROW_IDX_ATTR)), d.source || "chart", d.mark, true);
3168
- else if (Array.isArray(d.rows)) notify(validRowSet(d.rows), d.source || "chart", null, true);
3477
+ else if (Array.isArray(d.rows)) notify(validRowSet(d.rows), d.source || "chart", null, true, true, d.keepHidden === true ? { keepHidden: true } : void 0);
3169
3478
  };
3170
3479
  const resolver = createMarkResolver({
3171
3480
  root: container,
@@ -3199,6 +3508,9 @@ function createChartHost(container, config) {
3199
3508
  const wasDrag = clickWasDrag(container, e);
3200
3509
  clearPress(container);
3201
3510
  if (wasDrag) return;
3511
+ const inert = marksInert();
3512
+ container.classList?.[inert ? "add" : "remove"](MARKS_INERT_CLASS);
3513
+ if (inert) return;
3202
3514
  if (Date.now() - xfAt < XF_ECHO_MS) return;
3203
3515
  if (isInsideControl(e?.target, container)) return;
3204
3516
  let el = resolver.findMark(e?.target, clickSel, e);
@@ -3226,6 +3538,27 @@ function createChartHost(container, config) {
3226
3538
  container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
3227
3539
  const untrackPress = trackPress(container);
3228
3540
  container.addEventListener("click", onClick);
3541
+ if (keyboardOn) {
3542
+ const nameOfRow = (m) => {
3543
+ const r = parseRowIdxs(m.getAttribute(ROW_IDX_ATTR))[0];
3544
+ return rowNameOf(data.columns || [], r == null ? null : data.rows?.[r], resolved.cultureCode);
3545
+ };
3546
+ keyboard = installKeyboard({
3547
+ container,
3548
+ markSelector: clickSel,
3549
+ pressSelector: `.${CONTROL_CLASS}`,
3550
+ skipSelector: `.${ANNOTATION_LAYER_CLASS}, .${GLYPH_LAYER_CLASS}, .${LASSO_OUTLINE_CLASS}`,
3551
+ inert: marksInert,
3552
+ select: (m, mods) => {
3553
+ const ME = win?.MouseEvent ?? MouseEvent;
3554
+ m.dispatchEvent(new ME("click", { bubbles: true, cancelable: true, ctrlKey: mods.ctrl, shiftKey: mods.shift }));
3555
+ },
3556
+ clear: clearFromGesture,
3557
+ isSelected: (m) => !!m.classList?.contains(MARK_SELECTED_CLASS),
3558
+ labelOf: nameOfRow,
3559
+ keyOf: (m) => m.getAttribute(ROW_IDX_ATTR) ?? ""
3560
+ });
3561
+ }
3229
3562
  container.classList?.add(HOST_CONTAINER_CLASS);
3230
3563
  ensureAffordanceStyles();
3231
3564
  const fitOpts = config.fit ?? true;
@@ -3373,6 +3706,15 @@ function createChartHost(container, config) {
3373
3706
  } catch {
3374
3707
  }
3375
3708
  paintGlyphLayer();
3709
+ installLassoHere();
3710
+ keyboard?.refresh();
3711
+ const fw = focusWas;
3712
+ focusWas = null;
3713
+ restoreFocus(container, fw);
3714
+ selfRedraw?.takeRecords();
3715
+ watchSelfRedraw();
3716
+ };
3717
+ function installLassoHere() {
3376
3718
  try {
3377
3719
  lasso = installLasso({
3378
3720
  container,
@@ -3383,7 +3725,38 @@ function createChartHost(container, config) {
3383
3725
  });
3384
3726
  } catch {
3385
3727
  }
3728
+ }
3729
+ let selfRedraw = null;
3730
+ let focusWas = null;
3731
+ let notesHeld = (config.annotations ?? []).length > 0;
3732
+ const takesMarks = (n) => n.nodeType === 1 && (!!n.classList?.contains(MARK_CLASS) || !!n.querySelector?.(`.${MARK_CLASS}`));
3733
+ const lassoLost = () => !!lasso?.surface && !lasso.surface.isConnected;
3734
+ const repaintAfterChart = () => {
3735
+ if (destroyed) return;
3736
+ try {
3737
+ ensureCrossfilterHitTargets(container, doc);
3738
+ } catch {
3739
+ }
3740
+ paintSelection();
3741
+ try {
3742
+ drawAnnotations();
3743
+ } catch {
3744
+ }
3745
+ paintGlyphLayer(false);
3746
+ if (lassoLost()) installLassoHere();
3747
+ keyboard?.refresh();
3748
+ selfRedraw?.takeRecords();
3386
3749
  };
3750
+ function watchSelfRedraw() {
3751
+ const MO = win?.MutationObserver;
3752
+ if (selfRedraw || !MO || destroyed) return;
3753
+ selfRedraw = new MO((records) => {
3754
+ if (destroyed || !records.some((r) => Array.from(r.removedNodes).some(takesMarks))) return;
3755
+ if (!(current && current.length) && !notesHeld && !lassoLost()) return;
3756
+ repaintAfterChart();
3757
+ });
3758
+ selfRedraw.observe(container, { childList: true, subtree: true });
3759
+ }
3387
3760
  const renderFailure = (err) => {
3388
3761
  const message = String(err?.message ?? err ?? "");
3389
3762
  if (config.onInvalidSentinel && isInvalidSentinelError(message)) {
@@ -3403,7 +3776,9 @@ function createChartHost(container, config) {
3403
3776
  disposeLasso();
3404
3777
  flushZoom();
3405
3778
  if (hasRendered && keepScroll) rememberScroll(container, scrollKey());
3779
+ focusWas = rememberFocus(container);
3406
3780
  container.innerHTML = "";
3781
+ selfRedraw?.takeRecords();
3407
3782
  hasRendered = true;
3408
3783
  if (!renderFn) {
3409
3784
  if (!config.code) throw new Error("createChartHost: provide code or renderFn");
@@ -3429,6 +3804,7 @@ function createChartHost(container, config) {
3429
3804
  if (resolved.dateCellsAreUtcDays === void 0 && data.dateCellsAreUtcDays) resolved = { ...resolved, dateCellsAreUtcDays: true };
3430
3805
  if (resolved.suppressSelectionGlyph === void 0 && data.meta?.suppressSelectionGlyph === true)
3431
3806
  resolved = { ...resolved, suppressSelectionGlyph: true };
3807
+ if (keyboardOn) resolved = { ...resolved, keyboardMarks: true };
3432
3808
  let outcome;
3433
3809
  zoomCap = null;
3434
3810
  if (zoomMemory && config.code && !config.renderFn) {
@@ -3443,6 +3819,7 @@ function createChartHost(container, config) {
3443
3819
  zoomCap = null;
3444
3820
  }
3445
3821
  }
3822
+ if (raw.hostSelection == null) resolved.hostSelection = factFresh ? selectionFact() : void 0;
3446
3823
  try {
3447
3824
  try {
3448
3825
  outcome = renderFn(container, { columns: data.columns, rows: data.rows }, resolved);
@@ -3503,13 +3880,14 @@ function createChartHost(container, config) {
3503
3880
  // callback) must not clear, stop or annotate the chart that is on screen now.
3504
3881
  setOptions(partial) {
3505
3882
  if (destroyed) return;
3506
- raw = { ...raw, ...partial };
3883
+ raw = { ...raw, ...takeSeed(partial ?? {}) };
3507
3884
  resolved = resolveForHost(raw);
3508
3885
  host.render();
3509
3886
  },
3510
3887
  setData(next) {
3511
3888
  if (destroyed) return;
3512
3889
  data = next;
3890
+ factFresh = false;
3513
3891
  host.render();
3514
3892
  },
3515
3893
  get options() {
@@ -3517,6 +3895,7 @@ function createChartHost(container, config) {
3517
3895
  },
3518
3896
  setAnnotations(list) {
3519
3897
  if (destroyed) return;
3898
+ notesHeld = (list ?? []).length > 0;
3520
3899
  annotationLayer.set(list);
3521
3900
  try {
3522
3901
  drawAnnotations();
@@ -3555,6 +3934,15 @@ function createChartHost(container, config) {
3555
3934
  if (lasso && lasso.rows().length && !sameRowSet(rows, lasso.rows())) lasso.clear({ publish: false, source: "host" });
3556
3935
  notify(rows, "host");
3557
3936
  },
3937
+ setHidden(count) {
3938
+ if (destroyed) return;
3939
+ const n = Number(count);
3940
+ const next = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
3941
+ if (next === hiddenHeld && factFresh) return;
3942
+ hiddenHeld = next;
3943
+ factFresh = true;
3944
+ tellSelection();
3945
+ },
3558
3946
  get current() {
3559
3947
  return current;
3560
3948
  }
@@ -3569,6 +3957,12 @@ function createChartHost(container, config) {
3569
3957
  if (destroyed) return;
3570
3958
  destroyed = true;
3571
3959
  stopAnim();
3960
+ if (selfRedraw) {
3961
+ selfRedraw.disconnect();
3962
+ selfRedraw = null;
3963
+ }
3964
+ keyboard?.destroy();
3965
+ keyboard = null;
3572
3966
  container.removeEventListener(XFILTER_REFRESH_EVENT, onXf);
3573
3967
  container.removeEventListener("click", onClick);
3574
3968
  untrackPress();
@@ -3792,6 +4186,7 @@ export {
3792
4186
  exportFormatsFor,
3793
4187
  exportTableService,
3794
4188
  browserSaveFile,
4189
+ cleanHostSelection,
3795
4190
  resolveOptions,
3796
4191
  buildRenderPayload,
3797
4192
  stripJsComments,
@@ -3814,6 +4209,14 @@ export {
3814
4209
  invalidSentinelReason,
3815
4210
  ANNOTATION_LAYER_CLASS,
3816
4211
  ANNOTATION_CLASS,
4212
+ rememberFocus,
4213
+ restoreFocus,
4214
+ KEYBOARD_STAMP,
4215
+ codeHasKeyboardStamp,
4216
+ KEYBOARD_CLASS,
4217
+ WIRED_ATTR,
4218
+ rowNameOf,
4219
+ installKeyboard,
3817
4220
  sessionViewStateProvider,
3818
4221
  noopViewStateProvider,
3819
4222
  assembleD3,