@bicharts/chart-host 0.6.60 → 0.6.62

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.
@@ -2895,14 +2895,22 @@ function svgInkReach(svg) {
2895
2895
  const box = svg.getBoundingClientRect();
2896
2896
  if (!(box.width > 0) || !(box.height > 0)) return null;
2897
2897
  const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
2898
+ const clipOf = clipWindowReader(svg);
2898
2899
  let right = -Infinity, bottom = -Infinity, seen = 0;
2899
2900
  for (let i = 0; i < els.length && seen < FIT_WALK_CAP; i++) {
2900
2901
  const r2 = els[i].getBoundingClientRect();
2901
2902
  if (r2.width <= 0 && r2.height <= 0) continue;
2902
2903
  if (isPhantomBox(r2.width, r2.height, box.width, box.height)) continue;
2904
+ let rr = r2.right, rb = r2.bottom;
2905
+ const clip = clipOf(els[i]);
2906
+ if (clip) {
2907
+ if (r2.left >= clip.right || r2.top >= clip.bottom) continue;
2908
+ rr = Math.min(rr, clip.right);
2909
+ rb = Math.min(rb, clip.bottom);
2910
+ }
2903
2911
  seen++;
2904
- if (r2.right > right) right = r2.right;
2905
- if (r2.bottom > bottom) bottom = r2.bottom;
2912
+ if (rr > right) right = rr;
2913
+ if (rb > bottom) bottom = rb;
2906
2914
  }
2907
2915
  if (seen === 0) return null;
2908
2916
  return { right: right - box.left, bottom: bottom - box.top };
@@ -2910,6 +2918,56 @@ function svgInkReach(svg) {
2910
2918
  return null;
2911
2919
  }
2912
2920
  }
2921
+ function clipWindowReader(svg) {
2922
+ const own = /* @__PURE__ */ new Map();
2923
+ const byId = (id) => {
2924
+ try {
2925
+ return svg.querySelector(`clipPath[id="${id.replace(/"/g, '\\"')}"]`);
2926
+ } catch {
2927
+ return null;
2928
+ }
2929
+ };
2930
+ const windowOf = (el) => {
2931
+ if (own.has(el)) return own.get(el);
2932
+ let win = null;
2933
+ try {
2934
+ const ref = el.getAttribute("clip-path") || "";
2935
+ const m4 = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(ref);
2936
+ const cp = m4 ? byId(m4[1]) : null;
2937
+ const ctm = cp ? el.getScreenCTM?.() : null;
2938
+ if (cp && ctm && cp.getAttribute("clipPathUnits") !== "objectBoundingBox") {
2939
+ let r2 = -Infinity, b4 = -Infinity;
2940
+ for (let i = 0; i < cp.children.length; i++) {
2941
+ const bb = cp.children[i].getBBox?.();
2942
+ if (!bb || !(bb.width > 0) || !(bb.height > 0)) continue;
2943
+ for (const [x3, y2] of [
2944
+ [bb.x, bb.y],
2945
+ [bb.x + bb.width, bb.y],
2946
+ [bb.x, bb.y + bb.height],
2947
+ [bb.x + bb.width, bb.y + bb.height]
2948
+ ]) {
2949
+ const sx = ctm.a * x3 + ctm.c * y2 + ctm.e, sy = ctm.b * x3 + ctm.d * y2 + ctm.f;
2950
+ if (sx > r2) r2 = sx;
2951
+ if (sy > b4) b4 = sy;
2952
+ }
2953
+ }
2954
+ if (isFinite(r2) && isFinite(b4)) win = { right: r2, bottom: b4 };
2955
+ }
2956
+ } catch {
2957
+ win = null;
2958
+ }
2959
+ own.set(el, win);
2960
+ return win;
2961
+ };
2962
+ return (el) => {
2963
+ let acc = null;
2964
+ for (let a3 = el; a3 && a3 !== svg; a3 = a3.parentElement) {
2965
+ const w3 = windowOf(a3);
2966
+ if (w3) acc = acc ? { right: Math.min(acc.right, w3.right), bottom: Math.min(acc.bottom, w3.bottom) } : w3;
2967
+ }
2968
+ return acc;
2969
+ };
2970
+ }
2913
2971
  function measureContainerBoxes(c2, inkOf) {
2914
2972
  const boxes = [];
2915
2973
  if (!c2 || !c2.children) return boxes;
@@ -3772,6 +3830,47 @@ function applyLabelContrast(container, opts = {}) {
3772
3830
  }
3773
3831
  }
