@bicharts/chart-host 0.6.113 → 0.6.116

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.
Files changed (40) hide show
  1. package/README.md +94 -5
  2. package/dist/adapters.mjs +3 -3
  3. package/dist/card.mjs +2 -2
  4. package/dist/category-colours.mjs +275 -0
  5. package/dist/{chunk-7XT5OJ6M.mjs → chunk-63IUMWY2.mjs} +40 -3
  6. package/dist/{chunk-AEO5SFEQ.mjs → chunk-EFKGSDFF.mjs} +1 -1
  7. package/dist/{chunk-TE54AFRN.mjs → chunk-GZSYTDFN.mjs} +531 -393
  8. package/dist/{chunk-PELRAFYZ.mjs → chunk-IW6BKFF5.mjs} +1 -1
  9. package/dist/{chunk-OLCUKXKL.mjs → chunk-JMLSNV5Q.mjs} +1 -1
  10. package/dist/chunk-JTZTTJJK.mjs +244 -0
  11. package/dist/{chunk-FIG5DIW7.mjs → chunk-K2TH7D5V.mjs} +335 -335
  12. package/dist/{chunk-KY4FI2HA.mjs → chunk-QG5TENI3.mjs} +5 -1
  13. package/dist/{chunk-KGVNZR23.mjs → chunk-RLOIA7QK.mjs} +1 -1
  14. package/dist/{chunk-IL5M7K6Z.mjs → chunk-TUZCM7SX.mjs} +2 -2
  15. package/dist/{chunk-GTJG3SMD.mjs → chunk-VGJH63LZ.mjs} +1 -1
  16. package/dist/{chunk-2KLFIGTQ.mjs → chunk-ZZBX5ZJ6.mjs} +145 -18
  17. package/dist/dax.mjs +23 -1
  18. package/dist/index.mjs +97 -16
  19. package/dist/legend-fit.mjs +3 -3
  20. package/dist/page.mjs +7 -5
  21. package/dist/provinces.mjs +4 -4
  22. package/dist/react.mjs +18 -14
  23. package/dist/testing.mjs +1 -1
  24. package/dist/types/categoryColours.d.ts +71 -0
  25. package/dist/types/contract.d.ts +16 -3
  26. package/dist/types/dax.d.ts +26 -0
  27. package/dist/types/filterScope.d.ts +52 -4
  28. package/dist/types/fit.d.ts +6 -1
  29. package/dist/types/fitDom.d.ts +13 -0
  30. package/dist/types/host.d.ts +19 -5
  31. package/dist/types/index.d.ts +7 -4
  32. package/dist/types/keyboardMarks.d.ts +1 -1
  33. package/dist/types/markFollower.d.ts +21 -0
  34. package/dist/types/page.d.ts +1 -1
  35. package/dist/types/qualifyTable.d.ts +52 -0
  36. package/dist/types/react.d.ts +11 -3
  37. package/dist/types/selectionMarker.d.ts +92 -0
  38. package/dist/zoom-memory.mjs +1 -1
  39. package/package.json +6 -1
  40. /package/dist/{chunk-5YSZABCS.mjs → chunk-LXTW6YII.mjs} +0 -0
package/README.md CHANGED
@@ -113,6 +113,68 @@ of the affordance, they are re-applied on every render (so a manual `classList`
113
113
  the next data change), and getting row indices right across a filtered payload is the hazard
114
114
  above.
115
115
 
