@bicharts/chart-host 0.6.63 → 0.6.64

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.
@@ -567,15 +567,15 @@ function decideLabelColor(textColor, fill, fillOpacity, pageBg, minContrast = MI
567
567
  return useLight ? { fix: true, color: LIGHT_TEXT, reason: "fixed-light" } : { fix: true, color: DARK_TEXT, reason: "fixed-dark" };
568
568
  }
569
569
  var GLYPH_SAMPLE_N = 3;
570
- function glyphSampleGrid(box, n = GLYPH_SAMPLE_N) {
570
+ function glyphSampleGrid(box2, n = GLYPH_SAMPLE_N) {
571
571
  const out = [];
572
- if (!box || !(box.width > 0) || !(box.height > 0) || !(n >= 1)) return out;
572
+ if (!box2 || !(box2.width > 0) || !(box2.height > 0) || !(n >= 1)) return out;
573
573
  const k3 = Math.floor(n);
574
574
  for (let i = 0; i < k3; i++) {
575
575
  for (let j4 = 0; j4 < k3; j4++) {
576
576
  out.push({
577
- x: box.left + box.width * (i + 0.5) / k3,
578
- y: box.top + box.height * (j4 + 0.5) / k3
577
+ x: box2.left + box2.width * (i + 0.5) / k3,
578
+ y: box2.top + box2.height * (j4 + 0.5) / k3
579
579
  });
580
580
  }
581
581
  }
@@ -586,14 +586,14 @@ var STRADDLE_ROWS = 2;
586
586
  var STRADDLE_MIN_SHARE = 0.125;
587
587
  var STRADDLE_TARGET_CONTRAST = 4.5;
588
588
  var STRADDLE_PILL_ALPHA = 0.9;
589
- function straddleSampleGrid(box) {
589
+ function straddleSampleGrid(box2) {
590
590
  const out = [];
591
- if (!box || !(box.width > 0) || !(box.height > 0)) return out;
591
+ if (!box2 || !(box2.width > 0) || !(box2.height > 0)) return out;
592
592
  for (let i = 0; i < STRADDLE_COLS; i++) {
593
593
  for (let j4 = 0; j4 < STRADDLE_ROWS; j4++) {
594
594
  out.push({
595
- x: box.left + box.width * (i + 0.5) / STRADDLE_COLS,
596
- y: box.top + box.height * (j4 + 0.5) / STRADDLE_ROWS
595
+ x: box2.left + box2.width * (i + 0.5) / STRADDLE_COLS,
596
+ y: box2.top + box2.height * (j4 + 0.5) / STRADDLE_ROWS
597
597
  });
598
598
  }
599
599
  }
@@ -2451,9 +2451,9 @@ function createMarkResolver(env) {
2451
2451
  const r2 = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
2452
2452
  if (!r2 || r2.width <= 0 || r2.height <= 0) continue;
2453
2453
  if (x3 < r2.left || x3 > r2.right || y2 < r2.top || y2 > r2.bottom) continue;
2454
- const area = r2.width * r2.height;
2455
- if (area < bestArea) {
2456
- bestArea = area;
2454
+ const area2 = r2.width * r2.height;
2455
+ if (area2 < bestArea) {
2456
+ bestArea = area2;
2457
2457
  best = el;
2458
2458
  }
2459
2459
  }
@@ -3062,15 +3062,15 @@ function ctmScaleOf(m4) {
3062
3062
  }
3063
3063
  function svgInkReach(svg) {
3064
3064
  try {
3065
- const box = svg.getBoundingClientRect();
3066
- if (!(box.width > 0) || !(box.height > 0)) return null;
3065
+ const box2 = svg.getBoundingClientRect();
3066
+ if (!(box2.width > 0) || !(box2.height > 0)) return null;
3067
3067
  const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
3068
3068
  const clipOf = clipWindowReader(svg);
3069
3069
  let right = -Infinity, bottom = -Infinity, seen = 0;
3070
3070
  for (let i = 0; i < els.length && seen < FIT_WALK_CAP; i++) {
3071
3071
  const r2 = els[i].getBoundingClientRect();
3072
3072
  if (r2.width <= 0 && r2.height <= 0) continue;
3073
- if (isPhantomBox(r2.width, r2.height, box.width, box.height)) continue;
3073
+ if (isPhantomBox(r2.width, r2.height, box2.width, box2.height)) continue;
3074
3074
  let rr = r2.right, rb = r2.bottom;
3075
3075
  const clip = clipOf(els[i]);
3076
3076
  if (clip) {
@@ -3083,51 +3083,39 @@ function svgInkReach(svg) {
3083
3083
  if (rb > bottom) bottom = rb;
3084
3084
  }
3085
3085
  if (seen === 0) return null;
3086
- return { right: right - box.left, bottom: bottom - box.top };
3086
+ return { right: right - box2.left, bottom: bottom - box2.top };
3087
+ } catch {
3088
+ return null;
3089
+ }
3090
+ }
3091
+ function clipWindowOf(el) {
3092
+ try {
3093
+ const m4 = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(el.getAttribute("clip-path") || "");
3094
+ const cp = m4 ? (el.closest("svg") ?? el.ownerDocument).querySelector(`clipPath[id="${m4[1].replace(/"/g, '\\"')}"]`) : null;
3095
+ const ctm = cp ? el.getScreenCTM?.() : null;
3096
+ if (!cp || !ctm || cp.getAttribute("clipPathUnits") === "objectBoundingBox") return null;
3097
+ let l2 = Infinity, t = Infinity, r2 = -Infinity, b4 = -Infinity;
3098
+ for (let i = 0; i < cp.children.length; i++) {
3099
+ const bb = cp.children[i].getBBox?.();
3100
+ if (!bb || !(bb.width > 0) || !(bb.height > 0)) continue;
3101
+ for (const [x3, y2] of [[bb.x, bb.y], [bb.x + bb.width, bb.y], [bb.x, bb.y + bb.height], [bb.x + bb.width, bb.y + bb.height]]) {
3102
+ const sx = ctm.a * x3 + ctm.c * y2 + ctm.e, sy = ctm.b * x3 + ctm.d * y2 + ctm.f;
3103
+ l2 = Math.min(l2, sx);
3104
+ t = Math.min(t, sy);
3105
+ r2 = Math.max(r2, sx);
3106
+ b4 = Math.max(b4, sy);
3107
+ }
3108
+ }
3109
+ return isFinite(r2) && isFinite(b4) ? { left: l2, top: t, right: r2, bottom: b4 } : null;
3087
3110
  } catch {
3088
3111
  return null;
3089
3112
  }
3090
3113
  }
3091
3114
  function clipWindowReader(svg) {
3092
3115
  const own = /* @__PURE__ */ new Map();
3093
- const byId = (id) => {
3094
- try {
3095
- return svg.querySelector(`clipPath[id="${id.replace(/"/g, '\\"')}"]`);
3096
- } catch {
3097
- return null;
3098
- }
3099
- };
3100
3116
  const windowOf = (el) => {
3101
- if (own.has(el)) return own.get(el);
3102
- let win = null;
3103
- try {
3104
- const ref = el.getAttribute("clip-path") || "";
3105
- const m4 = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(ref);
3106
- const cp = m4 ? byId(m4[1]) : null;
3107
- const ctm = cp ? el.getScreenCTM?.() : null;
3108
- if (cp && ctm && cp.getAttribute("clipPathUnits") !== "objectBoundingBox") {
3109
- let r2 = -Infinity, b4 = -Infinity;
3110
- for (let i = 0; i < cp.children.length; i++) {
3111
- const bb = cp.children[i].getBBox?.();
3112
- if (!bb || !(bb.width > 0) || !(bb.height > 0)) continue;
3113
- for (const [x3, y2] of [
3114
- [bb.x, bb.y],
3115
- [bb.x + bb.width, bb.y],
3116
- [bb.x, bb.y + bb.height],
3117
- [bb.x + bb.width, bb.y + bb.height]
3118
- ]) {
3119
- const sx = ctm.a * x3 + ctm.c * y2 + ctm.e, sy = ctm.b * x3 + ctm.d * y2 + ctm.f;
3120
- if (sx > r2) r2 = sx;
3121
- if (sy > b4) b4 = sy;
3122
- }
3123
- }
3124
- if (isFinite(r2) && isFinite(b4)) win = { right: r2, bottom: b4 };
3125
- }
3126
- } catch {
3127
- win = null;
3128
- }
3129
- own.set(el, win);
3130
- return win;
3117
+ if (!own.has(el)) own.set(el, clipWindowOf(el));
3118
+ return own.get(el);
3131
3119
  };
3132
3120
  return (el) => {
3133
3121
  let acc = null;
@@ -3895,7 +3883,34 @@ function invalidSentinelReason(text2, max = 240) {
3895
3883
  // src/annotations.ts
3896
3884
  var ANNOTATION_LAYER_CLASS = "bic-annotations";
3897
3885
  var ANNOTATION_CLASS = "bic-annotation";
3886
+ var ZOOM_PAD_CLASS = "llm-zoom-pad";
3898
3887
  var text = (v) => v == null ? "" : String(v).trim();
3888
+ var FREE = { l: -Infinity, t: -Infinity, r: Infinity, b: Infinity };
3889
+ var box = (r2) => ({ l: r2.left, t: r2.top, r: r2.right, b: r2.bottom });
3890
+ var area = (b4) => (b4.r - b4.l) * (b4.b - b4.t);
3891
+ var cut = (a3, b4) => {
3892
+ if (!a3) return null;
3893
+ const c2 = { l: Math.max(a3.l, b4.l), t: Math.max(a3.t, b4.t), r: Math.min(a3.r, b4.r), b: Math.min(a3.b, b4.b) };
3894
+ return c2.r > c2.l && c2.b > c2.t ? c2 : null;
3895
+ };
3896
+ function shapeTest(mark) {
3897
+ const g5 = mark;
3898
+ let inv = null;
3899
+ try {
3900
+ inv = g5?.isPointInFill ? g5.getScreenCTM?.()?.inverse() ?? null : null;
3901
+ } catch {
3902
+ inv = null;
3903
+ }
3904
+ if (!g5 || !inv) return () => true;
3905
+ const m4 = inv;
3906
+ return (x3, y2) => {
3907
+ try {
3908
+ return g5.isPointInFill({ x: m4.a * x3 + m4.c * y2 + m4.e, y: m4.b * x3 + m4.d * y2 + m4.f });
3909
+ } catch {
3910
+ return true;
3911
+ }
3912
+ };
3913
+ }
3899
3914
  var parseIdxs = (s2) => (s2 || "").split(",").map((x3) => parseInt(x3, 10)).filter((n) => Number.isFinite(n));
3900
3915
  function createAnnotationLayer(container, onClick) {
3901
3916
  let list = [];
@@ -3952,6 +3967,37 @@ function createAnnotationLayer(container, onClick) {
3952
3967
  else marksByRow.set(r2, [m4]);
3953
3968
  }
3954
3969
  const origin = container.getBoundingClientRect();
3970
+ const open = origin.width > 0 && origin.height > 0 ? box(origin) : FREE;
3971
+ const clipOf = /* @__PURE__ */ new Map();
3972
+ const viewOf = (m4) => {
3973
+ let v = open;
3974
+ const svg = m4.closest("svg");
3975
+ if (svg) {
3976
+ const s2 = svg.getBoundingClientRect();
3977
+ if (s2.width > 0 && s2.height > 0) v = cut(v, box(s2));
3978
+ }
3979
+ for (let el = m4.parentElement; v && el && el !== svg && el !== container; el = el.parentElement) {
3980
+ if (!clipOf.has(el)) {
3981
+ const w4 = clipWindowOf(el);
3982
+ clipOf.set(el, w4 && box(w4));
3983
+ }
3984
+ const w3 = clipOf.get(el);
3985
+ if (w3) v = cut(v, w3);
3986
+ }
3987
+ return v;
3988
+ };
3989
+ const controls = Array.from(container.querySelectorAll(`.${CONTROL_CLASS}, .${ZOOM_PAD_CLASS}`)).map((c2) => c2.getBoundingClientRect()).filter((r2) => r2.width > 0 && r2.height > 0).map(box);
3990
+ const clear = (v, len, mark) => {
3991
+ const hw = Math.max(16, len * 6 + 8) / 2, hh = 8, g5 = 2;
3992
+ const cx = (v.l + v.r) / 2, cy = (v.t + v.b) / 2;
3993
+ const free = (x3, y2) => x3 >= v.l && x3 <= v.r && y2 >= v.t && y2 <= v.b && controls.every((c2) => x3 + hw <= c2.l || x3 - hw >= c2.r || y2 + hh <= c2.t || y2 - hh >= c2.b);
3994
+ const onShape = shapeTest(mark);
3995
+ const tries = [[cx, cy]];
3996
+ for (let i = 0; i <= 8; i++) for (let j4 = 0; j4 <= 6; j4++) tries.push([v.l + (v.r - v.l) * i / 8, v.t + (v.b - v.t) * j4 / 6]);
3997
+ tries.sort((p4, q2) => Math.hypot(p4[0] - cx, p4[1] - cy) - Math.hypot(q2[0] - cx, q2[1] - cy));
3998
+ for (const c2 of controls) tries.push([cx, c2.b + hh + g5], [cx, c2.t - hh - g5], [c2.l - hw - g5, cy], [c2.r + hw + g5, cy]);
3999
+ return tries.find(([x3, y2]) => free(x3, y2) && onShape(x3, y2)) ?? tries.find(([x3, y2]) => free(x3, y2)) ?? null;
4000
+ };
3955
4001
  let shown = 0;
3956
4002
  for (const a3 of list) {
3957
4003
  const pairs = (a3.where ? Object.entries(a3.where) : a3.column != null ? [[a3.column, a3.value]] : []).map(([c2, v]) => [names.indexOf(c2), text(v)]);
@@ -3961,23 +4007,30 @@ function createAnnotationLayer(container, onClick) {
3961
4007
  if (pairs.every(([ci, v]) => text(row?.[ci]) === v)) for (const m4 of marksByRow.get(r2) ?? []) hits.add(m4);
3962
4008
  });
3963
4009
  if (!hits.size) continue;
3964
- shown++;
3965
- let best = null;
4010
+ let best = null, bestMark = null, laidOut = false;
3966
4011
  for (const m4 of hits) {
3967
4012
  const b4 = m4.getBoundingClientRect();
3968
- if (!best || b4.width * b4.height > best.width * best.height) best = b4;
4013
+ if (!(b4.width > 0 && b4.height > 0)) continue;
4014
+ laidOut = true;
4015
+ const v = cut(box(b4), viewOf(m4));
4016
+ if (v && (!best || area(v) > area(best))) {
4017
+ best = v;
4018
+ bestMark = m4;
4019
+ }
3969
4020
  }
4021
+ const pt2 = best ? clear(best, (a3.label ?? "\u2022").length, bestMark) : null;
4022
+ if (laidOut && !pt2) continue;
4023
+ shown++;
3970
4024
  const badge = doc.createElement("div");
3971
4025
  badge.className = ANNOTATION_CLASS;
3972
4026
  badge.textContent = a3.label ?? "\u2022";
3973
4027
  if (a3.title) badge.setAttribute("title", a3.title);
3974
4028
  badge.setAttribute("role", "button");
3975
4029
  badge.setAttribute("aria-label", a3.title ? `Note: ${a3.title}` : `Note on ${want}`);
3976
- const laidOut = !!best && best.width > 0 && best.height > 0;
3977
4030
  Object.assign(badge.style, {
3978
4031
  position: "absolute",
3979
- left: `${Math.round(laidOut ? best.left - origin.left + best.width / 2 : 0)}px`,
3980
- top: `${Math.round(laidOut ? best.top - origin.top + best.height / 2 : 0)}px`,
4032
+ left: `${Math.round(pt2 ? pt2[0] - origin.left : 0)}px`,
4033
+ top: `${Math.round(pt2 ? pt2[1] - origin.top : 0)}px`,
3981
4034
  transform: "translate(-50%, -50%)",
3982
4035
  minWidth: "16px",
3983
4036
  height: "16px",
package/dist/index.mjs CHANGED
@@ -122,7 +122,7 @@ import {
122
122
  toSourceRows,
123
123
  unpinScrolledAxis,
124
124
  valuePlacementFlag
125
- } from "./chunk-ZTTBWP3I.mjs";
125
+ } from "./chunk-VPGFFVWA.mjs";
126
126
  import "./chunk-A2GMXZP7.mjs";
127
127
  import {
128
128
  ACTIVE_TICK_CLASS,
package/dist/react.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-ZTTBWP3I.mjs";
25
+ } from "./chunk-VPGFFVWA.mjs";
26
26
  import "./chunk-A2GMXZP7.mjs";
27
27
  import "./chunk-WNGJUPEI.mjs";
28
28
  import "./chunk-GHODWOAL.mjs";
@@ -23,6 +23,9 @@ export interface AnnotationReport {
23
23
  }
24
24
  export declare const ANNOTATION_LAYER_CLASS = "bic-annotations";
25
25
  export declare const ANNOTATION_CLASS = "bic-annotation";
26
+ /** The class the d3.llmGeoZoom helper gives its button pad - a control drawn by a generated chart without the
27
+ * contract's CONTROL_CLASS, so the badge layer names it too. */
28
+ export declare const ZOOM_PAD_CLASS = "llm-zoom-pad";
26
29
  export interface AnnotationLayer {
27
30
  set(list: MarkAnnotation[]): void;
28
31
  /** Redraw against the container's current marks and the given data. */
@@ -7,6 +7,15 @@ export declare function svgInkReach(svg: SVGSVGElement): {
7
7
  right: number;
8
8
  bottom: number;
9
9
  } | null;
10
+ /** The window ONE element's own clip-path attribute opens, in screen coordinates - or null: no clip, a clip in
11
+ * objectBoundingBox units, or one that won't measure. Shared by the frame fit (right/bottom) and the note badges
12
+ * (the whole window). The clipPath is looked up within the element's own svg. */
13
+ export declare function clipWindowOf(el: Element): {
14
+ left: number;
15
+ top: number;
16
+ right: number;
17
+ bottom: number;
18
+ } | null;
10
19
  export type InkReach = (el: Element) => {
11
20
  right: number;
12
21
  bottom: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.63",
3
+ "version": "0.6.64",
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",