@bicharts/chart-host 0.5.76 → 0.5.78
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-N6XJDJRN.mjs → chunk-WI2R6E6U.mjs} +312 -1
- package/dist/index.mjs +26 -4
- package/dist/react.mjs +1 -1
- package/dist/types/fit.d.ts +56 -0
- package/dist/types/fitDom.d.ts +31 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/qualifyFilter.d.ts +29 -3
- package/package.json +1 -1
|
@@ -1813,6 +1813,64 @@ function planFrameGrow(inkBottomPx, elHeightPx, viewBoxH, ctmScale, slackPx = SC
|
|
|
1813
1813
|
if (!(ctmScale > 0) || !isFinite(ctmScale)) return none("no-scale");
|
|
1814
1814
|
return { grow: true, heightPx, viewBoxH: viewBoxH + delta / ctmScale, reason: "grow" };
|
|
1815
1815
|
}
|
|
1816
|
+
var AXIS_PIN_MIN_LABELS = 2;
|
|
1817
|
+
var AXIS_PIN_BAND_PAD_PX = 2;
|
|
1818
|
+
var AXIS_PIN_MAX_BAND_FRACTION = 0.4;
|
|
1819
|
+
var AXIS_PIN_TRACK_REACH_PX = 12;
|
|
1820
|
+
function isHorizontalLabelRow(boxes) {
|
|
1821
|
+
if (!boxes || boxes.length < 2) return false;
|
|
1822
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1823
|
+
for (const b2 of boxes) {
|
|
1824
|
+
const cx = (b2.left + b2.right) / 2, cy = (b2.top + b2.bottom) / 2;
|
|
1825
|
+
if (!isFinite(cx) || !isFinite(cy)) return false;
|
|
1826
|
+
if (cx < minX) minX = cx;
|
|
1827
|
+
if (cx > maxX) maxX = cx;
|
|
1828
|
+
if (cy < minY) minY = cy;
|
|
1829
|
+
if (cy > maxY) maxY = cy;
|
|
1830
|
+
}
|
|
1831
|
+
return maxX - minX > maxY - minY;
|
|
1832
|
+
}
|
|
1833
|
+
function labelBand(boxes, tracks = [], pad = AXIS_PIN_BAND_PAD_PX, trackReach = AXIS_PIN_TRACK_REACH_PX) {
|
|
1834
|
+
if (!boxes || boxes.length === 0) return null;
|
|
1835
|
+
let top = Infinity, bottom = -Infinity;
|
|
1836
|
+
for (const b2 of boxes) {
|
|
1837
|
+
if (!isFinite(b2.top) || !isFinite(b2.bottom)) continue;
|
|
1838
|
+
if (b2.top < top) top = b2.top;
|
|
1839
|
+
if (b2.bottom > bottom) bottom = b2.bottom;
|
|
1840
|
+
}
|
|
1841
|
+
if (!isFinite(top) || !isFinite(bottom) || bottom <= top) return null;
|
|
1842
|
+
for (const t of tracks || []) {
|
|
1843
|
+
if (!isFinite(t.top) || !isFinite(t.bottom)) continue;
|
|
1844
|
+
const gap = t.bottom < top ? top - t.bottom : t.top > bottom ? t.top - bottom : 0;
|
|
1845
|
+
if (gap > trackReach) continue;
|
|
1846
|
+
if (t.top < top) top = t.top;
|
|
1847
|
+
if (t.bottom > bottom) bottom = t.bottom;
|
|
1848
|
+
}
|
|
1849
|
+
return { top: top - pad, bottom: bottom + pad };
|
|
1850
|
+
}
|
|
1851
|
+
function planAxisPin(candidates, viewportH, minLabels = AXIS_PIN_MIN_LABELS, maxFraction = AXIS_PIN_MAX_BAND_FRACTION) {
|
|
1852
|
+
const none = (reason) => ({ pin: false, index: -1, band: null, reason });
|
|
1853
|
+
if (!isFinite(viewportH) || viewportH <= 0) return none("unmeasurable");
|
|
1854
|
+
let sawAxis = false, best = null;
|
|
1855
|
+
for (const c of candidates || []) {
|
|
1856
|
+
if (!c || !c.labels || c.labels.length < minLabels) continue;
|
|
1857
|
+
sawAxis = true;
|
|
1858
|
+
if (!isHorizontalLabelRow(c.labels)) continue;
|
|
1859
|
+
if (!best || c.labels.length > best.labels.length) best = c;
|
|
1860
|
+
}
|
|
1861
|
+
if (!best) return none(sawAxis ? "vertical-only" : "no-axis");
|
|
1862
|
+
const band = labelBand(best.labels, best.tracks || []);
|
|
1863
|
+
if (!band) return none("unmeasurable");
|
|
1864
|
+
if (band.bottom - band.top > viewportH * maxFraction) return none("band-too-tall");
|
|
1865
|
+
return { pin: true, index: best.index, band, reason: "pinned" };
|
|
1866
|
+
}
|
|
1867
|
+
function axisPinPlacement(band, scrollTop, viewportH) {
|
|
1868
|
+
if (!band || !isFinite(band.top) || !isFinite(band.bottom)) return null;
|
|
1869
|
+
if (!isFinite(scrollTop) || !isFinite(viewportH) || viewportH <= 0) return null;
|
|
1870
|
+
if (band.top < scrollTop) return "top";
|
|
1871
|
+
if (band.bottom > scrollTop + viewportH) return "bottom";
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1816
1874
|
|
|
1817
1875
|
// src/fitDom.ts
|
|
1818
1876
|
var FIT_WALK_CAP = 8e3;
|
|
@@ -1898,6 +1956,237 @@ function fitReadingFor(lane, c, slackPx = SCROLL_SLACK_PX, inkOf) {
|
|
|
1898
1956
|
scrollH: c.scrollHeight
|
|
1899
1957
|
};
|
|
1900
1958
|
}
|
|
1959
|
+
var noPin = (reason) => ({ pinned: false, reason, bandPx: 0, axisAt: "", labels: 0, carried: 0, edge: null });
|
|
1960
|
+
var CONTAINER_SLOT_AXIS_PIN = "__bicAxisPin";
|
|
1961
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
1962
|
+
var AXIS_PIN_MAX_CARRIED = 12;
|
|
1963
|
+
function unpinScrolledAxis(container) {
|
|
1964
|
+
if (!container) return;
|
|
1965
|
+
const st = container[CONTAINER_SLOT_AXIS_PIN];
|
|
1966
|
+
if (!st) return;
|
|
1967
|
+
try {
|
|
1968
|
+
container.removeEventListener("scroll", st.onScroll);
|
|
1969
|
+
} catch {
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
if (st.overlay.parentNode) st.overlay.parentNode.removeChild(st.overlay);
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
try {
|
|
1976
|
+
delete container[CONTAINER_SLOT_AXIS_PIN];
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function textHidden(t, view) {
|
|
1981
|
+
if (t.getAttribute("display") === "none" || t.getAttribute("visibility") === "hidden") return true;
|
|
1982
|
+
try {
|
|
1983
|
+
const cs = view.getComputedStyle(t);
|
|
1984
|
+
if (cs && (cs.display === "none" || cs.visibility === "hidden")) return true;
|
|
1985
|
+
} catch {
|
|
1986
|
+
}
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
function backdropFill(svg, container, view, svgBox) {
|
|
1990
|
+
try {
|
|
1991
|
+
const rects = svg.querySelectorAll("rect");
|
|
1992
|
+
for (let i2 = 0; i2 < rects.length && i2 < 6; i2++) {
|
|
1993
|
+
const r = rects[i2];
|
|
1994
|
+
if (r.closest(".d3-mark")) continue;
|
|
1995
|
+
const b2 = r.getBoundingClientRect();
|
|
1996
|
+
if (b2.width >= svgBox.width * 0.9 && b2.height >= svgBox.height * 0.9) {
|
|
1997
|
+
let fill = "";
|
|
1998
|
+
try {
|
|
1999
|
+
fill = view.getComputedStyle(r).fill || "";
|
|
2000
|
+
} catch {
|
|
2001
|
+
}
|
|
2002
|
+
if (!fill || fill === "none") fill = r.getAttribute("fill") || "";
|
|
2003
|
+
if (fill && fill !== "none" && fill !== "transparent") return fill;
|
|
2004
|
+
break;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
} catch {
|
|
2008
|
+
}
|
|
2009
|
+
try {
|
|
2010
|
+
let el = container;
|
|
2011
|
+
for (let hops = 0; el && hops < 12; hops++, el = el.parentElement) {
|
|
2012
|
+
const bg = view.getComputedStyle(el).backgroundColor;
|
|
2013
|
+
if (bg && bg !== "transparent" && bg !== "rgba(0, 0, 0, 0)") return bg;
|
|
2014
|
+
}
|
|
2015
|
+
} catch {
|
|
2016
|
+
}
|
|
2017
|
+
return "#ffffff";
|
|
2018
|
+
}
|
|
2019
|
+
function cloneWithChain(el, svg, root) {
|
|
2020
|
+
const chain = [];
|
|
2021
|
+
let n = el.parentElement;
|
|
2022
|
+
while (n && n !== svg) {
|
|
2023
|
+
chain.unshift(n);
|
|
2024
|
+
n = n.parentElement;
|
|
2025
|
+
}
|
|
2026
|
+
let parent = root;
|
|
2027
|
+
for (const anc of chain) {
|
|
2028
|
+
const c = anc.cloneNode(false);
|
|
2029
|
+
parent.appendChild(c);
|
|
2030
|
+
parent = c;
|
|
2031
|
+
}
|
|
2032
|
+
parent.appendChild(el.cloneNode(true));
|
|
2033
|
+
const ids = root.querySelectorAll("[id]");
|
|
2034
|
+
for (let i2 = 0; i2 < ids.length; i2++) ids[i2].removeAttribute("id");
|
|
2035
|
+
}
|
|
2036
|
+
function pinScrolledAxis(container, svg) {
|
|
2037
|
+
unpinScrolledAxis(container);
|
|
2038
|
+
if (!container) return noPin("no-container");
|
|
2039
|
+
if (!svg) return noPin("no-svg");
|
|
2040
|
+
try {
|
|
2041
|
+
const doc = svg.ownerDocument;
|
|
2042
|
+
const view = doc && doc.defaultView || globalThis;
|
|
2043
|
+
const svgBox = svg.getBoundingClientRect();
|
|
2044
|
+
if (!(svgBox.width > 0) || !(svgBox.height > 0)) return noPin("unmeasurable");
|
|
2045
|
+
const m2 = typeof svg.getScreenCTM === "function" ? svg.getScreenCTM() : null;
|
|
2046
|
+
if (!m2 || !(m2.a > 0) || !(m2.d > 0) || !isFinite(m2.e) || !isFinite(m2.f)) return noPin("no-ctm");
|
|
2047
|
+
const viewportH = container.clientHeight;
|
|
2048
|
+
if (!(viewportH > 0)) return noPin("unmeasurable");
|
|
2049
|
+
const rel = (r) => ({ left: r.left - svgBox.left, top: r.top - svgBox.top, right: r.right - svgBox.left, bottom: r.bottom - svgBox.top });
|
|
2050
|
+
const ticks = svg.querySelectorAll(".tick");
|
|
2051
|
+
const byAxis = /* @__PURE__ */ new Map();
|
|
2052
|
+
for (let i2 = 0; i2 < ticks.length; i2++) {
|
|
2053
|
+
const t = ticks[i2];
|
|
2054
|
+
if (t.closest("svg") !== svg) continue;
|
|
2055
|
+
const p2 = t.parentElement;
|
|
2056
|
+
if (!p2) continue;
|
|
2057
|
+
const list = byAxis.get(p2);
|
|
2058
|
+
if (list) list.push(t);
|
|
2059
|
+
else byAxis.set(p2, [t]);
|
|
2060
|
+
}
|
|
2061
|
+
const axes = [];
|
|
2062
|
+
const cands = [];
|
|
2063
|
+
byAxis.forEach((group, axis2) => {
|
|
2064
|
+
const labels = [];
|
|
2065
|
+
for (const tick of group) {
|
|
2066
|
+
const txt = tick.querySelector("text");
|
|
2067
|
+
if (!txt || textHidden(txt, view)) continue;
|
|
2068
|
+
const r = txt.getBoundingClientRect();
|
|
2069
|
+
if (!(r.width > 0) || !(r.height > 0)) continue;
|
|
2070
|
+
labels.push(rel(r));
|
|
2071
|
+
}
|
|
2072
|
+
const tracks = [];
|
|
2073
|
+
for (let i2 = 0; i2 < axis2.children.length; i2++) {
|
|
2074
|
+
const ch = axis2.children[i2];
|
|
2075
|
+
if (ch.tagName.toLowerCase() !== "path" || !(ch.getAttribute("class") || "").split(/\s+/).includes("domain")) continue;
|
|
2076
|
+
const r = ch.getBoundingClientRect();
|
|
2077
|
+
if (r.width > 0 && r.height <= 4) tracks.push(rel(r));
|
|
2078
|
+
}
|
|
2079
|
+
cands.push({ index: axes.length, labels, tracks });
|
|
2080
|
+
axes.push(axis2);
|
|
2081
|
+
});
|
|
2082
|
+
const plan = planAxisPin(cands, viewportH);
|
|
2083
|
+
if (!plan.pin || !plan.band) return noPin(plan.reason);
|
|
2084
|
+
const axis = axes[plan.index];
|
|
2085
|
+
const band = { top: Math.max(0, plan.band.top), bottom: Math.min(svgBox.height, plan.band.bottom) };
|
|
2086
|
+
const bandPx = band.bottom - band.top;
|
|
2087
|
+
if (!(bandPx > 0)) return noPin("unmeasurable");
|
|
2088
|
+
const carried = [];
|
|
2089
|
+
const texts = svg.querySelectorAll("text");
|
|
2090
|
+
for (let i2 = 0; i2 < texts.length && carried.length < AXIS_PIN_MAX_CARRIED; i2++) {
|
|
2091
|
+
const t = texts[i2];
|
|
2092
|
+
if (t.closest("svg") !== svg) continue;
|
|
2093
|
+
if (t.closest(".tick") || t.closest(".d3-mark") || t.closest(".d3-legend-mark")) continue;
|
|
2094
|
+
if (axis.contains(t) || textHidden(t, view)) continue;
|
|
2095
|
+
const r = rel(t.getBoundingClientRect());
|
|
2096
|
+
if (!(r.right > r.left) || !(r.bottom > r.top)) continue;
|
|
2097
|
+
if (r.top >= band.top && r.bottom <= band.bottom) carried.push(t);
|
|
2098
|
+
}
|
|
2099
|
+
const pinSvg = doc.createElementNS(SVG_NS2, "svg");
|
|
2100
|
+
for (const a2 of ["font-family", "font-size", "font-weight", "font", "fill", "color", "text-rendering"]) {
|
|
2101
|
+
const v2 = svg.getAttribute(a2);
|
|
2102
|
+
if (v2) pinSvg.setAttribute(a2, v2);
|
|
2103
|
+
}
|
|
2104
|
+
const styles = svg.querySelectorAll("style");
|
|
2105
|
+
for (let i2 = 0; i2 < styles.length; i2++) pinSvg.appendChild(styles[i2].cloneNode(true));
|
|
2106
|
+
const vbX = (svgBox.left - m2.e) / m2.a, vbY = (svgBox.top + band.top - m2.f) / m2.d;
|
|
2107
|
+
const vbW = svgBox.width / m2.a, vbH = bandPx / m2.d;
|
|
2108
|
+
pinSvg.setAttribute("viewBox", `${vbX} ${vbY} ${vbW} ${vbH}`);
|
|
2109
|
+
pinSvg.setAttribute("preserveAspectRatio", "none");
|
|
2110
|
+
pinSvg.setAttribute("width", String(svgBox.width));
|
|
2111
|
+
pinSvg.setAttribute("height", String(bandPx));
|
|
2112
|
+
pinSvg.setAttribute("aria-hidden", "true");
|
|
2113
|
+
pinSvg.style.display = "block";
|
|
2114
|
+
pinSvg.style.pointerEvents = "none";
|
|
2115
|
+
const back = doc.createElementNS(SVG_NS2, "rect");
|
|
2116
|
+
back.setAttribute("x", String(vbX));
|
|
2117
|
+
back.setAttribute("y", String(vbY));
|
|
2118
|
+
back.setAttribute("width", String(vbW));
|
|
2119
|
+
back.setAttribute("height", String(vbH));
|
|
2120
|
+
back.setAttribute("fill", backdropFill(svg, container, view, svgBox));
|
|
2121
|
+
pinSvg.appendChild(back);
|
|
2122
|
+
cloneWithChain(axis, svg, pinSvg);
|
|
2123
|
+
for (const t of carried) cloneWithChain(t, svg, pinSvg);
|
|
2124
|
+
const overlay = doc.createElement("div");
|
|
2125
|
+
overlay.setAttribute("data-bic-axis-pin", "1");
|
|
2126
|
+
const o2 = overlay.style;
|
|
2127
|
+
o2.position = "absolute";
|
|
2128
|
+
o2.margin = "0";
|
|
2129
|
+
o2.padding = "0";
|
|
2130
|
+
o2.border = "0";
|
|
2131
|
+
o2.width = `${svgBox.width}px`;
|
|
2132
|
+
o2.height = `${bandPx}px`;
|
|
2133
|
+
o2.overflow = "hidden";
|
|
2134
|
+
o2.pointerEvents = "none";
|
|
2135
|
+
o2.zIndex = "3";
|
|
2136
|
+
o2.display = "none";
|
|
2137
|
+
overlay.appendChild(pinSvg);
|
|
2138
|
+
try {
|
|
2139
|
+
const cs = view.getComputedStyle(container);
|
|
2140
|
+
const pos = cs && cs.position || container.style.position;
|
|
2141
|
+
if (!pos || pos === "static") container.style.position = "relative";
|
|
2142
|
+
} catch {
|
|
2143
|
+
container.style.position = container.style.position || "relative";
|
|
2144
|
+
}
|
|
2145
|
+
const cRect = container.getBoundingClientRect();
|
|
2146
|
+
const svgLeft = svgBox.left - cRect.left - container.clientLeft + container.scrollLeft;
|
|
2147
|
+
const svgTop = svgBox.top - cRect.top - container.clientTop + container.scrollTop;
|
|
2148
|
+
const contentBand = { top: svgTop + band.top, bottom: svgTop + band.bottom };
|
|
2149
|
+
const place = () => {
|
|
2150
|
+
const st = container.scrollTop, vh = container.clientHeight;
|
|
2151
|
+
const edge2 = axisPinPlacement(contentBand, st, vh);
|
|
2152
|
+
if (!edge2) {
|
|
2153
|
+
o2.display = "none";
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
o2.display = "block";
|
|
2157
|
+
o2.left = `${svgLeft}px`;
|
|
2158
|
+
o2.top = `${edge2 === "top" ? st : st + vh - bandPx}px`;
|
|
2159
|
+
return edge2;
|
|
2160
|
+
};
|
|
2161
|
+
const onScroll = () => {
|
|
2162
|
+
try {
|
|
2163
|
+
if (!overlay.isConnected) {
|
|
2164
|
+
unpinScrolledAxis(container);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
place();
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
container.appendChild(overlay);
|
|
2172
|
+
container.addEventListener("scroll", onScroll, { passive: true });
|
|
2173
|
+
container[CONTAINER_SLOT_AXIS_PIN] = { overlay, onScroll };
|
|
2174
|
+
const edge = place();
|
|
2175
|
+
const mid = (band.top + band.bottom) / 2 / svgBox.height;
|
|
2176
|
+
return {
|
|
2177
|
+
pinned: true,
|
|
2178
|
+
reason: "pinned",
|
|
2179
|
+
bandPx: Math.round(bandPx),
|
|
2180
|
+
axisAt: mid < 1 / 3 ? "top" : mid > 2 / 3 ? "bottom" : "middle",
|
|
2181
|
+
labels: cands[plan.index].labels.length,
|
|
2182
|
+
carried: carried.length,
|
|
2183
|
+
edge
|
|
2184
|
+
};
|
|
2185
|
+
} catch {
|
|
2186
|
+
unpinScrolledAxis(container);
|
|
2187
|
+
return noPin("threw");
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
1901
2190
|
function fitRenderedChart(container, opts = {}) {
|
|
1902
2191
|
const slackPx = opts.slackPx ?? SCROLL_SLACK_PX;
|
|
1903
2192
|
const result = {
|
|
@@ -1907,7 +2196,8 @@ function fitRenderedChart(container, opts = {}) {
|
|
|
1907
2196
|
growReason: "no-container",
|
|
1908
2197
|
overflowX: "hidden",
|
|
1909
2198
|
overflowY: "hidden",
|
|
1910
|
-
reading: null
|
|
2199
|
+
reading: null,
|
|
2200
|
+
axisPin: noPin("no-container")
|
|
1911
2201
|
};
|
|
1912
2202
|
if (!container) return result;
|
|
1913
2203
|
try {
|
|
@@ -1968,6 +2258,16 @@ function fitRenderedChart(container, opts = {}) {
|
|
|
1968
2258
|
if (opts.applyOverflow !== false) {
|
|
1969
2259
|
container.style.overflowX = fit.overflowX;
|
|
1970
2260
|
container.style.overflowY = fit.overflowY;
|
|
2261
|
+
if (fit.overflowY !== "auto") {
|
|
2262
|
+
unpinScrolledAxis(container);
|
|
2263
|
+
result.axisPin = noPin("not-scrolling");
|
|
2264
|
+
} else if (opts.pinAxis === false) {
|
|
2265
|
+
unpinScrolledAxis(container);
|
|
2266
|
+
result.axisPin = noPin("disabled");
|
|
2267
|
+
} else result.axisPin = pinScrolledAxis(container, svg);
|
|
2268
|
+
} else {
|
|
2269
|
+
unpinScrolledAxis(container);
|
|
2270
|
+
result.axisPin = noPin("overflow-not-applied");
|
|
1971
2271
|
}
|
|
1972
2272
|
} catch {
|
|
1973
2273
|
}
|
|
@@ -2631,6 +2931,7 @@ function createChartHost(container, config) {
|
|
|
2631
2931
|
container.removeEventListener(XFILTER_REFRESH_EVENT, onXf);
|
|
2632
2932
|
container.removeEventListener("click", onClick);
|
|
2633
2933
|
subs.clear();
|
|
2934
|
+
unpinScrolledAxis(container);
|
|
2634
2935
|
container.innerHTML = "";
|
|
2635
2936
|
}
|
|
2636
2937
|
};
|
|
@@ -2703,10 +3004,20 @@ export {
|
|
|
2703
3004
|
isPhantomBox,
|
|
2704
3005
|
MAX_FRAME_GROW_FACTOR,
|
|
2705
3006
|
planFrameGrow,
|
|
3007
|
+
AXIS_PIN_MIN_LABELS,
|
|
3008
|
+
AXIS_PIN_BAND_PAD_PX,
|
|
3009
|
+
AXIS_PIN_MAX_BAND_FRACTION,
|
|
3010
|
+
AXIS_PIN_TRACK_REACH_PX,
|
|
3011
|
+
isHorizontalLabelRow,
|
|
3012
|
+
labelBand,
|
|
3013
|
+
planAxisPin,
|
|
3014
|
+
axisPinPlacement,
|
|
2706
3015
|
ctmScaleOf,
|
|
2707
3016
|
svgInkReach,
|
|
2708
3017
|
measureContainerBoxes,
|
|
2709
3018
|
fitReadingFor,
|
|
3019
|
+
unpinScrolledAxis,
|
|
3020
|
+
pinScrolledAxis,
|
|
2710
3021
|
fitRenderedChart,
|
|
2711
3022
|
DARK_TEXT,
|
|
2712
3023
|
LIGHT_TEXT,
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
ANIM_PLAY_SPEED_MIN,
|
|
12
12
|
APPROXIMATE_POSITIONS_DEFAULT,
|
|
13
13
|
AXIS_FILTER_CLASS,
|
|
14
|
+
AXIS_PIN_BAND_PAD_PX,
|
|
15
|
+
AXIS_PIN_MAX_BAND_FRACTION,
|
|
16
|
+
AXIS_PIN_MIN_LABELS,
|
|
17
|
+
AXIS_PIN_TRACK_REACH_PX,
|
|
14
18
|
BACKING_MAJORITY,
|
|
15
19
|
COLOR_SCALE_SELF_CLAMP_PCT_DEFAULT,
|
|
16
20
|
COLOR_SCALE_SELF_CLAMP_PCT_MAX,
|
|
@@ -56,6 +60,7 @@ import {
|
|
|
56
60
|
XFILTER_REFRESH_EVENT,
|
|
57
61
|
_e,
|
|
58
62
|
applyLabelContrast,
|
|
63
|
+
axisPinPlacement,
|
|
59
64
|
backingHoldsGlyph,
|
|
60
65
|
blankRenderFlag,
|
|
61
66
|
buildRenderPayload,
|
|
@@ -81,14 +86,18 @@ import {
|
|
|
81
86
|
glyphSampleGrid,
|
|
82
87
|
hitBandFlag,
|
|
83
88
|
isBlankRender,
|
|
89
|
+
isHorizontalLabelRow,
|
|
84
90
|
isPhantomBox,
|
|
85
91
|
isPillBackdropAlpha,
|
|
92
|
+
labelBand,
|
|
86
93
|
loadGeo,
|
|
87
94
|
measureContainerBoxes,
|
|
88
95
|
needsScroll,
|
|
89
96
|
noopViewStateProvider,
|
|
90
97
|
periodTickSuppressesFeedback,
|
|
91
98
|
pillBacksGlyph,
|
|
99
|
+
pinScrolledAxis,
|
|
100
|
+
planAxisPin,
|
|
92
101
|
planFrameGrow,
|
|
93
102
|
registerGeo,
|
|
94
103
|
registerGeoAsset,
|
|
@@ -99,8 +108,9 @@ import {
|
|
|
99
108
|
sessionViewStateProvider,
|
|
100
109
|
stripEsmExports,
|
|
101
110
|
svgInkReach,
|
|
102
|
-
toRGBA
|
|
103
|
-
|
|
111
|
+
toRGBA,
|
|
112
|
+
unpinScrolledAxis
|
|
113
|
+
} from "./chunk-WI2R6E6U.mjs";
|
|
104
114
|
import "./chunk-A2GMXZP7.mjs";
|
|
105
115
|
import "./chunk-GHODWOAL.mjs";
|
|
106
116
|
|
|
@@ -795,7 +805,9 @@ function computeQualifyFilterView(groups, term) {
|
|
|
795
805
|
};
|
|
796
806
|
}
|
|
797
807
|
function qualifyFilterCountText(view) {
|
|
798
|
-
|
|
808
|
+
if (view.tier === "all") return String(view.total);
|
|
809
|
+
const below = typeof view.refusalMatched === "number" && view.refusalMatched > 0 ? ` (+${view.refusalMatched})` : "";
|
|
810
|
+
return `${view.matched} of ${view.total}${below}`;
|
|
799
811
|
}
|
|
800
812
|
function inlineFilterGate(rowCount) {
|
|
801
813
|
return rowCount >= INLINE_FILTER_MIN_ROWS ? { show: true, why: "shown" } : { show: false, why: "no-overflow" };
|
|
@@ -1033,6 +1045,10 @@ export {
|
|
|
1033
1045
|
ANIM_PLAY_SPEED_MIN,
|
|
1034
1046
|
APPROXIMATE_POSITIONS_DEFAULT,
|
|
1035
1047
|
AXIS_FILTER_CLASS,
|
|
1048
|
+
AXIS_PIN_BAND_PAD_PX,
|
|
1049
|
+
AXIS_PIN_MAX_BAND_FRACTION,
|
|
1050
|
+
AXIS_PIN_MIN_LABELS,
|
|
1051
|
+
AXIS_PIN_TRACK_REACH_PX,
|
|
1036
1052
|
BACKING_MAJORITY,
|
|
1037
1053
|
CHOOSER_MIN_HEIGHT_PX,
|
|
1038
1054
|
CHOOSER_MIN_WIDTH_PX,
|
|
@@ -1082,6 +1098,7 @@ export {
|
|
|
1082
1098
|
actionFor,
|
|
1083
1099
|
applyLabelContrast,
|
|
1084
1100
|
askApplyImprovements,
|
|
1101
|
+
axisPinPlacement,
|
|
1085
1102
|
backingHoldsGlyph,
|
|
1086
1103
|
bareBase64,
|
|
1087
1104
|
blankRenderFlag,
|
|
@@ -1120,8 +1137,10 @@ export {
|
|
|
1120
1137
|
hitBandFlag,
|
|
1121
1138
|
inlineFilterGate,
|
|
1122
1139
|
isBlankRender,
|
|
1140
|
+
isHorizontalLabelRow,
|
|
1123
1141
|
isPhantomBox,
|
|
1124
1142
|
isPillBackdropAlpha,
|
|
1143
|
+
labelBand,
|
|
1125
1144
|
launchFavorStyle,
|
|
1126
1145
|
launchGenerates,
|
|
1127
1146
|
listNeedsFilter,
|
|
@@ -1136,6 +1155,8 @@ export {
|
|
|
1136
1155
|
orderRefusalsForDisplay,
|
|
1137
1156
|
periodTickSuppressesFeedback,
|
|
1138
1157
|
pillBacksGlyph,
|
|
1158
|
+
pinScrolledAxis,
|
|
1159
|
+
planAxisPin,
|
|
1139
1160
|
planFrameGrow,
|
|
1140
1161
|
planTrivialChart,
|
|
1141
1162
|
qualifyAuto,
|
|
@@ -1165,5 +1186,6 @@ export {
|
|
|
1165
1186
|
svgInkReach,
|
|
1166
1187
|
svgNaturalSize,
|
|
1167
1188
|
svgToDataUrl,
|
|
1168
|
-
toRGBA
|
|
1189
|
+
toRGBA,
|
|
1190
|
+
unpinScrolledAxis
|
|
1169
1191
|
};
|
package/dist/react.mjs
CHANGED
package/dist/types/fit.d.ts
CHANGED
|
@@ -34,3 +34,59 @@ export interface FrameGrowPlan {
|
|
|
34
34
|
* @param ctmScale px per user unit, from the live screen CTM
|
|
35
35
|
*/
|
|
36
36
|
export declare function planFrameGrow(inkBottomPx: number, elHeightPx: number, viewBoxH: number, ctmScale: number, slackPx?: number, maxFactor?: number): FrameGrowPlan;
|
|
37
|
+
export declare const AXIS_PIN_MIN_LABELS = 2;
|
|
38
|
+
export declare const AXIS_PIN_BAND_PAD_PX = 2;
|
|
39
|
+
export declare const AXIS_PIN_MAX_BAND_FRACTION = 0.4;
|
|
40
|
+
export declare const AXIS_PIN_TRACK_REACH_PX = 12;
|
|
41
|
+
export interface LabelRowBox {
|
|
42
|
+
left: number;
|
|
43
|
+
top: number;
|
|
44
|
+
right: number;
|
|
45
|
+
bottom: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Does a row of label boxes run ACROSS the chart rather than DOWN it? A horizontal axis spreads
|
|
49
|
+
* its label centres over x; a y-axis spreads them over y, and pinning a y-axis would pin the
|
|
50
|
+
* ROWS - the exact thing the scroll exists to reveal. Ties are vertical: a single label has no
|
|
51
|
+
* direction and is not an axis.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isHorizontalLabelRow(boxes: LabelRowBox[]): boolean;
|
|
54
|
+
export interface AxisBand {
|
|
55
|
+
top: number;
|
|
56
|
+
bottom: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The vertical band a label row occupies, padded, widened to take in a thin track (the domain
|
|
60
|
+
* line) that sits within reach of the labels. Tracks further away are ignored on purpose: a
|
|
61
|
+
* gridline drawn as a full-height tick line is part of the plot, not of the header.
|
|
62
|
+
*/
|
|
63
|
+
export declare function labelBand(boxes: LabelRowBox[], tracks?: LabelRowBox[], pad?: number, trackReach?: number): AxisBand | null;
|
|
64
|
+
export interface AxisPinCandidate {
|
|
65
|
+
/** The caller's handle for this axis - returned in the plan. */
|
|
66
|
+
index: number;
|
|
67
|
+
/** One box per VISIBLE tick label, in any one consistent coordinate space. */
|
|
68
|
+
labels: LabelRowBox[];
|
|
69
|
+
/** Thin tracks that may belong to the band (a horizontal domain line). Optional. */
|
|
70
|
+
tracks?: LabelRowBox[];
|
|
71
|
+
}
|
|
72
|
+
export interface AxisPinPlan {
|
|
73
|
+
pin: boolean;
|
|
74
|
+
index: number;
|
|
75
|
+
band: AxisBand | null;
|
|
76
|
+
reason: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Which axis to pin, and the band it occupies. Coordinates are whatever the caller measured in;
|
|
80
|
+
* the plan only compares them, and `viewportH` (same units) bounds the band. With two horizontal
|
|
81
|
+
* axes the one with more labels wins - it is the one carrying the scale.
|
|
82
|
+
*/
|
|
83
|
+
export declare function planAxisPin(candidates: AxisPinCandidate[], viewportH: number, minLabels?: number, maxFraction?: number): AxisPinPlan;
|
|
84
|
+
export type AxisPinEdge = "top" | "bottom";
|
|
85
|
+
/**
|
|
86
|
+
* Where the pinned copy sits for this scroll position, or null when the original axis is in view
|
|
87
|
+
* and the copy must get out of the way. `band` is in CONTENT coordinates (the scroll origin), the
|
|
88
|
+
* viewport is [scrollTop, scrollTop + viewportH]. The copy appears as soon as the original is CUT
|
|
89
|
+
* by an edge, not only once it has fully left: a half-visible label row reads worse than a whole
|
|
90
|
+
* one drawn over it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function axisPinPlacement(band: AxisBand, scrollTop: number, viewportH: number): AxisPinEdge | null;
|
package/dist/types/fitDom.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentExtent, MeasuredBox } from "./fit";
|
|
1
|
+
import type { ContentExtent, MeasuredBox, AxisPinEdge } from "./fit";
|
|
2
2
|
export declare function ctmScaleOf(m: {
|
|
3
3
|
a: number;
|
|
4
4
|
b: number;
|
|
@@ -43,6 +43,13 @@ export interface FitRenderedChartOptions {
|
|
|
43
43
|
* its own scrolling and only wants to be told.
|
|
44
44
|
*/
|
|
45
45
|
applyOverflow?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* When the container scrolls vertically, keep the chart's horizontal axis in view by pinning
|
|
48
|
+
* a copy of it at the viewport edge while the original is scrolled out. Default true. The
|
|
49
|
+
* copy hides itself whenever the original is on screen, so there is never a second axis.
|
|
50
|
+
* Set false for a host that draws its own sticky header.
|
|
51
|
+
*/
|
|
52
|
+
pinAxis?: boolean;
|
|
46
53
|
}
|
|
47
54
|
export interface FitRenderedChartResult {
|
|
48
55
|
/** The frame was grown, so content that was being clipped is now painted. */
|
|
@@ -55,5 +62,28 @@ export interface FitRenderedChartResult {
|
|
|
55
62
|
overflowY: "hidden" | "auto";
|
|
56
63
|
/** The post-grow reading, or null when nothing could be measured. */
|
|
57
64
|
reading: FitReading | null;
|
|
65
|
+
/** The axis pin: what was pinned when the container scrolls, or why nothing was. */
|
|
66
|
+
axisPin: AxisPinReport;
|
|
67
|
+
}
|
|
68
|
+
export interface AxisPinReport {
|
|
69
|
+
pinned: boolean;
|
|
70
|
+
/** "pinned", or the fail-open reason: not-scrolling, disabled, no-svg, no-ctm, no-axis,
|
|
71
|
+
* vertical-only, band-too-tall, unmeasurable, overflow-not-applied, threw. */
|
|
72
|
+
reason: string;
|
|
73
|
+
/** The pinned band's height on screen, px. */
|
|
74
|
+
bandPx: number;
|
|
75
|
+
/** Where the ORIGINAL axis sits in the chart body: top third, bottom third, or between. */
|
|
76
|
+
axisAt: "top" | "bottom" | "middle" | "";
|
|
77
|
+
/** Visible tick labels in the copy. */
|
|
78
|
+
labels: number;
|
|
79
|
+
/** Furniture texts that shared the band and came along (a "Today" caption, an axis title). */
|
|
80
|
+
carried: number;
|
|
81
|
+
/** Where the copy sat at the moment of the pass - null when the original was in view. */
|
|
82
|
+
edge: AxisPinEdge | null;
|
|
58
83
|
}
|
|
84
|
+
/** Remove a previous render's pinned axis - its overlay and its scroll listener. Safe to call
|
|
85
|
+
* when there is none. Hosts that clear the container themselves still need this for the
|
|
86
|
+
* listener, which outlives the overlay. */
|
|
87
|
+
export declare function unpinScrolledAxis(container: HTMLElement | null | undefined): void;
|
|
88
|
+
export declare function pinScrolledAxis(container: HTMLElement | null | undefined, svg: SVGSVGElement | null | undefined): AxisPinReport;
|
|
59
89
|
export declare function fitRenderedChart(container: HTMLElement | null | undefined, opts?: FitRenderedChartOptions): FitRenderedChartResult;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -22,3 +22,5 @@ export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type Blan
|
|
|
22
22
|
export { censusHitBands, hitBandFlag, MIN_HIT_BAND_PX, type HitBandCensus } from "./hitBands";
|
|
23
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";
|
|
24
24
|
export { fitRenderedChart, fitReadingFor, measureContainerBoxes, svgInkReach, ctmScaleOf, type FitReading, type InkReach, type FitRenderedChartOptions, type FitRenderedChartResult, } from "./fitDom";
|
|
25
|
+
export { AXIS_PIN_MIN_LABELS, AXIS_PIN_BAND_PAD_PX, AXIS_PIN_MAX_BAND_FRACTION, AXIS_PIN_TRACK_REACH_PX, isHorizontalLabelRow, labelBand, planAxisPin, axisPinPlacement, type LabelRowBox, type AxisBand, type AxisPinCandidate, type AxisPinPlan, type AxisPinEdge, } from "./fit";
|
|
26
|
+
export { pinScrolledAxis, unpinScrolledAxis, type AxisPinReport } from "./fitDom";
|
|
@@ -275,14 +275,40 @@ export interface QualifyFilterView<E> {
|
|
|
275
275
|
* groups were built, so a host writing them in sequence cannot reorder anything by accident.
|
|
276
276
|
*/
|
|
277
277
|
export declare function computeQualifyFilterView<E>(groups: readonly QualifyFilterGroup<E>[], term: string | null | undefined): QualifyFilterView<E>;
|
|
278
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* The counter beside the box: "12 of 137" while filtering, the plain total otherwise - and
|
|
280
|
+
* "0 of 19 (+1)" when the term also reached the refusal block.
|
|
281
|
+
*
|
|
282
|
+
* Digits only, assembled here so both hosts show the same shape of number - the words around it
|
|
283
|
+
* (there are none today) would be the host's business, the arithmetic is not.
|
|
284
|
+
*
|
|
285
|
+
* THE PARENTHESISED COUNT IS THE SAME DEFECT THE NOTE WAS ALREADY FIXED FOR, one control over.
|
|
279
286
|
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
287
|
+
* Genesis (2026-09-06): a card with six rows and one measure
|
|
288
|
+
* bound, "box" typed. `Box plot` was on screen under "Poor fit for these fields" carrying the
|
|
289
|
+
* gate's own sentence, the note said where it had gone - and the counter beside the input read
|
|
290
|
+
* "0 of 19". That was TRUE of the fitting list, which is all this function had ever been given,
|
|
291
|
+
* and it is read as a claim about the DIALOG: a reader who takes the number and stops has been
|
|
292
|
+
* told nothing was found, three lines above the row they asked for. The same shape of error as
|
|
293
|
+
* the `descAndRefusalName` note (a weaker scope silencing a stronger one), and the count it
|
|
294
|
+
* needed was already being computed for the log and simply never reached the screen.
|
|
295
|
+
*
|
|
296
|
+
* IT FIRES ON EVERY FILTERED TERM WITH A REFUSAL MATCH, not only when the fitting list came back
|
|
297
|
+
* empty - and that is deliberate, because the collapsed case is the one with no other signal at
|
|
298
|
+
* all. When `openRefusals` is true the reader can see the row and the note explains it; when the
|
|
299
|
+
* fitting list answered by name the block stays shut and the note stays silent (correctly - a
|
|
300
|
+
* section forced open on every shared word would be noise), so this "(+1)" is the ONLY thing
|
|
301
|
+
* telling them the checkbox below has something of theirs behind it.
|
|
302
|
+
*
|
|
303
|
+
* NEVER FOLDED INTO `matched`. "0 of 19" and the "(+1)" count two different populations - 19 is
|
|
304
|
+
* the types that FIT, and the catalogue behind the checkbox is not in that denominator. Adding
|
|
305
|
+
* them would produce a ratio that is true of no list on screen.
|
|
306
|
+
*/
|
|
282
307
|
export declare function qualifyFilterCountText(view: {
|
|
283
308
|
tier: QualifyFilterTier;
|
|
284
309
|
matched: number;
|
|
285
310
|
total: number;
|
|
311
|
+
refusalMatched?: number;
|
|
286
312
|
}): string;
|
|
287
313
|
/**
|
|
288
314
|
* The same question for a host whose chooser is an INLINE, SCROLLING PANEL - and it has no size
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bicharts/chart-host",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.78",
|
|
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",
|