@bicharts/chart-host 0.6.74 → 0.6.76

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.
package/README.md CHANGED
@@ -267,6 +267,17 @@ would show its selected lines at 38%, which the eye reads as dimmed. Such a char
267
267
  painted at full opacity. Undeclared charts are unchanged, so alpha used as an encoding (a depth
268
268
  cue, a hierarchy level) is never flattened by a fix meant for another chart.
269
269
 
270
+ **Which marks count as selected.** By default a mark is selected when *any* of the rows in its
271
+ `data-row-idx` list is selected, which is what lights a legend swatch together with its series. A
272
+ chart whose marks share rows declares the other rule by stamping
273
+ `data-lch-sel-rule="clicked-marks"` on an element it draws (its own `<svg>`): then exactly the
274
+ marks the reader clicked are selected and every other mark is dimmed, Ctrl/Cmd/Shift-click adds or
275
+ removes a mark, and a selection set from outside lights the marks whose rows equal it. A mark is
276
+ named by `data-mark-key` when it carries one, else by its row list. A word cloud is the case it was
277
+ made for: every word shares a comment with many others, so the default lit most of the cloud on one
278
+ click. The cross-filter sent out is the same under both rules. `onSelectionPaint` reports how each
279
+ paint of a declaring chart decided, including when no mark matched and the default was used.
280
+
270
281
  ## Architecture — three layers, and why it matters if you contribute
271
282
 
272
283
  Read this before adding code. The package has **three** concerns, and they are independent.
@@ -1,7 +1,7 @@
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
2
 
3
3
  // src/contract.ts
4
- var HOST_CONTRACT_VERSION = "1.13.0";
4
+ var HOST_CONTRACT_VERSION = "1.14.0";
5
5
  var GEO_POINT_PRECISIONS = ["latlon", "city", "zip3", "state", "country"];
6
6
  var MARK_CLASS = "d3-mark";
7
7
  var LEGEND_MARK_CLASS = "d3-legend-mark";
@@ -9,6 +9,9 @@ var AXIS_FILTER_CLASS = "d3-axis-filter";
9
9
  var LIFT_SELECTED_CLASS = "d3-lift-selected";
10
10
  var CONTROL_CLASS = "lch-control";
11
11
  var ROW_IDX_ATTR = "data-row-idx";
12
+ var SELECTION_RULE_ATTR = "data-lch-sel-rule";
13
+ var SELECTION_RULE_CLICKED_MARKS = "clicked-marks";
14
+ var MARK_KEY_ATTR = "data-mark-key";
12
15
  var NO_ROW_MARKS_ATTR = "data-lch-no-row-marks";
13
16
  var SNAPSHOT_SVG_ATTR = "data-lch-snapshot";
14
17
  var XFILTER_REFRESH_EVENT = "llm-xfilter-refresh";
@@ -16,6 +19,7 @@ var CONTAINER_SLOT_ANIM_STOP = "__llmAnimStop";
16
19
  var CONTAINER_SLOT_XF_CLEAR = "__llmXfClear";
17
20
  var CONTAINER_SLOT_INITIAL_XF_MARK = "__llmInitialXfMark";
18
21
  var CONTAINER_SLOT_UI_STATE = "__lchUiState";
22
+ var CONTAINER_SLOT_CLICKED_MARKS = "__lchClickedMarks";
19
23
  var VIEW_ONLY_UI_STATE_KEYS = Object.freeze(["camera", "llmZoom"]);
