@bicharts/chart-host 0.6.83 → 0.6.85

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.
@@ -1,22 +1,38 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
+ import {
3
+ LASSO_OUTLINE_CLASS,
4
+ installLasso
5
+ } from "./chunk-QIRLWJBI.mjs";
2
6
  import {
3
7
  Fn,
4
8
  J3,
5
9
  X,
6
10
  Xn,
7
11
  Yn
8
- } from "./chunk-2XWE6YFO.mjs";
12
+ } from "./chunk-2ZUUIZWL.mjs";
9
13
  import {
10
14
  censusHitBands,
15
+ ensureCrossfilterHitTargets
16
+ } from "./chunk-7HKVV5T5.mjs";
17
+ import {
18
+ clearPress,
19
+ clickWasDrag,
11
20
  createMarkResolver,
12
- ensureCrossfilterHitTargets,
13
21
  isInsideControl,
14
22
  markKeyOf,
15
23
  nextClickedMarks,
16
24
  nextSelection,
17
25
  planSelectionPaint,
18
- selectionRuleOf
19
- } from "./chunk-MGAO2DR7.mjs";
26
+ sameRowSet,
27
+ selectionRuleOf,
28
+ trackPress,
29
+ validRowSet
30
+ } from "./chunk-IURROJT7.mjs";
31
+ import {
32
+ clipWindowOf,
33
+ fitRenderedChart,
34
+ unpinScrolledAxis
35
+ } from "./chunk-BQFG5HWZ.mjs";
20
36
  import {
21
37
  NA_ISO3,
22
38
  NA_LAT_MAX,
@@ -58,12 +74,11 @@ import {
58
74
  ROW_IDX_ATTR,
59
75
  SEASONAL_MARKERS_DEFAULT,
60
76
  SELECTION_ACTIVE_CLASS,
61
- SNAPSHOT_SVG_ATTR,
62
77
  VALUE_AXIS_BASELINE_DEFAULT,
63
78
  XFILTER_REFRESH_EVENT,
64
79
  chartOwnsTimeline,
65
80
  periodTickSuppressesFeedback
66
- } from "./chunk-7JWL6LYE.mjs";
81
+ } from "./chunk-MR6TL7QZ.mjs";
67
82
 
68
83
  // src/labelContrast.ts