3774
3832
 
3833
+ // src/idScope.ts
3834
+ var seq = 0;
3835
+ function newIdScope() {
3836
+ seq += 1;
3837
+ return "h" + seq;
3838
+ }
3839
+ var URL_REF = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/g;
3840
+ function scopeChartIds(container, scope) {
3841
+ const report = { renamed: 0, collisions: [] };
3842
+ try {
3843
+ const tag = "--" + scope, doc = container.ownerDocument, map = /* @__PURE__ */ new Map();
3844
+ const refs = [], used = /* @__PURE__ */ new Set();
3845
+ container.querySelectorAll("*").forEach((el) => {
3846
+ for (const a3 of el.getAttributeNames()) {
3847
+ const v = el.getAttribute(a3);
3848
+ const href = (a3 === "href" || a3 === "xlink:href") && v[0] === "#";
3849
+ if (!href && v.indexOf("url(") < 0) continue;
3850
+ refs.push([el, a3, v]);
3851
+ if (href) used.add(v.slice(1));
3852
+ for (const m4 of v.matchAll(URL_REF)) used.add(m4[2]);
3853
+ }
3854
+ });
3855
+ container.querySelectorAll("[id]").forEach((el) => {
3856
+ const id = el.id;
3857
+ if (!used.has(id) || id.endsWith(tag) || id.startsWith("bic-")) return;
3858
+ const q2 = id.replace(/["\\]/g, "\\$&");
3859
+ if (doc && Array.from(doc.querySelectorAll(`[id="${q2}"],[id^="${q2}--h"]`)).some((o4) => !container.contains(o4))) {
3860
+ report.collisions.push(id);
3861
+ }
3862
+ map.set(id, el.id = id + tag);
3863
+ report.renamed++;
3864
+ });
3865
+ for (const [el, a3, v] of refs) {
3866
+ const nv = v[0] === "#" && map.has(v.slice(1)) ? "#" + map.get(v.slice(1)) : v.replace(URL_REF, (m4, q2, id) => map.has(id) ? `url(${q2}#${map.get(id)}${q2})` : m4);
3867
+ if (nv !== v) el.setAttribute(a3, nv);
3868
+ }
3869
+ } catch {
3870
+ }
3871
+ return report;
3872
+ }
3873
+
3775
3874
  // src/invalidSentinel.ts
3776
3875
  function isInvalidSentinelError(text2) {
3777
3876
  if (!text2) return false;
@@ -4078,6 +4177,8 @@ function createChartHost(container, config) {
4078
4177
  let destroyed = false;
4079
4178
  let warnedGeo = false;
4080
4179
  let warnedBlank = false;
4180
+ let warnedIds = false;
4181
+ const idScope = newIdScope();
4081
4182
  let renderSeq = 0;
4082
4183
  const annotationLayer = createAnnotationLayer(container, (a3) => config.onAnnotationClick?.(a3));
4083
4184
  const drawAnnotations = () => annotationLayer.draw(
@@ -4249,6 +4350,20 @@ function createChartHost(container, config) {
4249
4350
  }
4250
4351
  };
4251
4352
  const runPostRenderPasses = () => {
4353
+ try {
4354
+ const r2 = scopeChartIds(container, idScope);
4355
+ if (r2.collisions.length && !warnedIds) {
4356
+ warnedIds = true;
4357
+ try {
4358
+ (win?.console ?? console)?.warn(
4359
+ `[@bicharts/chart-host] this chart named ${r2.collisions.length} id(s) another element on the page already uses (${r2.collisions.slice(0, 5).join(", ")}). They were scoped to this chart, so its clip paths and gradients resolve to its own definitions.`
4360
+ );
4361
+ } catch {
4362
+ }
4363
+ }
4364
+ config.onIdScope?.(r2);
4365
+ } catch {
4366
+ }
4252
4367
  if (labelContrastOpts !== false) {
4253
4368
  try {
4254
4369
  const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
@@ -4328,7 +4443,7 @@ function createChartHost(container, config) {
4328
4443
  const host = {
4329
4444
  render() {
4330
4445
  if (destroyed) return;
4331
- const seq = ++renderSeq;
4446
+ const seq2 = ++renderSeq;
4332
4447
  stopAnim();
4333
4448
  container.innerHTML = "";
4334
4449
  if (!renderFn) {
@@ -4381,7 +4496,7 @@ function createChartHost(container, config) {
4381
4496
  };
4382
4497
  Promise.resolve(outcome).then(
4383
4498
  () => done(() => {
4384
- if (destroyed || seq !== renderSeq) {
4499
+ if (destroyed || seq2 !== renderSeq) {
4385
4500
  resolve();
4386
4501
  return;
4387
4502
  }
@@ -4389,7 +4504,7 @@ function createChartHost(container, config) {
4389
4504
  resolve();
4390
4505
  }),
4391
4506
  (err) => done(() => {
4392
- if (destroyed || seq !== renderSeq) {
4507
+ if (destroyed || seq2 !== renderSeq) {
4393
4508
  resolve();
4394
4509
  return;
4395
4510
  }
@@ -4715,6 +4830,8 @@ export {
4715
4830
  LABEL_CONTRAST_CAP,
4716
4831
  LABEL_PILL_CLASS,
4717
4832
  applyLabelContrast,
4833
+ newIdScope,
4834
+ scopeChartIds,
4718
4835
  isInvalidSentinelError,
4719
4836
  invalidSentinelReason,
4720
4837
  ANNOTATION_LAYER_CLASS,
package/dist/index.mjs CHANGED
@@ -91,6 +91,7 @@ import {
91
91
  measureContainerBoxes,
92
92
  minContrastOver,
93
93
  needsScroll,
94
+ newIdScope,
94
95
  nextSelection,
95
96
  noopViewStateProvider,
96
97
  o,
@@ -108,6 +109,7 @@ import {
108
109
  resolveAutoColorScale,
109
110
  resolveOptions,
110
111
  rowIdxsFromMark,
112
+ scopeChartIds,
111
113
  scrollFitFor,
112
114
  sessionViewStateProvider,
113
115
  straddleSampleGrid,
@@ -120,7 +122,7 @@ import {
120
122
  toSourceRows,
121
123
  unpinScrolledAxis,
122
124
  valuePlacementFlag
123
- } from "./chunk-5NSU3MYW.mjs";
125
+ } from "./chunk-ZLJM3EIP.mjs";
124
126
  import "./chunk-A2GMXZP7.mjs";
125
127
  import {
126
128
  ACTIVE_TICK_CLASS,
@@ -3008,6 +3010,7 @@ export {
3008
3010
  minContrastOver,
3009
3011
  mountProductFeedbackBox,
3010
3012
  needsScroll,
3013
+ newIdScope,
3011
3014
  newQualifyGroupState,
3012
3015
  newQualifyRefusalGroupState,
3013
3016
  nextSelection,
@@ -3058,6 +3061,7 @@ export {
3058
3061
  resolveOptions,
3059
3062
  rowIdxsFromMark,
3060
3063
  sameMarker,
3064
+ scopeChartIds,
3061
3065
  scrollFitFor,
3062
3066
  servedCorrelationProves,
3063
3067
  sessionViewStateProvider,
package/dist/react.mjs CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-5NSU3MYW.mjs";
25
+ } from "./chunk-ZLJM3EIP.mjs";
26
26
  import "./chunk-A2GMXZP7.mjs";
27
27
  import "./chunk-WNGJUPEI.mjs";
28
28
  import "./chunk-GHODWOAL.mjs";
@@ -6,6 +6,7 @@ import { type ColourSpreadCensus } from "./colourSpread";
6
6
  import { type ValuePlacementCensus } from "./valuePlacement";
7
7
  import { type FitRenderedChartOptions, type FitRenderedChartResult } from "./fitDom";
8
8
  import { type LabelContrastOptions, type LabelContrastReport } from "./labelContrastDom";
9
+ import { type IdScopeReport } from "./idScope";
9
10
  import { type MarkAnnotation, type AnnotationReport } from "./annotations";
10
11
  export type RenderFn = (container: HTMLElement, data: any, options: RenderOptions) => void;
11
12
  /**
@@ -166,6 +167,11 @@ export interface ChartHostConfig {
166
167
  labelContrast?: boolean | LabelContrastOptions;
167
168
  /** What the label-contrast pass did after each render - counts, so a host can log them. */
168
169
  onLabelContrast?: (report: LabelContrastReport) => void;
170
+ /**
171
+ * What the id-scoping pass did after each render: how many of the chart's ids got this host's suffix, and which
172
+ * of them also existed elsewhere in the page (a clash it prevented - two same-size charts naming one clip path).
173
+ */
174
+ onIdScope?: (report: IdScopeReport) => void;
169
175
  /**
170
176
  * THE CHART SAID THE DATA LACKS SOMETHING IT IS BUILT ON. Generated code has one sanctioned
171
177
  * hard stop at runtime: a column the chart needs is gone from `data.columns`, and it throws
@@ -0,0 +1,9 @@
1
+ /** A fresh suffix for one chart host. Unique within the page for the page's lifetime. */
2
+ export declare function newIdScope(): string;
3
+ export interface IdScopeReport {
4
+ /** How many ids were given the suffix. */
5
+ renamed: number;
6
+ /** Ids that also existed outside this chart when the pass ran - each one a clash this pass prevented. */
7
+ collisions: string[];
8
+ }
9
+ export declare function scopeChartIds(container: Element, scope: string): IdScopeReport;
@@ -27,6 +27,7 @@ export { formatSourceDate, formatSourceDateFor, isExcelDateFormat, type SourceFo
27
27
  export { composeSummaryText } from "./summaryText";
28
28
  export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
29
29
  export { applyLabelContrast, LABEL_CONTRAST_DONE_ATTR, LABEL_CONTRAST_CAP, LABEL_PILL_CLASS, type LabelContrastOptions, type LabelContrastReport, } from "./labelContrastDom";
30
+ export { scopeChartIds, newIdScope, type IdScopeReport } from "./idScope";
30
31
  export { decideLabelColor, toRGBA, compositeOver, relativeLuminance, contrastRatio, isPillBackdropAlpha, pillBacksGlyph, backingHoldsGlyph, cellSuppressesNormalize, glyphSampleGrid, DARK_TEXT, LIGHT_TEXT, MIN_CONTRAST, WHITE_TEXT_BG_LUM, PILL_MIN_ALPHA, PILL_OPAQUE_ALPHA, PILL_MIN_COVERAGE, PAGE_MATCH_TOLERANCE, BACKING_MAJORITY, GLYPH_SAMPLE_N, straddles, straddleSampleGrid, minContrastOver, pickColorOver, STRADDLE_COLS, STRADDLE_ROWS, STRADDLE_MIN_SHARE, STRADDLE_TARGET_CONTRAST, STRADDLE_PILL_ALPHA, type LabelDecision, } from "./labelContrast";
31
32
  export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
32
33
  export { isInvalidSentinelError, invalidSentinelReason } from "./invalidSentinel";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.60",
3
+ "version": "0.6.62",
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",