116
+ ### A panel whose query takes the page filter
117
+
118
+ A chart whose DAX query is filtered by a page filter (`filterQuery(dax, treatAs(filter, "Table[Column]"))` from
119
+ `@bicharts/chart-host/dax`) gets rows that are already narrowed, so it can't see the filter. `scopedBy` tells it,
120
+ and its titles say so ("North America · United States only"):
121
+
122
+ ```tsx
123
+ <BicChart id="kpis" code={kpiCode} d3={d3} data={kpiRows} scopedBy={country} />
124
+ ```
125
+
126
+ `treatAs` maps the key columns it's given, in the filter's order, so a query can take only part of a compound key.
127
+ `treatAs(cell, "Accounts[Region]")` on a Region × Stage pick filters by the region alone, and a stage-label pick
128
+ (any region) doesn't filter that query at all. Name the key columns the query takes, so the scope line names only
129
+ those:
130
+
131
+ ```tsx
132
+ <BicChart id="cases" code={casesCode} d3={d3} data={caseRows} scopedBy={{ filter: cell, takes: ["Region"] }} />
133
+ ```
134
+
135
+ A cell pick then reads "Europe", not "Europe → 3-Propose", and a pick whose taken parts are all "any" adds
136
+ nothing to the line, since that query keeps every row. A bare filter, `{ filter }` or a `takes` that names every
137
+ key column says the whole key. `label` sets what the line calls the filter. Outside React, `filterScopeOf` from
138
+ `@bicharts/chart-host/page` takes the same list and returns `options.filterScope`.
139
+
140
+ ### A query that feeds no chart
141
+
142
+ A form's choice list, a lookup or a count beside a chart reads its query's rows by column name. A DAX result names
143
+ its columns `Table[Column]` and its measures `[Measure]`; `queryRows` from `@bicharts/chart-host/dax` returns each row
144
+ keyed by the bare name. Never parse the headers by hand: a pattern written through a shell and a script can lose its
145
+ backslashes on the way to disk and leave every value blank while the app's tests pass.
146
+
147
+ ```tsx
148
+ const owners = queryRows(useSemanticModelQuery({ connection, query: OWNERS_DAX }).data);
149
+ owners.map(r => r["Sales owner"]); // Owners[Sales owner]
150
+ ```
151
+
152
+ Two columns with one bare name (`Accounts[Name]` and `Owners[Name]`) keep their whole headers. `columnName(header)`
153
+ gives one header's bare name.
154
+
155
+ ### Several panels that pick the same key
156
+
157
+ A map, a table and a scatter that each pick a country are ONE pick. Give their filters one `group`, and keep each
158
+ panel's query taking every member but its own:
159
+
160
+ ```tsx
161
+ const fromMap = useBicFilter("Country", { label: "Country (map)", group: "country" });
162
+ const fromTable = useBicFilter("Country", { label: "Country (table)", group: "country" });
163
+ const mapQuery = filterQuery(MAP_DAX, treatAs(fromTable, "Geo[Country]"));
164
+ const tableQuery = filterQuery(TABLE_DAX, treatAs(fromMap, "Geo[Country]"));
165
+ // <MapChart selects={fromMap} scopedBy={[fromTable]} /> <TableChart selects={fromTable} scopedBy={[fromMap]} />
166
+ ```
167
+
168
+ A pick in one member clears the others, so the most recent pick is the page's one pick, and only the member holding
169
+ it is active: one chip, one scope line. Every member's chart shows the pick, so clicking the picked mark again in
170
+ whichever panel shows it clears it, and the panel the reader picked in keeps all its marks. A list beside them (the
171
+ open items for the picked countries) reads the members' keys, which are that one pick. Without `group` the filters
172
+ are separate picks, ANDed: a country picked on the map and then in the table is held twice, and clicking it again on
173
+ the map leaves the page filtered by the table's. Outside React, `scope.filter(column, { group })` does the same.
174
+ A saved view or a link (`useBicUrlState`) names each member on its own (`Country`, `Country-2`, `Country-3`, in the
175
+ order the page makes them), so a lasso saved on the scatter comes back on the scatter; give a member `key` for a name
176
+ that doesn't depend on that order.
177
+
116
178
  ## Vanilla
117
179
 
118
180
  ```js
@@ -287,8 +349,34 @@ the host's own write cannot be told apart from it.
287
349
  createChartHost(el, { code, data, options: { dimOpacity: 0.15 } });
288
350
  ```
289
351
 
