@bicharts/chart-host 0.5.73 → 0.5.74
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/dist/{chunk-D3DERYG3.mjs → chunk-E6P2ZMGC.mjs} +297 -0
- package/dist/index.mjs +48 -2
- package/dist/react.mjs +1 -1
- package/dist/types/host.d.ts +20 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/labelContrast.d.ts +56 -0
- package/dist/types/labelContrastDom.d.ts +33 -0
- package/package.json +1 -1
|
@@ -1972,6 +1972,267 @@ function fitRenderedChart(container, opts = {}) {
|
|
|
1972
1972
|
return result;
|
|
1973
1973
|
}
|
|
1974
1974
|
|
|
1975
|
+
// src/labelContrast.ts
|
|
1976
|
+
function parseRGBA(c) {
|
|
1977
|
+
if (typeof c !== "string") return null;
|
|
1978
|
+
const s = c.trim();
|
|
1979
|
+
const m2 = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/i);
|
|
1980
|
+
if (m2) return [parseFloat(m2[1]), parseFloat(m2[2]), parseFloat(m2[3]), m2[4] !== void 0 ? parseFloat(m2[4]) : 1];
|
|
1981
|
+
let h2 = s.replace(/^#/, "");
|
|
1982
|
+
if (h2.length === 3) h2 = h2.split("").map((ch) => ch + ch).join("");
|
|
1983
|
+
if (h2.length === 6 && /^[0-9a-fA-F]{6}$/.test(h2)) {
|
|
1984
|
+
return [parseInt(h2.slice(0, 2), 16), parseInt(h2.slice(2, 4), 16), parseInt(h2.slice(4, 6), 16), 1];
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
var DARK_TEXT = "#111111";
|
|
1989
|
+
var LIGHT_TEXT = "#ffffff";
|
|
1990
|
+
var MIN_CONTRAST = 3;
|
|
1991
|
+
var WHITE_TEXT_BG_LUM = 0.5;
|
|
1992
|
+
var PILL_MIN_ALPHA = 0.05;
|
|
1993
|
+
var PILL_OPAQUE_ALPHA = 0.85;
|
|
1994
|
+
var PAGE_MATCH_TOLERANCE = 2;
|
|
1995
|
+
function isPillBackdropAlpha(a2) {
|
|
1996
|
+
return a2 >= PILL_MIN_ALPHA && a2 < PILL_OPAQUE_ALPHA;
|
|
1997
|
+
}
|
|
1998
|
+
var PILL_MIN_COVERAGE = 0.6;
|
|
1999
|
+
function pillBacksGlyph(overlapArea, glyphArea) {
|
|
2000
|
+
if (!(glyphArea > 0) || !isFinite(overlapArea) || overlapArea <= 0) return false;
|
|
2001
|
+
return overlapArea / glyphArea >= PILL_MIN_COVERAGE;
|
|
2002
|
+
}
|
|
2003
|
+
var BACKING_MAJORITY = 0.5;
|
|
2004
|
+
function backingHoldsGlyph(backedArea, glyphArea) {
|
|
2005
|
+
if (!(glyphArea > 0) || !isFinite(backedArea) || backedArea <= 0) return false;
|
|
2006
|
+
return backedArea / glyphArea >= BACKING_MAJORITY;
|
|
2007
|
+
}
|
|
2008
|
+
function cellSuppressesNormalize(cellClass) {
|
|
2009
|
+
return typeof cellClass === "string" && /(^|\s)lch-deck-panel(\s|$)/.test(cellClass);
|
|
2010
|
+
}
|
|
2011
|
+
var NAMED = {
|
|
2012
|
+
white: [255, 255, 255, 1],
|
|
2013
|
+
black: [0, 0, 0, 1],
|
|
2014
|
+
none: [0, 0, 0, 0],
|
|
2015
|
+
transparent: [0, 0, 0, 0]
|
|
2016
|
+
};
|
|
2017
|
+
function toRGBA(c) {
|
|
2018
|
+
if (typeof c !== "string") return null;
|
|
2019
|
+
const k2 = c.trim().toLowerCase();
|
|
2020
|
+
if (k2 in NAMED) return NAMED[k2];
|
|
2021
|
+
return parseRGBA(c);
|
|
2022
|
+
}
|
|
2023
|
+
function compositeOver(fg, bg) {
|
|
2024
|
+
const a2 = Math.max(0, Math.min(1, fg[3]));
|
|
2025
|
+
return [
|
|
2026
|
+
Math.round(fg[0] * a2 + bg[0] * (1 - a2)),
|
|
2027
|
+
Math.round(fg[1] * a2 + bg[1] * (1 - a2)),
|
|
2028
|
+
Math.round(fg[2] * a2 + bg[2] * (1 - a2))
|
|
2029
|
+
];
|
|
2030
|
+
}
|
|
2031
|
+
function relativeLuminance(rgb) {
|
|
2032
|
+
const lin = rgb.map((v2) => {
|
|
2033
|
+
const s = v2 / 255;
|
|
2034
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
2035
|
+
});
|
|
2036
|
+
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
|
2037
|
+
}
|
|
2038
|
+
function contrastRatio(l1, l2) {
|
|
2039
|
+
const hi = Math.max(l1, l2);
|
|
2040
|
+
const lo = Math.min(l1, l2);
|
|
2041
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
2042
|
+
}
|
|
2043
|
+
function decideLabelColor(textColor, fill, fillOpacity, pageBg, minContrast = MIN_CONTRAST, normalize = false) {
|
|
2044
|
+
const fillRGBA = toRGBA(fill);
|
|
2045
|
+
if (!fillRGBA) return { fix: false, color: "", reason: "fill-unparsed" };
|
|
2046
|
+
const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
|
|
2047
|
+
const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
|
|
2048
|
+
const effFill = [fillRGBA[0], fillRGBA[1], fillRGBA[2], fillRGBA[3] * clamp01(fillOpacity)];
|
|
2049
|
+
const bgRGB = compositeOver(effFill, pageRGB);
|
|
2050
|
+
const bgLum = relativeLuminance(bgRGB);
|
|
2051
|
+
const bgIsPage = Math.abs(bgRGB[0] - pageRGB[0]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[1] - pageRGB[1]) <= PAGE_MATCH_TOLERANCE && Math.abs(bgRGB[2] - pageRGB[2]) <= PAGE_MATCH_TOLERANCE;
|
|
2052
|
+
const effNormalize = normalize && !bgIsPage;
|
|
2053
|
+
const textRGBA = toRGBA(textColor);
|
|
2054
|
+
if (!effNormalize && textRGBA) {
|
|
2055
|
+
const txtRGB = compositeOver(textRGBA, bgRGB);
|
|
2056
|
+
const cur = contrastRatio(relativeLuminance(txtRGB), bgLum);
|
|
2057
|
+
if (cur >= minContrast) return { fix: false, color: "", reason: "ok" };
|
|
2058
|
+
}
|
|
2059
|
+
const darkC = contrastRatio(relativeLuminance(hexToRGB(DARK_TEXT)), bgLum);
|
|
2060
|
+
const lightC = contrastRatio(relativeLuminance(hexToRGB(LIGHT_TEXT)), bgLum);
|
|
2061
|
+
let useLight = bgLum <= WHITE_TEXT_BG_LUM;
|
|
2062
|
+
if (useLight && lightC < minContrast && darkC >= minContrast) useLight = false;
|
|
2063
|
+
if (!useLight && darkC < minContrast && lightC >= minContrast) useLight = true;
|
|
2064
|
+
return useLight ? { fix: true, color: LIGHT_TEXT, reason: "fixed-light" } : { fix: true, color: DARK_TEXT, reason: "fixed-dark" };
|
|
2065
|
+
}
|
|
2066
|
+
var GLYPH_SAMPLE_N = 3;
|
|
2067
|
+
function glyphSampleGrid(box, n = GLYPH_SAMPLE_N) {
|
|
2068
|
+
const out = [];
|
|
2069
|
+
if (!box || !(box.width > 0) || !(box.height > 0) || !(n >= 1)) return out;
|
|
2070
|
+
const k2 = Math.floor(n);
|
|
2071
|
+
for (let i = 0; i < k2; i++) {
|
|
2072
|
+
for (let j2 = 0; j2 < k2; j2++) {
|
|
2073
|
+
out.push({
|
|
2074
|
+
x: box.left + box.width * (i + 0.5) / k2,
|
|
2075
|
+
y: box.top + box.height * (j2 + 0.5) / k2
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
function clamp01(v2) {
|
|
2082
|
+
if (typeof v2 !== "number" || !isFinite(v2)) return 1;
|
|
2083
|
+
return Math.max(0, Math.min(1, v2));
|
|
2084
|
+
}
|
|
2085
|
+
function hexToRGB(hex) {
|
|
2086
|
+
const p2 = toRGBA(hex);
|
|
2087
|
+
return [p2[0], p2[1], p2[2]];
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/labelContrastDom.ts
|
|
2091
|
+
var LABEL_CONTRAST_DONE_ATTR = "data-lch-contrast";
|
|
2092
|
+
var LABEL_CONTRAST_CAP = 2e3;
|
|
2093
|
+
var EMPTY3 = { rects: 0, scanned: 0, fixed: 0, pillsBoosted: 0, offFill: 0, pageMajority: 0 };
|
|
2094
|
+
function backedSamples(el, pts) {
|
|
2095
|
+
try {
|
|
2096
|
+
const ge = el;
|
|
2097
|
+
if (typeof ge.isPointInFill !== "function" || typeof ge.getScreenCTM !== "function") return null;
|
|
2098
|
+
const m2 = ge.getScreenCTM();
|
|
2099
|
+
const svg = ge.ownerSVGElement;
|
|
2100
|
+
if (!m2 || !svg || typeof svg.createSVGPoint !== "function") return null;
|
|
2101
|
+
if (pts.length === 0) return null;
|
|
2102
|
+
const inv = m2.inverse();
|
|
2103
|
+
const hit = [];
|
|
2104
|
+
for (let i = 0; i < pts.length; i++) {
|
|
2105
|
+
const p2 = svg.createSVGPoint();
|
|
2106
|
+
p2.x = pts[i].x;
|
|
2107
|
+
p2.y = pts[i].y;
|
|
2108
|
+
hit.push(ge.isPointInFill(p2.matrixTransform(inv)));
|
|
2109
|
+
}
|
|
2110
|
+
return hit;
|
|
2111
|
+
} catch {
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function applyLabelContrast(container, opts = {}) {
|
|
2116
|
+
const report = { ...EMPTY3 };
|
|
2117
|
+
if (!container) {
|
|
2118
|
+
report.skipped = "no-container";
|
|
2119
|
+
return report;
|
|
2120
|
+
}
|
|
2121
|
+
const CAP = opts.cap ?? LABEL_CONTRAST_CAP;
|
|
2122
|
+
const pageBg = opts.pageBg || "#ffffff";
|
|
2123
|
+
try {
|
|
2124
|
+
const rects = [];
|
|
2125
|
+
let ord = 0;
|
|
2126
|
+
const pushShape = (el) => {
|
|
2127
|
+
const tag = el.tagName.toLowerCase();
|
|
2128
|
+
if (tag !== "rect" && tag !== "path") return;
|
|
2129
|
+
const r = el.getBoundingClientRect();
|
|
2130
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
2131
|
+
let fill = el.getAttribute("fill") || "";
|
|
2132
|
+
const win2 = container.ownerDocument?.defaultView;
|
|
2133
|
+
if ((!fill || fill === "none") && win2 && typeof win2.getComputedStyle === "function") {
|
|
2134
|
+
fill = win2.getComputedStyle(el).fill || "";
|
|
2135
|
+
}
|
|
2136
|
+
if (!fill || fill === "none") return;
|
|
2137
|
+
const opAttr = el.getAttribute("fill-opacity");
|
|
2138
|
+
const op = opAttr === null ? 1 : parseFloat(opAttr);
|
|
2139
|
+
rects.push({ r, fill, op: isFinite(op) ? op : 1, area: r.width * r.height, el, ord: ord++ });
|
|
2140
|
+
};
|
|
2141
|
+
const shapeEls = container.querySelectorAll("rect, path");
|
|
2142
|
+
if (shapeEls.length === 0) {
|
|
2143
|
+
report.skipped = "no-shapes";
|
|
2144
|
+
return report;
|
|
2145
|
+
}
|
|
2146
|
+
if (shapeEls.length > CAP) {
|
|
2147
|
+
report.skipped = "too-many-shapes";
|
|
2148
|
+
return report;
|
|
2149
|
+
}
|
|
2150
|
+
for (let i = 0; i < shapeEls.length; i++) pushShape(shapeEls[i]);
|
|
2151
|
+
if (rects.length === 0) {
|
|
2152
|
+
report.skipped = "no-shapes";
|
|
2153
|
+
return report;
|
|
2154
|
+
}
|
|
2155
|
+
report.rects = rects.length;
|
|
2156
|
+
const pageRGBA = toRGBA(pageBg) || [255, 255, 255, 1];
|
|
2157
|
+
const pageRGB = [pageRGBA[0], pageRGBA[1], pageRGBA[2]];
|
|
2158
|
+
const effAlpha = (mk) => {
|
|
2159
|
+
const c = toRGBA(mk.fill);
|
|
2160
|
+
return (c ? c[3] : 1) * mk.op;
|
|
2161
|
+
};
|
|
2162
|
+
const texts = container.querySelectorAll("text");
|
|
2163
|
+
if (texts.length > CAP) {
|
|
2164
|
+
report.skipped = "too-many-texts";
|
|
2165
|
+
return report;
|
|
2166
|
+
}
|
|
2167
|
+
const win = container.ownerDocument?.defaultView;
|
|
2168
|
+
for (let t = 0; t < texts.length; t++) {
|
|
2169
|
+
const tx = texts[t];
|
|
2170
|
+
if (tx.getAttribute(LABEL_CONTRAST_DONE_ATTR) === "1") continue;
|
|
2171
|
+
const tr = tx.getBoundingClientRect();
|
|
2172
|
+
if (tr.width <= 0 || tr.height <= 0) continue;
|
|
2173
|
+
const ovArea = (r) => Math.max(0, Math.min(r.right, tr.right) - Math.max(r.left, tr.left)) * Math.max(0, Math.min(r.bottom, tr.bottom) - Math.max(r.top, tr.top));
|
|
2174
|
+
const glyphArea = tr.width * tr.height;
|
|
2175
|
+
const boxed = rects.map((mk) => ({ mk, ov: ovArea(mk.r) })).filter((x2) => x2.ov > 0);
|
|
2176
|
+
const pts = glyphSampleGrid({ left: tr.left, top: tr.top, width: tr.width, height: tr.height });
|
|
2177
|
+
const painted = pts.map(() => false);
|
|
2178
|
+
let unmeasured = 0;
|
|
2179
|
+
const under = boxed.map((x2) => {
|
|
2180
|
+
const mask = backedSamples(x2.mk.el, pts);
|
|
2181
|
+
if (mask === null) {
|
|
2182
|
+
unmeasured++;
|
|
2183
|
+
return x2;
|
|
2184
|
+
}
|
|
2185
|
+
let inside = 0;
|
|
2186
|
+
const paints = effAlpha(x2.mk) >= PILL_MIN_ALPHA;
|
|
2187
|
+
for (let i = 0; i < mask.length; i++) {
|
|
2188
|
+
if (!mask[i]) continue;
|
|
2189
|
+
inside++;
|
|
2190
|
+
if (paints) painted[i] = true;
|
|
2191
|
+
}
|
|
2192
|
+
return { mk: x2.mk, ov: glyphArea * (inside / mask.length) };
|
|
2193
|
+
}).filter((x2) => x2.ov > 0);
|
|
2194
|
+
if (under.length === 0 && boxed.length > 0) report.offFill++;
|
|
2195
|
+
if (under.length === 0) continue;
|
|
2196
|
+
if (unmeasured === 0 && pts.length > 0) {
|
|
2197
|
+
let held = 0;
|
|
2198
|
+
for (let i = 0; i < painted.length; i++) if (painted[i]) held++;
|
|
2199
|
+
if (!backingHoldsGlyph(glyphArea * (held / pts.length), glyphArea)) {
|
|
2200
|
+
report.pageMajority++;
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
report.scanned++;
|
|
2205
|
+
const opaqueUnder = under.filter((x2) => effAlpha(x2.mk) >= PILL_OPAQUE_ALPHA);
|
|
2206
|
+
const cellEntry = (opaqueUnder.length ? opaqueUnder : under).reduce((a2, b2) => b2.ov > a2.ov || b2.ov === a2.ov && b2.mk.ord > a2.mk.ord ? b2 : a2);
|
|
2207
|
+
const cell = cellEntry.mk;
|
|
2208
|
+
const pill = under.find((x2) => x2.mk !== cell && isPillBackdropAlpha(effAlpha(x2.mk)) && x2.mk.ord > cell.ord && pillBacksGlyph(x2.ov, glyphArea))?.mk;
|
|
2209
|
+
const cellRGBA = toRGBA(cell.fill);
|
|
2210
|
+
let bgRGB = cellRGBA ? compositeOver([cellRGBA[0], cellRGBA[1], cellRGBA[2], cellRGBA[3] * cell.op], pageRGB) : pageRGB;
|
|
2211
|
+
if (pill) {
|
|
2212
|
+
const pr = toRGBA(pill.fill);
|
|
2213
|
+
if (pr) {
|
|
2214
|
+
bgRGB = compositeOver([pr[0], pr[1], pr[2], 0.9], bgRGB);
|
|
2215
|
+
pill.el.setAttribute("fill", `rgb(${pr[0]}, ${pr[1]}, ${pr[2]})`);
|
|
2216
|
+
pill.el.setAttribute("fill-opacity", "0.9");
|
|
2217
|
+
report.pillsBoosted++;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
const cur = tx.getAttribute("fill") || (win && typeof win.getComputedStyle === "function" ? win.getComputedStyle(tx).fill : "");
|
|
2221
|
+
const normalize = !cellSuppressesNormalize(cell.el.getAttribute("class"));
|
|
2222
|
+
const d = decideLabelColor(cur, `rgb(${bgRGB[0]}, ${bgRGB[1]}, ${bgRGB[2]})`, 1, pageBg, MIN_CONTRAST, normalize);
|
|
2223
|
+
if (d.fix) {
|
|
2224
|
+
tx.setAttribute("fill", d.color);
|
|
2225
|
+
report.fixed++;
|
|
2226
|
+
}
|
|
2227
|
+
if (pill || d.fix) tx.setAttribute(LABEL_CONTRAST_DONE_ATTR, "1");
|
|
2228
|
+
}
|
|
2229
|
+
return report;
|
|
2230
|
+
} catch {
|
|
2231
|
+
report.skipped = "error";
|
|
2232
|
+
return report;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
1975
2236
|
// src/host.ts
|
|
1976
2237
|
function sessionViewStateProvider(container) {
|
|
1977
2238
|
const holder = container;
|
|
@@ -2234,6 +2495,7 @@ function createChartHost(container, config) {
|
|
|
2234
2495
|
container.classList?.add(HOST_CONTAINER_CLASS);
|
|
2235
2496
|
ensureAffordanceStyles();
|
|
2236
2497
|
const fitOpts = config.fit ?? true;
|
|
2498
|
+
const labelContrastOpts = config.labelContrast ?? true;
|
|
2237
2499
|
const stopAnim = () => {
|
|
2238
2500
|
const s = container[CONTAINER_SLOT_ANIM_STOP];
|
|
2239
2501
|
if (typeof s === "function") {
|
|
@@ -2273,6 +2535,18 @@ function createChartHost(container, config) {
|
|
|
2273
2535
|
} catch (err) {
|
|
2274
2536
|
throw explainRenderFailure(err, d3);
|
|
2275
2537
|
}
|
|
2538
|
+
if (labelContrastOpts !== false) {
|
|
2539
|
+
try {
|
|
2540
|
+
const lc = labelContrastOpts === true ? {} : { ...labelContrastOpts };
|
|
2541
|
+
if (!lc.pageBg) {
|
|
2542
|
+
const bg = resolved.backgroundColor ?? resolved.themeBg;
|
|
2543
|
+
if (typeof bg === "string" && bg) lc.pageBg = bg;
|
|
2544
|
+
}
|
|
2545
|
+
const r = applyLabelContrast(container, lc);
|
|
2546
|
+
config.onLabelContrast?.(r);
|
|
2547
|
+
} catch {
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2276
2550
|
ensureCrossfilterHitTargets(container, doc);
|
|
2277
2551
|
paintSelection();
|
|
2278
2552
|
try {
|
|
@@ -2432,6 +2706,29 @@ export {
|
|
|
2432
2706
|
measureContainerBoxes,
|
|
2433
2707
|
fitReadingFor,
|
|
2434
2708
|
fitRenderedChart,
|
|
2709
|
+
DARK_TEXT,
|
|
2710
|
+
LIGHT_TEXT,
|
|
2711
|
+
MIN_CONTRAST,
|
|
2712
|
+
WHITE_TEXT_BG_LUM,
|
|
2713
|
+
PILL_MIN_ALPHA,
|
|
2714
|
+
PILL_OPAQUE_ALPHA,
|
|
2715
|
+
PAGE_MATCH_TOLERANCE,
|
|
2716
|
+
isPillBackdropAlpha,
|
|
2717
|
+
PILL_MIN_COVERAGE,
|
|
2718
|
+
pillBacksGlyph,
|
|
2719
|
+
BACKING_MAJORITY,
|
|
2720
|
+
backingHoldsGlyph,
|
|
2721
|
+
cellSuppressesNormalize,
|
|
2722
|
+
toRGBA,
|
|
2723
|
+
compositeOver,
|
|
2724
|
+
relativeLuminance,
|
|
2725
|
+
contrastRatio,
|
|
2726
|
+
decideLabelColor,
|
|
2727
|
+
GLYPH_SAMPLE_N,
|
|
2728
|
+
glyphSampleGrid,
|
|
2729
|
+
LABEL_CONTRAST_DONE_ATTR,
|
|
2730
|
+
LABEL_CONTRAST_CAP,
|
|
2731
|
+
applyLabelContrast,
|
|
2435
2732
|
sessionViewStateProvider,
|
|
2436
2733
|
noopViewStateProvider,
|
|
2437
2734
|
requiredD3Plugins,
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ANIM_PLAY_SPEED_MIN,
|
|
12
12
|
APPROXIMATE_POSITIONS_DEFAULT,
|
|
13
13
|
AXIS_FILTER_CLASS,
|
|
14
|
+
BACKING_MAJORITY,
|
|
14
15
|
COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT,
|
|
15
16
|
COLOR_SCALE_SELF_CLAMP_PCT_MAX,
|
|
16
17
|
COLOR_SCALE_SELF_CLAMP_PCT_MIN,
|
|
@@ -18,21 +19,31 @@ import {
|
|
|
18
19
|
CONTAINER_SLOT_INITIAL_XF_MARK,
|
|
19
20
|
CONTAINER_SLOT_UI_STATE,
|
|
20
21
|
CONTAINER_SLOT_XF_CLEAR,
|
|
22
|
+
DARK_TEXT,
|
|
21
23
|
DIM_OPACITY_DEFAULT,
|
|
22
24
|
DIM_OPACITY_VAR,
|
|
23
25
|
FIT_CONTENT_SELECTOR,
|
|
24
26
|
FLIP_MODE_DEFAULT,
|
|
25
27
|
GEO_POINT_PRECISIONS,
|
|
28
|
+
GLYPH_SAMPLE_N,
|
|
26
29
|
HOST_CONTAINER_CLASS,
|
|
27
30
|
HOST_CONTRACT_VERSION,
|
|
28
31
|
L3,
|
|
32
|
+
LABEL_CONTRAST_CAP,
|
|
33
|
+
LABEL_CONTRAST_DONE_ATTR,
|
|
29
34
|
LEGEND_MARK_CLASS,
|
|
30
35
|
LIFT_SELECTED_CLASS,
|
|
36
|
+
LIGHT_TEXT,
|
|
31
37
|
MARK_CLASS,
|
|
32
38
|
MARK_SELECTED_CLASS,
|
|
33
39
|
MAX_FRAME_GROW_FACTOR,
|
|
40
|
+
MIN_CONTRAST,
|
|
34
41
|
MIN_HIT_BAND_PX,
|
|
42
|
+
PAGE_MATCH_TOLERANCE,
|
|
35
43
|
PHANTOM_FRACTION,
|
|
44
|
+
PILL_MIN_ALPHA,
|
|
45
|
+
PILL_MIN_COVERAGE,
|
|
46
|
+
PILL_OPAQUE_ALPHA,
|
|
36
47
|
ROW_IDX_ATTR,
|
|
37
48
|
Rt,
|
|
38
49
|
SCROLL_SLACK_PX,
|
|
@@ -41,43 +52,55 @@ import {
|
|
|
41
52
|
Se,
|
|
42
53
|
Tt,
|
|
43
54
|
VALUE_AXIS_BASELINE_DEFAULT,
|
|
55
|
+
WHITE_TEXT_BG_LUM,
|
|
44
56
|
XFILTER_REFRESH_EVENT,
|
|
57
|
+
applyLabelContrast,
|
|
58
|
+
backingHoldsGlyph,
|
|
45
59
|
blankRenderFlag,
|
|
46
60
|
buildRenderPayload,
|
|
61
|
+
cellSuppressesNormalize,
|
|
47
62
|
censusHitBands,
|
|
48
63
|
censusMarks,
|
|
49
64
|
chartOwnsTimeline,
|
|
50
65
|
clearGeoCache,
|
|
51
66
|
compileRenderFn,
|
|
67
|
+
compositeOver,
|
|
52
68
|
contentExtentOf,
|
|
69
|
+
contrastRatio,
|
|
53
70
|
createChartHost,
|
|
54
71
|
createMarkResolver,
|
|
55
72
|
ctmScaleOf,
|
|
73
|
+
decideLabelColor,
|
|
56
74
|
ensureCrossfilterHitTargets,
|
|
57
75
|
explainRenderFailure,
|
|
58
76
|
fitReadingFor,
|
|
59
77
|
fitRenderedChart,
|
|
60
78
|
geoAssetFor,
|
|
61
79
|
geoFromCache,
|
|
80
|
+
glyphSampleGrid,
|
|
62
81
|
hitBandFlag,
|
|
63
82
|
isBlankRender,
|
|
64
83
|
isPhantomBox,
|
|
84
|
+
isPillBackdropAlpha,
|
|
65
85
|
loadGeo,
|
|
66
86
|
measureContainerBoxes,
|
|
67
87
|
needsScroll,
|
|
68
88
|
noopViewStateProvider,
|
|
69
89
|
periodTickSuppressesFeedback,
|
|
90
|
+
pillBacksGlyph,
|
|
70
91
|
planFrameGrow,
|
|
71
92
|
registerGeo,
|
|
72
93
|
registerGeoAsset,
|
|
94
|
+
relativeLuminance,
|
|
73
95
|
requiredD3Plugins,
|
|
74
96
|
resolveOptions,
|
|
75
97
|
scrollFitFor,
|
|
76
98
|
sessionViewStateProvider,
|
|
77
99
|
stripEsmExports,
|
|
78
100
|
svgInkReach,
|
|
101
|
+
toRGBA,
|
|
79
102
|
ye
|
|
80
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-E6P2ZMGC.mjs";
|
|
81
104
|
import "./chunk-A2GMXZP7.mjs";
|
|
82
105
|
import "./chunk-GHODWOAL.mjs";
|
|
83
106
|
|
|
@@ -1010,6 +1033,7 @@ export {
|
|
|
1010
1033
|
ANIM_PLAY_SPEED_MIN,
|
|
1011
1034
|
APPROXIMATE_POSITIONS_DEFAULT,
|
|
1012
1035
|
AXIS_FILTER_CLASS,
|
|
1036
|
+
BACKING_MAJORITY,
|
|
1013
1037
|
CHOOSER_MIN_HEIGHT_PX,
|
|
1014
1038
|
CHOOSER_MIN_WIDTH_PX,
|
|
1015
1039
|
COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT,
|
|
@@ -1019,6 +1043,7 @@ export {
|
|
|
1019
1043
|
CONTAINER_SLOT_INITIAL_XF_MARK,
|
|
1020
1044
|
CONTAINER_SLOT_UI_STATE,
|
|
1021
1045
|
CONTAINER_SLOT_XF_CLEAR,
|
|
1046
|
+
DARK_TEXT,
|
|
1022
1047
|
DIM_OPACITY_DEFAULT,
|
|
1023
1048
|
DIM_OPACITY_VAR,
|
|
1024
1049
|
FILTER_MIN_HEIGHT_PX,
|
|
@@ -1028,30 +1053,43 @@ export {
|
|
|
1028
1053
|
FIT_CONTENT_SELECTOR,
|
|
1029
1054
|
FLIP_MODE_DEFAULT,
|
|
1030
1055
|
GEO_POINT_PRECISIONS,
|
|
1056
|
+
GLYPH_SAMPLE_N,
|
|
1031
1057
|
HOST_CONTAINER_CLASS,
|
|
1032
1058
|
HOST_CONTRACT_VERSION,
|
|
1033
1059
|
INLINE_FILTER_MIN_ROWS,
|
|
1060
|
+
LABEL_CONTRAST_CAP,
|
|
1061
|
+
LABEL_CONTRAST_DONE_ATTR,
|
|
1034
1062
|
LEGEND_MARK_CLASS,
|
|
1035
1063
|
LIFT_SELECTED_CLASS,
|
|
1064
|
+
LIGHT_TEXT,
|
|
1036
1065
|
MARK_CLASS,
|
|
1037
1066
|
MARK_SELECTED_CLASS,
|
|
1038
1067
|
MAX_FRAME_GROW_FACTOR,
|
|
1068
|
+
MIN_CONTRAST,
|
|
1039
1069
|
MIN_HIT_BAND_PX,
|
|
1070
|
+
PAGE_MATCH_TOLERANCE,
|
|
1040
1071
|
PHANTOM_FRACTION,
|
|
1072
|
+
PILL_MIN_ALPHA,
|
|
1073
|
+
PILL_MIN_COVERAGE,
|
|
1074
|
+
PILL_OPAQUE_ALPHA,
|
|
1041
1075
|
ROW_IDX_ATTR,
|
|
1042
1076
|
SCROLL_SLACK_PX,
|
|
1043
1077
|
SEASONAL_MARKERS_DEFAULT,
|
|
1044
1078
|
SELECTION_ACTIVE_CLASS,
|
|
1045
1079
|
VALUE_AXIS_BASELINE_DEFAULT,
|
|
1080
|
+
WHITE_TEXT_BG_LUM,
|
|
1046
1081
|
XFILTER_REFRESH_EVENT,
|
|
1047
1082
|
actionFor,
|
|
1083
|
+
applyLabelContrast,
|
|
1048
1084
|
askApplyImprovements,
|
|
1085
|
+
backingHoldsGlyph,
|
|
1049
1086
|
bareBase64,
|
|
1050
1087
|
blankRenderFlag,
|
|
1051
1088
|
buildRenderPayload,
|
|
1052
1089
|
buildReviewWire,
|
|
1053
1090
|
canConfirmLaunch,
|
|
1054
1091
|
captureSvgSnapshot,
|
|
1092
|
+
cellSuppressesNormalize,
|
|
1055
1093
|
censusHitBands,
|
|
1056
1094
|
censusMarks,
|
|
1057
1095
|
chartOwnsTimeline,
|
|
@@ -1059,13 +1097,16 @@ export {
|
|
|
1059
1097
|
clearGeoCache,
|
|
1060
1098
|
compileRenderFn,
|
|
1061
1099
|
compileTrivialSource,
|
|
1100
|
+
compositeOver,
|
|
1062
1101
|
computeQualifyFilterView,
|
|
1063
1102
|
computeSelectionCard,
|
|
1064
1103
|
confirmLaunch,
|
|
1065
1104
|
contentExtentOf,
|
|
1105
|
+
contrastRatio,
|
|
1066
1106
|
createChartHost,
|
|
1067
1107
|
createMarkResolver,
|
|
1068
1108
|
ctmScaleOf,
|
|
1109
|
+
decideLabelColor,
|
|
1069
1110
|
ensureCrossfilterHitTargets,
|
|
1070
1111
|
explainRenderFailure,
|
|
1071
1112
|
filterFitsChooser,
|
|
@@ -1074,11 +1115,13 @@ export {
|
|
|
1074
1115
|
fitRenderedChart,
|
|
1075
1116
|
geoAssetFor,
|
|
1076
1117
|
geoFromCache,
|
|
1118
|
+
glyphSampleGrid,
|
|
1077
1119
|
hasRefusalsToShow,
|
|
1078
1120
|
hitBandFlag,
|
|
1079
1121
|
inlineFilterGate,
|
|
1080
1122
|
isBlankRender,
|
|
1081
1123
|
isPhantomBox,
|
|
1124
|
+
isPillBackdropAlpha,
|
|
1082
1125
|
launchFavorStyle,
|
|
1083
1126
|
launchGenerates,
|
|
1084
1127
|
listNeedsFilter,
|
|
@@ -1092,6 +1135,7 @@ export {
|
|
|
1092
1135
|
normalizeFilterTerm,
|
|
1093
1136
|
orderRefusalsForDisplay,
|
|
1094
1137
|
periodTickSuppressesFeedback,
|
|
1138
|
+
pillBacksGlyph,
|
|
1095
1139
|
planFrameGrow,
|
|
1096
1140
|
planTrivialChart,
|
|
1097
1141
|
qualifyAuto,
|
|
@@ -1109,6 +1153,7 @@ export {
|
|
|
1109
1153
|
registerCityTable,
|
|
1110
1154
|
registerGeo,
|
|
1111
1155
|
registerGeoAsset,
|
|
1156
|
+
relativeLuminance,
|
|
1112
1157
|
requiredD3Plugins,
|
|
1113
1158
|
resolveOptions,
|
|
1114
1159
|
scrollFitFor,
|
|
@@ -1119,5 +1164,6 @@ export {
|
|
|
1119
1164
|
stripEsmExports,
|
|
1120
1165
|
svgInkReach,
|
|
1121
1166
|
svgNaturalSize,
|
|
1122
|
-
svgToDataUrl
|
|
1167
|
+
svgToDataUrl,
|
|
1168
|
+
toRGBA
|
|
1123
1169
|
};
|
package/dist/react.mjs
CHANGED
package/dist/types/host.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ResolveOptionsInput } from "./defaults";
|
|
|
3
3
|
import { type MarkCensus } from "./blankRender";
|
|
4
4
|
import { type HitBandCensus } from "./hitBands";
|
|
5
5
|
import { type FitRenderedChartOptions, type FitRenderedChartResult } from "./fitDom";
|
|
6
|
+
import { type LabelContrastOptions, type LabelContrastReport } from "./labelContrastDom";
|
|
6
7
|
export type RenderFn = (container: HTMLElement, data: any, options: RenderOptions) => void;
|
|
7
8
|
/**
|
|
8
9
|
* THE SESSION PROVIDER — what every host got hard-coded before contract 1.6.0, now named.
|
|
@@ -115,6 +116,25 @@ export interface ChartHostConfig {
|
|
|
115
116
|
fit?: boolean | FitRenderedChartOptions;
|
|
116
117
|
/** The fit result after each render - what was grown, and how the chart measured. */
|
|
117
118
|
onFit?: (result: FitRenderedChartResult) => void;
|
|
119
|
+
/**
|
|
120
|
+
* CAN THE LABELS ON THE MARKS BE READ? Runs after every render, BEFORE the hit-target heal
|
|
121
|
+
* (which injects transparent rects this pass must not mistake for backdrops).
|
|
122
|
+
*
|
|
123
|
+
* Generated code picks an in-mark label's colour from the mark's NOMINAL hue; the mark's
|
|
124
|
+
* ACTUAL rendered fill - a 0.18-opacity band over white, a translucent pill over a tile, an
|
|
125
|
+
* arc whose hole is canvas - is something else, and only a post-render read of the real DOM
|
|
126
|
+
* knows it. This resolves which shapes really back each label (geometry, not bounding
|
|
127
|
+
* boxes), composites the real stack over the page, and recolours to black or white only
|
|
128
|
+
* where the label would otherwise be unreadable or a tile's labels would read as mixed.
|
|
129
|
+
*
|
|
130
|
+
* DEFAULT ON, because the alternative is text nobody can read, and the pass only ever sets a
|
|
131
|
+
* text's fill. Pass `false` for a host that owns its own label colours, or an options object
|
|
132
|
+
* to name the page background it composites onto (`pageBg`) - a themed or high-contrast
|
|
133
|
+
* canvas is not white, and every readability answer is measured against it.
|
|
134
|
+
*/
|
|
135
|
+
labelContrast?: boolean | LabelContrastOptions;
|
|
136
|
+
/** What the label-contrast pass did after each render - counts, so a host can log them. */
|
|
137
|
+
onLabelContrast?: (report: LabelContrastReport) => void;
|
|
118
138
|
}
|
|
119
139
|
export interface ChartHost {
|
|
120
140
|
render(): void;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export { qualifyPick, qualifyAuto, qualifyCancel, launchGenerates, launchFavorSt
|
|
|
16
16
|
export { normalizeFilterTerm, filterQualifyRows, readQualifyChartRow, readQualifyRefusalRow, 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";
|
|
17
17
|
export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
|
|
18
18
|
export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
|
|
19
|
+
export { applyLabelContrast, LABEL_CONTRAST_DONE_ATTR, LABEL_CONTRAST_CAP, type LabelContrastOptions, type LabelContrastReport, } from "./labelContrastDom";
|
|
20
|
+
export { decideLabelColor, toRGBA, compositeOver, relativeLuminance, contrastRatio, isPillBackdropAlpha, pillBacksGlyph, backingHoldsGlyph, cellSuppressesNormalize, glyphSampleGrid, DARK_TEXT, LIGHT_TEXT, MIN_CONTRAST, WHITE_TEXT_BG_LUM, PILL_MIN_ALPHA, PILL_OPAQUE_ALPHA, PILL_MIN_COVERAGE, PAGE_MATCH_TOLERANCE, BACKING_MAJORITY, GLYPH_SAMPLE_N, type LabelDecision, } from "./labelContrast";
|
|
19
21
|
export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
|
|
20
22
|
export { censusHitBands, hitBandFlag, MIN_HIT_BAND_PX, type HitBandCensus } from "./hitBands";
|
|
21
23
|
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,56 @@
|
|
|
1
|
+
export declare const DARK_TEXT = "#111111";
|
|
2
|
+
export declare const LIGHT_TEXT = "#ffffff";
|
|
3
|
+
export declare const MIN_CONTRAST = 3;
|
|
4
|
+
export declare const WHITE_TEXT_BG_LUM = 0.5;
|
|
5
|
+
export declare const PILL_MIN_ALPHA = 0.05;
|
|
6
|
+
export declare const PILL_OPAQUE_ALPHA = 0.85;
|
|
7
|
+
export declare const PAGE_MATCH_TOLERANCE = 2;
|
|
8
|
+
/** True when a shape's effective alpha lets it act as a label backdrop rather than the cell. */
|
|
9
|
+
export declare function isPillBackdropAlpha(a: number): boolean;
|
|
10
|
+
export declare const PILL_MIN_COVERAGE = 0.6;
|
|
11
|
+
/** True when a candidate backdrop covers enough of the glyph box to be the thing behind it. */
|
|
12
|
+
export declare function pillBacksGlyph(overlapArea: number, glyphArea: number): boolean;
|
|
13
|
+
export declare const BACKING_MAJORITY = 0.5;
|
|
14
|
+
/** True when the painted marks under a glyph hold enough of it to count as its background. */
|
|
15
|
+
export declare function backingHoldsGlyph(backedArea: number, glyphArea: number): boolean;
|
|
16
|
+
export declare function cellSuppressesNormalize(cellClass: string | null | undefined): boolean;
|
|
17
|
+
type RGB = [number, number, number];
|
|
18
|
+
type RGBA = [number, number, number, number];
|
|
19
|
+
/** Parse a colour string to RGBA, covering hex, rgb()/rgba() and a few keywords. */
|
|
20
|
+
export declare function toRGBA(c: string | null | undefined): RGBA | null;
|
|
21
|
+
/** Alpha-composite a foreground RGBA over an opaque RGB background. */
|
|
22
|
+
export declare function compositeOver(fg: RGBA, bg: RGB): RGB;
|
|
23
|
+
/** WCAG relative luminance of an opaque sRGB colour (0..1). */
|
|
24
|
+
export declare function relativeLuminance(rgb: RGB): number;
|
|
25
|
+
/** WCAG contrast ratio (1..21) between two luminances. */
|
|
26
|
+
export declare function contrastRatio(l1: number, l2: number): number;
|
|
27
|
+
export interface LabelDecision {
|
|
28
|
+
fix: boolean;
|
|
29
|
+
color: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether an in-mark label needs recolouring for legibility.
|
|
34
|
+
*
|
|
35
|
+
* @param textColor current text colour (any CSS form the SVG carries)
|
|
36
|
+
* @param fill the backing mark's fill colour
|
|
37
|
+
* @param fillOpacity the backing mark's fill-opacity (0..1)
|
|
38
|
+
* @param pageBg the page/visual background the mark sits on (opaque; '' → white)
|
|
39
|
+
* @param minContrast trip point; defaults to MIN_CONTRAST
|
|
40
|
+
*/
|
|
41
|
+
export declare function decideLabelColor(textColor: string | null | undefined, fill: string | null | undefined, fillOpacity: number, pageBg: string | null | undefined, minContrast?: number, normalize?: boolean): LabelDecision;
|
|
42
|
+
export declare const GLYPH_SAMPLE_N = 3;
|
|
43
|
+
/**
|
|
44
|
+
* Evenly-spaced sample points over a glyph box, in the box's own coordinate space.
|
|
45
|
+
* `n` per side (n² points), each at the centre of its cell so no point lands on an edge.
|
|
46
|
+
*/
|
|
47
|
+
export declare function glyphSampleGrid(box: {
|
|
48
|
+
left: number;
|
|
49
|
+
top: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
}, n?: number): {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
}[];
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The attribute a recoloured (or pill-backed) label carries, so a second pass in the same
|
|
2
|
+
* render leaves it alone. Generated code that rebuilds its text nodes clears it by construction. */
|
|
3
|
+
export declare const LABEL_CONTRAST_DONE_ATTR = "data-lch-contrast";
|
|
4
|
+
/** How many shapes / texts the pass will consider before declining. Layout reads in a loop, so
|
|
5
|
+
* it is a real ceiling: a 5,000-cell heatmap is the size of thing this exists for, a 50,000-node
|
|
6
|
+
* scatter is not, and on the latter the pass costs more than the defect it would fix. */
|
|
7
|
+
export declare const LABEL_CONTRAST_CAP = 2000;
|
|
8
|
+
export interface LabelContrastOptions {
|
|
9
|
+
/** The opaque canvas the marks sit on. Default white; a host with a themed or high-contrast
|
|
10
|
+
* background passes it, or every "is this label readable" answer is measured against the
|
|
11
|
+
* wrong page. */
|
|
12
|
+
pageBg?: string;
|
|
13
|
+
/** Override the DOM ceiling. */
|
|
14
|
+
cap?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface LabelContrastReport {
|
|
17
|
+
/** Filled shapes harvested as candidate backings. */
|
|
18
|
+
rects: number;
|
|
19
|
+
/** Labels that resolved a backing and were judged. */
|
|
20
|
+
scanned: number;
|
|
21
|
+
/** Labels recoloured. `fixed === scanned` is the module's own tell for a pass that is not
|
|
22
|
+
* measuring anything - see the incident notes in labelContrast.ts. */
|
|
23
|
+
fixed: number;
|
|
24
|
+
/** Translucent backdrops boosted to opaque so they actually back their label. */
|
|
25
|
+
pillsBoosted: number;
|
|
26
|
+
/** Labels whose candidates' BOXES contained them but whose FILLS did not - a ring's hole. */
|
|
27
|
+
offFill: number;
|
|
28
|
+
/** Labels mostly over the page with one end clipping a mark - handed back to the chart. */
|
|
29
|
+
pageMajority: number;
|
|
30
|
+
/** Why the pass did nothing, when it did nothing. */
|
|
31
|
+
skipped?: "no-container" | "no-shapes" | "too-many-shapes" | "too-many-texts" | "error";
|
|
32
|
+
}
|
|
33
|
+
export declare function applyLabelContrast(container: HTMLElement | null | undefined, opts?: LabelContrastOptions): LabelContrastReport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.74",
|
|
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",
|