20
24
  function withoutViewOnlyKeys(bag) {
21
25
  if (!bag || typeof bag !== "object") return bag;
@@ -74,6 +78,9 @@ export {
74
78
  LIFT_SELECTED_CLASS,
75
79
  CONTROL_CLASS,
76
80
  ROW_IDX_ATTR,
81
+ SELECTION_RULE_ATTR,
82
+ SELECTION_RULE_CLICKED_MARKS,
83
+ MARK_KEY_ATTR,
77
84
  NO_ROW_MARKS_ATTR,
78
85
  SNAPSHOT_SVG_ATTR,
79
86
  XFILTER_REFRESH_EVENT,
@@ -81,6 +88,7 @@ export {
81
88
  CONTAINER_SLOT_XF_CLEAR,
82
89
  CONTAINER_SLOT_INITIAL_XF_MARK,
83
90
  CONTAINER_SLOT_UI_STATE,
91
+ CONTAINER_SLOT_CLICKED_MARKS,
84
92
  VIEW_ONLY_UI_STATE_KEYS,
85
93
  withoutViewOnlyKeys,
86
94
  HOST_CONTAINER_CLASS,
@@ -20,6 +20,7 @@ import {
20
20
  COLOR_SCALE_SELF_CLAMP_PCT_MAX,
21
21
  COLOR_SCALE_SELF_CLAMP_PCT_MIN,
22
22
  CONTAINER_SLOT_ANIM_STOP,
23
+ CONTAINER_SLOT_CLICKED_MARKS,
23
24
  CONTAINER_SLOT_INITIAL_XF_MARK,
24
25
  CONTAINER_SLOT_UI_STATE,
25
26
  CONTAINER_SLOT_XF_CLEAR,
@@ -33,18 +34,21 @@ import {
33
34
  LEGEND_MARK_CLASS,
34
35
  LIFT_SELECTED_CLASS,
35
36
  MARK_CLASS,
37
+ MARK_KEY_ATTR,
36
38
  MARK_SELECTED_CLASS,
37
39
  MAX_MAP_POINTS_DEFAULT,
38
40
  NO_ROW_MARKS_ATTR,
39
41
  ROW_IDX_ATTR,
40
42
  SEASONAL_MARKERS_DEFAULT,
41
43
  SELECTION_ACTIVE_CLASS,
44
+ SELECTION_RULE_ATTR,
45
+ SELECTION_RULE_CLICKED_MARKS,
42
46
  SNAPSHOT_SVG_ATTR,
43
47
  VALUE_AXIS_BASELINE_DEFAULT,
44
48
  XFILTER_REFRESH_EVENT,
45
49
  chartOwnsTimeline,
46
50
  periodTickSuppressesFeedback
47
- } from "./chunk-WNGJUPEI.mjs";
51
+ } from "./chunk-7JWL6LYE.mjs";
48
52
  import {
49
53
  __commonJS,
50
54
  __toESM
@@ -252,69 +256,69 @@ var require_papaparse_min = __commonJS({
252
256
  return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
253
257
  }
254
258
  function E4(C2) {
255
- var S2 = (C2 = C2 || {}).delimiter, O4 = C2.newline, x3 = C2.comments, I2 = C2.step, A4 = C2.preview, T = C2.fastMode, D2 = null, L4 = false, F4 = null == C2.quoteChar ? '"' : C2.quoteChar, j4 = F4;
259
+ var S2 = (C2 = C2 || {}).delimiter, O4 = C2.newline, x3 = C2.comments, I2 = C2.step, A2 = C2.preview, T = C2.fastMode, D2 = null, L4 = false, F4 = null == C2.quoteChar ? '"' : C2.quoteChar, j4 = F4;
256
260
  if (void 0 !== C2.escapeChar && (j4 = C2.escapeChar), ("string" != typeof S2 || -1 < v.BAD_DELIMITERS.indexOf(S2)) && (S2 = ","), x3 === S2) throw new Error("Comment character same as delimiter");
257
261
  true === x3 ? x3 = "#" : ("string" != typeof x3 || -1 < v.BAD_DELIMITERS.indexOf(x3)) && (x3 = false), "\n" !== O4 && "\r" !== O4 && "\r\n" !== O4 && (O4 = "\n");
258
- var z4 = 0, M2 = false;
262
+ var z2 = 0, M2 = false;
259
263
  this.parse = function(i2, t, r4) {
260
264
  if ("string" != typeof i2) throw new Error("Input must be a string");
261
- var n2 = i2.length, e = S2.length, s4 = O4.length, a4 = x3.length, o5 = q2(I2), h5 = [], u5 = [], d4 = [], f5 = z4 = 0;
265
+ var n2 = i2.length, e = S2.length, s4 = O4.length, a4 = x3.length, o5 = q2(I2), h5 = [], u5 = [], d4 = [], f5 = z2 = 0;
262
266
  if (!i2) return w3();
263
267
  if (T || false !== T && -1 === i2.indexOf(F4)) {
264
268
  for (var l4 = i2.split(O4), c4 = 0; c4 < l4.length; c4++) {
265
- if (d4 = l4[c4], z4 += d4.length, c4 !== l4.length - 1) z4 += O4.length;
269
+ if (d4 = l4[c4], z2 += d4.length, c4 !== l4.length - 1) z2 += O4.length;
266
270
  else if (r4) return w3();
267
271
  if (!x3 || d4.substring(0, a4) !== x3) {
268
272
  if (o5) {
269
273
  if (h5 = [], k3(d4.split(S2)), R4(), M2) return w3();
270
274
  } else k3(d4.split(S2));
271
- if (A4 && A4 <= c4) return h5 = h5.slice(0, A4), w3(true);
275
+ if (A2 && A2 <= c4) return h5 = h5.slice(0, A2), w3(true);
272
276
  }
273
277
  }
274
278
  return w3();
275
279
  }
276
- for (var p5 = i2.indexOf(S2, z4), g6 = i2.indexOf(O4, z4), _5 = new RegExp(U2(j4) + U2(F4), "g"), m5 = i2.indexOf(F4, z4); ; ) if (i2[z4] === F4) for (m5 = z4, z4++; ; ) {
277
- if (-1 === (m5 = i2.indexOf(F4, m5 + 1))) return r4 || u5.push({ type: "Quotes", code: "MissingQuotes", message: "Quoted field unterminated", row: h5.length, index: z4 }), E5();
278
- if (m5 === n2 - 1) return E5(i2.substring(z4, m5).replace(_5, F4));
280
+ for (var p5 = i2.indexOf(S2, z2), g6 = i2.indexOf(O4, z2), _5 = new RegExp(U2(j4) + U2(F4), "g"), m5 = i2.indexOf(F4, z2); ; ) if (i2[z2] === F4) for (m5 = z2, z2++; ; ) {
281
+ if (-1 === (m5 = i2.indexOf(F4, m5 + 1))) return r4 || u5.push({ type: "Quotes", code: "MissingQuotes", message: "Quoted field unterminated", row: h5.length, index: z2 }), E5();
282
+ if (m5 === n2 - 1) return E5(i2.substring(z2, m5).replace(_5, F4));
279
283
  if (F4 === j4 && i2[m5 + 1] === j4) m5++;
280
284
  else if (F4 === j4 || 0 === m5 || i2[m5 - 1] !== j4) {
281
285
  -1 !== p5 && p5 < m5 + 1 && (p5 = i2.indexOf(S2, m5 + 1));
282
286
  var y4 = v2(-1 === (g6 = -1 !== g6 && g6 < m5 + 1 ? i2.indexOf(O4, m5 + 1) : g6) ? p5 : Math.min(p5, g6));
283
287
  if (i2.substr(m5 + 1 + y4, e) === S2) {
284
- d4.push(i2.substring(z4, m5).replace(_5, F4)), i2[z4 = m5 + 1 + y4 + e] !== F4 && (m5 = i2.indexOf(F4, z4)), p5 = i2.indexOf(S2, z4), g6 = i2.indexOf(O4, z4);
288
+ d4.push(i2.substring(z2, m5).replace(_5, F4)), i2[z2 = m5 + 1 + y4 + e] !== F4 && (m5 = i2.indexOf(F4, z2)), p5 = i2.indexOf(S2, z2), g6 = i2.indexOf(O4, z2);
285
289
  break;
286
290
  }
287
291
  y4 = v2(g6);
288
292
  if (i2.substring(m5 + 1 + y4, m5 + 1 + y4 + s4) === O4) {
289
- if (d4.push(i2.substring(z4, m5).replace(_5, F4)), b5(m5 + 1 + y4 + s4), p5 = i2.indexOf(S2, z4), m5 = i2.indexOf(F4, z4), o5 && (R4(), M2)) return w3();
290
- if (A4 && h5.length >= A4) return w3(true);
293
+ if (d4.push(i2.substring(z2, m5).replace(_5, F4)), b5(m5 + 1 + y4 + s4), p5 = i2.indexOf(S2, z2), m5 = i2.indexOf(F4, z2), o5 && (R4(), M2)) return w3();
294
+ if (A2 && h5.length >= A2) return w3(true);
291
295
  break;
292
296
  }
293
- u5.push({ type: "Quotes", code: "InvalidQuotes", message: "Trailing quote on quoted field is malformed", row: h5.length, index: z4 }), m5++;
297
+ u5.push({ type: "Quotes", code: "InvalidQuotes", message: "Trailing quote on quoted field is malformed", row: h5.length, index: z2 }), m5++;
294
298
  }
295
299
  }
296
- else if (x3 && 0 === d4.length && i2.substring(z4, z4 + a4) === x3) {
300
+ else if (x3 && 0 === d4.length && i2.substring(z2, z2 + a4) === x3) {
297
301
  if (-1 === g6) return w3();
298
- z4 = g6 + s4, g6 = i2.indexOf(O4, z4), p5 = i2.indexOf(S2, z4);
299
- } else if (-1 !== p5 && (p5 < g6 || -1 === g6)) d4.push(i2.substring(z4, p5)), z4 = p5 + e, p5 = i2.indexOf(S2, z4);
302
+ z2 = g6 + s4, g6 = i2.indexOf(O4, z2), p5 = i2.indexOf(S2, z2);
303
+ } else if (-1 !== p5 && (p5 < g6 || -1 === g6)) d4.push(i2.substring(z2, p5)), z2 = p5 + e, p5 = i2.indexOf(S2, z2);
300
304
  else {
301
305
  if (-1 === g6) break;
302
- if (d4.push(i2.substring(z4, g6)), b5(g6 + s4), o5 && (R4(), M2)) return w3();
303
- if (A4 && h5.length >= A4) return w3(true);
306
+ if (d4.push(i2.substring(z2, g6)), b5(g6 + s4), o5 && (R4(), M2)) return w3();
307
+ if (A2 && h5.length >= A2) return w3(true);
304
308
  }
305
309
  return E5();
306
310
  function k3(e2) {
307
- h5.push(e2), f5 = z4;
311
+ h5.push(e2), f5 = z2;
308
312
  }
309
313
  function v2(e2) {
310
314
  var t2 = 0;
311
315
  return t2 = -1 !== e2 && (e2 = i2.substring(m5 + 1, e2)) && "" === e2.trim() ? e2.length : t2;
312
316
  }
313
317
  function E5(e2) {
314
- return r4 || (void 0 === e2 && (e2 = i2.substring(z4)), d4.push(e2), z4 = n2, k3(d4), o5 && R4()), w3();
318
+ return r4 || (void 0 === e2 && (e2 = i2.substring(z2)), d4.push(e2), z2 = n2, k3(d4), o5 && R4()), w3();
315
319
  }
316
320
  function b5(e2) {
317
- z4 = e2, k3(d4), d4 = [], g6 = i2.indexOf(O4, z4);
321
+ z2 = e2, k3(d4), d4 = [], g6 = i2.indexOf(O4, z2);
318
322
  }
319
323
  function w3(e2) {
320
324
  if (C2.header && !t && h5.length && !L4) {
@@ -339,7 +343,7 @@ var require_papaparse_min = __commonJS({
339
343
  }, this.abort = function() {
340
344
  M2 = true;
341
345
  }, this.getCharIndex = function() {
342
- return z4;
346
+ return z2;
343
347
  };
344
348
  }
345
349
  function g5(e) {
@@ -918,15 +922,15 @@ function resolveAutoColorScale(p4) {
918
922
  };
919
923
  const candidates = p4.isHighContrast ? [normHex(p4.themeAccent), normHex(p4.themeFg)] : [...palette.map(normHex), normHex(p4.themeAccent), normHex(p4.themeFg)];
920
924
  const accent = candidates.find(usable) ?? FALLBACK_ACCENT;
921
- const A4 = hexToLab(accent);
925
+ const A2 = hexToLab(accent);
922
926
  let t = 0.1;
923
- let low = mixLab(bg, A4, t);
927
+ let low = mixLab(bg, A2, t);
924
928
  while (t < 0.6 && deltaE2000(paintedOver(labToHex(low), canvas, AUTO_RAMP_DRAWN_OPACITY), bg) < SAME_SHADE_DELTA_E) {
925
929
  t += 0.02;
926
- low = mixLab(bg, A4, t);
930
+ low = mixLab(bg, A2, t);
927
931
  }
928
932
  const lowHex = labToHex(low);
929
- let high = { ...A4 };
933
+ let high = { ...A2 };
930
934
  let highHex = labToHex(high);
931
935
  for (let step = 0; step < 80 && autoRampMinStep(lowHex, highHex, canvas) < AUTO_RAMP_MIN_STEP_DELTA_E; step++) {
932
936
  const L4 = Math.max(4, Math.min(98, high.L + (darkCanvas ? 1 : -1)));
@@ -1520,23 +1524,23 @@ function E3(n) {
1520
1524
  if (a3 && i && P3(a3) !== i && (a3 = null), n.city !== void 0 && n.city !== null) {
1521
1525
  let r2 = u2(String(n.city)), s2 = D(r2, R(String(n.city))), c2 = s2 ? _(s2, n.mapKind) : null;
1522
1526
  if (c2 && c2.length) {
1523
- let d2 = n.state === void 0 || n.state === null ? "" : String(n.state).trim(), T = d2 ? u2(d2) : "", y2 = d2 ? t3(d2) : null, f4 = [], N = -1;
1527
+ let d2 = n.state === void 0 || n.state === null ? "" : String(n.state).trim(), T = d2 ? u2(d2) : "", y2 = d2 ? t3(d2) : null, f4 = [], N2 = -1;
1524
1528
  for (let M2 of c2) {
1525
1529
  let l2 = M2.row, p4 = 0;
1526
1530
  if (d2 && l2.a1 !== "") if (y2 && l2.a1 === y2 || u2(l2.a1) === T) p4++;
1527
1531
  else continue;
1528
1532
  if (i) if (G(l2.cc) === i) p4++;
1529
1533
  else continue;
1530
- p4 > N ? (N = p4, f4 = [M2]) : p4 === N && f4.push(M2);
1534
+ p4 > N2 ? (N2 = p4, f4 = [M2]) : p4 === N2 && f4.push(M2);
1531
1535
  }
1532
1536
  f4.some((M2) => M2.primary) && (f4 = f4.filter((M2) => M2.primary));
1533
- let A4 = /* @__PURE__ */ new Map();
1534
- for (let M2 of f4) A4.set(M2.row.lon.toFixed(2) + "," + M2.row.lat.toFixed(2), M2.row);
1535
- if (A4.size === 1) {
1537
+ let A2 = /* @__PURE__ */ new Map();
1538
+ for (let M2 of f4) A2.set(M2.row.lon.toFixed(2) + "," + M2.row.lat.toFixed(2), M2.row);
1539
+ if (A2.size === 1) {
1536
1540
  let M2 = f4[0].row;
1537
1541
  return { lat: M2.lat, lon: M2.lon, precision: "city" };
1538
1542
  }
1539
- if (A4.size > 1) return { ambiguous: true, matches: A4.size };
1543
+ if (A2.size > 1) return { ambiguous: true, matches: A2.size };
1540
1544
  }
1541
1545
  }
1542
1546
  if (!i || L3.has(i)) for (let r2 of r3(n.zip)) {
@@ -1556,15 +1560,15 @@ function E3(n) {
1556
1560
  var b = { latlon: 0, city: 1, zip3: 2, state: 3, country: 4 };
1557
1561
  var e3 = 8;
1558
1562
  function J3(n, e) {
1559
- let t = new Array(n.length), o4 = new Array(n.length), i = new Array(n.length), a3 = /* @__PURE__ */ new Set(), r2 = [], s2 = /* @__PURE__ */ new Set(), c2 = [], d2 = /* @__PURE__ */ new Set(), T = [], y2 = { latlon: 0, city: 0, zip3: 0, state: 0, country: 0 }, f4 = 0, N = 0, A4 = null;
1563
+ let t = new Array(n.length), o4 = new Array(n.length), i = new Array(n.length), a3 = /* @__PURE__ */ new Set(), r2 = [], s2 = /* @__PURE__ */ new Set(), c2 = [], d2 = /* @__PURE__ */ new Set(), T = [], y2 = { latlon: 0, city: 0, zip3: 0, state: 0, country: 0 }, f4 = 0, N2 = 0, A2 = null;
1560
1564
  for (let l2 = 0; l2 < n.length; ++l2) {
1561
1565
  let p4 = n[l2], C2 = E3({ ...p4, mapKind: e });
1562
1566
  if (B3(C2)) {
1563
- t[l2] = null, o4[l2] = null, i[l2] = null, N++;
1567
+ t[l2] = null, o4[l2] = null, i[l2] = null, N2++;
1564
1568
  let S2 = W(p4);
1565
1569
  S2 && !d2.has(S2.toLowerCase()) && (d2.add(S2.toLowerCase()), T.length < e3 && T.push(S2));
1566
1570
  } else if (C2) {
1567
- if (t[l2] = C2.lat, o4[l2] = C2.lon, i[l2] = C2.precision, f4++, y2[C2.precision]++, (A4 === null || b[C2.precision] > b[A4]) && (A4 = C2.precision), b[C2.precision] > b.city) {
1571
+ if (t[l2] = C2.lat, o4[l2] = C2.lon, i[l2] = C2.precision, f4++, y2[C2.precision]++, (A2 === null || b[C2.precision] > b[A2]) && (A2 = C2.precision), b[C2.precision] > b.city) {
1568
1572
  let S2 = W(p4);
1569
1573
  S2 && !s2.has(S2.toLowerCase()) && (s2.add(S2.toLowerCase()), c2.length < e3 && c2.push(S2));
1570
1574
  }
@@ -1575,7 +1579,7 @@ function J3(n, e) {
1575
1579
  }
1576
1580
  }
1577
1581
  let M2 = !C3() && n.some((l2) => l2.city !== null && l2.city !== void 0 && String(l2.city).trim() !== "");
1578
- return { lat: t, lon: o4, precisions: i, precision: A4, precisionCounts: y2, coarseExamples: c2, unmatched: r2, ambiguousRows: N, ambiguousExamples: T, matchedRows: f4, totalRows: n.length, ...M2 ? { cityTableMissing: true } : {} };
1582
+ return { lat: t, lon: o4, precisions: i, precision: A2, precisionCounts: y2, coarseExamples: c2, unmatched: r2, ambiguousRows: N2, ambiguousExamples: T, matchedRows: f4, totalRows: n.length, ...M2 ? { cityTableMissing: true } : {} };
1579
1583
  }
1580
1584
  function W(n) {
1581
1585
  let e = [n.city, n.state, n.zip, n.country].filter((t) => t != null && String(t).trim() !== "").map((t) => String(t).trim()).join(", ");
@@ -1754,10 +1758,10 @@ function Rn(n, s2) {
1754
1758
  return s2 === "us-zip5" ? w2.has(n.slice(0, 3)) : true;
1755
1759
  }
1756
1760
  function Fn(n, s2) {
1757
- let o4 = new Array(n.length), r2 = /* @__PURE__ */ new Set(), I2 = [], N = 0;
1761
+ let o4 = new Array(n.length), r2 = /* @__PURE__ */ new Set(), I2 = [], N2 = 0;
1758
1762
  for (let h4 = 0; h4 < n.length; ++h4) {
1759
1763
  let M2 = En(n[h4], s2), O4 = M2 !== null && Rn(M2, s2) ? M2 : null;
1760
- if (o4[h4] = O4, O4 !== null) N++;
1764
+ if (o4[h4] = O4, O4 !== null) N2++;
1761
1765
  else {
1762
1766
  let d2 = n[h4];
1763
1767
  if (d2 != null) {
@@ -1769,7 +1773,7 @@ function Fn(n, s2) {
1769
1773
  }
1770
1774
  }
1771
1775
  }
1772
- return { iso: o4, unmatched: I2, matchedRows: N, totalRows: n.length };
1776
+ return { iso: o4, unmatched: I2, matchedRows: N2, totalRows: n.length };
1773
1777
  }
1774
1778
 
1775
1779
  // ../shape-core/dist/chunk-OPDY2YEA.mjs
@@ -1890,10 +1894,10 @@ function F3(e) {
1890
1894
  }
1891
1895
  return Array.from(t);
1892
1896
  }
1893
- function A2(e) {
1897
+ function N(e) {
1894
1898
  let t = F3(e);
1895
1899
  if (t.length === 0 || ne2(e) < 95) return false;
1896
- let n = t.filter((a3) => te(a3) && !t3(a3)).length, o4 = t.filter((a3) => !te(a3) && !!t3(a3)).length;
1900
+ let n = t.filter((l2) => te(l2) && !t3(l2)).length, o4 = t.filter((l2) => !te(l2) && !!t3(l2)).length;
1897
1901
  return n >= 1 && n >= o4;
1898
1902
  }
1899
1903
  function ce3(e, t) {
@@ -1908,7 +1912,7 @@ function ne2(e) {
1908
1912
  for (let o4 of t) te(o4) && n++;
1909
1913
  return Math.round(n / t.length * 1e3) / 10;
1910
1914
  }
1911
- function z2(e) {
1915
+ function $4(e) {
1912
1916
  let t = F3(e);
1913
1917
  if (t.length === 0) return 0;
1914
1918
  let n = 0;
@@ -1934,11 +1938,11 @@ function re2(e) {
1934
1938
  function de(e, t) {
1935
1939
  return y(t, fn) ? true : oe2(e).some((n) => /^\d{5}(-\d{4})?$/.test(n.replace(/\.0+$/, "")));
1936
1940
  }
1937
- function Bn2(e, t) {
1941
+ function Yn3(e, t) {
1938
1942
  let n = F3(e);
1939
1943
  if (n.length === 0) return 0;
1940
1944
  let o4 = 0;
1941
- for (let a3 of n) j3(a3, t) && o4++;
1945
+ for (let l2 of n) j3(l2, t) && o4++;
1942
1946
  return Math.round(o4 / n.length * 1e3) / 10;
1943
1947
  }
1944
1948
  function ee3(e, t) {
@@ -1946,18 +1950,18 @@ function ee3(e, t) {
1946
1950
  for (let o4 of F3(e)) t(o4) && n++;
1947
1951
  return n;
1948
1952
  }
1949
- function jn(e, t) {
1953
+ function Qn2(e, t) {
1950
1954
  let n = 0;
1951
1955
  for (let o4 of oe2(e)) t(o4) && n++;
1952
1956
  return n;
1953
1957
  }
1954
- function Yn3(e, t, n, o4, a3) {
1955
- let u4 = [], l2 = [], i = {}, h4 = {}, C2 = new Set(e), y2 = (s2) => t.map((d2) => d2[s2]);
1958
+ function Xn2(e, t, n, o4, l2) {
1959
+ let u4 = [], a3 = [], i = {}, h4 = {}, C2 = new Set(e), y2 = (s2) => t.map((d2) => d2[s2]);
1956
1960
  for (let s2 of ["city", "country"]) {
1957
1961
  let d2 = n?.[s2];
1958
1962
  d2 && C2.has(d2) && (i[s2] = d2);
1959
1963
  }
1960
- let g5 = new Set(a3 ?? e);
1964
+ let g5 = new Set(l2 ?? e);
1961
1965
  for (let s2 of ["lat", "lon"]) {
1962
1966
  let d2 = n?.[s2];
1963
1967
  d2 && g5.has(d2) && (i[s2] = d2);
@@ -1965,67 +1969,67 @@ function Yn3(e, t, n, o4, a3) {
1965
1969
  let f4 = n?.zip;
1966
1970
  if (f4 && C2.has(f4)) {
1967
1971
  let s2 = y2(f4), d2 = re2(s2);
1968
- d2 < 95 ? l2.push(`zip=${f4} (only ${d2}% of values read as ZIPs)`) : de(s2, f4) ? i.zip = f4 : l2.push(`zip=${f4} (all values are 3-4 digits with nothing to say they are postal)`);
1972
+ d2 < 95 ? a3.push(`zip=${f4} (only ${d2}% of values read as ZIPs)`) : de(s2, f4) ? i.zip = f4 : a3.push(`zip=${f4} (all values are 3-4 digits with nothing to say they are postal)`);
1969
1973
  }
1970
1974
  let c2 = n?.state;
1971
1975
  if (c2 && C2.has(c2)) {
1972
1976
  let s2 = y2(c2), d2 = F3(s2);
1973
- d2.length > 0 && d2.every((S2) => ce3(S2, o4)) ? l2.push(`state=${c2} (every value is "CA", which is both Canada and California)`) : A2(s2) ? (l2.push(`state=${c2} (every value is a country, not a state)`), i.country || (i.country = c2)) : z2(s2) < 95 ? l2.push(`state=${c2} (only ${z2(s2)}% of values are states/provinces)`) : i.state = c2;
1977
+ d2.length > 0 && d2.every((S2) => ce3(S2, o4)) ? a3.push(`state=${c2} (every value is "CA", which is both Canada and California)`) : N(s2) ? (a3.push(`state=${c2} (every value is a country, not a state)`), i.country || (i.country = c2)) : $4(s2) < 95 ? a3.push(`state=${c2} (only ${$4(s2)}% of values are states/provinces)`) : i.state = c2;
1974
1978
  }
1975
1979
  let p4 = new Set(Object.values(i).filter(Boolean)), b4 = e.filter((s2) => !p4.has(s2) && !s2.startsWith("__"));
1976
1980
  if (!i.state) {
1977
1981
  let s2 = null;
1978
1982
  for (let d2 of b4) {
1979
1983
  let S2 = y2(d2);
1980
- if (A2(S2)) continue;
1981
- let P4 = z2(S2);
1982
- if (P4 < 95) continue;
1984
+ if (N(S2)) continue;
1985
+ let O4 = $4(S2);
1986
+ if (O4 < 95) continue;
1983
1987
  let K3 = ee3(S2, (G4) => !!t3(G4)), Ne = ee3(S2, (G4) => !!t3(G4) && !ce3(G4, o4));
1984
- K3 < Z3 && Ne < 1 || (!s2 || P4 > s2.pct) && (s2 = { name: d2, pct: P4 });
1988
+ K3 < Z3 && Ne < 1 || (!s2 || O4 > s2.pct) && (s2 = { name: d2, pct: O4 });
1985
1989
  }
1986
1990
  s2 && (i.state = s2.name, p4.add(s2.name), u4.push(`state=${s2.name} (${s2.pct}% states/provinces)`));
1987
1991
  }
1988
1992
  if (!i.zip) for (let s2 of b4) {
1989
1993
  if (p4.has(s2)) continue;
1990
1994
  let d2 = y2(s2), S2 = re2(d2);
1991
- if (!(S2 < 95) && de(d2, s2) && !(jn(d2, (P4) => r3(P4).length > 0) < Z3)) {
1995
+ if (!(S2 < 95) && de(d2, s2) && !(Qn2(d2, (O4) => r3(O4).length > 0) < Z3)) {
1992
1996
  i.zip = s2, p4.add(s2), u4.push(`zip=${s2} (${S2}% ZIP codes)`);
1993
1997
  break;
1994
1998
  }
1995
1999
  }
1996
2000
  let T = i.country;
1997
- if (!T || !A2(y2(T))) {
2001
+ if (!T || !N(y2(T))) {
1998
2002
  let s2 = null;
1999
2003
  for (let d2 of b4) {
2000
- if (p4.has(d2) || !A2(y2(d2))) continue;
2004
+ if (p4.has(d2) || !N(y2(d2))) continue;
2001
2005
  let S2 = ne2(y2(d2));
2002
2006
  (!s2 || S2 > s2.pct) && (s2 = { name: d2, pct: S2 });
2003
2007
  }
2004
- s2 && (T && (l2.push(`country=${T} (only ${ne2(y2(T))}% of values are country identifiers; using ${s2.name} instead)`), h4.country = T), i.country = s2.name, p4.add(s2.name), u4.push(`country=${s2.name}`));
2008
+ s2 && (T && (a3.push(`country=${T} (only ${ne2(y2(T))}% of values are country identifiers; using ${s2.name} instead)`), h4.country = T), i.country = s2.name, p4.add(s2.name), u4.push(`country=${s2.name}`));
2005
2009
  }
2006
2010
  if (!i.city) {
2007
2011
  let s2 = null;
2008
2012
  for (let d2 of b4) {
2009
2013
  if (p4.has(d2)) continue;
2010
- let S2 = y2(d2), P4 = Bn2(S2, o4);
2011
- P4 < D3 || ee3(S2, (K3) => j3(K3, o4)) < Z3 || (!s2 || P4 > s2.pct) && (s2 = { name: d2, pct: P4 });
2014
+ let S2 = y2(d2), O4 = Yn3(S2, o4);
2015
+ O4 < D3 || ee3(S2, (K3) => j3(K3, o4)) < Z3 || (!s2 || O4 > s2.pct) && (s2 = { name: d2, pct: O4 });
2012
2016
  }
2013
2017
  s2 && (i.city = s2.name, u4.push(`city=${s2.name} (${s2.pct}% known cities)`));
2014
2018
  }
2015
- let O4 = i.country ? y2(i.country) : [], R4 = !!i.country && A2(O4), L4 = R4 ? new Set(O4.map((s2) => k(s2 == null ? null : String(s2))).filter(Boolean)).size : 0, x3 = R4 && L4 >= 2, w3 = !!(i.lat && i.lon) && t.some((s2) => {
2019
+ let L4 = i.country ? y2(i.country) : [], R4 = !!i.country && N(L4), A2 = R4 ? new Set(L4.map((s2) => k(s2 == null ? null : String(s2))).filter(Boolean)).size : 0, x3 = R4 && A2 >= 2, w3 = !!(i.lat && i.lon) && t.some((s2) => {
2016
2020
  let d2 = +String(s2[i.lat] ?? "").trim(), S2 = +String(s2[i.lon] ?? "").trim();
2017
2021
  return isFinite(d2) && isFinite(S2) && d2 >= -90 && d2 <= 90 && S2 >= -180 && S2 <= 180 && String(s2[i.lat] ?? "").trim() !== "" && String(s2[i.lon] ?? "").trim() !== "";
2018
2022
  });
2019
- return i.lat && i.lon && !w3 && l2.push(`lat/lon=${i.lat}/${i.lon} (no row holds a usable coordinate pair)`), i.country && !x3 && !(i.city || i.state || i.zip || w3) && l2.push(R4 ? `country=${i.country} (every row resolves to the same country - nothing to place apart)` : `country=${i.country} (values are not country identifiers)`), { bind: !!(i.city || i.state || i.zip || x3 || w3) ? i : null, backfilled: u4, refused: l2, ...Object.keys(h4).length ? { labelFallback: h4 } : {} };
2023
+ return i.lat && i.lon && !w3 && a3.push(`lat/lon=${i.lat}/${i.lon} (no row holds a usable coordinate pair)`), i.country && !x3 && !(i.city || i.state || i.zip || w3) && a3.push(R4 ? `country=${i.country} (every row resolves to the same country - nothing to place apart)` : `country=${i.country} (values are not country identifiers)`), { bind: !!(i.city || i.state || i.zip || x3 || w3) ? i : null, backfilled: u4, refused: a3, ...Object.keys(h4).length ? { labelFallback: h4 } : {} };
2020
2024
  }
2021
2025
  var r = (e, ...t) => ({ part: e, langs: Object.freeze(t) });
2022
2026
  var ie2 = Object.freeze({ jaar: r("year", "nl"), kwartaal: r("quarter", "nl"), maand: r("month", "nl"), dag: r("day", "nl", "sv", "da", "nb"), jahr: r("year", "de"), quartal: r("quarter", "de"), monat: r("month", "de"), tag: r("day", "de"), ann\u00E9e: r("year", "fr"), mois: r("month", "fr"), jour: r("day", "fr"), trimestre: r("quarter", "fr", "es", "pt", "it"), a\u00F1o: r("year", "es"), mes: r("month", "es"), d\u00EDa: r("day", "es"), ano: r("year", "pt"), m\u00EAs: r("month", "pt"), dia: r("day", "pt"), anno: r("year", "it"), mese: r("month", "it"), giorno: r("day", "it"), \u0433\u043E\u0434: r("year", "ru"), \u043C\u0435\u0441\u044F\u0446: r("month", "ru"), \u0440\u0456\u043A: r("year", "uk"), \u043C\u0456\u0441\u044F\u0446\u044C: r("month", "uk"), \u043A\u0432\u0430\u0440\u0442\u0430\u043B: r("quarter", "ru", "uk"), \u0434\u0435\u043D\u044C: r("day", "ru", "uk"), rok: r("year", "pl", "cs", "sk"), kwarta\u0142: r("quarter", "pl"), miesi\u0105c: r("month", "pl"), dzie\u0144: r("day", "pl"), \u010Dtvrtlet\u00ED: r("quarter", "cs"), m\u011Bs\u00EDc: r("month", "cs"), den: r("day", "cs"), \u0161tvr\u0165rok: r("quarter", "sk"), mesiac: r("month", "sk"), de\u0148: r("day", "sk"), \u00E5r: r("year", "sv", "da", "nb"), kvartal: r("quarter", "sv", "da", "nb"), m\u00E5nad: r("month", "sv"), m\u00E5ned: r("month", "da", "nb"), vuosi: r("year", "fi"), vuosinelj\u00E4nnes: r("quarter", "fi"), kuukausi: r("month", "fi"), p\u00E4iv\u00E4: r("day", "fi"), \u00E9v: r("year", "hu"), negyed\u00E9v: r("quarter", "hu"), h\u00F3nap: r("month", "hu"), nap: r("day", "hu"), y\u0131l: r("year", "tr"), \u00E7eyrek: r("quarter", "tr"), ay: r("month", "tr"), g\u00FCn: r("day", "tr"), \u03AD\u03C4\u03BF\u03C2: r("year", "el"), \u03C4\u03C1\u03AF\u03BC\u03B7\u03BD\u03BF: r("quarter", "el"), \u03BC\u03AE\u03BD\u03B1\u03C2: r("month", "el"), \u03B7\u03BC\u03AD\u03C1\u03B1: r("day", "el"), an: r("year", "ro"), trimestru: r("quarter", "ro"), lun\u0103: r("month", "ro"), zi: r("day", "ro"), godina: r("year", "hr"), tromjese\u010Dje: r("quarter", "hr"), mjesec: r("month", "hr"), dan: r("day", "hr"), \u5E74: r("year", "ja", "zh"), \u56DB\u534A\u671F: r("quarter", "ja"), \u5B63\u5EA6: r("quarter", "zh"), \u6708: r("month", "ja", "zh"), \u65E5: r("day", "ja", "zh"), \uC5F0\uB3C4: r("year", "ko"), \uBD84\uAE30: r("quarter", "ko"), \uC6D4: r("month", "ko"), \uC77C: r("day", "ko"), tahun: r("year", "id"), kuartal: r("quarter", "id"), triwulan: r("quarter", "id"), bulan: r("month", "id"), hari: r("day", "id"), n\u0103m: r("year", "vi"), qu\u00FD: r("quarter", "vi"), th\u00E1ng: r("month", "vi"), ng\u00E0y: r("day", "vi"), \u0E1B\u0E35: r("year", "th"), \u0E44\u0E15\u0E23\u0E21\u0E32\u0E2A: r("quarter", "th"), \u0E40\u0E14\u0E37\u0E2D\u0E19: r("month", "th"), \u0E27\u0E31\u0E19: r("day", "th"), \u0627\u0644\u0633\u0646\u0629: r("year", "ar"), \u0633\u0646\u0629: r("year", "ar"), \u0627\u0644\u0639\u0627\u0645: r("year", "ar"), \u0639\u0627\u0645: r("year", "ar"), \u0627\u0644\u0631\u0628\u0639: r("quarter", "ar"), \u0631\u0628\u0639: r("quarter", "ar"), "\u0631\u0628\u0639 \u0627\u0644\u0633\u0646\u0629": r("quarter", "ar"), \u0627\u0644\u0634\u0647\u0631: r("month", "ar"), \u0634\u0647\u0631: r("month", "ar"), \u0627\u0644\u064A\u0648\u0645: r("day", "ar"), \u064A\u0648\u0645: r("day", "ar"), \u05E9\u05E0\u05D4: r("year", "he"), \u05E8\u05D1\u05E2\u05D5\u05DF: r("quarter", "he"), \u05D7\u05D5\u05D3\u05E9: r("month", "he"), \u05D9\u05D5\u05DD: r("day", "he"), \u0935\u0930\u094D\u0937: r("year", "hi"), \u0938\u093E\u0932: r("year", "hi"), \u0924\u093F\u092E\u093E\u0939\u0940: r("quarter", "hi"), \u092E\u0939\u0940\u0928\u093E: r("month", "hi"), \u092E\u093E\u0939: r("month", "hi"), \u0926\u093F\u0928: r("day", "hi"), \u0633\u0627\u0644: r("year", "fa"), \u0645\u0627\u0647: r("month", "fa"), \u0631\u0648\u0632: r("day", "fa") });
2023
- var Qn2 = Object.freeze(Array.from(new Set(Object.values(ie2).flatMap((e) => e.langs))));
2027
+ var Jn = Object.freeze(Array.from(new Set(Object.values(ie2).flatMap((e) => e.langs))));
2024
2028
  var fe = Object.freeze(["fa", "ar", "he", "th"]);
2025
- var wr = Object.freeze({ integer: "number", decimal: "number", double: "number", datetime: "date", date: "date", boolean: "boolean", string: "text" });
2026
- var Nr = Object.freeze({ RENDER_START: "render-start", RENDER_OK: "render-ok", RENDER_ERROR: "render-error", VIEW: "view", VIEW_VERDICT: "view-verdict", REDRAW: "redraw", INTERACT: "interact" });
2027
- var Ar = Object.freeze(["open", "generate", "version", "rebind", "data", "sample", "fix", "resize", "other"]);
2028
- var Le = 2 ** 31 - 1;
2029
+ var Pr = Object.freeze({ integer: "number", decimal: "number", double: "number", datetime: "date", date: "date", boolean: "boolean", string: "text" });
2030
+ var kr = Object.freeze({ RENDER_START: "render-start", RENDER_OK: "render-ok", RENDER_ERROR: "render-error", VIEW: "view", VIEW_VERDICT: "view-verdict", REDRAW: "redraw", INTERACT: "interact" });
2031
+ var Gr = Object.freeze(["open", "generate", "version", "rebind", "data", "sample", "fix", "resize", "other"]);
2032
+ var Ae = 2 ** 31 - 1;
2029
2033
 
2030
2034
  // src/payload.ts
2031
2035
  var ROW_IDX_COLUMN = "__rowIdx__";
@@ -2040,7 +2044,7 @@ function resolvePointChannel(cols, rowObjs, point) {
2040
2044
  let labelCol;
2041
2045
  if (bind) {
2042
2046
  const dims = cols.filter((c2) => !c2.isMeasure).map((c2) => c2.name);
2043
- const res = Yn3(dims, rowObjs, bind, point?.mapKind, cols.map((c2) => c2.name));
2047
+ const res = Xn2(dims, rowObjs, bind, point?.mapKind, cols.map((c2) => c2.name));
2044
2048
  bind = res.bind;
2045
2049
  rolesBackfilled = res.backfilled;
2046
2050
  rolesRefused = res.refused;
@@ -2346,6 +2350,48 @@ function nextSelection(current, rowIdxs, mods = {}) {
2346
2350
  const same = cur.length === rowIdxs.length && rowIdxs.every((r2) => cur.includes(r2));
2347
2351
  return same ? [] : rowIdxs.slice();
2348
2352
  }
2353
+ function selectionRuleOf(container) {
2354
+ try {
2355
+ const el = container?.querySelector?.(`[${SELECTION_RULE_ATTR}]`);
2356
+ return el?.getAttribute(SELECTION_RULE_ATTR) === SELECTION_RULE_CLICKED_MARKS ? "clicked-marks" : "any-row";
2357
+ } catch {
2358
+ return "any-row";
2359
+ }
2360
+ }
2361
+ function markKeyOf(el) {
2362
+ if (!el) return "";
2363
+ const k3 = el.getAttribute(MARK_KEY_ATTR);
2364
+ if (k3 != null && k3 !== "") return k3;
2365
+ return "rows:" + (el.getAttribute(ROW_IDX_ATTR) || "").replace(/\s+/g, "");
2366
+ }
2367
+ var toSet = (s2) => new Set(Array.from(s2 ?? []));
2368
+ function sameRowSet(a3, b4) {
2369
+ const A2 = toSet(a3), B4 = toSet(b4);
2370
+ if (A2.size !== B4.size) return false;
2371
+ for (const r2 of A2) if (!B4.has(r2)) return false;
2372
+ return true;
2373
+ }
2374
+ function planSelectionPaint(marks, selection, rule, clicked) {
2375
+ const sel = toSet(selection);
2376
+ if (sel.size === 0) return { on: marks.map(() => false), mode: "none", keys: [] };
2377
+ const anyRow = () => marks.map((m4) => m4.rows.some((r2) => sel.has(r2)));
2378
+ if (rule !== "clicked-marks") return { on: anyRow(), mode: "any-row", keys: [] };
2379
+ if (clicked && clicked.keys.length && sameRowSet(clicked.selection, sel)) {
2380
+ const want = new Set(clicked.keys);
2381
+ const on = marks.map((m4) => m4.rows.length > 0 && want.has(m4.key));
2382
+ if (on.some(Boolean)) return { on, mode: "clicked", keys: marks.filter((_4, i) => on[i]).map((m4) => m4.key) };
2383
+ }
2384
+ const exact = marks.map((m4) => m4.rows.length > 0 && sameRowSet(m4.rows, sel));
2385
+ if (exact.some(Boolean)) return { on: exact, mode: "exact", keys: marks.filter((_4, i) => exact[i]).map((m4) => m4.key) };
2386
+ return { on: anyRow(), mode: "fallback-any-row", keys: [] };
2387
+ }
2388
+ function nextClickedMarks(prev, current, key, next, mods = {}) {
2389
+ if (!next.length) return null;
2390
+ if (!(mods.ctrl || mods.shift)) return { keys: [key], selection: next.slice() };
2391
+ const held = prev && sameRowSet(prev.selection, current) ? prev.keys : [];
2392
+ const keys = held.includes(key) ? held.filter((k3) => k3 !== key) : [...held, key];
2393
+ return { keys, selection: next.slice() };
2394
+ }
2349
2395
  function isInsideControl(target, container) {
2350
2396
  let el = target;
2351
2397
  if (el && el.nodeType !== 1) el = el.parentElement ?? el.parentNode ?? null;
@@ -2384,9 +2430,9 @@ function createMarkResolver(env) {
2384
2430
  const m4 = el.getScreenCTM();
2385
2431
  if (!m4) return Number.MAX_VALUE;
2386
2432
  let best = Number.MAX_VALUE;
2387
- const N = 64;
2388
- for (let i = 0; i <= N; i++) {
2389
- const pt2 = el.getPointAtLength(L4 * i / N);
2433
+ const N2 = 64;
2434
+ for (let i = 0; i <= N2; i++) {
2435
+ const pt2 = el.getPointAtLength(L4 * i / N2);
2390
2436
  const dx = m4.a * pt2.x + m4.c * pt2.y + m4.e - x3;
2391
2437
  const dy = m4.b * pt2.x + m4.d * pt2.y + m4.f - y2;
2392
2438
  const d2 = dx * dx + dy * dy;
@@ -2542,14 +2588,14 @@ function collectD3GlyphCenters(container, baseRect, out) {
2542
2588
  const isArcSector = /[Aa]/.test(dAttr) && !/[CcSsQqTt]/.test(dAttr);
2543
2589
  let local;
2544
2590
  if (isArcSector) {
2545
- const N = 24;
2591
+ const N2 = 24;
2546
2592
  let ax = 0, ay = 0;
2547
- for (let k3 = 0; k3 < N; k3++) {
2548
- const sp = el.getPointAtLength(len * k3 / N);
2593
+ for (let k3 = 0; k3 < N2; k3++) {
2594
+ const sp = el.getPointAtLength(len * k3 / N2);
2549
2595
  ax += sp.x;
2550
2596
  ay += sp.y;
2551
2597
  }
2552
- const cand = new Point(ax / N, ay / N);
2598
+ const cand = new Point(ax / N2, ay / N2);
2553
2599
  let inside = true;
2554
2600
  try {
2555
2601
  if (typeof el.isPointInFill === "function") {
@@ -2828,6 +2874,49 @@ function hitBandFlag(c2) {
2828
2874
  return c2.uncovered > 0 ? "hitband:d3:thin" : "hitband:d3:ok";
2829
2875
  }
2830
2876
 
2877
+ // src/clippedText.ts
2878
+ var CLIP_TOLERANCE_PX = 1;
2879
+ var MAX_REPORTED = 8;
2880
+ function censusClippedText(container) {
2881
+ const none = { measured: 0, clipped: [], clippedCount: 0, laidOut: false };
2882
+ try {
2883
+ const svg = container?.querySelector?.("svg");
2884
+ if (!svg) return none;
2885
+ const f4 = svg.getBoundingClientRect();
2886
+ if (!f4 || !(f4.width > 0) || !(f4.height > 0)) return none;
2887
+ const view = svg.ownerDocument?.defaultView;
2888
+ const clipped = [];
2889
+ let measured = 0;
2890
+ for (const t of Array.from(svg.querySelectorAll("text"))) {
2891
+ const r2 = t.getBoundingClientRect();
2892
+ if (!r2 || !(r2.width > 0) || !(r2.height > 0)) continue;
2893
+ const s2 = view?.getComputedStyle?.(t);
2894
+ if (s2 && (s2.display === "none" || s2.visibility === "hidden" || s2.opacity === "0")) continue;
2895
+ measured++;
2896
+ const top = f4.top - r2.top, left = f4.left - r2.left, bottom = r2.bottom - f4.bottom, right = r2.right - f4.right;
2897
+ if (top > CLIP_TOLERANCE_PX || left > CLIP_TOLERANCE_PX || bottom > CLIP_TOLERANCE_PX || right > CLIP_TOLERANCE_PX) {
2898
+ clipped.push({
2899
+ text: String(t.textContent ?? "").slice(0, 40),
2900
+ top: Math.max(0, Math.round(top)),
2901
+ left: Math.max(0, Math.round(left)),
2902
+ bottom: Math.max(0, Math.round(bottom)),
2903
+ right: Math.max(0, Math.round(right))
2904
+ });
2905
+ }
2906
+ }
2907
+ clipped.sort((a3, b4) => Math.max(b4.top, b4.left, b4.bottom, b4.right) - Math.max(a3.top, a3.left, a3.bottom, a3.right));
2908
+ return { measured, clipped: clipped.slice(0, MAX_REPORTED), clippedCount: clipped.length, laidOut: true };
2909
+ } catch {
2910
+ return none;
2911
+ }
2912
+ }
2913
+ function clippedTextFlag(c2) {
2914
+ if (!c2.laidOut || c2.clippedCount === 0) return "";
2915
+ const worst = c2.clipped[0];
2916
+ const side = ["top", "left", "bottom", "right"].reduce((a3, k3) => worst[k3] > worst[a3] ? k3 : a3, "top");
2917
+ return `${c2.clippedCount} of ${c2.measured} text label(s) are cut off by the frame; the worst, "${worst.text}", by ${worst[side]}px at the ${side}.`;
2918
+ }
2919
+
2831
2920
  // src/valuePlacement.ts
2832
2921
  var MIN_MARKS2 = 24;
2833
2922
  var ELEMENT_CAP4 = 5e3;
@@ -4269,6 +4358,13 @@ function createChartHost(container, config) {
4269
4358
  let activeTickEl = null;
4270
4359
  let selectionIsPeriod = false;
4271
4360
  let selectionFromChart = false;
4361
+ const clickedMarks = () => container[CONTAINER_SLOT_CLICKED_MARKS] ?? null;
4362
+ const setClickedMarks = (c2) => {
4363
+ try {
4364
+ container[CONTAINER_SLOT_CLICKED_MARKS] = c2;
4365
+ } catch {
4366
+ }
4367
+ };
4272
4368
  const subs = /* @__PURE__ */ new Set();
4273
4369
  const ensureAffordanceStyles = () => {
4274
4370
  try {
@@ -4340,6 +4436,31 @@ function createChartHost(container, config) {
4340
4436
  if (active && tickStillDrawn) activeTickEl.classList?.add(ACTIVE_TICK_CLASS);
4341
4437
  const wanted = new Set(sel ?? []);
4342
4438
  const marks = container.querySelectorAll(`.${MARK_CLASS}[${ROW_IDX_ATTR}], .${LEGEND_MARK_CLASS}[${ROW_IDX_ATTR}]`);
4439
+ const rule = selectionRuleOf(container);
4440
+ if (rule !== "any-row") {
4441
+ const list = Array.from(marks);
4442
+ const plan = planSelectionPaint(
4443
+ list.map((m4) => ({ key: markKeyOf(m4), rows: parseRowIdxs(m4.getAttribute(ROW_IDX_ATTR)) })),
4444
+ active ? wanted : [],
4445
+ rule,
4446
+ clickedMarks()
4447
+ );
4448
+ list.forEach((m4, i) => m4.classList?.[plan.on[i] ? "add" : "remove"](MARK_SELECTED_CLASS));
4449
+ setClickedMarks(plan.keys.length ? { keys: plan.keys, selection: Array.from(wanted) } : null);
4450
+ if (active) {
4451
+ try {
4452
+ config.onSelectionPaint?.({
4453
+ rule,
4454
+ mode: plan.mode,
4455
+ lit: plan.on.filter(Boolean).length,
4456
+ marks: list.length,
4457
+ selection: wanted.size
4458
+ });
4459
+ } catch {
4460
+ }
4461
+ }
4462
+ return;
4463
+ }
4343
4464
  for (const m4 of Array.from(marks)) {
4344
4465
  const rows = parseRowIdxs(m4.getAttribute(ROW_IDX_ATTR));
4345
4466
  const on = active && rows.some((r2) => wanted.has(r2));
@@ -4401,6 +4522,13 @@ function createChartHost(container, config) {
4401
4522
  const multi = !!(e?.ctrlKey || e?.metaKey || e?.shiftKey);
4402
4523
  const next = nextSelection(current, rows, { ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey });
4403
4524
  const toggledOff = !multi && next.length === 0;
4525
+ setClickedMarks(nextClickedMarks(
4526
+ clickedMarks(),
4527
+ current,
4528
+ markKeyOf(el),
4529
+ next,
4530
+ { ctrl: !!(e?.ctrlKey || e?.metaKey), shift: !!e?.shiftKey }
4531
+ ));
4404
4532
  notify(next, "user", toggledOff ? null : tick);
4405
4533
  };
4406
4534
  container.addEventListener(XFILTER_REFRESH_EVENT, onXf);
@@ -4481,6 +4609,12 @@ function createChartHost(container, config) {
4481
4609
  } catch {
4482
4610
  }
4483
4611
  }
4612
+ if (config.onClippedTextCensus) {
4613
+ try {
4614
+ config.onClippedTextCensus(censusClippedText(container));
4615
+ } catch {
4616
+ }
4617
+ }
4484
4618
  if (config.onValuePlacementCensus) {
4485
4619
  try {
4486
4620
  config.onValuePlacementCensus(censusValuePlacement(container, data));
@@ -4624,6 +4758,7 @@ function createChartHost(container, config) {
4624
4758
  } catch {
4625
4759
  }
4626
4760
  }
4761
+ setClickedMarks(null);
4627
4762
  notify([], "host");
4628
4763
  },
4629
4764
  highlight(rowIdxs) {
@@ -4855,6 +4990,11 @@ export {
4855
4990
  stripJsComments,
4856
4991
  rowIdxsFromMark,
4857
4992
  nextSelection,
4993
+ selectionRuleOf,
4994
+ markKeyOf,
4995
+ sameRowSet,
4996
+ planSelectionPaint,
4997
+ nextClickedMarks,
4858
4998
  isInsideControl,
4859
4999
  createMarkResolver,
4860
5000
  collectD3GlyphCenters,
@@ -4868,6 +5008,9 @@ export {
4868
5008
  MIN_HIT_BAND_PX,
4869
5009
  censusHitBands,
4870
5010
  hitBandFlag,
5011
+ CLIP_TOLERANCE_PX,
5012
+ censusClippedText,
5013
+ clippedTextFlag,
4871
5014
  VALUE_AXIS_MIN_FIT,
4872
5015
  censusValuePlacement,
4873
5016
  valuePlacementFlag,
package/dist/index.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  AXIS_PIN_MIN_LABELS,
11
11
  AXIS_PIN_TRACK_REACH_PX,
12
12
  BACKING_MAJORITY,
13
+ CLIP_TOLERANCE_PX,
13
14
  D3_PLUGIN_PACKAGES,
14
15
  DARK_TEXT,
15
16
  FIT_CONTENT_SELECTOR,
@@ -51,12 +52,14 @@ import {
51
52
  blankRenderFlag,
52
53
  buildRenderPayload,
53
54
  cellSuppressesNormalize,
55
+ censusClippedText,
54
56
  censusColourSpread,
55
57
  censusHitBands,
56
58
  censusMarks,
57
59
  censusValuePlacement,
58
60
  clearGeoCache,
59
61
  clearSelectionGlyphs,
62
+ clippedTextFlag,
60
63
  collectD3GlyphCenters,
61
64
  colourSpreadFlag,
62
65
  compileRenderFn,
@@ -88,10 +91,12 @@ import {
88
91
  labelBand,
89
92
  le,
90
93
  loadGeo,
94
+ markKeyOf,
91
95
  measureContainerBoxes,
92
96
  minContrastOver,
93
97
  needsScroll,
94
98
  newIdScope,
99
+ nextClickedMarks,
95
100
  nextSelection,
96
101
  noopViewStateProvider,
97
102
  o,
@@ -101,6 +106,7 @@ import {
101
106
  pinScrolledAxis,
102
107
  planAxisPin,
103
108
  planFrameGrow,
109
+ planSelectionPaint,
104
110
  qe,
105
111
  registerGeo,
106
112
  registerGeoAsset,
@@ -109,8 +115,10 @@ import {
109
115
  resolveAutoColorScale,
110
116
  resolveOptions,
111
117
  rowIdxsFromMark,
118
+ sameRowSet,
112
119
  scopeChartIds,
113
120
  scrollFitFor,
121
+ selectionRuleOf,
114
122
  sessionViewStateProvider,
115
123
  straddleSampleGrid,
116
124
  straddles,
@@ -122,7 +130,7 @@ import {
122
130
  toSourceRows,
123
131
  unpinScrolledAxis,
124
132
  valuePlacementFlag
125
- } from "./chunk-4FTLE3ZK.mjs";
133
+ } from "./chunk-ZEXMBZHP.mjs";
126
134
  import "./chunk-A2GMXZP7.mjs";
127
135
  import {
128
136
  ACTIVE_TICK_CLASS,
@@ -141,6 +149,7 @@ import {
141
149
  COLOR_SCALE_SELF_CLAMP_PCT_MAX,
142
150
  COLOR_SCALE_SELF_CLAMP_PCT_MIN,
143
151
  CONTAINER_SLOT_ANIM_STOP,
152
+ CONTAINER_SLOT_CLICKED_MARKS,
144
153
  CONTAINER_SLOT_INITIAL_XF_MARK,
145
154
  CONTAINER_SLOT_UI_STATE,
146
155
  CONTAINER_SLOT_XF_CLEAR,
@@ -157,12 +166,15 @@ import {
157
166
  LEGEND_MARK_CLASS,
158
167
  LIFT_SELECTED_CLASS,
159
168
  MARK_CLASS,
169
+ MARK_KEY_ATTR,
160
170
  MARK_SELECTED_CLASS,
161
171
  MAX_MAP_POINTS_DEFAULT,
162
172
  NO_ROW_MARKS_ATTR,
163
173
  ROW_IDX_ATTR,
164
174
  SEASONAL_MARKERS_DEFAULT,
165
175
  SELECTION_ACTIVE_CLASS,
176
+ SELECTION_RULE_ATTR,
177
+ SELECTION_RULE_CLICKED_MARKS,
166
178
  SNAPSHOT_SVG_ATTR,
167
179
  VALUE_AXIS_BASELINE_DEFAULT,
168
180
  VIEW_ONLY_UI_STATE_KEYS,
@@ -170,7 +182,7 @@ import {
170
182
  chartOwnsTimeline,
171
183
  periodTickSuppressesFeedback,
172
184
  withoutViewOnlyKeys
173
- } from "./chunk-WNGJUPEI.mjs";
185
+ } from "./chunk-7JWL6LYE.mjs";
174
186
  import "./chunk-GHODWOAL.mjs";
175
187
 
176
188
  // src/optionsVocabulary.ts
@@ -1328,6 +1340,20 @@ function inlineFilterGate(rowCount) {
1328
1340
  }
1329
1341
  var INLINE_FILTER_MIN_ROWS = 8;
1330
1342
 
1343
+ // src/qualifyTable.ts
1344
+ function qualifyTableRows(rows) {
1345
+ return rows.map((r2) => ({
1346
+ rank: typeof r2.rank === "number" && Number.isFinite(r2.rank) ? r2.rank : null,
1347
+ name: r2.name,
1348
+ preview: r2.isPreview === true,
1349
+ fit: r2.recommended === false ? "can-also-render" : "fits",
1350
+ description: r2.description ?? "",
1351
+ language: r2.language ? r2.language : null,
1352
+ renderer: r2.renderer ? r2.renderer : null,
1353
+ alsoWorthALook: r2.alsoWorthALook === true
1354
+ }));
1355
+ }
1356
+
1331
1357
  // src/sourceDateFormat.ts
1332
1358
  function formatSourceDate(raw, opts = {}) {
1333
1359
  const d2 = toDate(raw);
@@ -2873,10 +2899,12 @@ export {
2873
2899
  BACKING_MAJORITY,
2874
2900
  CHOOSER_MIN_HEIGHT_PX,
2875
2901
  CHOOSER_MIN_WIDTH_PX,
2902
+ CLIP_TOLERANCE_PX,
2876
2903
  COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT,
2877
2904
  COLOR_SCALE_SELF_CLAMP_PCT_MAX,
2878
2905
  COLOR_SCALE_SELF_CLAMP_PCT_MIN,
2879
2906
  CONTAINER_SLOT_ANIM_STOP,
2907
+ CONTAINER_SLOT_CLICKED_MARKS,
2880
2908
  CONTAINER_SLOT_INITIAL_XF_MARK,
2881
2909
  CONTAINER_SLOT_UI_STATE,
2882
2910
  CONTAINER_SLOT_XF_CLEAR,
@@ -2915,6 +2943,7 @@ export {
2915
2943
  LIMIT_COUNT_MAX,
2916
2944
  LIVE_OPTION_KEYS,
2917
2945
  MARK_CLASS,
2946
+ MARK_KEY_ATTR,
2918
2947
  MARK_SELECTED_CLASS,
2919
2948
  MAX_FRAME_GROW_FACTOR,
2920
2949
  MAX_MAP_POINTS_DEFAULT,
@@ -2949,6 +2978,8 @@ export {
2949
2978
  SEASONAL_MARKERS_DEFAULT,
2950
2979
  SELECTION_ACTIVE_CLASS,
2951
2980
  SELECTION_GLYPH_CLASS,
2981
+ SELECTION_RULE_ATTR,
2982
+ SELECTION_RULE_CLICKED_MARKS,
2952
2983
  SNAPSHOT_SVG_ATTR,
2953
2984
  STRADDLE_COLS,
2954
2985
  STRADDLE_MIN_SHARE,
@@ -2981,6 +3012,7 @@ export {
2981
3012
  canShowLastChart,
2982
3013
  captureSvgSnapshot,
2983
3014
  cellSuppressesNormalize,
3015
+ censusClippedText,
2984
3016
  censusColourSpread,
2985
3017
  censusHitBands,
2986
3018
  censusMarks,
@@ -2990,6 +3022,7 @@ export {
2990
3022
  chooserFitsViewport,
2991
3023
  clearGeoCache,
2992
3024
  clearSelectionGlyphs,
3025
+ clippedTextFlag,
2993
3026
  codeReadsDatesInLocalTime,
2994
3027
  collectD3GlyphCenters,
2995
3028
  colourSpreadFlag,
@@ -3051,6 +3084,7 @@ export {
3051
3084
  listNeedsFilter,
3052
3085
  loadGeo,
3053
3086
  markDefaultModel,
3087
+ markKeyOf,
3054
3088
  markerArmedBlind,
3055
3089
  measureContainerBoxes,
3056
3090
  minContrastOver,
@@ -3059,6 +3093,7 @@ export {
3059
3093
  newIdScope,
3060
3094
  newQualifyGroupState,
3061
3095
  newQualifyRefusalGroupState,
3096
+ nextClickedMarks,
3062
3097
  nextSelection,
3063
3098
  noopViewStateProvider,
3064
3099
  normaliseAggregation,
@@ -3077,6 +3112,7 @@ export {
3077
3112
  pinScrolledAxis,
3078
3113
  planAxisPin,
3079
3114
  planFrameGrow,
3115
+ planSelectionPaint,
3080
3116
  planTrivialChart,
3081
3117
  pollGiveUpKeepsMarker,
3082
3118
  productFeedbackSendable,
@@ -3089,6 +3125,7 @@ export {
3089
3125
  qualifyPick,
3090
3126
  qualifyRefusalHeadingFor,
3091
3127
  qualifyRefusalReason,
3128
+ qualifyTableRows,
3092
3129
  rasterizeSvgToPngDataUrl,
3093
3130
  readQualifyChartRow,
3094
3131
  readQualifyRefusalRow,
@@ -3112,8 +3149,10 @@ export {
3112
3149
  resolveOptions,
3113
3150
  rowIdxsFromMark,
3114
3151
  sameMarker,
3152
+ sameRowSet,
3115
3153
  scopeChartIds,
3116
3154
  scrollFitFor,
3155
+ selectionRuleOf,
3117
3156
  servedCorrelationProves,
3118
3157
  sessionViewStateProvider,
3119
3158
  shouldCaptureThumbnail,
package/dist/react.mjs CHANGED
@@ -22,9 +22,9 @@ import {
22
22
  loadGeo,
23
23
  syncMemberSelection,
24
24
  toSourceRows
25
- } from "./chunk-4FTLE3ZK.mjs";
25
+ } from "./chunk-ZEXMBZHP.mjs";
26
26
  import "./chunk-A2GMXZP7.mjs";
27
- import "./chunk-WNGJUPEI.mjs";
27
+ import "./chunk-7JWL6LYE.mjs";
28
28
  import "./chunk-GHODWOAL.mjs";
29
29
 
30
30
  // src/react.tsx
package/dist/testing.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  MARK_CLASS,
5
5
  MARK_SELECTED_CLASS,
6
6
  ROW_IDX_ATTR
7
- } from "./chunk-WNGJUPEI.mjs";
7
+ } from "./chunk-7JWL6LYE.mjs";
8
8
  import "./chunk-GHODWOAL.mjs";
9
9
 
10
10
  // src/testing/interact.ts
@@ -0,0 +1,24 @@
1
+ export interface ClippedText {
2
+ text: string;
3
+ /** Pixels past the frame on each side (0 = inside). */
4
+ top: number;
5
+ left: number;
6
+ bottom: number;
7
+ right: number;
8
+ }
9
+ export interface ClippedTextCensus {
10
+ /** Visible text elements measured. */
11
+ measured: number;
12
+ /** Those with any side more than the tolerance outside the frame, worst first, at most MAX_REPORTED. */
13
+ clipped: ClippedText[];
14
+ /** How many were clipped in all (can exceed clipped.length). */
15
+ clippedCount: number;
16
+ /** False when there was no SVG or no layout (0x0 frame): nothing was measured, and that is not a pass. */
17
+ laidOut: boolean;
18
+ }
19
+ export declare const CLIP_TOLERANCE_PX = 1;
20
+ export declare const MAX_REPORTED = 8;
21
+ /** Counts only visible text whose box leaves the SVG's box. A host without layout (jsdom) reports laidOut false. */
22
+ export declare function censusClippedText(container: any): ClippedTextCensus;
23
+ /** A one-line verdict for a log or a report; "" when nothing is clipped or nothing could be measured. */
24
+ export declare function clippedTextFlag(c: ClippedTextCensus): string;
@@ -1,4 +1,4 @@
1
- export declare const HOST_CONTRACT_VERSION = "1.13.0";
1
+ export declare const HOST_CONTRACT_VERSION = "1.14.0";
2
2
  export type GeoPointPrecision = "latlon" | "city" | "zip3" | "state" | "country";
3
3
  /** Every valid tier, ordered most precise → coarsest. Runtime form of GeoPointPrecision. */
4
4
  export declare const GEO_POINT_PRECISIONS: readonly GeoPointPrecision[];
@@ -13,6 +13,9 @@ export declare const AXIS_FILTER_CLASS = "d3-axis-filter";
13
13
  export declare const LIFT_SELECTED_CLASS = "d3-lift-selected";
14
14
  export declare const CONTROL_CLASS = "lch-control";
15
15
  export declare const ROW_IDX_ATTR = "data-row-idx";
16
+ export declare const SELECTION_RULE_ATTR = "data-lch-sel-rule";
17
+ export declare const SELECTION_RULE_CLICKED_MARKS = "clicked-marks";
18
+ export declare const MARK_KEY_ATTR = "data-mark-key";
16
19
  export declare const NO_ROW_MARKS_ATTR = "data-lch-no-row-marks";
17
20
  export declare const SNAPSHOT_SVG_ATTR = "data-lch-snapshot";
18
21
  export declare const XFILTER_REFRESH_EVENT = "llm-xfilter-refresh";
@@ -20,6 +23,7 @@ export declare const CONTAINER_SLOT_ANIM_STOP = "__llmAnimStop";
20
23
  export declare const CONTAINER_SLOT_XF_CLEAR = "__llmXfClear";
21
24
  export declare const CONTAINER_SLOT_INITIAL_XF_MARK = "__llmInitialXfMark";
22
25
  export declare const CONTAINER_SLOT_UI_STATE = "__lchUiState";
26
+ export declare const CONTAINER_SLOT_CLICKED_MARKS = "__lchClickedMarks";
23
27
  export declare const VIEW_ONLY_UI_STATE_KEYS: readonly string[];
24
28
  /** The bag with every view-only key removed: what a host persists when the reader has not asked
25
29
  * for their view to be remembered. Returns the SAME object when there was nothing to remove. */
@@ -1,8 +1,18 @@
1
1
  import { type RenderOptions, type ViewStateProvider } from "./contract";
2
2
  import { type ResolveOptionsInput } from "./defaults";
3
+ import { type SelectionRule, type SelectionPaintMode } from "./selection";
4
+ /** What onSelectionPaint reports: the declared rule, how this paint decided, and how many marks it lit. */
5
+ export interface SelectionPaintReport {
6
+ rule: SelectionRule;
7
+ mode: SelectionPaintMode;
8
+ lit: number;
9
+ marks: number;
10
+ selection: number;
11
+ }
3
12
  import { type MarkCensus } from "./blankRender";
4
13
  import { type HitBandCensus } from "./hitBands";
5
14
  import { type ColourSpreadCensus } from "./colourSpread";
15
+ import { type ClippedTextCensus } from "./clippedText";
6
16
  import { type ValuePlacementCensus } from "./valuePlacement";
7
17
  import { type FitRenderedChartOptions, type FitRenderedChartResult } from "./fitDom";
8
18
  import { type LabelContrastOptions, type LabelContrastReport } from "./labelContrastDom";
@@ -113,6 +123,12 @@ export interface ChartHostConfig {
113
123
  * own skew is measured on the raw column while the ramp encodes an aggregate.
114
124
  */
115
125
  onColourSpreadCensus?: (census: ColourSpreadCensus) => void;
126
+ /**
127
+ * IS ANY TEXT CUT OFF BY THE FRAME? Runs after every render, counts only, and never moves anything: a title
128
+ * drawn above its own origin is clipped without anything throwing, so only a measurement against the SVG box
129
+ * sees it. A host without layout reports `laidOut: false`, which is not a pass.
130
+ */
131
+ onClippedTextCensus?: (census: ClippedTextCensus) => void;
116
132
  /**
117
133
  * IS EACH DOT DRAWN AT ITS VALUE? Runs after every render, counts only.
118
134
  *
@@ -124,6 +140,13 @@ export interface ChartHostConfig {
124
140
  * a radius off it. It declines on anything that is not that shape.
125
141
  */
126
142
  onValuePlacementCensus?: (census: ValuePlacementCensus) => void;
143
+ /**
144
+ * HOW A DECLARED SELECTION RULE LIT THE MARKS. Called on every selection paint of a chart that
145
+ * declares a rule other than the default (SELECTION_RULE_ATTR), never for one that declares none.
146
+ * `mode` "fallback-any-row" means no clicked or exactly-matching mark was found for the selection,
147
+ * so the default rule lit the chart for that paint: worth a log line.
148
+ */
149
+ onSelectionPaint?: (report: SelectionPaintReport) => void;
127
150
  /** This chart declares time keyframes: frame one is allowed to be empty, so no blank verdict
128
151
  * is issued for it. */
129
152
  animated?: boolean;
@@ -1,4 +1,4 @@
1
- export { HOST_CONTRACT_VERSION, GEO_POINT_PRECISIONS, MARK_CLASS, LEGEND_MARK_CLASS, AXIS_FILTER_CLASS, ROW_IDX_ATTR, LIFT_SELECTED_CLASS, CONTROL_CLASS, NO_ROW_MARKS_ATTR, SNAPSHOT_SVG_ATTR, XFILTER_REFRESH_EVENT, CONTAINER_SLOT_ANIM_STOP, CONTAINER_SLOT_XF_CLEAR, CONTAINER_SLOT_INITIAL_XF_MARK, CONTAINER_SLOT_UI_STATE, VIEW_ONLY_UI_STATE_KEYS, withoutViewOnlyKeys, HOST_CONTAINER_CLASS, SELECTION_ACTIVE_CLASS, MARK_SELECTED_CLASS, ACTIVE_TICK_CLASS, DIM_OPACITY_VAR, DIM_OPACITY_DEFAULT, chartOwnsTimeline, periodTickSuppressesFeedback, ANIM_PLAY_SPEED_DEFAULT, ANIM_PLAY_SPEED_MIN, ANIM_PLAY_SPEED_MAX, ANIM_LOOP_DELAY_DEFAULT, ANIM_LOOP_DELAY_MIN, ANIM_MAX_IDEAL_FRAMES_DEFAULT, ANIM_MAX_IDEAL_FRAMES_MIN, ANIM_MAX_IDEAL_FRAMES_MAX, ANIMATION_OPTION_KEYS, COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT, COLOR_SCALE_SELF_CLAMP_PCT_MIN, COLOR_SCALE_SELF_CLAMP_PCT_MAX, FLIP_MODE_DEFAULT, FLIP_INTERVAL_MIN_MS, FLIP_INTERVAL_MAX_MS, FLIP_DIRECTION_DEFAULT, APPROXIMATE_POSITIONS_DEFAULT, VALUE_AXIS_BASELINE_DEFAULT, SEASONAL_MARKERS_DEFAULT, MAX_MAP_POINTS_DEFAULT, type GeoPointPrecision, type GeoMapKind, type TimelineStyle, type FlipMode, type FlipDirection, type ApproximatePositions, type ValueAxisBaseline, type SeasonalMarkers, type ColorScaleScope, type RenderOptions, type ViewStateProvider, type ChartControlChange, } from "./contract";
1
+ export { HOST_CONTRACT_VERSION, GEO_POINT_PRECISIONS, MARK_CLASS, LEGEND_MARK_CLASS, AXIS_FILTER_CLASS, ROW_IDX_ATTR, LIFT_SELECTED_CLASS, CONTROL_CLASS, NO_ROW_MARKS_ATTR, SELECTION_RULE_ATTR, SELECTION_RULE_CLICKED_MARKS, MARK_KEY_ATTR, CONTAINER_SLOT_CLICKED_MARKS, SNAPSHOT_SVG_ATTR, XFILTER_REFRESH_EVENT, CONTAINER_SLOT_ANIM_STOP, CONTAINER_SLOT_XF_CLEAR, CONTAINER_SLOT_INITIAL_XF_MARK, CONTAINER_SLOT_UI_STATE, VIEW_ONLY_UI_STATE_KEYS, withoutViewOnlyKeys, HOST_CONTAINER_CLASS, SELECTION_ACTIVE_CLASS, MARK_SELECTED_CLASS, ACTIVE_TICK_CLASS, DIM_OPACITY_VAR, DIM_OPACITY_DEFAULT, chartOwnsTimeline, periodTickSuppressesFeedback, ANIM_PLAY_SPEED_DEFAULT, ANIM_PLAY_SPEED_MIN, ANIM_PLAY_SPEED_MAX, ANIM_LOOP_DELAY_DEFAULT, ANIM_LOOP_DELAY_MIN, ANIM_MAX_IDEAL_FRAMES_DEFAULT, ANIM_MAX_IDEAL_FRAMES_MIN, ANIM_MAX_IDEAL_FRAMES_MAX, ANIMATION_OPTION_KEYS, COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT, COLOR_SCALE_SELF_CLAMP_PCT_MIN, COLOR_SCALE_SELF_CLAMP_PCT_MAX, FLIP_MODE_DEFAULT, FLIP_INTERVAL_MIN_MS, FLIP_INTERVAL_MAX_MS, FLIP_DIRECTION_DEFAULT, APPROXIMATE_POSITIONS_DEFAULT, VALUE_AXIS_BASELINE_DEFAULT, SEASONAL_MARKERS_DEFAULT, MAX_MAP_POINTS_DEFAULT, type GeoPointPrecision, type GeoMapKind, type TimelineStyle, type FlipMode, type FlipDirection, type ApproximatePositions, type ValueAxisBaseline, type SeasonalMarkers, type ColorScaleScope, type RenderOptions, type ViewStateProvider, type ChartControlChange, } from "./contract";
2
2
  export { resolveOptions, type ResolveOptionsInput } from "./defaults";
3
3
  export { OPTIONS_VOCABULARY, LIVE_OPTION_KEYS, MAX_MAP_POINTS_MIN, MAX_MAP_POINTS_MAX, LIMIT_COUNT_MAX, wireLimitCount, type OptionVocabularyEntry, type OptionTiming, type OptionWire, type OptionKey, } from "./optionsVocabulary";
4
4
  export { loadGeo, geoFromCache, registerGeo, registerGeoAsset, geoAssetFor, clearGeoCache, type GeoAssetName, } from "./geoLazy";
@@ -7,13 +7,13 @@ export { loadGeo, geoFromCache, registerGeo, registerGeoAsset, geoAssetFor, clea
7
7
  export declare function registerCityTable(packed: string): void;
8
8
  export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./payload";
9
9
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
10
- export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn } from "./host";
10
+ export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn, type SelectionPaintReport } from "./host";
11
11
  export { ANNOTATION_CLASS, ANNOTATION_LAYER_CLASS, type MarkAnnotation, type AnnotationReport } from "./annotations";
12
12
  export type { MarkNoteLike, NoteBadgeOptions } from "./markNotes";
13
13
  export type { Controls, ControlInfo, ControlsAttachment } from "./controls";
14
14
  export { createChartGroup, toSourceRows, syncMemberSelection, type ChartGroup, type ChartGroupMember, type ChartGroupSelection, type ChartGroupChange, type ChartGroupMemberOptions, type ChartGroupSourceOptions, type ChartGroupPayload } from "./group";
15
15
  export type { Filter, FilterScope, FilterOptions, FilterKey, FilterChangeReason, FilterBinding, BindFilterOptions, VegaInteractionEvent, PageView } from "./filterScope";
16
- export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
16
+ export { createMarkResolver, isInsideControl, rowIdxsFromMark, nextSelection, selectionRuleOf, markKeyOf, planSelectionPaint, nextClickedMarks, sameRowSet, type SelectionRule, type ClickedMarks, type SelectionPaintMark, type SelectionPaintMode, type SelectionPaintPlan, collectD3GlyphCenters, clearSelectionGlyphs, paintSelectionGlyphs, SELECTION_GLYPH_CLASS, type MarkResolver, type MarkResolverEnv, type GlyphCenter, type SelectionGlyphStyle, type SelectionModifiers, } from "./selection";
17
17
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
18
18
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, tableHostOf, chartSvgOf, htmlToSvgDataUrl, htmlNaturalSize, type SnapshotOptions } from "./snapshot";
19
19
  export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
@@ -22,6 +22,7 @@ export { mountProductFeedbackBox, normaliseProductFeedbackText, productFeedbackS
22
22
  export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, type QualifyGroupState, type QualifyGroupHeading, orderRefusalsForDisplay, refusalIsSelectable, hasRefusalsToShow, qualifyRefusalHeadingFor, newQualifyRefusalGroupState, qualifyRefusalReason, QUALIFY_REFUSAL_UNSPECIFIED, type QualifyRefusalRow, type QualifyRefusalGroupState, type QualifyRefusalHeading, } from "./qualifyGroups";
23
23
  export { qualifyPick, qualifyAuto, qualifyCancel, launchGenerates, launchFavorStyle, chooserFitsViewport, shouldOpenChooserOnGenerate, shouldOpenInlineChooserOnGenerate, canConfirmLaunch, confirmLaunch, qualifyFailureFallsOpen, CHOOSER_MIN_WIDTH_PX, CHOOSER_MIN_HEIGHT_PX, type QualifyLaunchOutcome, type ChooserGateInput, } from "./qualifyLaunch";
24
24
  export { normalizeFilterTerm, filterQualifyRows, readQualifyChartRow, readQualifyRefusalRow, DEFAULT_FILTER_MATCH_STYLE, filterMatchStyleOf, type FilterMatchStyle, filterFitsChooser, listNeedsFilter, qualifyFilterGate, inlineFilterGate, FILTER_MIN_TERM_CHARS, FILTER_ROW_PX, FILTER_MIN_WIDTH_PX, FILTER_MIN_HEIGHT_PX, INLINE_FILTER_MIN_ROWS, computeQualifyFilterView, qualifyFilterCountText, type QualifyFilterTier, type QualifyFilterResult, type QualifyFilterRead, type QualifyFilterGateReason, type QualifyFilterGateInput, type QualifyFilterRow, type QualifyFilterGroup, type QualifyFilterView, type QualifyFilterNoteKind, } from "./qualifyFilter";
25
+ export { qualifyTableRows, type QualifyTableRow, type QualifyTableSource } from "./qualifyTable";
25
26
  export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
26
27
  export { formatSourceDate, formatSourceDateFor, isExcelDateFormat, type SourceFormats, type SourceFormatDialect, type SourceDateOptions, } from "./sourceDateFormat";
27
28
  export { composeSummaryText } from "./summaryText";
@@ -32,6 +33,7 @@ export { decideLabelColor, toRGBA, compositeOver, relativeLuminance, contrastRat
32
33
  export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
33
34
  export { isInvalidSentinelError, invalidSentinelReason } from "./invalidSentinel";
34
35
  export { censusHitBands, hitBandFlag, MIN_HIT_BAND_PX, type HitBandCensus } from "./hitBands";
36
+ export { censusClippedText, clippedTextFlag, CLIP_TOLERANCE_PX, type ClippedTextCensus, type ClippedText } from "./clippedText";
35
37
  export { censusColourSpread, colourSpreadFlag, SAME_SHADE_DELTA_E, MIN_RAMP_FILLS, type ColourSpreadCensus } from "./colourSpread";
36
38
  export { censusValuePlacement, valuePlacementFlag, VALUE_AXIS_MIN_FIT, type ValuePlacementCensus } from "./valuePlacement";
37
39
  export { SCROLL_SLACK_PX, MAX_FRAME_GROW_FACTOR, PHANTOM_FRACTION, FIT_CONTENT_SELECTOR, needsScroll, contentExtentOf, scrollFitFor, planFrameGrow, isPhantomBox, type MeasuredBox, type ContentExtent, type ScrollFit, type FrameGrowPlan, } from "./fit";
@@ -0,0 +1,27 @@
1
+ /** What a what-fits row must carry to become a table row (the shape-core parse result satisfies it). */
2
+ export interface QualifyTableSource {
3
+ name: string;
4
+ description?: string | null;
5
+ rank?: number | null;
6
+ language?: string | null;
7
+ renderer?: string | null;
8
+ recommended?: boolean | null;
9
+ isPreview?: boolean | null;
10
+ alsoWorthALook?: boolean | null;
11
+ }
12
+ export interface QualifyTableRow {
13
+ /** The position in the order asked for, or null for a row the server did not rank. */
14
+ rank: number | null;
15
+ name: string;
16
+ /** True for a newer, less proven type (still selectable by name). */
17
+ preview: boolean;
18
+ /** "fits" = the ontology judges it a fit for this data; "can-also-render" = it can draw this, but it is a weaker fit. */
19
+ fit: "fits" | "can-also-render";
20
+ description: string;
21
+ language: string | null;
22
+ renderer: string | null;
23
+ /** One of the "something different" picks drawn from below the top fits. */
24
+ alsoWorthALook: boolean;
25
+ }
26
+ /** The rows as table rows, in the order given (the server's order IS the ranking). */
27
+ export declare function qualifyTableRows(rows: readonly QualifyTableSource[]): QualifyTableRow[];
@@ -25,6 +25,63 @@ export interface SelectionModifiers {
25
25
  * clear from elsewhere, a re-derived selection), and the next click on the mark then did nothing.
26
26
  */
27
27
  export declare function nextSelection(current: Iterable<number> | null | undefined, rowIdxs: readonly number[], mods?: SelectionModifiers): number[];
28
+ /**
29
+ * HOW A SELECTION LIGHTS A CHART'S OWN MARKS (see SELECTION_RULE_ATTR in contract.ts).
30
+ * "any-row": a mark is lit when any of its rows is selected - the default, and what every chart
31
+ * that declares nothing gets. "clicked-marks": the marks the reader clicked are lit and every other
32
+ * mark is dimmed - for a chart whose marks share rows, where any-row lights most of the chart.
33
+ */
34
+ export type SelectionRule = "any-row" | "clicked-marks";
35
+ /** The rule the chart in `container` declared, read off any element it drew; "any-row" when none. */
36
+ export declare function selectionRuleOf(container: Element | null | undefined): SelectionRule;
37
+ /**
38
+ * A mark's identity under "clicked-marks": its MARK_KEY_ATTR when it carries one, else its
39
+ * data-row-idx list (prefixed, so a key can never be mistaken for a row list).
40
+ */
41
+ export declare function markKeyOf(el: Element | null | undefined): string;
42
+ /** The marks the reader clicked under "clicked-marks", and the selection those clicks left. */
43
+ export interface ClickedMarks {
44
+ keys: string[];
45
+ selection: number[];
46
+ }
47
+ /** One mark as the paint plan sees it. */
48
+ export interface SelectionPaintMark {
49
+ key: string;
50
+ rows: readonly number[];
51
+ }
52
+ /**
53
+ * Which marks a paint lit, and how it decided.
54
+ * none - nothing is selected;
55
+ * any-row - the default rule;
56
+ * clicked - the clicked marks (their keys were recorded for this very selection);
57
+ * exact - no click recorded for this selection: the marks whose rows equal it;
58
+ * fallback-any-row - neither found a mark, so any-row decided this paint. A host logs it.
59
+ */
60
+ export type SelectionPaintMode = "none" | "any-row" | "clicked" | "exact" | "fallback-any-row";
61
+ export interface SelectionPaintPlan {
62
+ /** One flag per mark, in the order given: true when the mark is drawn selected. */
63
+ on: boolean[];
64
+ mode: SelectionPaintMode;
65
+ /** The keys of the marks lit by "clicked" or "exact": the clicked set to keep. Empty otherwise. */
66
+ keys: string[];
67
+ }
68
+ /** True when the two hold the same rows, ignoring order and repeats. */
69
+ export declare function sameRowSet(a: Iterable<number> | null | undefined, b: Iterable<number> | null | undefined): boolean;
70
+ /**
71
+ * THE ONE PAINT RULE every host applies to its marks: which of `marks` are drawn selected under
72
+ * `selection`. Under "any-row" a mark is lit when any of its rows is selected. Under
73
+ * "clicked-marks" it is lit when its key is in `clicked` - but only when `clicked` was recorded for
74
+ * this same selection, because a selection changed from elsewhere makes those keys stale; failing
75
+ * that, the marks whose rows equal the selection; failing that, any-row, reported as a fallback.
76
+ */
77
+ export declare function planSelectionPaint(marks: readonly SelectionPaintMark[], selection: Iterable<number> | null | undefined, rule: SelectionRule, clicked: ClickedMarks | null | undefined): SelectionPaintPlan;
78
+ /**
79
+ * The clicked set a click leaves under "clicked-marks". `prev` is the set held before the click and
80
+ * `current` the selection then; `next` is the selection the click leaves (nextSelection's answer).
81
+ * A plain click makes the clicked mark the whole set; Ctrl/Cmd/Shift adds it or takes it off. A held
82
+ * set recorded for a different selection is stale and starts over. An empty selection holds no set.
83
+ */
84
+ export declare function nextClickedMarks(prev: ClickedMarks | null | undefined, current: Iterable<number> | null | undefined, key: string, next: readonly number[], mods?: SelectionModifiers): ClickedMarks | null;
28
85
  /**
29
86
  * True when a click's target sits inside a reader-operated control the chart drew (CONTROL_CLASS
30
87
  * on the target or an ancestor), looking no further out than `container` - a page that wraps the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.74",
3
+ "version": "0.6.76",
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",