69
84
  function parseRGBA(c) {
@@ -560,6 +575,11 @@ function resolveOptions(p) {
560
575
  geoPointDest: p.geoPointDest,
561
576
  // A host FACT about the data it serialised (contract 1.9.0), passed through untouched.
562
577
  dateCellsAreUtcDays: p.dateCellsAreUtcDays,
578
+ // Host FACTS about whether every row arrived (contract 1.15.0), passed through untouched: an
579
+ // absent flag means the host does not know, and must stay absent rather than become true.
580
+ dataComplete: p.dataComplete,
581
+ rowsWithheld: p.rowsWithheld,
582
+ rowsFilteredOut: p.rowsFilteredOut,
563
583
  // NO LONGER A PASS-THROUGH. A host without a Max Map Points setting used to leave this
564
584
  // undefined, and every map chart then fell back to whatever number its own code carried -
565
585
  // 1000 in every archetype written so far - while the offer gate used the server's. One
@@ -592,6 +612,9 @@ function resolveOptions(p) {
592
612
  // Control gestures (1.10.0). Passed straight through like onSelect: the host owns what a gesture MEANS,
593
613
  // and a host that supplies nothing leaves every control behaving exactly as before.
594
614
  onControlChange: p.onControlChange,
615
+ // The host lasso setting (1.16.0): the host's own facts and choices - what the server sent, what the reader
616
+ // chose - passed through untouched. lasso.ts reads it as untrusted, so there is nothing to clamp here.
617
+ lasso: p.lasso,
595
618
  // ---- live-restyle knobs: `raw || undefined` / `.toString() || undefined` ----
596
619
  colorScaleLow: p.colorScaleLow || void 0,
597
620
  colorScaleHigh: p.colorScaleHigh || void 0,
@@ -1299,556 +1322,6 @@ function valuePlacementFlag(c) {
1299
1322
  return "place:d3:off" + bucket;
1300
1323
  }
1301
1324
 
1302
- // src/fit.ts
1303
- var SCROLL_SLACK_PX = 8;
1304
- function needsScroll(contentPx, viewportPx, slackPx = SCROLL_SLACK_PX) {
1305
- if (!isFinite(contentPx) || !isFinite(viewportPx)) return false;
1306
- if (contentPx <= 0 || viewportPx <= 0) return false;
1307
- return contentPx - viewportPx > slackPx;
1308
- }
1309
- function contentExtentOf(boxes) {
1310
- if (!boxes || boxes.length === 0) return { w: 0, h: 0, source: "none" };
1311
- const usable = boxes.filter((b) => b && isFinite(b.right) && isFinite(b.bottom) && (b.right > 0 || b.bottom > 0));
1312
- if (usable.length === 0) return { w: 0, h: 0, source: "none" };
1313
- const inFlow = usable.filter((b) => !b.floating);
1314
- const counted = inFlow.length > 0 ? inFlow : usable;
1315
- let w = 0, h = 0;
1316
- for (const b of counted) {
1317
- if (b.right > w) w = b.right;
1318
- if (b.bottom > h) h = b.bottom;
1319
- }
1320
- return { w, h, source: inFlow.length > 0 ? "in-flow" : "all-floating" };
1321
- }
1322
- function scrollFitFor(contentW, contentH, viewportW, viewportH, slackPx = SCROLL_SLACK_PX) {
1323
- return {
1324
- overflowX: needsScroll(contentW, viewportW, slackPx) ? "auto" : "hidden",
1325
- overflowY: needsScroll(contentH, viewportH, slackPx) ? "auto" : "hidden"
1326
- };
1327
- }
1328
- var FIT_CONTENT_SELECTOR = "text, .d3-mark, .d3-legend-mark";
1329
- var PHANTOM_FRACTION = 0.95;
1330
- function isPhantomBox(w, h, refW, refH) {
1331
- return w > refW * PHANTOM_FRACTION || h > refH * PHANTOM_FRACTION;
1332
- }
1333
- var MAX_FRAME_GROW_FACTOR = 20;
1334
- function planFrameGrow(inkBottomPx, elHeightPx, viewBoxH, ctmScale, slackPx = SCROLL_SLACK_PX, maxFactor = MAX_FRAME_GROW_FACTOR) {
1335
- const none = (reason) => ({ grow: false, heightPx: elHeightPx, viewBoxH, reason });
1336
- if (!isFinite(inkBottomPx) || !isFinite(elHeightPx) || elHeightPx <= 0) return none("unmeasurable");
1337
- if (!needsScroll(inkBottomPx, elHeightPx, slackPx)) return none("fits");
1338
- if (inkBottomPx > elHeightPx * maxFactor) return none("beyond-ceiling");
1339
- const heightPx = Math.ceil(inkBottomPx);
1340
- const delta = heightPx - elHeightPx;
1341
- if (!(viewBoxH > 0)) return { grow: true, heightPx, viewBoxH, reason: "grow-no-viewbox" };
1342
- if (!(ctmScale > 0) || !isFinite(ctmScale)) return none("no-scale");
1343
- return { grow: true, heightPx, viewBoxH: viewBoxH + delta / ctmScale, reason: "grow" };
1344
- }
1345
- var AXIS_PIN_MIN_LABELS = 2;
1346
- var AXIS_PIN_BAND_PAD_PX = 2;
1347
- var AXIS_PIN_MAX_BAND_FRACTION = 0.4;
1348
- var AXIS_PIN_TRACK_REACH_PX = 12;
1349
- function isHorizontalLabelRow(boxes) {
1350
- if (!boxes || boxes.length < 2) return false;
1351
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1352
- for (const b of boxes) {
1353
- const cx = (b.left + b.right) / 2, cy = (b.top + b.bottom) / 2;
1354
- if (!isFinite(cx) || !isFinite(cy)) return false;
1355
- if (cx < minX) minX = cx;
1356
- if (cx > maxX) maxX = cx;
1357
- if (cy < minY) minY = cy;
1358
- if (cy > maxY) maxY = cy;
1359
- }
1360
- return maxX - minX > maxY - minY;
1361
- }
1362
- function labelBand(boxes, tracks = [], pad = AXIS_PIN_BAND_PAD_PX, trackReach = AXIS_PIN_TRACK_REACH_PX) {
1363
- if (!boxes || boxes.length === 0) return null;
1364
- let top = Infinity, bottom = -Infinity;
1365
- for (const b of boxes) {
1366
- if (!isFinite(b.top) || !isFinite(b.bottom)) continue;
1367
- if (b.top < top) top = b.top;
1368
- if (b.bottom > bottom) bottom = b.bottom;
1369
- }
1370
- if (!isFinite(top) || !isFinite(bottom) || bottom <= top) return null;
1371
- for (const t of tracks || []) {
1372
- if (!isFinite(t.top) || !isFinite(t.bottom)) continue;
1373
- const gap = t.bottom < top ? top - t.bottom : t.top > bottom ? t.top - bottom : 0;
1374
- if (gap > trackReach) continue;
1375
- if (t.top < top) top = t.top;
1376
- if (t.bottom > bottom) bottom = t.bottom;
1377
- }
1378
- return { top: top - pad, bottom: bottom + pad };
1379
- }
1380
- function planAxisPin(candidates, viewportH, minLabels = AXIS_PIN_MIN_LABELS, maxFraction = AXIS_PIN_MAX_BAND_FRACTION) {
1381
- const none = (reason) => ({ pin: false, index: -1, band: null, reason });
1382
- if (!isFinite(viewportH) || viewportH <= 0) return none("unmeasurable");
1383
- let sawAxis = false, best = null;
1384
- for (const c of candidates || []) {
1385
- if (!c || !c.labels || c.labels.length < minLabels) continue;
1386
- sawAxis = true;
1387
- if (!isHorizontalLabelRow(c.labels)) continue;
1388
- if (!best || c.labels.length > best.labels.length) best = c;
1389
- }
1390
- if (!best) return none(sawAxis ? "vertical-only" : "no-axis");
1391
- const band = labelBand(best.labels, best.tracks || []);
1392
- if (!band) return none("unmeasurable");
1393
- if (band.bottom - band.top > viewportH * maxFraction) return none("band-too-tall");
1394
- return { pin: true, index: best.index, band, reason: "pinned" };
1395
- }
1396
- function axisPinPlacement(band, scrollTop, viewportH) {
1397
- if (!band || !isFinite(band.top) || !isFinite(band.bottom)) return null;
1398
- if (!isFinite(scrollTop) || !isFinite(viewportH) || viewportH <= 0) return null;
1399
- if (band.top < scrollTop) return "top";
1400
- if (band.bottom > scrollTop + viewportH) return "bottom";
1401
- return null;
1402
- }
1403
-
1404
- // src/fitDom.ts
1405
- var FIT_WALK_CAP = 8e3;
1406
- function ctmScaleOf(m) {
1407
- if (!m) return 0;
1408
- const s = Math.hypot(m.a, m.b);
1409
- return isFinite(s) && s > 0 ? s : 0;
1410
- }
1411
- function svgInkReach(svg) {
1412
- try {
1413
- const box2 = svg.getBoundingClientRect();
1414
- if (!(box2.width > 0) || !(box2.height > 0)) return null;
1415
- const els = svg.querySelectorAll(FIT_CONTENT_SELECTOR);
1416
- const clipOf = clipWindowReader(svg);
1417
- let right = -Infinity, bottom = -Infinity, seen = 0;
1418
- for (let i = 0; i < els.length && seen < FIT_WALK_CAP; i++) {
1419
- const r = els[i].getBoundingClientRect();
1420
- if (r.width <= 0 && r.height <= 0) continue;
1421
- if (isPhantomBox(r.width, r.height, box2.width, box2.height)) continue;
1422
- let rr = r.right, rb = r.bottom;
1423
- const clip = clipOf(els[i]);
1424
- if (clip) {
1425
- if (r.left >= clip.right || r.top >= clip.bottom) continue;
1426
- rr = Math.min(rr, clip.right);
1427
- rb = Math.min(rb, clip.bottom);
1428
- }
1429
- seen++;
1430
- if (rr > right) right = rr;
1431
- if (rb > bottom) bottom = rb;
1432
- }
1433
- if (seen === 0) return null;
1434
- return { right: right - box2.left, bottom: bottom - box2.top };
1435
- } catch {
1436
- return null;
1437
- }
1438
- }
1439
- function clipWindowOf(el) {
1440
- try {
1441
- const m = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(el.getAttribute("clip-path") || "");
1442
- const cp = m ? (el.closest("svg") ?? el.ownerDocument).querySelector(`clipPath[id="${m[1].replace(/"/g, '\\"')}"]`) : null;
1443
- const ctm = cp ? el.getScreenCTM?.() : null;
1444
- if (!cp || !ctm || cp.getAttribute("clipPathUnits") === "objectBoundingBox") return null;
1445
- let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
1446
- for (let i = 0; i < cp.children.length; i++) {
1447
- const bb = cp.children[i].getBBox?.();
1448
- if (!bb || !(bb.width > 0) || !(bb.height > 0)) continue;
1449
- for (const [x, y] 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]]) {
1450
- const sx = ctm.a * x + ctm.c * y + ctm.e, sy = ctm.b * x + ctm.d * y + ctm.f;
1451
- l = Math.min(l, sx);
1452
- t = Math.min(t, sy);
1453
- r = Math.max(r, sx);
1454
- b = Math.max(b, sy);
1455
- }
1456
- }
1457
- return isFinite(r) && isFinite(b) ? { left: l, top: t, right: r, bottom: b } : null;
1458
- } catch {
1459
- return null;
1460
- }
1461
- }
1462
- function clipWindowReader(svg) {
1463
- const own = /* @__PURE__ */ new Map();
1464
- const windowOf = (el) => {
1465
- if (!own.has(el)) own.set(el, clipWindowOf(el));
1466
- return own.get(el);
1467
- };
1468
- return (el) => {
1469
- let acc = null;
1470
- for (let a = el; a && a !== svg; a = a.parentElement) {
1471
- const w = windowOf(a);
1472
- if (w) acc = acc ? { right: Math.min(acc.right, w.right), bottom: Math.min(acc.bottom, w.bottom) } : w;
1473
- }
1474
- return acc;
1475
- };
1476
- }
1477
- function measureContainerBoxes(c, inkOf) {
1478
- const boxes = [];
1479
- if (!c || !c.children) return boxes;
1480
- const view = c.ownerDocument && c.ownerDocument.defaultView || globalThis;
1481
- const cRect = c.getBoundingClientRect();
1482
- for (let i = 0; i < c.children.length; i++) {
1483
- const el = c.children[i];
1484
- let floating = false;
1485
- try {
1486
- const cs = view.getComputedStyle(el);
1487
- if (cs.display === "none") continue;
1488
- floating = cs.position === "absolute" || cs.position === "fixed";
1489
- } catch {
1490
- }
1491
- const r = el.getBoundingClientRect();
1492
- let right = r.right - cRect.left - c.clientLeft + c.scrollLeft;
1493
- let bottom = r.bottom - cRect.top - c.clientTop + c.scrollTop;
1494
- if (inkOf) {
1495
- try {
1496
- const ink = inkOf(el);
1497
- if (ink) {
1498
- const originX = r.left - cRect.left - c.clientLeft + c.scrollLeft;
1499
- const originY = r.top - cRect.top - c.clientTop + c.scrollTop;
1500
- if (isFinite(ink.right)) right = Math.max(right, originX + ink.right);
1501
- if (isFinite(ink.bottom)) bottom = Math.max(bottom, originY + ink.bottom);
1502
- }
1503
- } catch {
1504
- }
1505
- }
1506
- boxes.push({ right, bottom, floating });
1507
- }
1508
- return boxes;
1509
- }
1510
- function fitReadingFor(lane, c, slackPx = SCROLL_SLACK_PX, inkOf) {
1511
- if (!c) return null;
1512
- const viewW = c.clientWidth, viewH = c.clientHeight;
1513
- if (!(viewW > 0) || !(viewH > 0)) return null;
1514
- const boxes = measureContainerBoxes(c, inkOf);
1515
- const extent = contentExtentOf(boxes);
1516
- const contentW = extent.source === "none" ? c.scrollWidth : extent.w;
1517
- const contentH = extent.source === "none" ? c.scrollHeight : extent.h;
1518
- if (!isFinite(contentW) || !isFinite(contentH)) return null;
1519
- return {
1520
- lane,
1521
- contentW: Math.round(contentW),
1522
- contentH: Math.round(contentH),
1523
- viewW,
1524
- viewH,
1525
- overflowsX: needsScroll(contentW, viewW, slackPx),
1526
- overflowsY: needsScroll(contentH, viewH, slackPx),
1527
- extentSource: extent.source,
1528
- boxes: boxes.length,
1529
- floating: boxes.filter((b) => b.floating).length,
1530
- scrollW: c.scrollWidth,
1531
- scrollH: c.scrollHeight
1532
- };
1533
- }
1534
- var noPin = (reason) => ({ pinned: false, reason, bandPx: 0, axisAt: "", labels: 0, carried: 0, edge: null });
1535
- var CONTAINER_SLOT_AXIS_PIN = "__bicAxisPin";
1536
- var SVG_NS = "http://www.w3.org/2000/svg";
1537
- var AXIS_PIN_MAX_CARRIED = 12;
1538
- function unpinScrolledAxis(container) {
1539
- if (!container) return;
1540
- const st = container[CONTAINER_SLOT_AXIS_PIN];
1541
- if (!st) return;
1542
- try {
1543
- container.removeEventListener("scroll", st.onScroll);
1544
- } catch {
1545
- }
1546
- try {
1547
- if (st.overlay.parentNode) st.overlay.parentNode.removeChild(st.overlay);
1548
- } catch {
1549
- }
1550
- try {
1551
- delete container[CONTAINER_SLOT_AXIS_PIN];
1552
- } catch {
1553
- }
1554
- }
1555
- function textHidden(t, view) {
1556
- if (t.getAttribute("display") === "none" || t.getAttribute("visibility") === "hidden") return true;
1557
- try {
1558
- const cs = view.getComputedStyle(t);
1559
- if (cs && (cs.display === "none" || cs.visibility === "hidden")) return true;
1560
- } catch {
1561
- }
1562
- return false;
1563
- }
1564
- function backdropFill(svg, container, view, svgBox) {
1565
- try {
1566
- const rects = svg.querySelectorAll("rect");
1567
- for (let i = 0; i < rects.length && i < 6; i++) {
1568
- const r = rects[i];
1569
- if (r.closest(".d3-mark")) continue;
1570
- const b = r.getBoundingClientRect();
1571
- if (b.width >= svgBox.width * 0.9 && b.height >= svgBox.height * 0.9) {
1572
- let fill = "";
1573
- try {
1574
- fill = view.getComputedStyle(r).fill || "";
1575
- } catch {
1576
- }
1577
- if (!fill || fill === "none") fill = r.getAttribute("fill") || "";
1578
- if (fill && fill !== "none" && fill !== "transparent") return fill;
1579
- break;
1580
- }
1581
- }
1582
- } catch {
1583
- }
1584
- try {
1585
- let el = container;
1586
- for (let hops = 0; el && hops < 12; hops++, el = el.parentElement) {
1587
- const bg = view.getComputedStyle(el).backgroundColor;
1588
- if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") return bg;
1589
- }
1590
- } catch {
1591
- }
1592
- return "#ffffff";
1593
- }
1594
- function cloneWithChain(el, svg, root) {
1595
- const chain = [];
1596
- let n = el.parentElement;
1597
- while (n && n !== svg) {
1598
- chain.unshift(n);
1599
- n = n.parentElement;
1600
- }
1601
- let parent = root;
1602
- for (const anc of chain) {
1603
- const c = anc.cloneNode(false);
1604
- parent.appendChild(c);
1605
- parent = c;
1606
- }
1607
- parent.appendChild(el.cloneNode(true));
1608
- const ids = root.querySelectorAll("[id]");
1609
- for (let i = 0; i < ids.length; i++) ids[i].removeAttribute("id");
1610
- }
1611
- function pinScrolledAxis(container, svg) {
1612
- unpinScrolledAxis(container);
1613
- if (!container) return noPin("no-container");
1614
- if (!svg) return noPin("no-svg");
1615
- try {
1616
- const doc = svg.ownerDocument;
1617
- const view = doc && doc.defaultView || globalThis;
1618
- const svgBox = svg.getBoundingClientRect();
1619
- if (!(svgBox.width > 0) || !(svgBox.height > 0)) return noPin("unmeasurable");
1620
- const m = typeof svg.getScreenCTM === "function" ? svg.getScreenCTM() : null;
1621
- if (!m || !(m.a > 0) || !(m.d > 0) || !isFinite(m.e) || !isFinite(m.f)) return noPin("no-ctm");
1622
- const viewportH = container.clientHeight;
1623
- if (!(viewportH > 0)) return noPin("unmeasurable");
1624
- const rel = (r) => ({ left: r.left - svgBox.left, top: r.top - svgBox.top, right: r.right - svgBox.left, bottom: r.bottom - svgBox.top });
1625
- const ticks = svg.querySelectorAll(".tick");
1626
- const byAxis = /* @__PURE__ */ new Map();
1627
- for (let i = 0; i < ticks.length; i++) {
1628
- const t = ticks[i];
1629
- if (t.closest("svg") !== svg) continue;
1630
- const p = t.parentElement;
1631
- if (!p) continue;
1632
- const list = byAxis.get(p);
1633
- if (list) list.push(t);
1634
- else byAxis.set(p, [t]);
1635
- }
1636
- const axes = [];
1637
- const cands = [];
1638
- byAxis.forEach((group, axis2) => {
1639
- const labels = [];
1640
- for (const tick of group) {
1641
- const txt = tick.querySelector("text");
1642
- if (!txt || textHidden(txt, view)) continue;
1643
- const r = txt.getBoundingClientRect();
1644
- if (!(r.width > 0) || !(r.height > 0)) continue;
1645
- labels.push(rel(r));
1646
- }
1647
- const tracks = [];
1648
- for (let i = 0; i < axis2.children.length; i++) {
1649
- const ch = axis2.children[i];
1650
- if (ch.tagName.toLowerCase() !== "path" || !(ch.getAttribute("class") || "").split(/\s+/).includes("domain")) continue;
1651
- const r = ch.getBoundingClientRect();
1652
- if (r.width > 0 && r.height <= 4) tracks.push(rel(r));
1653
- }
1654
- cands.push({ index: axes.length, labels, tracks });
1655
- axes.push(axis2);
1656
- });
1657
- const plan = planAxisPin(cands, viewportH);
1658
- if (!plan.pin || !plan.band) return noPin(plan.reason);
1659
- const axis = axes[plan.index];
1660
- const band = { top: Math.max(0, plan.band.top), bottom: Math.min(svgBox.height, plan.band.bottom) };
1661
- const bandPx = band.bottom - band.top;
1662
- if (!(bandPx > 0)) return noPin("unmeasurable");
1663
- const carried = [];
1664
- const texts = svg.querySelectorAll("text");
1665
- for (let i = 0; i < texts.length && carried.length < AXIS_PIN_MAX_CARRIED; i++) {
1666
- const t = texts[i];
1667
- if (t.closest("svg") !== svg) continue;
1668
- if (t.closest(".tick") || t.closest(".d3-mark") || t.closest(".d3-legend-mark")) continue;
1669
- if (axis.contains(t) || textHidden(t, view)) continue;
1670
- const r = rel(t.getBoundingClientRect());
1671
- if (!(r.right > r.left) || !(r.bottom > r.top)) continue;
1672
- if (r.top >= band.top && r.bottom <= band.bottom) carried.push(t);
1673
- }
1674
- const pinSvg = doc.createElementNS(SVG_NS, "svg");
1675
- for (const a of ["font-family", "font-size", "font-weight", "font", "fill", "color", "text-rendering"]) {
1676
- const v = svg.getAttribute(a);
1677
- if (v) pinSvg.setAttribute(a, v);
1678
- }
1679
- const styles = svg.querySelectorAll("style");
1680
- for (let i = 0; i < styles.length; i++) pinSvg.appendChild(styles[i].cloneNode(true));
1681
- const vbX = (svgBox.left - m.e) / m.a, vbY = (svgBox.top + band.top - m.f) / m.d;
1682
- const vbW = svgBox.width / m.a, vbH = bandPx / m.d;
1683
- pinSvg.setAttribute("viewBox", `${vbX} ${vbY} ${vbW} ${vbH}`);
1684
- pinSvg.setAttribute("preserveAspectRatio", "none");
1685
- pinSvg.setAttribute("width", String(svgBox.width));
1686
- pinSvg.setAttribute("height", String(bandPx));
1687
- pinSvg.setAttribute("aria-hidden", "true");
1688
- pinSvg.style.display = "block";
1689
- pinSvg.style.pointerEvents = "none";
1690
- const back = doc.createElementNS(SVG_NS, "rect");
1691
- back.setAttribute("x", String(vbX));
1692
- back.setAttribute("y", String(vbY));
1693
- back.setAttribute("width", String(vbW));
1694
- back.setAttribute("height", String(vbH));
1695
- back.setAttribute("fill", backdropFill(svg, container, view, svgBox));
1696
- pinSvg.appendChild(back);
1697
- cloneWithChain(axis, svg, pinSvg);
1698
- for (const t of carried) cloneWithChain(t, svg, pinSvg);
1699
- const overlay = doc.createElement("div");
1700
- overlay.setAttribute("data-bic-axis-pin", "1");
1701
- const o = overlay.style;
1702
- o.position = "absolute";
1703
- o.margin = "0";
1704
- o.padding = "0";
1705
- o.border = "0";
1706
- o.width = `${svgBox.width}px`;
1707
- o.height = `${bandPx}px`;
1708
- o.overflow = "hidden";
1709
- o.pointerEvents = "none";
1710
- o.zIndex = "3";
1711
- o.display = "none";
1712
- overlay.appendChild(pinSvg);
1713
- try {
1714
- const cs = view.getComputedStyle(container);
1715
- const pos = cs && cs.position || container.style.position;
1716
- if (!pos || pos === "static") container.style.position = "relative";
1717
- } catch {
1718
- container.style.position = container.style.position || "relative";
1719
- }
1720
- const cRect = container.getBoundingClientRect();
1721
- const svgLeft = svgBox.left - cRect.left - container.clientLeft + container.scrollLeft;
1722
- const svgTop = svgBox.top - cRect.top - container.clientTop + container.scrollTop;
1723
- const contentBand = { top: svgTop + band.top, bottom: svgTop + band.bottom };
1724
- const place = () => {
1725
- const st = container.scrollTop, vh = container.clientHeight;
1726
- const edge2 = axisPinPlacement(contentBand, st, vh);
1727
- if (!edge2) {
1728
- o.display = "none";
1729
- return null;
1730
- }
1731
- o.display = "block";
1732
- o.left = `${svgLeft}px`;
1733
- o.top = `${edge2 === "top" ? st : st + vh - bandPx}px`;
1734
- return edge2;
1735
- };
1736
- const onScroll = () => {
1737
- try {
1738
- if (!overlay.isConnected) {
1739
- unpinScrolledAxis(container);
1740
- return;
1741
- }
1742
- place();
1743
- } catch {
1744
- }
1745
- };
1746
- container.appendChild(overlay);
1747
- container.addEventListener("scroll", onScroll, { passive: true });
1748
- container[CONTAINER_SLOT_AXIS_PIN] = { overlay, onScroll };
1749
- const edge = place();
1750
- const mid = (band.top + band.bottom) / 2 / svgBox.height;
1751
- return {
1752
- pinned: true,
1753
- reason: "pinned",
1754
- bandPx: Math.round(bandPx),
1755
- axisAt: mid < 1 / 3 ? "top" : mid > 2 / 3 ? "bottom" : "middle",
1756
- labels: cands[plan.index].labels.length,
1757
- carried: carried.length,
1758
- edge
1759
- };
1760
- } catch {
1761
- unpinScrolledAxis(container);
1762
- return noPin("threw");
1763
- }
1764
- }
1765
- function fitRenderedChart(container, opts = {}) {
1766
- const slackPx = opts.slackPx ?? SCROLL_SLACK_PX;
1767
- const result = {
1768
- grew: false,
1769
- growFrom: 0,
1770
- growTo: 0,
1771
- growReason: "no-container",
1772
- overflowX: "hidden",
1773
- overflowY: "hidden",
1774
- reading: null,
1775
- axisPin: noPin("no-container")
1776
- };
1777
- if (!container) return result;
1778
- try {
1779
- let svg = container.querySelector(`svg[${SNAPSHOT_SVG_ATTR}]`);
1780
- for (let i = 0; i < container.children.length && !svg; i++) {
1781
- const el = container.children[i];
1782
- if (el.tagName && el.tagName.toLowerCase() === "svg") svg = el;
1783
- }
1784
- if (!svg) svg = container.querySelector("svg");
1785
- let inkCache;
1786
- const svgInk = (el) => {
1787
- if (inkCache === void 0) inkCache = svgInkReach(el);
1788
- return inkCache;
1789
- };
1790
- const inkOf = (el) => el && el.tagName && el.tagName.toLowerCase() === "svg" ? svgInk(el) : null;
1791
- if (svg && opts.grow !== false) {
1792
- try {
1793
- const ink = svgInk(svg);
1794
- const elH = svg.getBoundingClientRect().height;
1795
- const vb = svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal.height : 0;
1796
- const ctm = typeof svg.getScreenCTM === "function" ? svg.getScreenCTM() : null;
1797
- const plan = planFrameGrow(
1798
- ink ? ink.bottom : NaN,
1799
- elH,
1800
- vb,
1801
- ctmScaleOf(ctm),
1802
- slackPx
1803
- );
1804
- result.growFrom = Math.round(elH);
1805
- result.growTo = plan.heightPx;
1806
- result.growReason = plan.reason;
1807
- if (plan.grow) {
1808
- svg.setAttribute("height", String(plan.heightPx));
1809
- if (vb > 0) {
1810
- const b = svg.viewBox.baseVal;
1811
- svg.setAttribute("viewBox", `${b.x} ${b.y} ${b.width} ${plan.viewBoxH}`);
1812
- }
1813
- result.grew = true;
1814
- }
1815
- } catch {
1816
- result.growReason = "grow-threw";
1817
- }
1818
- } else if (!svg) {
1819
- result.growReason = "no-svg";
1820
- } else {
1821
- result.growReason = "grow-disabled";
1822
- }
1823
- const reading = fitReadingFor(opts.lane ?? "chart", container, slackPx, inkOf);
1824
- result.reading = reading;
1825
- const boxes = measureContainerBoxes(container, inkOf);
1826
- const extent = contentExtentOf(boxes);
1827
- const svgBox = svg ? svg.getBoundingClientRect() : null;
1828
- const contentW = extent.source === "none" ? Math.max(container.scrollWidth, svgBox ? svgBox.width : 0) : extent.w;
1829
- const contentH = extent.source === "none" ? Math.max(container.scrollHeight, svgBox ? svgBox.height : 0) : extent.h;
1830
- const fit = scrollFitFor(contentW, contentH, container.clientWidth, container.clientHeight, slackPx);
1831
- result.overflowX = fit.overflowX;
1832
- result.overflowY = fit.overflowY;
1833
- if (opts.applyOverflow !== false) {
1834
- container.style.overflowX = fit.overflowX;
1835
- container.style.overflowY = fit.overflowY;
1836
- if (fit.overflowY !== "auto") {
1837
- unpinScrolledAxis(container);
1838
- result.axisPin = noPin("not-scrolling");
1839
- } else if (opts.pinAxis === false) {
1840
- unpinScrolledAxis(container);
1841
- result.axisPin = noPin("disabled");
1842
- } else result.axisPin = pinScrolledAxis(container, svg);
1843
- } else {
1844
- unpinScrolledAxis(container);
1845
- result.axisPin = noPin("overflow-not-applied");
1846
- }
1847
- } catch {
1848
- }
1849
- return result;
1850
- }
1851
-
1852
1325
  // src/labelContrastDom.ts
1853
1326
  var LABEL_CONTRAST_DONE_ATTR = "data-lch-contrast";
1854
1327
  var LABEL_CONTRAST_CAP = 2e3;
@@ -2282,7 +1755,7 @@ function createAnnotationLayer(container, onClick) {
2282
1755
  const MO = win?.MutationObserver;
2283
1756
  if (on && !observer && MO) {
2284
1757
  observer = new MO((records) => {
2285
- if (records.some((r) => !r.target.closest?.(`.${ANNOTATION_LAYER_CLASS}`))) redrawSoon();
1758
+ if (records.some((r) => !r.target.closest?.(`.${ANNOTATION_LAYER_CLASS}, .${LASSO_OUTLINE_CLASS}`))) redrawSoon();
2286
1759
  });
2287
1760
  observer.observe(container, { subtree: true, attributes: true, attributeFilter: ["transform", "d", "cx", "cy", "x", "y", "points"] });
2288
1761
  } else if (!on && observer) {
@@ -2738,6 +2211,7 @@ function createChartHost(container, config) {
2738
2211
  xfAt = Date.now();
2739
2212
  if (d.clear) notify([], d.source || "chart");
2740
2213
  else if (d.mark) notify(parseRowIdxs(d.mark.getAttribute?.(ROW_IDX_ATTR)), d.source || "chart", d.mark, true);
2214
+ else if (Array.isArray(d.rows)) notify(validRowSet(d.rows), d.source || "chart", null, true);
2741
2215
  };
2742
2216
  const resolver = createMarkResolver({
2743
2217
  root: container,
@@ -2745,24 +2219,39 @@ function createChartHost(container, config) {
2745
2219
  log: () => {
2746
2220
  }
2747
2221
  });
2222
+ const clearFromGesture = () => {
2223
+ if (current && current.length) {
2224
+ const slot = container[CONTAINER_SLOT_XF_CLEAR];
2225
+ if (selectionFromChart && typeof slot === "function") {
2226
+ try {
2227
+ slot();
2228
+ } catch {
2229
+ }
2230
+ }
2231
+ if (current && current.length) notify([], "user");
2232
+ }
2233
+ };
2234
+ let lasso = null;
2235
+ const disposeLasso = () => {
2236
+ const l = lasso;
2237
+ lasso = null;
2238
+ try {
2239
+ l?.destroy();
2240
+ } catch {
2241
+ }
2242
+ };
2748
2243
  const clickSel = `.${MARK_CLASS}[${ROW_IDX_ATTR}], .${LEGEND_MARK_CLASS}[${ROW_IDX_ATTR}], .${AXIS_FILTER_CLASS}[${ROW_IDX_ATTR}]`;
2749
2244
  const onClick = (e) => {
2245
+ const wasDrag = clickWasDrag(container, e);
2246
+ clearPress(container);
2247
+ if (wasDrag) return;
2750
2248
  if (Date.now() - xfAt < XF_ECHO_MS) return;
2751
2249
  if (isInsideControl(e?.target, container)) return;
2752
2250
  let el = resolver.findMark(e?.target, clickSel, e);
2753
2251
  if (!el) el = resolver.penetrateOverlayAt(e?.clientX, e?.clientY, clickSel);
2754
2252
  if (!el) el = resolver.resolveByGeometry(e?.clientX, e?.clientY, clickSel);
2755
2253
  if (!el) {
2756
- if (current && current.length) {
2757
- const slot = container[CONTAINER_SLOT_XF_CLEAR];
2758
- if (selectionFromChart && typeof slot === "function") {
2759
- try {
2760
- slot();
2761
- } catch {
2762
- }
2763
- }
2764
- if (current && current.length) notify([], "user");
2765
- }
2254
+ clearFromGesture();
2766
2255
  return;
2767
2256
  }
2768
2257
  const rows = parseRowIdxs(el.getAttribute(ROW_IDX_ATTR));
@@ -2781,6 +2270,7 @@ function createChartHost(container, config) {
2781
2270
  notify(next, "user", toggledOff ? null : tick);
2782
2271
  };
2783
2272
  container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
2273
+ const untrackPress = trackPress(container);
2784
2274
  container.addEventListener("click", onClick);
2785
2275
  container.classList?.add(HOST_CONTAINER_CLASS);
2786
2276
  ensureAffordanceStyles();
@@ -2881,6 +2371,16 @@ function createChartHost(container, config) {
2881
2371
  drawAnnotations();
2882
2372
  } catch {
2883
2373
  }
2374
+ try {
2375
+ lasso = installLasso({
2376
+ container,
2377
+ options: () => resolved,
2378
+ rows: data.rows ? data.rows.length : 0,
2379
+ select: (rows) => notify(rows, "user", null, true),
2380
+ clearSelection: clearFromGesture
2381
+ });
2382
+ } catch {
2383
+ }
2884
2384
  };
2885
2385
  const renderFailure = (err) => {
2886
2386
  const message = String(err?.message ?? err ?? "");
@@ -2897,6 +2397,7 @@ function createChartHost(container, config) {
2897
2397
  if (destroyed) return;
2898
2398
  const seq2 = ++renderSeq;
2899
2399
  stopAnim();
2400
+ disposeLasso();
2900
2401
  container.innerHTML = "";
2901
2402
  if (!renderFn) {
2902
2403
  if (!config.code) throw new Error("createChartHost: provide code or renderFn");
@@ -3008,10 +2509,13 @@ function createChartHost(container, config) {
3008
2509
  }
3009
2510
  }
3010
2511
  setClickedMarks(null);
2512
+ lasso?.clear({ publish: false, source: "host" });
3011
2513
  notify([], "host");
3012
2514
  },
3013
2515
  highlight(rowIdxs) {
3014
- notify(Array.isArray(rowIdxs) ? rowIdxs.slice() : [], "host");
2516
+ const rows = Array.isArray(rowIdxs) ? rowIdxs.slice() : [];
2517
+ if (lasso && lasso.rows().length && !sameRowSet(rows, lasso.rows())) lasso.clear({ publish: false, source: "host" });
2518
+ notify(rows, "host");
3015
2519
  },
3016
2520
  get current() {
3017
2521
  return current;
@@ -3027,6 +2531,9 @@ function createChartHost(container, config) {
3027
2531
  stopAnim();
3028
2532
  container.removeEventListener(XFILTER_REFRESH_EVENT, onXf);
3029
2533
  container.removeEventListener("click", onClick);
2534
+ untrackPress();
2535
+ clearPress(container);
2536
+ disposeLasso();
3030
2537
  subs.clear();
3031
2538
  annotationLayer.destroy();
3032
2539
  unpinScrolledAxis(container);
@@ -3237,30 +2744,6 @@ export {
3237
2744
  VALUE_AXIS_MIN_FIT,
3238
2745
  censusValuePlacement,
3239
2746
  valuePlacementFlag,
3240
- SCROLL_SLACK_PX,
3241
- needsScroll,
3242
- contentExtentOf,
3243
- scrollFitFor,
3244
- FIT_CONTENT_SELECTOR,
3245
- PHANTOM_FRACTION,
3246
- isPhantomBox,
3247
- MAX_FRAME_GROW_FACTOR,
3248
- planFrameGrow,
3249
- AXIS_PIN_MIN_LABELS,
3250
- AXIS_PIN_BAND_PAD_PX,
3251
- AXIS_PIN_MAX_BAND_FRACTION,
3252
- AXIS_PIN_TRACK_REACH_PX,
3253
- isHorizontalLabelRow,
3254
- labelBand,
3255
- planAxisPin,
3256
- axisPinPlacement,
3257
- ctmScaleOf,
3258
- svgInkReach,
3259
- measureContainerBoxes,
3260
- fitReadingFor,
3261
- unpinScrolledAxis,
3262
- pinScrolledAxis,
3263
- fitRenderedChart,
3264
2747
  LABEL_CONTRAST_DONE_ATTR,
3265
2748
  LABEL_CONTRAST_CAP,
3266
2749
  LABEL_PILL_CLASS,