@bicharts/chart-host 0.6.62 → 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.
@@ -475,6 +475,170 @@ var require_papaparse_min = __commonJS({
475
475
  }
476
476
  });
477
477
 
478
+ // src/labelContrast.ts
479
+ function parseRGBA(c2) {
480
+ if (typeof c2 !== "string") return null;
481
+ const s2 = c2.trim();
482
+ const m4 = s2.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
483
+ if (m4) return [parseFloat(m4[1]), parseFloat(m4[2]), parseFloat(m4[3]), m4[4] !== void 0 ? parseFloat(m4[4]) : 1];
484
+ let h4 = s2.replace(/^#/, "");
485
+ if (h4.length === 3) h4 = h4.split("").map((ch) => ch + ch).join("");
486
+ if (h4.length === 6 && /^[0-9a-fA-F]{6}$/.test(h4)) {
487
+ return [parseInt(h4.slice(0, 2), 16), parseInt(h4.slice(2, 4), 16), parseInt(h4.slice(4, 6), 16), 1];
488
+ }
489
+ return null;
490
+ }
491
+ var DARK_TEXT = "#111111";
492
+ var LIGHT_TEXT = "#ffffff";
493
+ var MIN_CONTRAST = 3;
494
+ var WHITE_TEXT_BG_LUM = 0.5;
495
+ var PILL_MIN_ALPHA = 0.05;
496
+ var PILL_OPAQUE_ALPHA = 0.85;
497
+ var PAGE_MATCH_TOLERANCE = 2;
498
+ function isPillBackdropAlpha(a3) {
499
+ return a3 >= PILL_MIN_ALPHA && a3 < PILL_OPAQUE_ALPHA;
500
+ }
501
+ var PILL_MIN_COVERAGE = 0.6;
502
+ function pillBacksGlyph(overlapArea, glyphArea) {
503
+ if (!(glyphArea > 0) || !isFinite(overlapArea) || overlapArea <= 0) return false;
504
+ return overlapArea / glyphArea >= PILL_MIN_COVERAGE;
505
+ }
506
+ var BACKING_MAJORITY = 0.5;
507
+ function backingHoldsGlyph(backedArea, glyphArea) {
508
+ if (!(glyphArea > 0) || !isFinite(backedArea) || backedArea <= 0) return false;
509
+ return backedArea / glyphArea >= BACKING_MAJORITY;
510
+ }
511
+ function cellSuppressesNormalize(cellClass) {
512
+ return typeof cellClass === "string" && /(^|\s)lch-deck-panel(\s|$)/.test(cellClass);
513
+ }
514
+ var NAMED = {
515
+ white: [255, 255, 255, 1],
516
+ black: [0, 0, 0, 1],
517
+ none: [0, 0, 0, 0],
518
+ transparent: [0, 0, 0, 0]
519
+ };
520
+ function toRGBA(c2) {
521
+ if (typeof c2 !== "string") return null;
522
+ const k3 = c2.trim().toLowerCase();
523
+ if (k3 in NAMED) return NAMED[k3];
524
+ return parseRGBA(c2);
525
+ }
526
+ function compositeOver(fg, bg) {
527
+ const a3 = Math.max(0, Math.min(1, fg[3]));
528
+ return [
529
+ Math.round(fg[0] * a3 + bg[0] * (1 - a3)),
530
+ Math.round(fg[1] * a3 + bg[1] * (1 - a3)),
531
+ Math.round(fg[2] * a3 + bg[2] * (1 - a3))
532
+ ];
533
+ }
534
+ function relativeLuminance(rgb) {
535
+ const lin = rgb.map((v) => {
536
+ const s2 = v / 255;
537
+ return s2 <= 0.03928 ? s2 / 12.92 : Math.pow((s2 + 0.055) / 1.055, 2.4);
538
+ });
539
+ return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
540
+ }
541
+ function contrastRatio(l1, l2) {
542
+ const hi = Math.max(l1, l2);
543
+ const lo = Math.min(l1, l2);
544
+ return (hi + 0.05) / (lo + 0.05);
545
+ }
546
+ function decideLabelColor(textColor, fill, fillOpacity, pageBg, minContrast = MIN_CONTRAST, normalize = false) {
547
+ const fillRGBA = toRGBA(fill);
548
+ if (!fillRGBA) return { fix: false, color: "", reason: "fill-unparsed" };
549
+ const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
550
+ const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
551
+ const effFill = [fillRGBA[0], fillRGBA[1], fillRGBA[2], fillRGBA[3] * clamp01(fillOpacity)];
552
+ const bgRGB = compositeOver(effFill, pageRGB);
553
+ const bgLum = relativeLuminance(bgRGB);
554
+ const bgIsPage = Math.abs(bgRGB[0] - pageRGB[0]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[1] - pageRGB[1]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[2] - pageRGB[2]) <= PAGE_MATCH_TOLERANCE;
555
+ const effNormalize = normalize && !bgIsPage;
556
+ const textRGBA = toRGBA(textColor);
557
+ if (!effNormalize && textRGBA) {
558
+ const txtRGB = compositeOver(textRGBA, bgRGB);
559
+ const cur = contrastRatio(relativeLuminance(txtRGB), bgLum);
560
+ if (cur >= minContrast) return { fix: false, color: "", reason: "ok" };
561
+ }
562
+ const darkC = contrastRatio(relativeLuminance(hexToRGB(DARK_TEXT)), bgLum);
563
+ const lightC = contrastRatio(relativeLuminance(hexToRGB(LIGHT_TEXT)), bgLum);
564
+ let useLight = bgLum <= WHITE_TEXT_BG_LUM;
565
+ if (useLight && lightC < minContrast && darkC >= minContrast) useLight = false;
566
+ if (!useLight && darkC < minContrast && lightC >= minContrast) useLight = true;
567
+ return useLight ? { fix: true, color: LIGHT_TEXT, reason: "fixed-light" } : { fix: true, color: DARK_TEXT, reason: "fixed-dark" };
568
+ }
569
+ var GLYPH_SAMPLE_N = 3;
570
+ function glyphSampleGrid(box2, n = GLYPH_SAMPLE_N) {
571
+ const out = [];
572
+ if (!box2 || !(box2.width > 0) || !(box2.height > 0) || !(n >= 1)) return out;
573
+ const k3 = Math.floor(n);
574
+ for (let i = 0; i < k3; i++) {
575
+ for (let j4 = 0; j4 < k3; j4++) {
576
+ out.push({
577
+ x: box2.left + box2.width * (i + 0.5) / k3,
578
+ y: box2.top + box2.height * (j4 + 0.5) / k3
579
+ });
580
+ }
581
+ }
582
+ return out;
583
+ }
584
+ var STRADDLE_COLS = 8;
585
+ var STRADDLE_ROWS = 2;
586
+ var STRADDLE_MIN_SHARE = 0.125;
587
+ var STRADDLE_TARGET_CONTRAST = 4.5;
588
+ var STRADDLE_PILL_ALPHA = 0.9;
589
+ function straddleSampleGrid(box2) {
590
+ const out = [];
591
+ if (!box2 || !(box2.width > 0) || !(box2.height > 0)) return out;
592
+ for (let i = 0; i < STRADDLE_COLS; i++) {
593
+ for (let j4 = 0; j4 < STRADDLE_ROWS; j4++) {
594
+ out.push({
595
+ x: box2.left + box2.width * (i + 0.5) / STRADDLE_COLS,
596
+ y: box2.top + box2.height * (j4 + 0.5) / STRADDLE_ROWS
597
+ });
598
+ }
599
+ }
600
+ return out;
601
+ }
602
+ function backgroundGroups(bgs, tol = 8) {
603
+ const groups = [];
604
+ for (const b4 of bgs || []) {
605
+ const g5 = groups.find((x3) => Math.abs(x3.rgb[0] - b4[0]) <= tol && Math.abs(x3.rgb[1] - b4[1]) <= tol && Math.abs(x3.rgb[2] - b4[2]) <= tol);
606
+ if (g5) g5.n++;
607
+ else groups.push({ rgb: [b4[0], b4[1], b4[2]], n: 1 });
608
+ }
609
+ return groups.sort((a3, b4) => b4.n - a3.n);
610
+ }
611
+ function straddles(bgs) {
612
+ if (!bgs || bgs.length < 2) return false;
613
+ const g5 = backgroundGroups(bgs);
614
+ return g5.length >= 2 && g5[1].n / bgs.length >= STRADDLE_MIN_SHARE;
615
+ }
616
+ function minContrastOver(textColor, bgs) {
617
+ const t = toRGBA(textColor);
618
+ if (!t || !bgs || !bgs.length) return 0;
619
+ let worst = Infinity;
620
+ for (const b4 of bgs) {
621
+ const tx = compositeOver(t, b4);
622
+ worst = Math.min(worst, contrastRatio(relativeLuminance(tx), relativeLuminance(b4)));
623
+ }
624
+ return worst;
625
+ }
626
+ function pickColorOver(textColor, bgs, target = STRADDLE_TARGET_CONTRAST) {
627
+ const cur = minContrastOver(textColor, bgs);
628
+ if (cur >= target) return { color: String(textColor), contrast: cur, keep: true };
629
+ const d2 = minContrastOver(DARK_TEXT, bgs), l2 = minContrastOver(LIGHT_TEXT, bgs);
630
+ const best = d2 >= l2 ? { color: DARK_TEXT, contrast: d2 } : { color: LIGHT_TEXT, contrast: l2 };
631
+ return best.contrast > cur ? { ...best, keep: false } : { color: String(textColor), contrast: cur, keep: true };
632
+ }
633
+ function clamp01(v) {
634
+ if (typeof v !== "number" || !isFinite(v)) return 1;
635
+ return Math.max(0, Math.min(1, v));
636
+ }
637
+ function hexToRGB(hex2) {
638
+ const p4 = toRGBA(hex2);
639
+ return [p4[0], p4[1], p4[2]];
640
+ }
641
+
478
642
  // src/deltaE.ts
479
643
  function linearise(c2) {
480
644
  const v = c2 / 255;
@@ -776,6 +940,12 @@ function resolveAutoColorScale(p4) {
776
940
  }
777
941
 
778
942
  // src/defaults.ts
943
+ function themedLand(p4) {
944
+ const bg = p4.geoLandColor ? null : toRGBA(p4.backgroundColor);
945
+ if (!bg || bg[3] < 1 || relativeLuminance([bg[0], bg[1], bg[2]]) > 0.2) return void 0;
946
+ const fg = toRGBA(p4.themeFg) ?? [255, 255, 255, 1];
947
+ 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("");
948
+ }
779
949
  var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
780
950
  var numberOr = (raw, dflt) => {
781
951
  const n = raw == null || raw === "" ? NaN : Number(raw);
@@ -846,7 +1016,7 @@ function resolveOptions(p4) {
846
1016
  // Map fills: `raw || undefined` like the colour-scale endpoints. The no-data ->
847
1017
  // land cascade is left to the chart (so the fallback chain is visible in the
848
1018
  // generated code); HC substitution has already happened in the caller.
849
- geoLandColor: p4.geoLandColor || void 0,
1019
+ geoLandColor: p4.geoLandColor || themedLand(p4) || void 0,
850
1020
  geoNoDataColor: p4.geoNoDataColor || void 0,
851
1021
  aggregation: (p4.aggregation == null ? "" : String(p4.aggregation)) || void 0,
852
1022
  // A recognised value wins, anything else falls to the default: an unknown string must
@@ -2281,9 +2451,9 @@ function createMarkResolver(env) {
2281
2451
  const r2 = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
2282
2452
  if (!r2 || r2.width <= 0 || r2.height <= 0) continue;
2283
2453
  if (x3 < r2.left || x3 > r2.right || y2 < r2.top || y2 > r2.bottom) continue;
2284
- const area = r2.width * r2.height;
2285
- if (area < bestArea) {
2286
- bestArea = area;
2454
+ const area2 = r2.width * r2.height;
2455
+ if (area2 < bestArea) {
2456
+ bestArea = area2;
2287
2457
  best = el;
2288
2458
  }
2289
2459
  }
@@ -2892,15 +3062,15 @@ function ctmScaleOf(m4) {
2892
3062
  }
2893
3063
  function svgInkReach(svg) {
2894
3064
  try {
2895
- const box = svg.getBoundingClientRect();
2896
- if (!(box.width > 0) || !(box.height > 0)) return null;
3065
+ const box2 = svg.getBoundingClientRect();
3066
+ if (!(box2.width > 0) || !(box2.height > 0)) return null;
2897
3067
  const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
2898
3068
  const clipOf = clipWindowReader(svg);
2899
3069
  let right = -Infinity, bottom = -Infinity, seen = 0;
2900
3070
  for (let i = 0; i < els.length && seen < FIT_WALK_CAP; i++) {
2901
3071
  const r2 = els[i].getBoundingClientRect();
2902
3072
  if (r2.width <= 0 && r2.height <= 0) continue;
2903
- if (isPhantomBox(r2.width, r2.height, box.width, box.height)) continue;
3073
+ if (isPhantomBox(r2.width, r2.height, box2.width, box2.height)) continue;
2904
3074
  let rr = r2.right, rb = r2.bottom;
2905
3075
  const clip = clipOf(els[i]);
2906
3076
  if (clip) {
@@ -2913,51 +3083,39 @@ function svgInkReach(svg) {
2913
3083
  if (rb > bottom) bottom = rb;
2914
3084
  }
2915
3085
  if (seen === 0) return null;
2916
- 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;
2917
3110
  } catch {
2918
3111
  return null;
2919
3112
  }
2920
3113
  }
2921
3114
  function clipWindowReader(svg) {
2922
3115
  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
3116
  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;
3117
+ if (!own.has(el)) own.set(el, clipWindowOf(el));
3118
+ return own.get(el);
2961
3119
  };
2962
3120
  return (el) => {
2963
3121
  let acc = null;
@@ -3343,170 +3501,6 @@ function fitRenderedChart(container, opts = {}) {
3343
3501
  return result;
3344
3502
  }
3345
3503
 
3346
- // src/labelContrast.ts
3347
- function parseRGBA(c2) {
3348
- if (typeof c2 !== "string") return null;
3349
- const s2 = c2.trim();
3350
- const m4 = s2.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
3351
- if (m4) return [parseFloat(m4[1]), parseFloat(m4[2]), parseFloat(m4[3]), m4[4] !== void 0 ? parseFloat(m4[4]) : 1];
3352
- let h4 = s2.replace(/^#/, "");
3353
- if (h4.length === 3) h4 = h4.split("").map((ch) => ch + ch).join("");
3354
- if (h4.length === 6 && /^[0-9a-fA-F]{6}$/.test(h4)) {
3355
- return [parseInt(h4.slice(0, 2), 16), parseInt(h4.slice(2, 4), 16), parseInt(h4.slice(4, 6), 16), 1];
3356
- }
3357
- return null;
3358
- }
3359
- var DARK_TEXT = "#111111";
3360
- var LIGHT_TEXT = "#ffffff";
3361
- var MIN_CONTRAST = 3;
3362
- var WHITE_TEXT_BG_LUM = 0.5;
3363
- var PILL_MIN_ALPHA = 0.05;
3364
- var PILL_OPAQUE_ALPHA = 0.85;
3365
- var PAGE_MATCH_TOLERANCE = 2;
3366
- function isPillBackdropAlpha(a3) {
3367
- return a3 >= PILL_MIN_ALPHA && a3 < PILL_OPAQUE_ALPHA;
3368
- }
3369
- var PILL_MIN_COVERAGE = 0.6;
3370
- function pillBacksGlyph(overlapArea, glyphArea) {
3371
- if (!(glyphArea > 0) || !isFinite(overlapArea) || overlapArea <= 0) return false;
3372
- return overlapArea / glyphArea >= PILL_MIN_COVERAGE;
3373
- }
3374
- var BACKING_MAJORITY = 0.5;
3375
- function backingHoldsGlyph(backedArea, glyphArea) {
3376
- if (!(glyphArea > 0) || !isFinite(backedArea) || backedArea <= 0) return false;
3377
- return backedArea / glyphArea >= BACKING_MAJORITY;
3378
- }
3379
- function cellSuppressesNormalize(cellClass) {
3380
- return typeof cellClass === "string" && /(^|\s)lch-deck-panel(\s|$)/.test(cellClass);
3381
- }
3382
- var NAMED = {
3383
- white: [255, 255, 255, 1],
3384
- black: [0, 0, 0, 1],
3385
- none: [0, 0, 0, 0],
3386
- transparent: [0, 0, 0, 0]
3387
- };
3388
- function toRGBA(c2) {
3389
- if (typeof c2 !== "string") return null;
3390
- const k3 = c2.trim().toLowerCase();
3391
- if (k3 in NAMED) return NAMED[k3];
3392
- return parseRGBA(c2);
3393
- }
3394
- function compositeOver(fg, bg) {
3395
- const a3 = Math.max(0, Math.min(1, fg[3]));
3396
- return [
3397
- Math.round(fg[0] * a3 + bg[0] * (1 - a3)),
3398
- Math.round(fg[1] * a3 + bg[1] * (1 - a3)),
3399
- Math.round(fg[2] * a3 + bg[2] * (1 - a3))
3400
- ];
3401
- }
3402
- function relativeLuminance(rgb) {
3403
- const lin = rgb.map((v) => {
3404
- const s2 = v / 255;
3405
- return s2 <= 0.03928 ? s2 / 12.92 : Math.pow((s2 + 0.055) / 1.055, 2.4);
3406
- });
3407
- return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
3408
- }
3409
- function contrastRatio(l1, l2) {
3410
- const hi = Math.max(l1, l2);
3411
- const lo = Math.min(l1, l2);
3412
- return (hi + 0.05) / (lo + 0.05);
3413
- }
3414
- function decideLabelColor(textColor, fill, fillOpacity, pageBg, minContrast = MIN_CONTRAST, normalize = false) {
3415
- const fillRGBA = toRGBA(fill);
3416
- if (!fillRGBA) return { fix: false, color: "", reason: "fill-unparsed" };
3417
- const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
3418
- const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
3419
- const effFill = [fillRGBA[0], fillRGBA[1], fillRGBA[2], fillRGBA[3] * clamp01(fillOpacity)];
3420
- const bgRGB = compositeOver(effFill, pageRGB);
3421
- const bgLum = relativeLuminance(bgRGB);
3422
- const bgIsPage = Math.abs(bgRGB[0] - pageRGB[0]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[1] - pageRGB[1]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[2] - pageRGB[2]) <= PAGE_MATCH_TOLERANCE;
3423
- const effNormalize = normalize && !bgIsPage;
3424
- const textRGBA = toRGBA(textColor);
3425
- if (!effNormalize && textRGBA) {
3426
- const txtRGB = compositeOver(textRGBA, bgRGB);
3427
- const cur = contrastRatio(relativeLuminance(txtRGB), bgLum);
3428
- if (cur >= minContrast) return { fix: false, color: "", reason: "ok" };
3429
- }
3430
- const darkC = contrastRatio(relativeLuminance(hexToRGB(DARK_TEXT)), bgLum);
3431
- const lightC = contrastRatio(relativeLuminance(hexToRGB(LIGHT_TEXT)), bgLum);
3432
- let useLight = bgLum <= WHITE_TEXT_BG_LUM;
3433
- if (useLight && lightC < minContrast && darkC >= minContrast) useLight = false;
3434
- if (!useLight && darkC < minContrast && lightC >= minContrast) useLight = true;
3435
- return useLight ? { fix: true, color: LIGHT_TEXT, reason: "fixed-light" } : { fix: true, color: DARK_TEXT, reason: "fixed-dark" };
3436
- }
3437
- var GLYPH_SAMPLE_N = 3;
3438
- function glyphSampleGrid(box, n = GLYPH_SAMPLE_N) {
3439
- const out = [];
3440
- if (!box || !(box.width > 0) || !(box.height > 0) || !(n >= 1)) return out;
3441
- const k3 = Math.floor(n);
3442
- for (let i = 0; i < k3; i++) {
3443
- for (let j4 = 0; j4 < k3; j4++) {
3444
- out.push({
3445
- x: box.left + box.width * (i + 0.5) / k3,
3446
- y: box.top + box.height * (j4 + 0.5) / k3
3447
- });
3448
- }
3449
- }
3450
- return out;
3451
- }
3452
- var STRADDLE_COLS = 8;
3453
- var STRADDLE_ROWS = 2;
3454
- var STRADDLE_MIN_SHARE = 0.125;
3455
- var STRADDLE_TARGET_CONTRAST = 4.5;
3456
- var STRADDLE_PILL_ALPHA = 0.9;
3457
- function straddleSampleGrid(box) {
3458
- const out = [];
3459
- if (!box || !(box.width > 0) || !(box.height > 0)) return out;
3460
- for (let i = 0; i < STRADDLE_COLS; i++) {
3461
- for (let j4 = 0; j4 < STRADDLE_ROWS; j4++) {
3462
- out.push({
3463
- x: box.left + box.width * (i + 0.5) / STRADDLE_COLS,
3464
- y: box.top + box.height * (j4 + 0.5) / STRADDLE_ROWS
3465
- });
3466
- }
3467
- }
3468
- return out;
3469
- }
3470
- function backgroundGroups(bgs, tol = 8) {
3471
- const groups = [];
3472
- for (const b4 of bgs || []) {
3473
- const g5 = groups.find((x3) => Math.abs(x3.rgb[0] - b4[0]) <= tol && Math.abs(x3.rgb[1] - b4[1]) <= tol && Math.abs(x3.rgb[2] - b4[2]) <= tol);
3474
- if (g5) g5.n++;
3475
- else groups.push({ rgb: [b4[0], b4[1], b4[2]], n: 1 });
3476
- }
3477
- return groups.sort((a3, b4) => b4.n - a3.n);
3478
- }
3479
- function straddles(bgs) {
3480
- if (!bgs || bgs.length < 2) return false;
3481
- const g5 = backgroundGroups(bgs);
3482
- return g5.length >= 2 && g5[1].n / bgs.length >= STRADDLE_MIN_SHARE;
3483
- }
3484
- function minContrastOver(textColor, bgs) {
3485
- const t = toRGBA(textColor);
3486
- if (!t || !bgs || !bgs.length) return 0;
3487
- let worst = Infinity;
3488
- for (const b4 of bgs) {
3489
- const tx = compositeOver(t, b4);
3490
- worst = Math.min(worst, contrastRatio(relativeLuminance(tx), relativeLuminance(b4)));
3491
- }
3492
- return worst;
3493
- }
3494
- function pickColorOver(textColor, bgs, target = STRADDLE_TARGET_CONTRAST) {
3495
- const cur = minContrastOver(textColor, bgs);
3496
- if (cur >= target) return { color: String(textColor), contrast: cur, keep: true };
3497
- const d2 = minContrastOver(DARK_TEXT, bgs), l2 = minContrastOver(LIGHT_TEXT, bgs);
3498
- const best = d2 >= l2 ? { color: DARK_TEXT, contrast: d2 } : { color: LIGHT_TEXT, contrast: l2 };
3499
- return best.contrast > cur ? { ...best, keep: false } : { color: String(textColor), contrast: cur, keep: true };
3500
- }
3501
- function clamp01(v) {
3502
- if (typeof v !== "number" || !isFinite(v)) return 1;
3503
- return Math.max(0, Math.min(1, v));
3504
- }
3505
- function hexToRGB(hex2) {
3506
- const p4 = toRGBA(hex2);
3507
- return [p4[0], p4[1], p4[2]];
3508
- }
3509
-
3510
3504
  // src/labelContrastDom.ts
3511
3505
  var LABEL_CONTRAST_DONE_ATTR = "data-lch-contrast";
3512
3506
  var LABEL_CONTRAST_CAP = 2e3;
@@ -3837,12 +3831,12 @@ function newIdScope() {
3837
3831
  return "h" + seq;
3838
3832
  }
3839
3833
  var URL_REF = /url\(\s*(['"]?)#([^'")\s]+)\1\s*\)/g;
3840
- function scopeChartIds(container, scope) {
3841
- const report = { renamed: 0, collisions: [] };
3834
+ function scopeChartIds(c2, scope) {
3835
+ const r2 = { renamed: 0, collisions: [] };
3842
3836
  try {
3843
- const tag = "--" + scope, doc = container.ownerDocument, map = /* @__PURE__ */ new Map();
3837
+ const tag = "--" + scope, doc = c2.ownerDocument, map = /* @__PURE__ */ new Map();
3844
3838
  const refs = [], used = /* @__PURE__ */ new Set();
3845
- container.querySelectorAll("*").forEach((el) => {
3839
+ c2.querySelectorAll("*").forEach((el) => {
3846
3840
  for (const a3 of el.getAttributeNames()) {
3847
3841
  const v = el.getAttribute(a3);
3848
3842
  const href = (a3 === "href" || a3 === "xlink:href") && v[0] === "#";
@@ -3852,15 +3846,15 @@ function scopeChartIds(container, scope) {
3852
3846
  for (const m4 of v.matchAll(URL_REF)) used.add(m4[2]);
3853
3847
  }
3854
3848
  });
3855
- container.querySelectorAll("[id]").forEach((el) => {
3849
+ c2.querySelectorAll("[id]").forEach((el) => {
3856
3850
  const id = el.id;
3857
- if (!used.has(id) || id.endsWith(tag) || id.startsWith("bic-")) return;
3851
+ if (!used.has(id) || id.endsWith(tag)) return;
3858
3852
  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);
3853
+ if (doc && Array.from(doc.querySelectorAll(`[id="${q2}"],[id^="${q2}--h"]`)).some((o4) => !c2.contains(o4))) {
3854
+ r2.collisions.push(id);
3861
3855
  }
3862
3856
  map.set(id, el.id = id + tag);
3863
- report.renamed++;
3857
+ r2.renamed++;
3864
3858
  });
3865
3859
  for (const [el, a3, v] of refs) {
3866
3860
  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);
@@ -3868,7 +3862,7 @@ function scopeChartIds(container, scope) {
3868
3862
  }
3869
3863
  } catch {
3870
3864
  }
3871
- return report;
3865
+ return r2;
3872
3866
  }
3873
3867
 
3874
3868
  // src/invalidSentinel.ts
@@ -3889,7 +3883,34 @@ function invalidSentinelReason(text2, max = 240) {
3889
3883
  // src/annotations.ts
3890
3884
  var ANNOTATION_LAYER_CLASS = "bic-annotations";
3891
3885
  var ANNOTATION_CLASS = "bic-annotation";
3886
+ var ZOOM_PAD_CLASS = "llm-zoom-pad";
3892
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
+ }
3893
3914
  var parseIdxs = (s2) => (s2 || "").split(",").map((x3) => parseInt(x3, 10)).filter((n) => Number.isFinite(n));
3894
3915
  function createAnnotationLayer(container, onClick) {
3895
3916
  let list = [];
@@ -3946,6 +3967,37 @@ function createAnnotationLayer(container, onClick) {
3946
3967
  else marksByRow.set(r2, [m4]);
3947
3968
  }
3948
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
+ };
3949
4001
  let shown = 0;
3950
4002
  for (const a3 of list) {
3951
4003
  const pairs = (a3.where ? Object.entries(a3.where) : a3.column != null ? [[a3.column, a3.value]] : []).map(([c2, v]) => [names.indexOf(c2), text(v)]);
@@ -3955,23 +4007,30 @@ function createAnnotationLayer(container, onClick) {
3955
4007
  if (pairs.every(([ci, v]) => text(row?.[ci]) === v)) for (const m4 of marksByRow.get(r2) ?? []) hits.add(m4);
3956
4008
  });
3957
4009
  if (!hits.size) continue;
3958
- shown++;
3959
- let best = null;
4010
+ let best = null, bestMark = null, laidOut = false;
3960
4011
  for (const m4 of hits) {
3961
4012
  const b4 = m4.getBoundingClientRect();
3962
- 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
+ }
3963
4020
  }
4021
+ const pt2 = best ? clear(best, (a3.label ?? "\u2022").length, bestMark) : null;
4022
+ if (laidOut && !pt2) continue;
4023
+ shown++;
3964
4024
  const badge = doc.createElement("div");
3965
4025
  badge.className = ANNOTATION_CLASS;
3966
4026
  badge.textContent = a3.label ?? "\u2022";
3967
4027
  if (a3.title) badge.setAttribute("title", a3.title);
3968
4028
  badge.setAttribute("role", "button");
3969
4029
  badge.setAttribute("aria-label", a3.title ? `Note: ${a3.title}` : `Note on ${want}`);
3970
- const laidOut = !!best && best.width > 0 && best.height > 0;
3971
4030
  Object.assign(badge.style, {
3972
4031
  position: "absolute",
3973
- left: `${Math.round(laidOut ? best.left - origin.left + best.width / 2 : 0)}px`,
3974
- 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`,
3975
4034
  transform: "translate(-50%, -50%)",
3976
4035
  minWidth: "16px",
3977
4036
  height: "16px",
@@ -4356,7 +4415,7 @@ function createChartHost(container, config) {
4356
4415
  warnedIds = true;
4357
4416
  try {
4358
4417
  (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.`
4418
+ `[@bicharts/chart-host] ids shared with another chart, scoped: ${r2.collisions.slice(0, 5).join(", ")}`
4360
4419
  );
4361
4420
  } catch {
4362
4421
  }
@@ -4726,6 +4785,35 @@ function createChartGroup(columns, rows, opts = {}) {
4726
4785
  }
4727
4786
 
4728
4787
  export {
4788
+ DARK_TEXT,
4789
+ LIGHT_TEXT,
4790
+ MIN_CONTRAST,
4791
+ WHITE_TEXT_BG_LUM,
4792
+ PILL_MIN_ALPHA,
4793
+ PILL_OPAQUE_ALPHA,
4794
+ PAGE_MATCH_TOLERANCE,
4795
+ isPillBackdropAlpha,
4796
+ PILL_MIN_COVERAGE,
4797
+ pillBacksGlyph,
4798
+ BACKING_MAJORITY,
4799
+ backingHoldsGlyph,
4800
+ cellSuppressesNormalize,
4801
+ toRGBA,
4802
+ compositeOver,
4803
+ relativeLuminance,
4804
+ contrastRatio,
4805
+ decideLabelColor,
4806
+ GLYPH_SAMPLE_N,
4807
+ glyphSampleGrid,
4808
+ STRADDLE_COLS,
4809
+ STRADDLE_ROWS,
4810
+ STRADDLE_MIN_SHARE,
4811
+ STRADDLE_TARGET_CONTRAST,
4812
+ STRADDLE_PILL_ALPHA,
4813
+ straddleSampleGrid,
4814
+ straddles,
4815
+ minContrastOver,
4816
+ pickColorOver,
4729
4817
  deltaEHex,
4730
4818
  SAME_SHADE_DELTA_E,
4731
4819
  MIN_RAMP_FILLS,
@@ -4797,35 +4885,6 @@ export {
4797
4885
  unpinScrolledAxis,
4798
4886
  pinScrolledAxis,
4799
4887
  fitRenderedChart,
4800
- DARK_TEXT,
4801
- LIGHT_TEXT,
4802
- MIN_CONTRAST,
4803
- WHITE_TEXT_BG_LUM,
4804
- PILL_MIN_ALPHA,
4805
- PILL_OPAQUE_ALPHA,
4806
- PAGE_MATCH_TOLERANCE,
4807
- isPillBackdropAlpha,
4808
- PILL_MIN_COVERAGE,
4809
- pillBacksGlyph,
4810
- BACKING_MAJORITY,
4811
- backingHoldsGlyph,
4812
- cellSuppressesNormalize,
4813
- toRGBA,
4814
- compositeOver,
4815
- relativeLuminance,
4816
- contrastRatio,
4817
- decideLabelColor,
4818
- GLYPH_SAMPLE_N,
4819
- glyphSampleGrid,
4820
- STRADDLE_COLS,
4821
- STRADDLE_ROWS,
4822
- STRADDLE_MIN_SHARE,
4823
- STRADDLE_TARGET_CONTRAST,
4824
- STRADDLE_PILL_ALPHA,
4825
- straddleSampleGrid,
4826
- straddles,
4827
- minContrastOver,
4828
- pickColorOver,
4829
4888
  LABEL_CONTRAST_DONE_ATTR,
4830
4889
  LABEL_CONTRAST_CAP,
4831
4890
  LABEL_PILL_CLASS,
package/dist/index.mjs CHANGED
@@ -122,7 +122,7 @@ import {
122
122
  toSourceRows,
123
123
  unpinScrolledAxis,
124
124
  valuePlacementFlag
125
- } from "./chunk-ZLJM3EIP.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-ZLJM3EIP.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;
@@ -6,4 +6,4 @@ export interface IdScopeReport {
6
6
  /** Ids that also existed outside this chart when the pass ran - each one a clash this pass prevented. */
7
7
  collisions: string[];
8
8
  }
9
- export declare function scopeChartIds(container: Element, scope: string): IdScopeReport;
9
+ export declare function scopeChartIds(c: Element, scope: string): IdScopeReport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.62",
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",