ngraph.svg
Version:
SVG-based graph visualization library with adaptive rendering
1,413 lines (1,412 loc) • 123 kB
JavaScript
var ut = Object.defineProperty;
var mt = (a, t, e) => t in a ? ut(a, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[t] = e;
var st = (a, t, e) => mt(a, typeof t != "symbol" ? t + "" : t, e);
import pt from "ngraph.graph";
class gt {
constructor() {
this.viewBox = { left: -100, top: -100, right: 100, bottom: 100 }, this.transform = { scale: 1, x: 0, y: 0 }, this.width = 0, this.height = 0, this.pixelRatio = typeof window < "u" ? window.devicePixelRatio : 1, this._visibleBounds = { left: 0, top: 0, right: 0, bottom: 0 }, this._visibleBoundsDirty = !0, this._reusableScenePoint = { x: 0, y: 0 }, this._reusableScreenPoint = { x: 0, y: 0 };
}
/**
* Update the viewport dimensions
*/
setSize(t, e) {
this.width = t, this.height = e, this._visibleBoundsDirty = !0;
}
/**
* Update the viewBox bounds
*/
setViewBox(t, e, s, i) {
this.viewBox.left = t, this.viewBox.top = e, this.viewBox.right = s, this.viewBox.bottom = i;
}
/**
* Update the transform state
*/
setTransform(t, e, s) {
this.transform.scale = t, this.transform.x = e, this.transform.y = s, this._visibleBoundsDirty = !0;
}
/**
* Calculate how many pixels a node of given size occupies on screen.
* This is the core metric for adaptive rendering decisions.
*/
getNodeScreenSize(t) {
return t * this.transform.scale;
}
/**
* Convert screen coordinates to scene coordinates
*/
screenToScene(t, e) {
const s = this._reusableScenePoint;
return s.x = (t - this.transform.x) / this.transform.scale, s.y = (e - this.transform.y) / this.transform.scale, s;
}
/**
* Convert scene coordinates to screen coordinates
*/
sceneToScreen(t, e) {
const s = this._reusableScreenPoint;
return s.x = t * this.transform.scale + this.transform.x, s.y = e * this.transform.scale + this.transform.y, s;
}
/**
* Get the visible bounds in scene coordinates
*/
/**
* Returns the cached visible bounds object. Callers must not modify it.
*/
getVisibleBounds() {
return this._updateVisibleBounds(), this._visibleBounds;
}
_updateVisibleBounds() {
if (this._visibleBoundsDirty) {
this._visibleBoundsDirty = !1;
const t = this.transform.scale, e = this.transform.x, s = this.transform.y;
this._visibleBounds.left = (0 - e) / t, this._visibleBounds.top = (0 - s) / t, this._visibleBounds.right = (this.width - e) / t, this._visibleBounds.bottom = (this.height - s) / t;
}
}
/**
* Check if a point with given radius is visible in the viewport
*/
isVisible(t, e, s = 0) {
this._updateVisibleBounds();
const i = this._visibleBounds;
return t + s >= i.left && t - s <= i.right && e + s >= i.top && e - s <= i.bottom;
}
/**
* Get the SVG transform string for the current state
*/
getTransformString() {
return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
}
}
function yt(a, t) {
const { onPanStart: e, onPanMove: s, onPanEnd: i, onZoom: n } = t;
let o = !1, h = 0, l = 0;
const c = [];
function r(d, m, _, y) {
d.addEventListener(m, _, y), c.push({ target: d, event: m, handler: _, options: y });
}
function u(d) {
d.button === 0 && (o = !0, h = d.clientX, l = d.clientY, e && e({ x: h, y: l, event: d }), d.preventDefault());
}
function p(d) {
if (!o) return;
const m = d.clientX - h, _ = d.clientY - l;
h = d.clientX, l = d.clientY, s && s({ dx: m, dy: _, x: h, y: l, event: d });
}
function x(d) {
o && (o = !1, i && i({ x: d.clientX, y: d.clientY, event: d }));
}
function g(d) {
d.preventDefault();
let m = d.deltaY;
d.deltaMode === 1 ? m *= 40 : d.deltaMode === 2 && (m *= 800);
const _ = 1 - m * 3e-3, y = a.getBoundingClientRect(), v = d.clientX - y.left, b = d.clientY - y.top;
n && n({ factor: _, x: v, y: b, event: d });
}
function f(d) {
const m = a.getBoundingClientRect(), _ = d.clientX - m.left, y = d.clientY - m.top;
n && n({ factor: 2, x: _, y, event: d });
}
return r(a, "mousedown", u), r(window, "mousemove", p), r(window, "mouseup", x), r(a, "wheel", g, { passive: !1 }), r(a, "dblclick", f), {
dispose() {
for (const { target: d, event: m, handler: _, options: y } of c)
d.removeEventListener(m, _, y);
c.length = 0;
}
};
}
class xt {
constructor(t) {
this.id = t.identifier, this.x = t.clientX, this.y = t.clientY, this.lastX = this.x, this.lastY = this.y;
}
move(t) {
this.lastX = this.x, this.lastY = this.y, this.x = t.clientX, this.y = t.clientY;
}
}
function vt(a, t) {
const { onPanStart: e, onPanMove: s, onPanEnd: i, onZoom: n } = t;
let o = /* @__PURE__ */ new Map(), h = !1, l = 0, c = 0, r = null;
a.addEventListener("touchstart", u, { passive: !1 });
function u(d) {
h || (g(), h = !0);
for (let m = 0; m < d.touches.length; m++) {
const _ = d.touches[m];
o.has(_.identifier) || o.set(_.identifier, new xt(_));
}
if (d.touches.length === 1) {
const m = d.touches[0];
e && e({ x: m.clientX, y: m.clientY, event: d });
}
d.preventDefault(), d.stopPropagation();
}
function p(d) {
const m = Date.now();
let _ = 0, y = 0, v = 0, b = 0, C = null, S = null;
for (let Y = 0; Y < d.touches.length; Y++) {
const X = d.touches[Y], P = o.get(X.identifier);
P && (P.move(X), v += P.x, b += P.y, _ += P.x - P.lastX, y += P.y - P.lastY, C ? S || (S = P) : C = P);
}
const w = d.touches.length;
if (w === 0) return;
if (_ /= w, y /= w, v /= w, b /= w, C && S) {
c = m;
const Y = Math.hypot(S.x - C.x, S.y - C.y), X = Math.hypot(S.lastX - C.lastX, S.lastY - C.lastY);
if (X > 0) {
const P = Y / X, G = a.getBoundingClientRect(), H = v - G.left, R = b - G.top;
n && n({ factor: P, x: H, y: R, event: d });
}
d.preventDefault(), d.stopPropagation();
}
const L = m - c;
d.touches.length === 1 && L > 100 && (_ !== 0 || y !== 0) && s && s({ dx: _, dy: y, x: v, y: b, event: d });
}
function x(d) {
const m = Date.now(), _ = m - l;
l = m;
for (let y = 0; y < d.changedTouches.length; y++) {
const v = d.changedTouches[y];
o.delete(v.identifier);
}
if (d.touches.length === 0) {
if (o.clear(), h = !1, f(), d.changedTouches.length === 1 && m - c > 350) {
const y = d.changedTouches[0];
if (_ < 350 && r && Math.hypot(
y.clientX - r.clientX,
y.clientY - r.clientY
) < 30) {
const b = a.getBoundingClientRect(), C = y.clientX - b.left, S = y.clientY - b.top;
n && n({ factor: 2, x: C, y: S, event: d });
}
r = { clientX: y.clientX, clientY: y.clientY };
}
i && i({ x: 0, y: 0, event: d });
}
}
function g() {
document.addEventListener("touchmove", p, { passive: !1 }), document.addEventListener("touchend", x, { passive: !1 }), document.addEventListener("touchcancel", x, { passive: !1 });
}
function f() {
document.removeEventListener("touchmove", p, { passive: !1 }), document.removeEventListener("touchend", x, { passive: !1 }), document.removeEventListener("touchcancel", x, { passive: !1 });
}
return {
dispose() {
a.removeEventListener("touchstart", u, { passive: !1 }), f(), o.clear();
}
};
}
function bt(a = {}) {
const {
friction: t = 0.92,
minVelocity: e = 0.5,
onUpdate: s = () => {
}
} = a;
let i = 0, n = 0, o = 0, h = null, l = !1;
const c = [], r = 5;
function u(g, f) {
const d = performance.now();
for (c.push({ dx: g, dy: f, time: d }); c.length > r; )
c.shift();
}
function p() {
if (c.length < 2)
return { vx: 0, vy: 0 };
let g = 0, f = 0, d = 0;
for (let m = 1; m < c.length; m++) {
const _ = c[m].time - c[m - 1].time;
_ > 0 && (g += c[m].dx, f += c[m].dy, d += _);
}
return d === 0 ? { vx: 0, vy: 0 } : {
vx: g / d * 16,
// Scale to approx per-frame velocity
vy: f / d * 16
};
}
function x() {
h = null;
const g = performance.now(), f = Math.min(g - o, 32);
o = g;
const d = f / 16, m = Math.pow(t, d);
if (i *= m, n *= m, Math.sqrt(i * i + n * n) < e) {
i = 0, n = 0;
return;
}
s(i * d, n * d), h = requestAnimationFrame(x);
}
return {
/**
* Start tracking movements
*/
startTracking() {
l = !0, c.length = 0, this.stop();
},
/**
* Record a movement during pan
*/
track(g, f) {
l && u(g, f);
},
/**
* Stop tracking and start kinetic animation
*/
release() {
l = !1;
const { vx: g, vy: f } = p();
i = g, n = f, c.length = 0, Math.sqrt(i * i + n * n) >= e && (o = performance.now(), h = requestAnimationFrame(x));
},
/**
* Stop any ongoing animation
*/
stop() {
h !== null && (cancelAnimationFrame(h), h = null), i = 0, n = 0;
},
/**
* Check if animation is running
*/
isAnimating() {
return h !== null;
},
/**
* Dispose of the animation
*/
dispose() {
this.stop(), c.length = 0;
}
};
}
function Mt(a, t, e = {}) {
const {
onTransform: s = () => {
}
} = e;
let i = e.minZoom || 0.1, n = e.maxZoom || 20, o = null, h = null, l = null, c = null, r = null;
function u(f, d) {
const { x: m, y: _, scale: y } = t.transform;
t.setTransform(y, m + f, _ + d), s();
}
function p(f, d, m) {
const { x: _, y, scale: v } = t.transform;
let b = v * f;
if (b = Math.max(i, Math.min(n, b)), b === v) return;
const C = b / v, S = d - (d - _) * C, w = m - (m - y) * C;
t.setTransform(b, S, w), s();
}
l = bt({
onUpdate(f, d) {
u(f, d);
}
});
const x = {
onPanStart() {
l.startTracking();
},
onPanMove({ dx: f, dy: d }) {
u(f, d), l.track(f, d);
},
onPanEnd() {
l.release();
},
onZoom({ factor: f, x: d, y: m }) {
p(f, d, m);
}
};
o = yt(a, x), h = vt(a, x);
function g() {
c !== null && (cancelAnimationFrame(c), c = null), r && (r(), r = null);
}
return {
/**
* Set the zoom limits
*/
setZoomLimits(f, d) {
i = f, n = d;
},
/**
* Get the current zoom level
*/
getZoom() {
return t.transform.scale;
},
/**
* Set the zoom level
*/
setZoom(f, d = !1) {
if (d) {
const y = (t.width / 2 - t.transform.x) / t.transform.scale, v = (t.height / 2 - t.transform.y) / t.transform.scale;
return this.flyTo(y, v, f);
}
const { width: m, height: _ } = t;
p(f / t.transform.scale, m / 2, _ / 2);
},
/**
* Pan by a delta
*/
pan(f, d) {
u(f, d);
},
/**
* Animate camera to a specific position
*/
flyTo(f, d, m, _ = 300) {
return new Promise((y) => {
l.stop(), g(), r = y;
const v = t.transform.x, b = t.transform.y, C = t.transform.scale, S = Math.max(i, Math.min(n, m)), { width: w, height: L } = t, D = w / 2 - f * S, Y = L / 2 - d * S, X = performance.now();
function P() {
const H = performance.now() - X, R = Math.min(H / _, 1), U = 1 - Math.pow(1 - R, 3), dt = v + (D - v) * U, ft = b + (Y - b) * U, _t = C + (S - C) * U;
t.setTransform(_t, dt, ft), s(), R < 1 ? c = requestAnimationFrame(P) : (c = null, r = null, y());
}
c = requestAnimationFrame(P);
});
},
/**
* Stop any ongoing animation
*/
stop() {
l.stop(), g();
},
/**
* Dispose of the pan/zoom controller
*/
dispose() {
g(), o && (o.dispose(), o = null), h && (h.dispose(), h = null), l && (l.dispose(), l = null);
}
};
}
const nt = "http://www.w3.org/2000/svg";
function Kt(a, t = {}) {
const {
viewBox: e = { left: -100, top: -100, right: 100, bottom: 100 },
panZoom: s = {}
} = t, i = document.createElementNS(nt, "svg");
i.style.position = "relative", i.style.width = "100%", i.style.height = "100%", i.style.display = "block", i.style.overflow = "hidden", a.appendChild(i);
const n = document.createElementNS(nt, "g");
n.setAttribute("class", "scene-transform"), i.appendChild(n);
const o = new gt();
o.setViewBox(e.left, e.top, e.right, e.bottom);
const h = [], l = {
render: [],
transform: [],
resize: []
};
let c = null, r = !0;
function u() {
const _ = a.getBoundingClientRect();
o.setSize(_.width, _.height);
const y = e.right - e.left, v = e.bottom - e.top, b = _.width / y, C = _.height / v, S = Math.min(b, C), w = _.width / 2, L = _.height / 2, D = (e.left + e.right) / 2, Y = (e.top + e.bottom) / 2;
o.setTransform(
S,
w - D * S,
L - Y * S
), p("resize", { width: _.width, height: _.height }), g();
}
function p(_, y) {
const v = l[_];
if (v)
for (let b = 0; b < v.length; b++)
v[b](y);
}
function x() {
if (c = null, !!r) {
r = !1, n.setAttribute("transform", o.getTransformString());
for (let _ = 0; _ < h.length; _++)
h[_].render(o);
p("render", o);
}
}
function g() {
r = !0, c === null && (c = requestAnimationFrame(x));
}
let f = null;
s.enabled !== !1 && (f = Mt(i, o, {
minZoom: s.minZoom || 0.1,
maxZoom: s.maxZoom || 20,
onTransform: () => {
p("transform", o.transform), g();
}
}));
const d = new ResizeObserver(() => {
u();
});
d.observe(a), u();
const m = {
/**
* The SVG root element
*/
svg: i,
/**
* The transform group containing all scene content
*/
root: n,
/**
* The current draw context
*/
drawContext: o,
/**
* Add a collection to the scene
*/
addCollection(_) {
h.push(_);
const y = _.getRoot();
return y && n.appendChild(y), g(), _;
},
/**
* Remove a collection from the scene
*/
removeCollection(_) {
const y = h.indexOf(_);
if (y !== -1) {
h.splice(y, 1);
const v = _.getRoot();
v && v.parentNode && v.parentNode.removeChild(v), g();
}
},
/**
* Request a render on the next animation frame
*/
requestRender: g,
/**
* Add an event listener
*/
on(_, y) {
return l[_] && l[_].push(y), m;
},
/**
* Remove an event listener
*/
off(_, y) {
if (l[_]) {
const v = l[_].indexOf(y);
v !== -1 && l[_].splice(v, 1);
}
return m;
},
/**
* Get the pan/zoom controller
*/
getPanZoom() {
return f;
},
/**
* Animate camera to a specific position
*/
flyTo(_, y, v, b = 300) {
return f ? f.flyTo(_, y, v, b) : Promise.resolve();
},
/**
* Fit the view to show all content
*/
fitToView(_, y = 20) {
const { width: v, height: b } = o, C = _.right - _.left, S = _.bottom - _.top, w = (v - y * 2) / C, L = (b - y * 2) / S, D = Math.min(w, L), Y = (_.left + _.right) / 2, X = (_.top + _.bottom) / 2;
return this.flyTo(Y, X, D);
},
/**
* Dispose of the scene and clean up resources
*/
dispose() {
c !== null && (cancelAnimationFrame(c), c = null), d.disconnect(), f && (f.dispose(), f = null);
for (let _ = 0; _ < h.length; _++)
h[_].dispose && h[_].dispose();
h.length = 0;
for (const _ in l)
l[_].length = 0;
i.parentNode && i.parentNode.removeChild(i);
}
};
return m;
}
function Ct(a, t, e, s, i) {
lt(a, t, e || 0, s || a.length - 1, i || St);
}
function lt(a, t, e, s, i) {
for (; s > e; ) {
if (s - e > 600) {
var n = s - e + 1, o = t - e + 1, h = Math.log(n), l = 0.5 * Math.exp(2 * h / 3), c = 0.5 * Math.sqrt(h * l * (n - l) / n) * (o - n / 2 < 0 ? -1 : 1), r = Math.max(e, Math.floor(t - o * l / n + c)), u = Math.min(s, Math.floor(t + (n - o) * l / n + c));
lt(a, t, r, u, i);
}
var p = a[t], x = e, g = s;
for (N(a, e, t), i(a[s], p) > 0 && N(a, e, s); x < g; ) {
for (N(a, x, g), x++, g--; i(a[x], p) < 0; ) x++;
for (; i(a[g], p) > 0; ) g--;
}
i(a[e], p) === 0 ? N(a, e, g) : (g++, N(a, g, s)), g <= t && (e = g + 1), t <= g && (s = g - 1);
}
}
function N(a, t, e) {
var s = a[t];
a[t] = a[e], a[e] = s;
}
function St(a, t) {
return a < t ? -1 : a > t ? 1 : 0;
}
class F {
constructor(t = 9) {
this._maxEntries = Math.max(4, t), this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)), this.clear();
}
all() {
return this._all(this.data, []);
}
search(t) {
let e = this.data;
const s = [];
if (!$(t, e)) return s;
const i = this.toBBox, n = [];
for (; e; ) {
for (let o = 0; o < e.children.length; o++) {
const h = e.children[o], l = e.leaf ? i(h) : h;
$(t, l) && (e.leaf ? s.push(h) : j(t, l) ? this._all(h, s) : n.push(h));
}
e = n.pop();
}
return s;
}
collides(t) {
let e = this.data;
if (!$(t, e)) return !1;
const s = [];
for (; e; ) {
for (let i = 0; i < e.children.length; i++) {
const n = e.children[i], o = e.leaf ? this.toBBox(n) : n;
if ($(t, o)) {
if (e.leaf || j(t, o)) return !0;
s.push(n);
}
}
e = s.pop();
}
return !1;
}
load(t) {
if (!(t && t.length)) return this;
if (t.length < this._minEntries) {
for (let s = 0; s < t.length; s++)
this.insert(t[s]);
return this;
}
let e = this._build(t.slice(), 0, t.length - 1, 0);
if (!this.data.children.length)
this.data = e;
else if (this.data.height === e.height)
this._splitRoot(this.data, e);
else {
if (this.data.height < e.height) {
const s = this.data;
this.data = e, e = s;
}
this._insert(e, this.data.height - e.height - 1, !0);
}
return this;
}
insert(t) {
return t && this._insert(t, this.data.height - 1), this;
}
clear() {
return this.data = E([]), this;
}
remove(t, e) {
if (!t) return this;
let s = this.data;
const i = this.toBBox(t), n = [], o = [];
let h, l, c;
for (; s || n.length; ) {
if (s || (s = n.pop(), l = n[n.length - 1], h = o.pop(), c = !0), s.leaf) {
const r = wt(t, s.children, e);
if (r !== -1)
return s.children.splice(r, 1), n.push(s), this._condense(n), this;
}
!c && !s.leaf && j(s, i) ? (n.push(s), o.push(h), h = 0, l = s, s = s.children[0]) : l ? (h++, s = l.children[h], c = !1) : s = null;
}
return this;
}
toBBox(t) {
return t;
}
compareMinX(t, e) {
return t.minX - e.minX;
}
compareMinY(t, e) {
return t.minY - e.minY;
}
toJSON() {
return this.data;
}
fromJSON(t) {
return this.data = t, this;
}
_all(t, e) {
const s = [];
for (; t; )
t.leaf ? e.push(...t.children) : s.push(...t.children), t = s.pop();
return e;
}
_build(t, e, s, i) {
const n = s - e + 1;
let o = this._maxEntries, h;
if (n <= o)
return h = E(t.slice(e, s + 1)), T(h, this.toBBox), h;
i || (i = Math.ceil(Math.log(n) / Math.log(o)), o = Math.ceil(n / Math.pow(o, i - 1))), h = E([]), h.leaf = !1, h.height = i;
const l = Math.ceil(n / o), c = l * Math.ceil(Math.sqrt(o));
ot(t, e, s, c, this.compareMinX);
for (let r = e; r <= s; r += c) {
const u = Math.min(r + c - 1, s);
ot(t, r, u, l, this.compareMinY);
for (let p = r; p <= u; p += l) {
const x = Math.min(p + l - 1, u);
h.children.push(this._build(t, p, x, i - 1));
}
}
return T(h, this.toBBox), h;
}
_chooseSubtree(t, e, s, i) {
for (; i.push(e), !(e.leaf || i.length - 1 === s); ) {
let n = 1 / 0, o = 1 / 0, h;
for (let l = 0; l < e.children.length; l++) {
const c = e.children[l], r = K(c), u = Lt(t, c) - r;
u < o ? (o = u, n = r < n ? r : n, h = c) : u === o && r < n && (n = r, h = c);
}
e = h || e.children[0];
}
return e;
}
_insert(t, e, s) {
const i = s ? t : this.toBBox(t), n = [], o = this._chooseSubtree(i, this.data, e, n);
for (o.children.push(t), z(o, i); e >= 0 && n[e].children.length > this._maxEntries; )
this._split(n, e), e--;
this._adjustParentBBoxes(i, n, e);
}
// split overflowed node into two
_split(t, e) {
const s = t[e], i = s.children.length, n = this._minEntries;
this._chooseSplitAxis(s, n, i);
const o = this._chooseSplitIndex(s, n, i), h = E(s.children.splice(o, s.children.length - o));
h.height = s.height, h.leaf = s.leaf, T(s, this.toBBox), T(h, this.toBBox), e ? t[e - 1].children.push(h) : this._splitRoot(s, h);
}
_splitRoot(t, e) {
this.data = E([t, e]), this.data.height = t.height + 1, this.data.leaf = !1, T(this.data, this.toBBox);
}
_chooseSplitIndex(t, e, s) {
let i, n = 1 / 0, o = 1 / 0;
for (let h = e; h <= s - e; h++) {
const l = A(t, 0, h, this.toBBox), c = A(t, h, s, this.toBBox), r = Xt(l, c), u = K(l) + K(c);
r < n ? (n = r, i = h, o = u < o ? u : o) : r === n && u < o && (o = u, i = h);
}
return i || s - e;
}
// sorts node children by the best axis for split
_chooseSplitAxis(t, e, s) {
const i = t.leaf ? this.compareMinX : Pt, n = t.leaf ? this.compareMinY : Yt, o = this._allDistMargin(t, e, s, i), h = this._allDistMargin(t, e, s, n);
o < h && t.children.sort(i);
}
// total margin of all possible split distributions where each node is at least m full
_allDistMargin(t, e, s, i) {
t.children.sort(i);
const n = this.toBBox, o = A(t, 0, e, n), h = A(t, s - e, s, n);
let l = V(o) + V(h);
for (let c = e; c < s - e; c++) {
const r = t.children[c];
z(o, t.leaf ? n(r) : r), l += V(o);
}
for (let c = s - e - 1; c >= e; c--) {
const r = t.children[c];
z(h, t.leaf ? n(r) : r), l += V(h);
}
return l;
}
_adjustParentBBoxes(t, e, s) {
for (let i = s; i >= 0; i--)
z(e[i], t);
}
_condense(t) {
for (let e = t.length - 1, s; e >= 0; e--)
t[e].children.length === 0 ? e > 0 ? (s = t[e - 1].children, s.splice(s.indexOf(t[e]), 1)) : this.clear() : T(t[e], this.toBBox);
}
}
function wt(a, t, e) {
if (!e) return t.indexOf(a);
for (let s = 0; s < t.length; s++)
if (e(a, t[s])) return s;
return -1;
}
function T(a, t) {
A(a, 0, a.children.length, t, a);
}
function A(a, t, e, s, i) {
i || (i = E(null)), i.minX = 1 / 0, i.minY = 1 / 0, i.maxX = -1 / 0, i.maxY = -1 / 0;
for (let n = t; n < e; n++) {
const o = a.children[n];
z(i, a.leaf ? s(o) : o);
}
return i;
}
function z(a, t) {
return a.minX = Math.min(a.minX, t.minX), a.minY = Math.min(a.minY, t.minY), a.maxX = Math.max(a.maxX, t.maxX), a.maxY = Math.max(a.maxY, t.maxY), a;
}
function Pt(a, t) {
return a.minX - t.minX;
}
function Yt(a, t) {
return a.minY - t.minY;
}
function K(a) {
return (a.maxX - a.minX) * (a.maxY - a.minY);
}
function V(a) {
return a.maxX - a.minX + (a.maxY - a.minY);
}
function Lt(a, t) {
return (Math.max(t.maxX, a.maxX) - Math.min(t.minX, a.minX)) * (Math.max(t.maxY, a.maxY) - Math.min(t.minY, a.minY));
}
function Xt(a, t) {
const e = Math.max(a.minX, t.minX), s = Math.max(a.minY, t.minY), i = Math.min(a.maxX, t.maxX), n = Math.min(a.maxY, t.maxY);
return Math.max(0, i - e) * Math.max(0, n - s);
}
function j(a, t) {
return a.minX <= t.minX && a.minY <= t.minY && t.maxX <= a.maxX && t.maxY <= a.maxY;
}
function $(a, t) {
return t.minX <= a.maxX && t.minY <= a.maxY && t.maxX >= a.minX && t.maxY >= a.minY;
}
function E(a) {
return {
children: a,
height: 1,
leaf: !0,
minX: 1 / 0,
minY: 1 / 0,
maxX: -1 / 0,
maxY: -1 / 0
};
}
function ot(a, t, e, s, i) {
const n = [t, e];
for (; n.length; ) {
if (e = n.pop(), t = n.pop(), e - t <= s) continue;
const o = t + Math.ceil((e - t) / s / 2) * s;
Ct(a, o, t, e, i), n.push(t, o, o, e);
}
}
class It {
constructor(t) {
this._svg = t, this._elements = /* @__PURE__ */ new Map(), this._restoreTimer = 0, this._onSvgWheel = this._handleSvgWheel.bind(this), this._svg.addEventListener("wheel", this._onSvgWheel), this._container = this._createContainer(), this._injectStyle();
}
_createContainer() {
const t = document.createElement("div");
t.style.position = "absolute", t.style.top = "0", t.style.left = "0", t.style.width = "0", t.style.height = "0", t.style.transformOrigin = "0 0", t.style.pointerEvents = "none";
const e = this._svg.parentNode;
return e && (getComputedStyle(e).position === "static" && (e.style.position = "relative"), e.insertBefore(t, this._svg.nextSibling)), t;
}
/**
* Inject a one-off stylesheet rule that suppresses pointer events on all
* children when the container has the suppressed class. The !important
* overrides any inline pointer-events:auto set by the consumer.
*/
_injectStyle() {
if (document.getElementById("dom-overlay-style")) return;
const t = document.createElement("style");
t.id = "dom-overlay-style", t.textContent = ".dom-overlay--suppressed > * { pointer-events: none !important; }", document.head.appendChild(t);
}
/**
* When the user wheels on the SVG (zooming), suppress interactivity on
* all DOM overlay children so cards that drift under the cursor can't
* capture subsequent wheel ticks. Restore after 150ms of no wheel events.
*/
_handleSvgWheel() {
this._container.classList.add("dom-overlay--suppressed"), clearTimeout(this._restoreTimer), this._restoreTimer = setTimeout(() => {
this._container.classList.remove("dom-overlay--suppressed");
}, 150);
}
/**
* Sync the overlay transform with the SVG scene transform.
* Called once per frame.
*/
syncTransform(t) {
const e = t.transform;
this._container.style.transform = `translate(${e.x}px,${e.y}px) scale(${e.scale})`;
}
/**
* Get or create a DOM element for the given node.
* `createFn(data, ctx)` is only called on first encounter.
*/
ensureElement(t, e, s, i) {
let n = this._elements.get(t);
if (!n) {
const o = i(e, s);
o.style.position = "absolute", o.style.left = "0", o.style.top = "0", o.style.transformOrigin = "0 0", o.style.pointerEvents = "none", n = { el: o, attached: !1, stateVersion: -1 }, this._elements.set(t, n);
}
return n;
}
/**
* Call the update callback if the node's state version has changed.
*/
updateState(t, e, s, i, n) {
const o = this._elements.get(t);
!o || o.stateVersion === n || (o.stateVersion = n, i(e, s, o.el));
}
/**
* Set a DOM element's position in world coordinates with counter-scale.
* contentScale matches NodeCollection's maxScale / zoom capping.
* halfW/halfH offset the element so its center aligns with (worldX, worldY),
* matching SVG node centering behavior.
*/
setPosition(t, e, s, i, n, o) {
const h = this._elements.get(t);
if (h) {
const l = e - (n || 0) * i, c = s - (o || 0) * i;
h.el.style.transform = `translate(${l}px,${c}px) scale(${i})`;
}
}
/**
* Attach the element to the overlay container (make visible).
*/
attach(t) {
const e = this._elements.get(t);
e && !e.attached && (this._container.appendChild(e.el), e.attached = !0);
}
/**
* Detach the element from the overlay container (hide, but keep cached).
*/
detach(t) {
const e = this._elements.get(t);
e && e.attached && (this._container.removeChild(e.el), e.attached = !1);
}
/**
* Remove a node's DOM element entirely (detach + delete from cache).
*/
remove(t) {
const e = this._elements.get(t);
e && (e.attached && e.el.parentNode && e.el.parentNode.removeChild(e.el), this._elements.delete(t));
}
dispose() {
clearTimeout(this._restoreTimer), this._svg.removeEventListener("wheel", this._onSvgWheel), this._container && this._container.parentNode && this._container.parentNode.removeChild(this._container), this._elements.clear(), this._container = null;
}
}
const at = "http://www.w3.org/2000/svg";
function M(a, t, e) {
return typeof a == "function" ? a(t, e) : a;
}
function Q(a) {
return a ? String(a).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """) : "";
}
function Dt(a) {
let t;
if (a.type && !a.layers) {
const { minZoom: i, maxZoom: n, importance: o, hitArea: h, ...l } = a;
t = {
minZoom: i || 0,
maxZoom: n,
importance: o,
hitArea: h,
layers: [l]
};
} else
t = {
minZoom: a.minZoom || 0,
maxZoom: a.maxZoom,
importance: a.importance,
hitArea: a.hitArea,
layers: a.layers || []
};
const e = t.layers.find((i) => i.type === "dom");
e && (t._domLayer = e);
const s = t.layers.find((i) => i.type === "svg" && i.update);
return s && (t._svgUpdate = s.update), t;
}
function Tt(a, t, e) {
if (a.visible !== void 0 && !M(a.visible, t, e))
return "";
const s = a.type;
if (s === "circle") {
const i = M(a.radius, t, e) || 4, n = M(a.fill, t, e) || "none", o = M(a.stroke, t, e) || "none", h = M(a.strokeWidth, t, e) || 0, l = M(a.opacity, t, e), c = M(a.filter, t, e);
let r = `r="${i}" fill="${n}"`;
return o !== "none" && (r += ` stroke="${o}"`), h && (r += ` stroke-width="${h}"`), l != null && (r += ` opacity="${l}"`), c && (r += ` filter="${c}"`), `<circle ${r}/>`;
}
if (s === "rect") {
const i = M(a.width, t, e) || 10, n = M(a.height, t, e) || 10, o = M(a.rx, t, e), h = M(a.ry, t, e), l = M(a.fill, t, e) || "none", c = M(a.stroke, t, e) || "none", r = M(a.strokeWidth, t, e) || 0, u = M(a.opacity, t, e), p = M(a.filter, t, e);
let x = `x="${-i / 2}" y="${-n / 2}" width="${i}" height="${n}" fill="${l}"`;
return o != null && (x += ` rx="${o}"`), h != null && (x += ` ry="${h}"`), c !== "none" && (x += ` stroke="${c}"`), r && (x += ` stroke-width="${r}"`), u != null && (x += ` opacity="${u}"`), p && (x += ` filter="${p}"`), `<rect ${x}/>`;
}
if (s === "text") {
const i = M(a.text, t, e);
if (i == null || i === "") return "";
const n = M(a.fontSize, t, e) || 10, o = M(a.fill, t, e) || "#000", h = M(a.fontFamily, t, e), l = M(a.fontWeight, t, e), c = M(a.anchor, t, e) || "center", r = M(a.offset, t, e) || [0, 0], u = M(a.opacity, t, e), p = M(a.maxWidth, t, e);
let x = "middle", g = r[0], f = r[1];
c === "center" && (f += n * 0.35);
let d = `x="${g}" y="${f}" text-anchor="${x}" font-size="${n}" fill="${o}"`;
h && (d += ` font-family="${h}"`), l && (d += ` font-weight="${l}"`), u != null && (d += ` opacity="${u}"`);
const m = String(i);
if (p && m.length * n * 0.6 > p) {
const _ = ct(m, n, p), y = n * 1.3;
let v = "";
for (let b = 0; b < _.length; b++)
b === 0 ? v += `<tspan x="${g}">${Q(_[b])}</tspan>` : v += `<tspan x="${g}" dy="${y}">${Q(_[b])}</tspan>`;
return `<text ${d}>${v}</text>`;
}
return `<text ${d}>${Q(m)}</text>`;
}
return s === "svg" && typeof a.create == "function" && a.create(t, e) || "";
}
function ct(a, t, e) {
const s = t * 0.6, i = Math.max(1, Math.floor(e / s)), n = a.split(/\s+/), o = [];
let h = "";
for (let l = 0; l < n.length; l++) {
const c = n[l], r = h ? h + " " + c : c;
r.length > i && h ? (o.push(h), h = c) : h = r;
}
return h && o.push(h), o;
}
function W(a, t, e) {
const s = a.layers;
if (!s || s.length === 0) return "";
let i = "";
for (let n = 0; n < s.length; n++)
i += Tt(s[n], t, e);
return i;
}
function J(a, t, e) {
const s = a.layers;
let i = 0, n = 0;
for (let o = 0; o < s.length; o++) {
const h = s[o];
if (h.visible !== void 0 && !M(h.visible, t, e)) continue;
const l = h.type;
if (l === "circle") {
const r = (M(h.radius, t, e) || 4) * 2;
r > i && (i = r), r > n && (n = r);
} else if (l === "rect") {
const c = M(h.width, t, e) || 10, r = M(h.height, t, e) || 10;
c > i && (i = c), r > n && (n = r);
} else if (l === "svg") {
const c = M(h.width, t, e), r = M(h.height, t, e);
c && c > i && (i = c), r && r > n && (n = r);
} else if (l === "dom") {
const c = M(h.width, t, e) || 0, r = M(h.height, t, e) || 0;
c > i && (i = c), r > n && (n = r);
} else if (l === "text") {
const c = M(h.text, t, e);
if (!c) continue;
const r = M(h.fontSize, t, e) || 10, u = M(h.offset, t, e) || [0, 0], p = M(h.maxWidth, t, e), x = String(c);
let g, f;
if (p && x.length * r * 0.6 > p) {
const d = ct(x, r, p);
g = p + Math.abs(u[0]) * 2, f = d.length * r * 1.3 + Math.abs(u[1]);
} else
g = x.length * r * 0.6 + Math.abs(u[0]) * 2, f = r + Math.abs(u[1]);
g > i && (i = g), n += f;
}
}
if (a.hitArea) {
const o = a.hitArea;
o.type === "rect" && (i = M(o.width, t, e) || i, n = M(o.height, t, e) || n);
}
return { width: i, height: n };
}
function Et(a, t, e) {
const s = a.layers;
for (let i = 0; i < s.length; i++) {
const n = s[i];
if (n.type === "circle")
return {
type: "circle",
radius: M(n.radius, t, e) || 4
};
if (n.type === "rect")
return {
type: "rect",
width: M(n.width, t, e) || 10,
height: M(n.height, t, e) || 10
};
}
return null;
}
class jt {
constructor(t = {}) {
st(this, "_transitionRafId", null);
this._dataFn = t.data || null, this._maxScale = t.maxScale ?? 1, this._levels = (t.levels || []).map(Dt), this._collisionTrees = /* @__PURE__ */ new Map(), this._spatialIndex = new F(), this._spatialValid = !1, this._positionsDirty = !0, this._attachedNodes = /* @__PURE__ */ new Set(), this._swapAttachedSet = /* @__PURE__ */ new Set(), this._maxNodeSize = 0, this._root = document.createElementNS(at, "g"), this._root.setAttribute("class", "node-collection"), this._nodes = [], this._nodeMap = /* @__PURE__ */ new Map(), this._freeIndices = [], this._batchDepth = 0, this._batchDirty = !1, this._elementPool = [], this._lastDrawContext = null, this._lastScale = 1, this._lastCollisionZoom = 0, this._lastCandidateLevel = void 0, this._state = /* @__PURE__ */ new Map(), this._resolvedLevels = /* @__PURE__ */ new Map(), this._prevResolvedLevels = /* @__PURE__ */ new Map(), this._transitions = /* @__PURE__ */ new Map(), this._transitionDuration = 150, this._reusableCtx = { zoom: 1 }, this._reusableCtxKeys = [], this._collisionInterval = t.collisionInterval ?? 200, this._lastCollisionTime = 0, this._collisionNodes = [], this._collisionCandidates = [], this._collisionStable = [], this._hasDomLayers = this._levels.some((e) => e._domLayer), this._domOverlay = null, this._graph = t.graph || null, this._graphChangeListener = null, this._graph && this._dataFn && this._bindGraph();
}
/**
* Get the root SVG group element
*/
getRoot() {
return this._root;
}
/**
* Add a node to the collection
* @returns {Object} Node handle
*/
add(t) {
const {
id: e = this._generateId(),
x: s = 0,
y: i = 0,
data: n = {}
} = t, o = this._computeNodeSize(n);
let h;
this._freeIndices.length > 0 ? h = this._freeIndices.pop() : h = this._nodes.length;
const l = {
id: e,
index: h,
x: s,
y: i,
size: o,
data: n,
visible: !0,
_element: null,
_currentLevel: -1,
_inDOM: !1,
_collection: this,
_stateVersion: 0,
_renderedLevel: -1,
_renderedStateVersion: -1
};
return this._nodes[h] = l, this._nodeMap.set(e, l), o > this._maxNodeSize && (this._maxNodeSize = o), this._createNodeElement(l), this._spatialValid = !1, this._batchDepth === 0 ? this._addNodeToScene(l, this._lastDrawContext) : this._batchDirty = !0, l;
}
/**
* Remove a node from the collection
*/
remove(t) {
const e = typeof t == "object" ? t : this._nodeMap.get(t);
e && (e._element && (e._inDOM && (this._root.removeChild(e._element), e._inDOM = !1), this._elementPool.push(e._element), e._element = null), this._domOverlay && this._domOverlay.remove(e.id), this._freeIndices.push(e.index), this._nodes[e.index] = null, this._nodeMap.delete(e.id), this._attachedNodes.delete(e), this._state.delete(e.id), this._resolvedLevels.delete(e.id), this._prevResolvedLevels.delete(e.id), this._transitions.delete(e.id), this._spatialValid = !1, this._batchDepth > 0 && (this._batchDirty = !0));
}
/**
* Get a node by ID
*/
get(t) {
return this._nodeMap.get(t);
}
/**
* Set node position (GC-friendly)
*/
setPosition(t, e, s) {
t.x = e, t.y = s, this._positionsDirty = !0, this._batchDepth === 0 && t._element && t._inDOM && this._applyTransform(t);
}
/**
* Apply transform to a node element
*/
_applyTransform(t) {
if (!t._element) return;
const e = Math.min(1, this._maxScale / this._lastScale);
e !== 1 ? t._element.setAttribute(
"transform",
`translate(${t.x}, ${t.y}) scale(${e})`
) : t._element.setAttribute("transform", `translate(${t.x}, ${t.y})`);
}
// ── Model-driven state ──────────────────────────────────────────────
/**
* Set a state key on a node. State keys are available in ctx for property functions.
* Also applied as CSS classes for external styling.
*/
setState(t, e, s) {
const i = typeof t == "object" ? t.id : t;
let n = this._state.get(i);
s ? (n || (n = /* @__PURE__ */ new Map(), this._state.set(i, n)), n.set(e, !0)) : n && (n.delete(e), n.size === 0 && this._state.delete(i));
const o = this._nodeMap.get(i);
o && (o._stateVersion++, o._element && (s ? o._element.classList.add(e) : o._element.classList.remove(e)));
}
/**
* Get a state value for a node
*/
getState(t, e) {
const s = typeof t == "object" ? t.id : t, i = this._state.get(s);
return i && i.get(e) || !1;
}
/**
* Force re-render of all nodes on the next frame.
* Useful when external data used by property callbacks (e.g. fontSize)
* has changed without any level or state change.
*/
invalidateContent() {
for (const t of this._nodeMap.values())
t._stateVersion++;
}
/**
* Remove a state key from ALL nodes and sync CSS classes
*/
clearState(t) {
for (const [e, s] of this._state)
if (s.has(t)) {
s.delete(t), s.size === 0 && this._state.delete(e);
const i = this._nodeMap.get(e);
i && (i._stateVersion++, i._element && i._element.classList.remove(t));
}
}
/**
* Reapply all state keys as CSS classes on a node element.
*/
_reapplyState(t) {
if (!t._element) return;
t._element.setAttribute("class", "node");
const e = this._state.get(t.id);
if (e)
for (const s of e.keys())
t._element.classList.add(s);
}
/**
* Build ctx object for a node's property functions.
*/
_buildCtx(t) {
const e = this._reusableCtx, s = this._reusableCtxKeys;
for (let n = 0; n < s.length; n++)
e[s[n]] = void 0;
s.length = 0, e.zoom = this._lastScale;
const i = this._state.get(t);
if (i)
for (const [n, o] of i)
e[n] = o, s.push(n);
return e;
}
// ── Shape descriptors ───────────────────────────────────────────────
/**
* Get the current shape of a node in world coordinates.
*/
getNodeShape(t) {
const e = this._nodeMap.get(t);
if (!e) return null;
const s = this._resolvedLevels.get(t) ?? 0;
if (s < 0 || s >= this._levels.length) return null;
const i = this._levels[s], n = this._buildCtx(t), o = Et(i, e.data, n);
if (!o) return null;
const h = Math.min(1, this._maxScale / this._lastScale);
return o.type === "circle" ? {
type: "circle",
radius: o.radius * h,
x: e.x,
y: e.y
} : o.type === "rect" ? {
type: "rect",
width: o.width * h,
height: o.height * h,
x: e.x,
y: e.y
} : null;
}
/**
* Get the current content scale factor.
*/
getContentScale() {
return Math.min(1, this._maxScale / this._lastScale);
}
// ── Sync positions ─────────────────────────────────────────────────
syncPositions(t) {
this.beginBatch();
for (const [e, s] of t) {
const i = this._nodeMap.get(e);
i && (i.x = s.x, i.y = s.y, i.visible || (i.visible = !0));
}
for (let e = 0; e < this._nodes.length; e++) {
const s = this._nodes[e];
s && s.visible && !t.has(s.id) && (s.visible = !1);
}
this._positionsDirty = !0, this.endBatch();
}
// ── Hit testing ────────────────────────────────────────────────────
getNodeAt(t, e, s) {
if (s || (s = this._lastDrawContext), !s) return null;
const i = s.screenToScene(t, e), n = Math.max(this._maxNodeSize, 80 / this._lastScale);
this._spatialValid || (this._rebuildSpatialIndex(), this._spatialValid = !0);
const o = this._spatialIndex.search({
minX: i.x - n,
minY: i.y - n,
maxX: i.x + n,
maxY: i.y + n
});
let h = null, l = 1 / 0;
const c = Math.min(1, this._maxScale / this._lastScale), r = 10 / this._lastScale;
for (let u = 0; u < o.length; u++) {
const p = o[u].node, x = i.x - p.x, g = i.y - p.y, f = x * x + g * g;
let d = !1;
if (this._levels.length > 0) {
const m = this._resolvedLevels.get(p.id) ?? 0;
if (m >= 0 && m < this._levels.length) {
const _ = this._levels[m], y = this._buildCtx(p.id), v = J(_, p.data, y), b = Math.max(v.width * c / 2, r), C = Math.max(v.height * c / 2, r);
d = Math.abs(x) <= b && Math.abs(g) <= C;
}
}
if (!d) {
const m = this.getNodeShape(p.id);
if (m && m.type === "circle") {
const _ = Math.max(m.radius, r);
d = f <= _ * _;
} else if (m && m.type === "rect") {
const _ = Math.max(m.width / 2, r), y = Math.max(m.height / 2, r);
d = Math.abs(x) <= _ && Math.abs(g) <= y;
} else {
const _ = Math.max(r, p.size * 0.5);
d = f <= _ * _;
}
}
d && f < l && (l = f, h = p);
}
return h ? h.id : null;
}
// ── Batch updates ──────────────────────────────────────────────────
beginBatch() {
this._batchDepth++;
}
endBatch() {
this._batchDepth > 0 && this._batchDepth--, this._batchDepth === 0 && this._batchDirty && (this._batchDirty = !1, this.render(this._lastDrawContext));
}
// ── Iteration ──────────────────────────────────────────────────────
forEach(t) {
for (let e = 0; e < this._nodes.length; e++) {
const s = this._nodes[e];
s && t(s, s.id);
}
}
get count() {
return this._nodeMap.size;
}
// ── Render ─────────────────────────────────────────────────────────
render(t) {
if (this._lastDrawContext = t, !t) return;
const e = t.transform.scale, s = e !== this._lastScale;
this._lastScale = e;
const i = performance.now(), n = this._getCandidateLevelIndex(e), o = this._lastCandidateLevel !== void 0 && n !== this._lastCandidateLevel;
this._lastCandidateLevel = n;
const h = this._lastCollisionZoom > 0 ? e / this._lastCollisionZoom : 1 / 0, l = h > 1.2, c = h < 0.83;
if (o || c || this._positionsDirty && i - this._lastCollisionTime >= this._collisionInterval ? (this._computeResolvedLevels(t, !1), this._lastCollisionTime = i, this._lastCollisionZoom = e) : l && (this._computeResolvedLevels(t, !0), this._lastCollisionTime = i, this._lastCollisionZoom = e), this._hasDomLayers && !this._domOverlay) {
const r = this._root.ownerSVGElement;
r && (this._domOverlay = new It(r));
}
this._domOverlay && this._domOverlay.syncTransform(t), this._positionsDirty ? (this._positionsDirty = !1, this._spatialValid = !1, this._renderAllNodes(t, s)) : (this._spatialValid || (this._rebuildSpatialIndex(), this._spatialValid = !0), this._renderWithSpatialQuery(t, s));
}
/**
* Compute which level each node should render at, considering zoom and collision.
*
* For each node:
* 1. Find the candidate level based on zoom (last level where minZoom <= zoom)
* 2. If level has importance, check collision with more important nodes
* 3. On collision, fall back to previous level
*/
_computeResolvedLevels(t, e = !1) {
const s = this._lastScale, i = this._levels;
if (i.length === 0) return;
const n = this._prevResolvedLevels;
this._prevResolvedLevels = this._resolvedLevels, this._resolvedLevels = n, this._resolvedLevels.clear();
const o = this._getCandidateLevelIndex(s), h = this._collisionNodes;
h.length = 0;
for (let f = 0; f < this._nodes.length; f++) {
const d = this._nodes[f];
if (!d || !d.visible) continue;
const m = Math.max(d.size, 50 / s);
t.isVisible(d.x, d.y, m) && h.push(d);
}
const l = Math.min(1, this._maxScale / s), c = this._collisionCandidates, r = this._collisionStable, u = t.width / 2, p = t.height / 2, x = Math.hypot(u, p) || 1, g = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
for (let f = o; f >= 0; f--) {
const d = i[f];
if (s < d.minZoom || d.maxZoom !== void 0 && s >= d.maxZoom) continue;
if (!d.importance) {
for (let _ = 0; _ < h.length; _++) {
const y = h[_];
this._resolvedLevels.has(y.id) || this._resolvedLevels.set(y.id, f);
}
break;
}
let m = this._collisionTrees.get(f);
if (m ? m.clear() : (m = new F(), this._collisionTrees.set(f, m)), e) {
r.length = 0;
for (let _ = 0; _ < h.length; _++) {
const y = h[_];
if (this._resolvedLevels.has(y.id) || this._prevResolvedLevels.get(y.id) !== f) continue;
const v = this._buildCtx(y.id), b = t.sceneToScreen(y.x, y.y);
v.centerProximity = 1 - Math.min(1, Math.hypot(b.x - u, b.y - p) / x);
const C = M(d.importance, y.data, v);
r.push(y, C || 0);
}
this._sortPairs(r);
for (let _ = 0; _ < r.length; _ += 2) {
const y = r[_];
this._fillBbox(g, y, d, l, s, t), m.collides(g) || (m.insert({
minX: g.minX,
minY: g.minY,
maxX: g.maxX,
maxY: g.maxY
}), this._resolvedLevels.set(y.id, f));
}
}
c.length = 0;
for (let _ = 0; _ < h.length; _++) {
const y = h[_];
if (this._resolvedLevels.has(y.id)) continue;
const v = this._buildCtx(y.id), b = t.sceneToScreen(y.x, y.y);
v.centerProximity = 1 - Math.min(1, Math.hypot(b.x - u, b.y - p) / x);
const C = M(d.importance, y.data, v);
c.push(y, C || 0);
}
this._sortPairs(c);
for (let _ = 0; _ < c.length; _ += 2) {
const y = c[_];
this._fillBbox(g, y, d, l, s, t), m.collides(g) || (m.insert({
minX: g.minX,
minY: g.minY,
maxX: g.maxX,
maxY: g.maxY
}), this._resolvedLevels.set(y.id, f));
}
}
for (let f = 0; f < h.length; f++) {
const d = h[f];
this._resolvedLevels.has(d.id) || this._resolvedLevels.set(d.id, 0);
}
}
/**
* Find the highest candidate level index for the current zoom.
*/
_getCandidateLevelIndex(t) {
const e = this._levels;
let s = 0;
for (let i = 0; i < e.length; i++)
t >= e[i].minZoom && (e[i].maxZoom === void 0 || t < e[i].maxZoom) && (s = i);
return s;
}
/**
* O(N) render path — used during layout when positions change every frame.
*/
_renderAllNodes(t, e) {
this._attachedNodes.clear();
for (let s = 0; s < this._nodes.length; s++) {
const i = this._nodes[s];
if (!i || !i._element) continue;
const n = Math.max(i.size, 50 / this._lastScale);
i.visible && t.isVisible(i.x, i.y, n) ? (i._inDOM || (this._root.appendChild(i._element), i._inDOM = !0, this._reapplyState(i)), this._applyTransform(i), this._updateNodeContent(i, t, e), this._attachedNodes.add(i)) : (i._inDOM && (this._root.removeChild(i._element), i._inDOM = !1), this._domOverlay && this._domOverlay.detach(i.id));
}
}
/**
* R-tree render path — used during pan/zoom when positions are stable.
*/
_renderWithSpatialQuery(t, e) {
const s = t.getVisibleBounds(), i = Math.max(this._maxNodeSize, 100 / this._lastScale), n = this._spatialIndex.search({
minX: s.left - i,
minY: s.top - i,
maxX: s.right + i,
maxY: s.bottom + i
}), o = this._swapAttachedSet;
o.clear();
for (let h = 0; h < n.length; h++) {
const l = n[h].node;
if (!l._element) continue;
const c = Math.max(l.size, 50 / this._lastScale);
t.isVisible(l.x, l.y, c) && (l._inDOM || (this._root.appendChild(l._element), l._inDOM = !0, this._reapplyState(l), this._applyTransform(l)), this._updateNodeContent(l, t, e), o.add(l));
}
for (const h of this._attachedNodes)
o.has(h) || (h._inDOM && (this._root.removeChild(h._element), h._inDOM = !1), this._domOverlay && this._domOverlay.detach(h.id));
this._swapAttachedSet = this._attachedNodes, this._attachedNodes = o;
}
_rebuildSpatialIndex() {
const t = [];
for (const e of this._nodes)
!e || !e.visible || t.push({
minX: e.x,
minY: e.y,
maxX: e.x,
maxY: e.y,
node: e
});
this._spatialIndex.clear(), t.length > 0 && this._spatialIndex.load(t);
}
_addNodeToScene(t, e) {
if (!t._element || !e) return;
const s = Math.max(t.size, 50 / this._lastScale);
e.isVisible(t.x, t.y, s) && (t._inDOM || (this._root.appendChild(t._element), t._inDOM = !0, this._reapplyState(t), this._attachedNodes.add(t)), this._updateNodeContent(t, e));
}
/**
* Update node content based on resolved level.
*/
_updateNodeContent(t, e, s = !1) {
if (!t._element || !e || (s && this._applyTransform(t), this._levels.length === 0)) return;