290
- **A glyph on each selected mark.** `options.selectionGlyph` (`"dot"` or `"check"`; absent or
291
- `"none"` draws nothing, the default) puts a small glyph on every selected mark, in a layer the host
352
+ **The selection marker: an outline or a glyph on each selected mark.** The dim alone fails where a
353
+ selected mark's colour sits close to the canvas: a diverging ramp's centre is a neutral just off the
354
+ canvas, and the dim pulls every other mark toward it too, so a selected region near the centre reads
355
+ the same as the dimmed ones. Two options choose what marks the selection beyond the dim:
356
+
357
+ - `options.selectionGlyph`, the *selection marker* on every chart that isn't a region map (the key
358
+ keeps its first name): `"auto"` (the default), `"none"`, `"dot"`, `"check"` or `"outline"`.
359
+ `"auto"` is an outline on a chart that declares a neutral-centre ramp (the shared value ramp's
360
+ diverging legend, `.llm-value-ramp[data-kind="diverging"]`) and nothing on any other chart, which
361
+ draws exactly as it did before.
362
+ - `options.mapSelectionGlyph`, the marker on a *region map* (a chart whose rows carry the host's
363
+ `__geoIso__` join column: a choropleth, a cartogram, a province map): `"outline"` (the default),
364
+ `"none"`, `"dot"` or `"check"`. A point map places its rows by coordinates and has no join column,
365
+ so it takes `selectionGlyph`.
366
+
367
+ An outline (`.lch-sel-outline`, in the same host layer as the glyphs) is each selected AREA mark's own
368
+ shape (a filled path, rect or polygon) stroked 2px in the chart's text colour (`themeFg`, else the
369
+ colour the container's text inherits) over a halo of its canvas (`themeBg`). It's drawn over the chart,
370
+ so a border shared with a neighbour isn't painted over, and the marks themselves are neither restyled
371
+ nor moved: the keyboard's order, a chart's transitions and what a click hits are unchanged. The chart's own
372
+ controls (every element it declares `lch-control`, such as a map's zoom pad) are holes in the outline (`controlBoxesIn`, a mask): a
373
+ region that runs under the pad is outlined up to it, never across the buttons. A line or a
374
+ point keeps its geometry and gets no outline. `resolveOptions` gives the whole choice as
375
+ `selectionMarker` and `mapSelectionMarker`; `selectionMarkerFor` and the traits behind it
376
+ (`isRegionMapData`, `hasNeutralCentreRamp`) and the painter (`collectOutlineMarks`,
377
+ `paintSelectionOutlines`) are exported for a host that draws its own layer.
378
+
379
+ A glyph (`"dot"` or `"check"`) is a small mark on every selected mark, in a layer the host
292
380
  owns inside the container (`.bic-selection-glyphs`, each glyph `.lch-sel-glyph`, never hit-tested).
293
381
  It is painted after every selection change and, on a redraw, after the layout pass and the fit, so
294
382
  nothing measures it as part of the chart. Only a mark the reader can see gets one: a mark hidden by
@@ -311,13 +399,14 @@ every point. The generate answer says so (`suppressSelectionGlyph`, read by shap
311
399
  `parseGenerateResponse`); pass it as `options.suppressSelectionGlyph`, or leave it in the payload's
312
400
  `meta` (`data.meta.suppressSelectionGlyph`) and the host promotes it, the way it promotes
313
401
  `geoPoint`. Keep it with the chart's code, so a redraw from a cache honors it too.
314
- `onSelectionGlyphs` reports what each paint did (`{ mode, drawn, total, suppressed }`), and is
315
- called only when a glyph mode is set:
402
+ The same fact keeps an outline off such a chart. `onSelectionGlyphs` reports what each paint did
403
+ (`{ mode, drawn, total, suppressed }`, `mode` one of `"dot"`, `"check"` or `"outline"`), and is called
404
+ only when the chart's marker is one of those:
316
405
 
317
406
  ```js
318
407
  createChartHost(el, {
319
408
  code, data,
320
- options: { selectionGlyph: "dot", suppressSelectionGlyph: answer.suppressSelectionGlyph },
409
+ options: { selectionGlyph: "dot", mapSelectionGlyph: "outline", suppressSelectionGlyph: answer.suppressSelectionGlyph },
321
410
  onSelectionGlyphs: r => console.debug("glyphs", r),
322
411
  });
323
412
  ```
package/dist/adapters.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  censusHitBands,
4
4
  ensureCrossfilterHitTargets,
5
5
  hitBandFlag
6
- } from "./chunk-AEO5SFEQ.mjs";
6
+ } from "./chunk-EFKGSDFF.mjs";
7
7
  import {
8
8
  clickWasDrag,
9
9
  collectD3GlyphCenters,
@@ -12,13 +12,13 @@ import {
12
12
  markKeyOf,
13
13
  rowIdxsFromMark,
14
14
  selectionRuleOf
15
- } from "./chunk-KGVNZR23.mjs";
15
+ } from "./chunk-RLOIA7QK.mjs";
16
16
  import {
17
17
  AXIS_FILTER_CLASS,
18
18
  LEGEND_MARK_CLASS,
19
19
  MARK_CLASS,
20
20
  ROW_IDX_ATTR
21
- } from "./chunk-KY4FI2HA.mjs";
21
+ } from "./chunk-QG5TENI3.mjs";
22
22
  import "./chunk-GHODWOAL.mjs";
