@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.
- package/README.md +94 -5
- package/dist/adapters.mjs +3 -3
- package/dist/card.mjs +2 -2
- package/dist/category-colours.mjs +275 -0
- package/dist/{chunk-7XT5OJ6M.mjs → chunk-63IUMWY2.mjs} +40 -3
- package/dist/{chunk-AEO5SFEQ.mjs → chunk-EFKGSDFF.mjs} +1 -1
- package/dist/{chunk-TE54AFRN.mjs → chunk-GZSYTDFN.mjs} +531 -393
- package/dist/{chunk-PELRAFYZ.mjs → chunk-IW6BKFF5.mjs} +1 -1
- package/dist/{chunk-OLCUKXKL.mjs → chunk-JMLSNV5Q.mjs} +1 -1
- package/dist/chunk-JTZTTJJK.mjs +244 -0
- package/dist/{chunk-FIG5DIW7.mjs → chunk-K2TH7D5V.mjs} +335 -335
- package/dist/{chunk-KY4FI2HA.mjs → chunk-QG5TENI3.mjs} +5 -1
- package/dist/{chunk-KGVNZR23.mjs → chunk-RLOIA7QK.mjs} +1 -1
- package/dist/{chunk-IL5M7K6Z.mjs → chunk-TUZCM7SX.mjs} +2 -2
- package/dist/{chunk-GTJG3SMD.mjs → chunk-VGJH63LZ.mjs} +1 -1
- package/dist/{chunk-2KLFIGTQ.mjs → chunk-ZZBX5ZJ6.mjs} +145 -18
- package/dist/dax.mjs +23 -1
- package/dist/index.mjs +97 -16
- package/dist/legend-fit.mjs +3 -3
- package/dist/page.mjs +7 -5
- package/dist/provinces.mjs +4 -4
- package/dist/react.mjs +18 -14
- package/dist/testing.mjs +1 -1
- package/dist/types/categoryColours.d.ts +71 -0
- package/dist/types/contract.d.ts +16 -3
- package/dist/types/dax.d.ts +26 -0
- package/dist/types/filterScope.d.ts +52 -4
- package/dist/types/fit.d.ts +6 -1
- package/dist/types/fitDom.d.ts +13 -0
- package/dist/types/host.d.ts +19 -5
- package/dist/types/index.d.ts +7 -4
- package/dist/types/keyboardMarks.d.ts +1 -1
- package/dist/types/markFollower.d.ts +21 -0
- package/dist/types/page.d.ts +1 -1
- package/dist/types/qualifyTable.d.ts +52 -0
- package/dist/types/react.d.ts +11 -3
- package/dist/types/selectionMarker.d.ts +92 -0
- package/dist/zoom-memory.mjs +1 -1
- package/package.json +6 -1
- /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
|
-
**
|
|
291
|
-
|
|
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
|
|
315
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|