23
23
 
24
24
  // src/adapters/types.ts
package/dist/card.mjs CHANGED
@@ -1,8 +1,8 @@
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
  import {
3
3
  computeSelectionCard
4
- } from "./chunk-OLCUKXKL.mjs";
5
- import "./chunk-FIG5DIW7.mjs";
4
+ } from "./chunk-JMLSNV5Q.mjs";
5
+ import "./chunk-K2TH7D5V.mjs";
6
6
  import "./chunk-GHODWOAL.mjs";
7
7
 
8
8
  // src/selectionCardChrome.ts
@@ -0,0 +1,275 @@
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
+ MIN_RAMP_FILLS,
4
+ RAMP_SLACK_MAX,
5
+ deltaE2000,
6
+ hexToLab,
7
+ rgbToLab
8
+ } from "./chunk-JTZTTJJK.mjs";
9
+ import {
10
+ CATEGORY_DELTA_E_FLOOR,
11
+ DIM_OPACITY_DEFAULT,
12
+ LEGEND_MARK_CLASS,
13
+ MARK_CLASS,
14
+ ROW_IDX_ATTR
15
+ } from "./chunk-QG5TENI3.mjs";
16
+ import "./chunk-GHODWOAL.mjs";
17
+
18
+ // src/categoryColours.ts
19
+ var MAX_CATEGORY_SLOTS = 24;
20
+ var RAMP_DECLINE_MIN_SLOTS = 6;
21
+ var ELEMENT_CAP = 5e3;
22
+ var MAX_LISTED_PAIRS = 10;
23
+ function emptyCensus(declined, dimOpacity, slots = 0, source = "") {
24
+ return {
25
+ source,
26
+ declined,
27
+ slots,
28
+ closestPair: [],
29
+ closestDeltaE: null,
30
+ closestDeltaEPainted: null,
31
+ closestDeltaEDimmed: null,
32
+ dimOpacity,
33
+ rampLike: false,
34
+ legendMatchesMarks: null,
35
+ floor: CATEGORY_DELTA_E_FLOOR,
36
+ belowFloor: null,
37
+ pairsBelowFloorCount: 0,
38
+ pairsBelowFloor: [],
39
+ closestPairPainted: [],
40
+ closestDeltaEDeutan: null,
41
+ closestDeltaEProtan: null
42
+ };
43
+ }
44
+ function styleOf(el, doc) {
45
+ try {
46
+ const view = doc?.defaultView;
47
+ return view?.getComputedStyle ? view.getComputedStyle(el) : null;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function parsePaint(raw) {
53
+ if (raw === null || raw === void 0) return null;
54
+ const s = String(raw).trim().toLowerCase();
55
+ if (!s || s === "none" || s === "transparent" || s.startsWith("url(")) return null;
56
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(s);
57
+ if (hex) {
58
+ const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
59
+ return { rgb: [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)], alpha: 1 };
60
+ }
61
+ const m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*(?:[,/]\s*([\d.]+%?)\s*)?\)$/.exec(s);
62
+ if (!m) return null;
63
+ let alpha = 1;
64
+ if (m[4] !== void 0) alpha = m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
65
+ if (!(alpha > 0)) return null;
66
+ return { rgb: [Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3])], alpha: Math.min(1, alpha) };
67
+ }
68
+ function numberOr1(v) {
69
+ if (v === null || v === void 0 || String(v).trim() === "") return 1;
70
+ const n = parseFloat(String(v));
71
+ return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1;
72
+ }
73
+ function ownOpacity(el, container) {
74
+ let a = 1;
75
+ for (let n = el; n && n !== container; n = n.parentNode) {
76
+ if (typeof n.getAttribute !== "function") break;
77
+ a *= numberOr1(n.getAttribute("opacity"));
78
+ a *= numberOr1(n.style?.opacity);
79
+ }
80
+ return a;
81
+ }
82
+ function paintOf(el, doc) {
83
+ const cs = styleOf(el, doc);
84
+ const fill = parsePaint(firstSet(el.getAttribute?.("fill"), el.style?.fill, cs?.fill));
85
+ if (fill) return { rgb: fill.rgb, alpha: fill.alpha * numberOr1(firstSet(el.getAttribute?.("fill-opacity"), el.style?.fillOpacity)) };
86
+ const stroke = parsePaint(firstSet(el.getAttribute?.("stroke"), el.style?.stroke, cs?.stroke));
87
+ if (stroke) return { rgb: stroke.rgb, alpha: stroke.alpha * numberOr1(firstSet(el.getAttribute?.("stroke-opacity"), el.style?.strokeOpacity)) };
88
+ return null;
89
+ }
90
+ function firstSet(...vals) {
91
+ for (const v of vals) if (v !== null && v !== void 0 && String(v).trim() !== "") return String(v);
92
+ return null;
93
+ }
94
+ function paintWithin(el, doc, container) {
95
+ let p = paintOf(el, doc);
96
+ let at = el;
97
+ if (!p && typeof el.querySelectorAll === "function") {
98
+ for (const child of Array.from(el.querySelectorAll("*"))) {
99
+ p = paintOf(child, doc);
100
+ if (p) {
101
+ at = child;
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ return p ? { rgb: p.rgb, alpha: p.alpha * ownOpacity(at, container) } : null;
107
+ }
108
+ var hexOf = (rgb) => "#" + rgb.map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
109
+ function blend(rgb, alpha, bg) {
110
+ const a = Math.max(0, Math.min(1, alpha));
111
+ return [rgb[0] * a + bg[0] * (1 - a), rgb[1] * a + bg[1] * (1 - a), rgb[2] * a + bg[2] * (1 - a)].map(Math.round);
112
+ }
113
+ function slotsOf(paints) {
114
+ const byColour = /* @__PURE__ */ new Map();
115
+ for (const p of paints) {
116
+ const k = p.rgb.join(",");
117
+ const slot = byColour.get(k) ?? { rgb: p.rgb, alphas: /* @__PURE__ */ new Map() };
118
+ const a = Math.round(p.alpha * 100) / 100;
119
+ slot.alphas.set(a, (slot.alphas.get(a) ?? 0) + 1);
120
+ byColour.set(k, slot);
121
+ }
122
+ return [...byColour.values()].map((s) => ({
123
+ rgb: s.rgb,
124
+ alpha: [...s.alphas].sort((x, y) => y[1] - x[1] || y[0] - x[0])[0][0]
125
+ }));
126
+ }
127
+ var MACHADO_DEUTAN = [[0.367322, 0.860646, -0.227968], [0.280085, 0.672501, 0.047413], [-0.01182, 0.04294, 0.968881]];
128
+ var MACHADO_PROTAN = [[0.152286, 1.052583, -0.204868], [0.114503, 0.786281, 0.099216], [-3882e-6, -0.048116, 1.051998]];
129
+ var toLinear = (c) => {
130
+ const v = c / 255;
131
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
132
+ };
133
+ var toSrgb = (v) => {
134
+ const x = Math.max(0, Math.min(1, v));
135
+ return 255 * (x <= 31308e-7 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
136
+ };
137
+ function simulatedLab(rgb, m) {
138
+ const lin = rgb.map(toLinear);
139
+ const out = m.map((row) => toSrgb(row[0] * lin[0] + row[1] * lin[1] + row[2] * lin[2]));
140
+ return rgbToLab(out[0], out[1], out[2]);
141
+ }
142
+ function pairsUnder(labs, floor) {
143
+ const out = [];
144
+ for (let i = 0; i < labs.length; i++) {
145
+ for (let j = i + 1; j < labs.length; j++) {
146
+ const d = deltaE2000(labs[i], labs[j]);
147
+ if (d < floor) out.push({ i, j, d });
148
+ }
149
+ }
150
+ return out.sort((x, y) => x.d - y.d);
151
+ }
152
+ function closest(labs) {
153
+ let best = { i: 0, j: 1, d: Infinity };
154
+ for (let i = 0; i < labs.length; i++) {
155
+ for (let j = i + 1; j < labs.length; j++) {
156
+ const d = deltaE2000(labs[i], labs[j]);
157
+ if (d < best.d) best = { i, j, d };
158
+ }
159
+ }
160
+ return best;
161
+ }
162
+ function isRampShaped(labs) {
163
+ if (labs.length < MIN_RAMP_FILLS) return false;
164
+ let span = 0, lo = 0, hi = 1;
165
+ for (let i = 0; i < labs.length; i++) {
166
+ for (let j = i + 1; j < labs.length; j++) {
167
+ const v = deltaE2000(labs[i], labs[j]);
168
+ if (v > span) {
169
+ span = v;
170
+ lo = i;
171
+ hi = j;
172
+ }
173
+ }
174
+ }
175
+ if (span <= 0) return false;
176
+ let slack = 0, interior = 0;
177
+ for (let i = 0; i < labs.length; i++) {
178
+ if (i === lo || i === hi) continue;
179
+ slack += (deltaE2000(labs[lo], labs[i]) + deltaE2000(labs[i], labs[hi])) / span - 1;
180
+ interior++;
181
+ }
182
+ return interior >= 2 && slack / interior <= RAMP_SLACK_MAX;
183
+ }
184
+ var LIGHTNESS_STEP_MIN = 1.5;
185
+ function lightnessFollowsPath(labs) {
186
+ let span = -1, lo = 0;
187
+ for (let i = 0; i < labs.length; i++) {
188
+ for (let j = i + 1; j < labs.length; j++) {
189
+ const v = deltaE2000(labs[i], labs[j]);
190
+ if (v > span) {
191
+ span = v;
192
+ lo = i;
193
+ }
194
+ }
195
+ }
196
+ const order = labs.map((l) => ({ L: l.L, d: deltaE2000(labs[lo], l) })).sort((a, b) => a.d - b.d);
197
+ let turns = 0, dir = 0;
198
+ for (let k = 1; k < order.length; k++) {
199
+ const step = order[k].L - order[k - 1].L;
200
+ if (Math.abs(step) < LIGHTNESS_STEP_MIN) continue;
201
+ const sign = Math.sign(step);
202
+ if (dir !== 0 && sign !== dir) turns++;
203
+ dir = sign;
204
+ }
205
+ return turns <= 1;
206
+ }
207
+ var round1 = (n) => Math.round(n * 10) / 10;
208
+ function censusCategoryColours(container, doc, options = {}) {
209
+ const dimOpacity = typeof options.dimOpacity === "number" && options.dimOpacity >= 0 && options.dimOpacity <= 1 ? options.dimOpacity : DIM_OPACITY_DEFAULT;
210
+ if (!container || typeof container.querySelectorAll !== "function") return emptyCensus("none", dimOpacity);
211
+ try {
212
+ const ownerDoc = doc ?? container.ownerDocument;
213
+ const legendEls = Array.from(container.querySelectorAll(`.${LEGEND_MARK_CLASS}`));
214
+ const markEls = Array.from(container.querySelectorAll(`.${MARK_CLASS}, [${ROW_IDX_ATTR}]`)).filter((el) => !el.classList?.contains(LEGEND_MARK_CLASS));
215
+ if (legendEls.length + markEls.length > ELEMENT_CAP) return emptyCensus("too-many", dimOpacity);
216
+ const paintsOf = (els) => els.map((el) => paintWithin(el, ownerDoc, container)).filter((p) => p !== null);
217
+ const legendSlots = slotsOf(paintsOf(legendEls));
218
+ const markSlots = slotsOf(paintsOf(markEls));
219
+ const useLegend = legendSlots.length >= 2;
220
+ const slots = useLegend ? legendSlots : markSlots;
221
+ const source = useLegend ? "legend" : markSlots.length ? "marks" : "";
222
+ if (slots.length < 2) return emptyCensus("none", dimOpacity, slots.length, source);
223
+ if (slots.length > MAX_CATEGORY_SLOTS) return emptyCensus("too-many", dimOpacity, slots.length, source);
224
+ const labs = slots.map((s) => hexToLab(hexOf(s.rgb))).filter((l) => l !== null);
225
+ if (labs.length !== slots.length) return emptyCensus("none", dimOpacity, slots.length, source);
226
+ const rampLike = isRampShaped(labs);
227
+ if (rampLike && slots.length >= RAMP_DECLINE_MIN_SLOTS && lightnessFollowsPath(labs)) return { ...emptyCensus("ramp", dimOpacity, slots.length, source), rampLike };
228
+ const bgPaint = parsePaint(options.pageBg);
229
+ const bg = bgPaint ? blend(bgPaint.rgb, bgPaint.alpha, [255, 255, 255]) : [255, 255, 255];
230
+ const labsAt = (alphaOf) => slots.map((s) => hexToLab(hexOf(blend(s.rgb, alphaOf(s), bg))));
231
+ const full = closest(labs);
232
+ const paintedLabs = labsAt((s) => s.alpha);
233
+ const painted = closest(paintedLabs);
234
+ const dimmed = closest(labsAt((s) => s.alpha * dimOpacity));
235
+ const under = pairsUnder(paintedLabs, CATEGORY_DELTA_E_FLOOR);
236
+ const paintedRgb = slots.map((s) => blend(s.rgb, s.alpha, bg));
237
+ const deutan = closest(paintedRgb.map((c) => simulatedLab(c, MACHADO_DEUTAN)));
238
+ const protan = closest(paintedRgb.map((c) => simulatedLab(c, MACHADO_PROTAN)));
239
+ let legendMatchesMarks = null;
240
+ if (useLegend && markSlots.length > 0) {
241
+ const markColours = new Set(markSlots.map((s) => s.rgb.join(",")));
242
+ legendMatchesMarks = legendSlots.every((s) => markColours.has(s.rgb.join(",")));
243
+ }
244
+ return {
245
+ source,
246
+ declined: "",
247
+ slots: slots.length,
248
+ closestPair: [hexOf(slots[full.i].rgb), hexOf(slots[full.j].rgb)],
249
+ closestDeltaE: round1(full.d),
250
+ closestDeltaEPainted: round1(painted.d),
251
+ closestDeltaEDimmed: round1(dimmed.d),
252
+ dimOpacity,
253
+ rampLike,
254
+ legendMatchesMarks,
255
+ floor: CATEGORY_DELTA_E_FLOOR,
256
+ belowFloor: painted.d < CATEGORY_DELTA_E_FLOOR,
257
+ pairsBelowFloorCount: under.length,
258
+ pairsBelowFloor: under.slice(0, MAX_LISTED_PAIRS).map((p) => ({
259
+ colours: [hexOf(slots[p.i].rgb), hexOf(slots[p.j].rgb)],
260
+ deltaE: round1(p.d)
261
+ })),
262
+ closestPairPainted: [hexOf(slots[painted.i].rgb), hexOf(slots[painted.j].rgb)],
263
+ closestDeltaEDeutan: round1(deutan.d),
264
+ closestDeltaEProtan: round1(protan.d)
265
+ };
266
+ } catch {
267
+ return emptyCensus("none", dimOpacity);
268
+ }
269
+ }
270
+ export {
271
+ MAX_CATEGORY_SLOTS,
272
+ MAX_LISTED_PAIRS,
273
+ RAMP_DECLINE_MIN_SLOTS,
274
+ censusCategoryColours
275
+ };
@@ -1,8 +1,9 @@
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
  import {
3
3
  CONTAINER_SLOT_SCROLL,
4
- SNAPSHOT_SVG_ATTR
5
- } from "./chunk-KY4FI2HA.mjs";
4
+ SNAPSHOT_SVG_ATTR,
5
+ TABLE_SCROLLER_CLASS
6
+ } from "./chunk-QG5TENI3.mjs";
6
7
 
7
8
  // src/fit.ts
8
9
  var SCROLL_SLACK_PX = 8;
@@ -273,10 +274,45 @@ function rememberScroll(container, key) {
273
274
  if (slot[CONTAINER_SLOT_SCROLL]) return;
274
275
  if (container.querySelector(".llm-mm-scroll")) return;
275
276
  const x = container.scrollLeft, y = container.scrollTop;
276
- if (x > 0 || y > 0) slot[CONTAINER_SLOT_SCROLL] = { key, x, y };
277
+ const inner = innerScrollsOf(container);
278
+ if (x > 0 || y > 0 || inner) slot[CONTAINER_SLOT_SCROLL] = inner ? { key, x, y, inner } : { key, x, y };
277
279
  } catch {
278
280
  }
279
281
  }
282
+ var INNER_SCROLLER_CAP = 16;
283
+ var offset = (n) => typeof n === "number" && isFinite(n) && n > 0 ? n : 0;
284
+ function innerScrollsOf(container) {
285
+ const els = container.querySelectorAll("." + TABLE_SCROLLER_CLASS);
286
+ if (!els.length) return null;
287
+ const out = [];
288
+ let any = false;
289
+ for (let i = 0; i < els.length && i < INNER_SCROLLER_CAP; i++) {
290
+ const el = els[i];
291
+ const x = offset(el.scrollLeft), y = offset(el.scrollTop);
292
+ if (x > 0 || y > 0) any = true;
293
+ out.push({ x, y });
294
+ }
295
+ return any ? out : null;
296
+ }
297
+ function restoreInnerScroll(container, memory, key) {
298
+ if (!container || !memory || !memory.inner || !memory.inner.length) return null;
299
+ if (key === void 0 || key === null || memory.key !== key) return null;
300
+ try {
301
+ const els = container.querySelectorAll("." + TABLE_SCROLLER_CLASS);
302
+ if (!els.length) return null;
303
+ const out = [];
304
+ for (let i = 0; i < els.length && i < memory.inner.length; i++) {
305
+ const el = els[i], want = memory.inner[i];
306
+ const x = offset(want && want.x), y = offset(want && want.y);
307
+ if (x > 0 && el.scrollLeft !== x) el.scrollLeft = x;
308
+ if (y > 0 && el.scrollTop !== y) el.scrollTop = y;
309
+ out.push({ x: offset(el.scrollLeft), y: offset(el.scrollTop) });
310
+ }
311
+ return out;
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
280
316
  function takeScroll(container) {
281
317
  if (!container) return null;
282
318
  try {
@@ -625,6 +661,7 @@ export {
625
661
  fitReadingFor,
626
662
  unpinScrolledAxis,
627
663
  rememberScroll,
664
+ restoreInnerScroll,
628
665
  takeScroll,
629
666
  pinScrolledAxis,
630
667
  fitRenderedChart
@@ -4,7 +4,7 @@ import {
4
4
  LEGEND_MARK_CLASS,
5
5
  MARK_CLASS,
6
6
  ROW_IDX_ATTR
7
- } from "./chunk-KY4FI2HA.mjs";
7
+ } from "./chunk-QG5TENI3.mjs";
8
8
 
9
9
  // src/hitTargets.ts
10
10
  var SVG_NS = "http://www.w3.org/2000/svg";