@qe-libs/qeviz 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -3
- package/dist/qeviz.es.js +767 -63
- package/dist/qeviz.es.js.map +1 -1
- package/dist/qeviz.umd.js +767 -63
- package/dist/qeviz.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/qeviz.es.js
CHANGED
|
@@ -595,9 +595,10 @@ const _Color = class _Color {
|
|
|
595
595
|
__name(_Color, "Color");
|
|
596
596
|
let Color = _Color;
|
|
597
597
|
const _WeightedColor = class _WeightedColor extends Color {
|
|
598
|
-
constructor(base, weight) {
|
|
598
|
+
constructor(base, weight, alpha) {
|
|
599
599
|
super(base);
|
|
600
600
|
this.weight = weight;
|
|
601
|
+
this.alpha = alpha;
|
|
601
602
|
}
|
|
602
603
|
// Parse hex → HSV (h in [0, 360), s/v in [0, 1]).
|
|
603
604
|
static toHSV(hex) {
|
|
@@ -668,7 +669,8 @@ const _WeightedColor = class _WeightedColor extends Color {
|
|
|
668
669
|
toString() {
|
|
669
670
|
const { h, v } = _WeightedColor.toHSV(this.base);
|
|
670
671
|
const w = Math.min(1, Math.abs(this.weight));
|
|
671
|
-
|
|
672
|
+
const a = this.alpha === void 0 ? w : Math.min(1, Math.max(0, this.alpha));
|
|
673
|
+
return _WeightedColor.hsvToRgba(h, w, v, a);
|
|
672
674
|
}
|
|
673
675
|
};
|
|
674
676
|
__name(_WeightedColor, "WeightedColor");
|
|
@@ -2180,9 +2182,22 @@ const _EdgeArrow = class _EdgeArrow extends PlottedObject {
|
|
|
2180
2182
|
return __privateGet(this, _edge);
|
|
2181
2183
|
}
|
|
2182
2184
|
get path() {
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2185
|
+
const _this = __privateGet(this, _edge);
|
|
2186
|
+
const T = [_this.to.x, _this.to.y];
|
|
2187
|
+
const A = _this.from_point;
|
|
2188
|
+
const [B0, B1] = _this.base_pts;
|
|
2189
|
+
const L = Math.hypot(A[0] - T[0], A[1] - T[1]);
|
|
2190
|
+
const W = Math.hypot(B0[0] - B1[0], B0[1] - B1[1]);
|
|
2191
|
+
if (!(L > 0) || !(W > 0)) return "";
|
|
2192
|
+
const u = [(A[0] - T[0]) / L, (A[1] - T[1]) / L];
|
|
2193
|
+
const b = Math.hypot(B0[0] - T[0], B0[1] - T[1]);
|
|
2194
|
+
const v = [(B0[0] - T[0]) / b, (B0[1] - T[1]) / b];
|
|
2195
|
+
const halfW = /* @__PURE__ */ __name((d) => W / 2 * Math.max(0, 1 - d / L), "halfW");
|
|
2196
|
+
const at = /* @__PURE__ */ __name((d, s) => `${T[0] + u[0] * d + v[0] * s} ${T[1] + u[1] * d + v[1] * s}`, "at");
|
|
2197
|
+
const dh = 0.2 * L;
|
|
2198
|
+
const c = Math.min(0.15 * L, 0.8 * 2 * halfW(dh));
|
|
2199
|
+
const dm = dh + c / 2, dn = dh + c;
|
|
2200
|
+
const p = `M${at(dh, 0)} L${at(dm, halfW(dm))} L${at(dn, halfW(dn))} L${at(dm, 0)} L${at(dn, -halfW(dn))} L${at(dm, -halfW(dm))}z`;
|
|
2186
2201
|
return p;
|
|
2187
2202
|
}
|
|
2188
2203
|
};
|
|
@@ -2325,6 +2340,46 @@ const _Drawable = class _Drawable {
|
|
|
2325
2340
|
};
|
|
2326
2341
|
__name(_Drawable, "Drawable");
|
|
2327
2342
|
let Drawable = _Drawable;
|
|
2343
|
+
const DEFAULT_EDGE_WIDTH = 0.04;
|
|
2344
|
+
const SATURATION_RANGE = [0.25, 1];
|
|
2345
|
+
const OPACITY_RANGE = [0.3, 1];
|
|
2346
|
+
function rampFor(w, lo, hi, range) {
|
|
2347
|
+
const a = Math.abs(w);
|
|
2348
|
+
if (!(a > 0)) return 0;
|
|
2349
|
+
if (!(hi > lo)) return range[1];
|
|
2350
|
+
const t = Math.min(1, Math.max(0, (a - lo) / (hi - lo)));
|
|
2351
|
+
return range[0] + t * (range[1] - range[0]);
|
|
2352
|
+
}
|
|
2353
|
+
__name(rampFor, "rampFor");
|
|
2354
|
+
const EDGE_WIDTH_PER_EXTENT = 0.011;
|
|
2355
|
+
const TAPERED_OUTLINE_PX = 0.75;
|
|
2356
|
+
const CHEVRON_DARKEN = 0.8;
|
|
2357
|
+
const CHEVRON_HALO_PX = 0.75;
|
|
2358
|
+
const POINT_RADIUS_PER_EXTENT = 6e-3;
|
|
2359
|
+
const UNCONNECTED_NODE_SIZE = 0.6;
|
|
2360
|
+
function widthPerWeight(edgeWidth) {
|
|
2361
|
+
return 2 * edgeWidth;
|
|
2362
|
+
}
|
|
2363
|
+
__name(widthPerWeight, "widthPerWeight");
|
|
2364
|
+
function nodeSizeForEdges(weightSum, widthFrac) {
|
|
2365
|
+
return weightSum * widthFrac / (2 * POINT_RADIUS_PER_EXTENT);
|
|
2366
|
+
}
|
|
2367
|
+
__name(nodeSizeForEdges, "nodeSizeForEdges");
|
|
2368
|
+
function flattenOnWhite(col) {
|
|
2369
|
+
const m = /rgba?\(([^)]+)\)/.exec(col);
|
|
2370
|
+
if (!m) return col;
|
|
2371
|
+
const [r, g, b, a = 1] = m[1].split(",").map(Number);
|
|
2372
|
+
const f = /* @__PURE__ */ __name((c) => Math.round(255 + (c - 255) * a), "f");
|
|
2373
|
+
return `rgb(${f(r)},${f(g)},${f(b)})`;
|
|
2374
|
+
}
|
|
2375
|
+
__name(flattenOnWhite, "flattenOnWhite");
|
|
2376
|
+
function darkenRgba(col, k) {
|
|
2377
|
+
const m = /rgba?\(([^)]+)\)/.exec(col);
|
|
2378
|
+
if (!m) return col;
|
|
2379
|
+
const [r, g, b, a = 1] = m[1].split(",").map(Number);
|
|
2380
|
+
return `rgba(${Math.round(r * k)},${Math.round(g * k)},${Math.round(b * k)},${a})`;
|
|
2381
|
+
}
|
|
2382
|
+
__name(darkenRgba, "darkenRgba");
|
|
2328
2383
|
const _QELine = class _QELine extends Drawable {
|
|
2329
2384
|
constructor(__data__, parent, graph, __children__ = new QEArray()) {
|
|
2330
2385
|
super(__data__, parent, graph, __children__);
|
|
@@ -2334,8 +2389,22 @@ const _QELine = class _QELine extends Drawable {
|
|
|
2334
2389
|
this.__children__ = __children__;
|
|
2335
2390
|
this.__name__ = "unnamed-line";
|
|
2336
2391
|
this._arrow_el = null;
|
|
2392
|
+
this._hit_el = null;
|
|
2337
2393
|
this.svg_element = this.create();
|
|
2338
2394
|
}
|
|
2395
|
+
/** Stamp edge identity onto an element so <qe-graph> delegation can resolve
|
|
2396
|
+
* qe-edge-* events. No-op for non-edges (e.g. axis lines). */
|
|
2397
|
+
_stampEdge(el) {
|
|
2398
|
+
const meta = this.__data__.meta ?? {};
|
|
2399
|
+
if (!meta.isMeanEdge) return;
|
|
2400
|
+
el.setAttribute("data-edge-source", String(meta.source ?? ""));
|
|
2401
|
+
el.setAttribute("data-edge-target", String(meta.target ?? ""));
|
|
2402
|
+
el.setAttribute("data-edge-weight", String(meta.weight ?? 0));
|
|
2403
|
+
el.setAttribute("data-edge-directed", meta.isDirected ? "true" : "false");
|
|
2404
|
+
if (meta.isSelf) el.setAttribute("data-edge-self", "true");
|
|
2405
|
+
if (meta.group != null) el.setAttribute("data-edge-group", String(meta.group));
|
|
2406
|
+
if (meta.unit != null) el.setAttribute("data-edge-unit", String(meta.unit));
|
|
2407
|
+
}
|
|
2339
2408
|
create() {
|
|
2340
2409
|
if (this.__data__ instanceof NodeEdge) {
|
|
2341
2410
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
@@ -2372,24 +2441,28 @@ const _QELine = class _QELine extends Drawable {
|
|
|
2372
2441
|
const extent = ((_a = this.graph.__data__.extremes) == null ? void 0 : _a.x_max) ?? 1;
|
|
2373
2442
|
const meta = this.__data__.meta ?? {};
|
|
2374
2443
|
if (meta.isMeanEdge) {
|
|
2444
|
+
this._stampEdge(this.svg_element);
|
|
2375
2445
|
const weight = typeof meta.weight === "number" ? meta.weight : 0;
|
|
2376
2446
|
const maxW = typeof meta.maxWeight === "number" ? meta.maxWeight : 1;
|
|
2377
2447
|
const opacity = typeof meta.opacity === "number" ? meta.opacity : Math.min(1, weight / (maxW || 1));
|
|
2378
|
-
const
|
|
2448
|
+
const ext = extent > 0 ? extent : 1;
|
|
2449
|
+
const sw = typeof meta.widthFrac === "number" ? ext * meta.widthFrac * weight : ext * EDGE_WIDTH_PER_EXTENT * (weight / (maxW || 1));
|
|
2379
2450
|
const baseColor = this.__data__._color || new Color("#000000");
|
|
2380
|
-
const
|
|
2451
|
+
const saturation = typeof meta.saturation === "number" ? meta.saturation : opacity;
|
|
2452
|
+
const edgeColor = new WeightedColor(baseColor, saturation, opacity).toString();
|
|
2381
2453
|
if (this.__data__ instanceof NodeEdge) {
|
|
2382
|
-
|
|
2383
|
-
const nodeR = extent * 6e-3 * nodeSize;
|
|
2384
|
-
this.__data__.weight = 2 * nodeR * 0.55 * opacity;
|
|
2454
|
+
this.__data__.weight = sw;
|
|
2385
2455
|
this.svg_element.setAttribute("d", this.__data__.path);
|
|
2386
2456
|
this.svg_element.setAttribute("fill", edgeColor);
|
|
2387
2457
|
this.svg_element.setAttribute("stroke", "none");
|
|
2388
2458
|
} else if (this.__data__ instanceof TaperedEdge) {
|
|
2389
|
-
this.__data__.weight = sw
|
|
2459
|
+
this.__data__.weight = sw;
|
|
2390
2460
|
this.svg_element.setAttribute("d", this.__data__.path);
|
|
2391
2461
|
this.svg_element.setAttribute("fill", edgeColor);
|
|
2392
|
-
this.svg_element.setAttribute("stroke",
|
|
2462
|
+
this.svg_element.setAttribute("stroke", edgeColor);
|
|
2463
|
+
this.svg_element.setAttribute("stroke-width", String(TAPERED_OUTLINE_PX));
|
|
2464
|
+
this.svg_element.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2465
|
+
this.svg_element.setAttribute("stroke-linejoin", "round");
|
|
2393
2466
|
if (meta.isDirected) {
|
|
2394
2467
|
try {
|
|
2395
2468
|
const arrow = new EdgeArrow(this.__data__);
|
|
@@ -2403,20 +2476,39 @@ const _QELine = class _QELine extends Drawable {
|
|
|
2403
2476
|
this.parent.appendChild(this._arrow_el);
|
|
2404
2477
|
}
|
|
2405
2478
|
this._arrow_el.setAttribute("d", arrowPath);
|
|
2406
|
-
|
|
2407
|
-
this._arrow_el.setAttribute("
|
|
2479
|
+
this._arrow_el.setAttribute("fill", darkenRgba(flattenOnWhite(edgeColor), CHEVRON_DARKEN));
|
|
2480
|
+
this._arrow_el.setAttribute("stroke", "#ffffff");
|
|
2481
|
+
this._arrow_el.setAttribute("stroke-width", String(CHEVRON_HALO_PX));
|
|
2482
|
+
this._arrow_el.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2483
|
+
this._arrow_el.setAttribute("stroke-linejoin", "round");
|
|
2408
2484
|
}
|
|
2409
2485
|
} catch (_) {
|
|
2410
2486
|
}
|
|
2411
2487
|
}
|
|
2412
2488
|
} else {
|
|
2413
|
-
this.
|
|
2414
|
-
this.
|
|
2415
|
-
this.svg_element.setAttribute("
|
|
2416
|
-
this.svg_element.setAttribute("
|
|
2489
|
+
const x1 = this.__data__.from.x, y1 = this.__data__.from.y;
|
|
2490
|
+
const x2 = this.__data__.to.x, y2 = this.__data__.to.y;
|
|
2491
|
+
this.svg_element.setAttribute("x1", x1.toString());
|
|
2492
|
+
this.svg_element.setAttribute("y1", y1.toString());
|
|
2493
|
+
this.svg_element.setAttribute("x2", x2.toString());
|
|
2494
|
+
this.svg_element.setAttribute("y2", y2.toString());
|
|
2417
2495
|
this.svg_element.setAttribute("stroke-width", sw.toString());
|
|
2418
2496
|
this.svg_element.setAttribute("stroke", edgeColor);
|
|
2497
|
+
this.svg_element.setAttribute("pointer-events", "stroke");
|
|
2419
2498
|
this.svg_element.removeAttribute("marker-end");
|
|
2499
|
+
if (!this._hit_el) {
|
|
2500
|
+
this._hit_el = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
2501
|
+
this._hit_el.setAttribute("stroke", "transparent");
|
|
2502
|
+
this._hit_el.setAttribute("stroke-linecap", "round");
|
|
2503
|
+
this._hit_el.setAttribute("pointer-events", "stroke");
|
|
2504
|
+
}
|
|
2505
|
+
if (!this._hit_el.parentElement) this.parent.appendChild(this._hit_el);
|
|
2506
|
+
this._hit_el.setAttribute("x1", x1.toString());
|
|
2507
|
+
this._hit_el.setAttribute("y1", y1.toString());
|
|
2508
|
+
this._hit_el.setAttribute("x2", x2.toString());
|
|
2509
|
+
this._hit_el.setAttribute("y2", y2.toString());
|
|
2510
|
+
this._hit_el.setAttribute("stroke-width", Math.max(sw, extent * 0.03).toString());
|
|
2511
|
+
this._stampEdge(this._hit_el);
|
|
2420
2512
|
}
|
|
2421
2513
|
} else {
|
|
2422
2514
|
this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
|
|
@@ -2430,10 +2522,11 @@ const _QELine = class _QELine extends Drawable {
|
|
|
2430
2522
|
}
|
|
2431
2523
|
}
|
|
2432
2524
|
draw() {
|
|
2433
|
-
var _a, _b;
|
|
2434
|
-
(_a = this.parent) == null ? void 0 : _a.appendChild(this.
|
|
2525
|
+
var _a, _b, _c;
|
|
2526
|
+
if (this._hit_el) (_a = this.parent) == null ? void 0 : _a.appendChild(this._hit_el);
|
|
2527
|
+
(_b = this.parent) == null ? void 0 : _b.appendChild(this.svg_element);
|
|
2435
2528
|
if (this._arrow_el) {
|
|
2436
|
-
(
|
|
2529
|
+
(_c = this.parent) == null ? void 0 : _c.appendChild(this._arrow_el);
|
|
2437
2530
|
}
|
|
2438
2531
|
}
|
|
2439
2532
|
};
|
|
@@ -2463,8 +2556,23 @@ const _QEPoint = class _QEPoint extends Drawable {
|
|
|
2463
2556
|
* (≈ 1–2 % of the coordinate half-extent for a typical ENA network).
|
|
2464
2557
|
*/
|
|
2465
2558
|
computeRadius() {
|
|
2466
|
-
|
|
2467
|
-
|
|
2559
|
+
var _a;
|
|
2560
|
+
const r = this.baseRadius();
|
|
2561
|
+
const cap = (_a = this.__data__.meta) == null ? void 0 : _a.maxRadius;
|
|
2562
|
+
return typeof cap === "number" && cap > 0 ? Math.min(r, cap) : r;
|
|
2563
|
+
}
|
|
2564
|
+
baseRadius() {
|
|
2565
|
+
var _a, _b, _c, _d;
|
|
2566
|
+
const extent = this.graph.__data__.extremes.x_max || 1;
|
|
2567
|
+
const sizeScale = ((_a = this.__data__.meta) == null ? void 0 : _a.sizeScale) ?? 1;
|
|
2568
|
+
if ((_b = this.__data__.meta) == null ? void 0 : _b.fixedSize) {
|
|
2569
|
+
const rect = (_d = (_c = this.graph.svg_element) == null ? void 0 : _c.getBoundingClientRect) == null ? void 0 : _d.call(_c);
|
|
2570
|
+
const zoom = this.graph.zoom_level || 1;
|
|
2571
|
+
const pxPerUnit = rect && rect.width > 0 ? rect.width / (2 * extent) : 1;
|
|
2572
|
+
const targetPx = this.__data__.size * 2 * sizeScale;
|
|
2573
|
+
return targetPx / pxPerUnit / zoom;
|
|
2574
|
+
}
|
|
2575
|
+
return extent * POINT_RADIUS_PER_EXTENT * this.__data__.size * sizeScale;
|
|
2468
2576
|
}
|
|
2469
2577
|
/** Optional halo stroke (e.g. white outline on unit points to separate
|
|
2470
2578
|
* overlapping dots in dense clusters). Width scales with the dot so it holds
|
|
@@ -2480,6 +2588,11 @@ const _QEPoint = class _QEPoint extends Drawable {
|
|
|
2480
2588
|
const tag = this.isSquare ? "rect" : "circle";
|
|
2481
2589
|
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
2482
2590
|
el.setAttribute("data-point-id", this.__data__.ID);
|
|
2591
|
+
const meta = this.__data__.meta ?? {};
|
|
2592
|
+
if (meta.kind) el.setAttribute("data-kind", meta.kind);
|
|
2593
|
+
if (meta.name != null) el.setAttribute("data-name", String(meta.name));
|
|
2594
|
+
if (meta.group != null) el.setAttribute("data-group", String(meta.group));
|
|
2595
|
+
if (meta.unit != null) el.setAttribute("data-unit", String(meta.unit));
|
|
2483
2596
|
el.setAttribute("fill", this.__data__.color.toString());
|
|
2484
2597
|
this.applyStroke(el);
|
|
2485
2598
|
const r = this.computeRadius();
|
|
@@ -2494,9 +2607,7 @@ const _QEPoint = class _QEPoint extends Drawable {
|
|
|
2494
2607
|
el.setAttribute("r", r.toString());
|
|
2495
2608
|
}
|
|
2496
2609
|
this.svg_element = el;
|
|
2497
|
-
el.addEventListener("click", (
|
|
2498
|
-
console.log("Point clicked: ", this.__data__);
|
|
2499
|
-
event.stopPropagation();
|
|
2610
|
+
el.addEventListener("click", () => {
|
|
2500
2611
|
this.__data__.active = !this.__data__.active;
|
|
2501
2612
|
this.update();
|
|
2502
2613
|
const pointEvent = new CustomEvent("point-clicked", {
|
|
@@ -2843,6 +2954,9 @@ const _QELabel = class _QELabel {
|
|
|
2843
2954
|
this.customX = customX;
|
|
2844
2955
|
this.customY = customY;
|
|
2845
2956
|
this._visible = false;
|
|
2957
|
+
this._offsetX = 0;
|
|
2958
|
+
this._offsetY = 0;
|
|
2959
|
+
this.pinned = false;
|
|
2846
2960
|
this.svg_element = this._create();
|
|
2847
2961
|
if (displayMode === Label.DISPLAY_TYPES.ON) {
|
|
2848
2962
|
this._visible = true;
|
|
@@ -2856,10 +2970,52 @@ const _QELabel = class _QELabel {
|
|
|
2856
2970
|
const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
2857
2971
|
el.textContent = this.text;
|
|
2858
2972
|
el.setAttribute("data-label-for", this.point.ID);
|
|
2859
|
-
el.setAttribute("pointer-events", "
|
|
2973
|
+
el.setAttribute("pointer-events", "auto");
|
|
2974
|
+
el.style.cursor = "grab";
|
|
2975
|
+
this._attachDrag(el);
|
|
2860
2976
|
this.parent.appendChild(el);
|
|
2861
2977
|
return el;
|
|
2862
2978
|
}
|
|
2979
|
+
/** Drag to reposition; pins the label open and emits qe-label-move on drop. */
|
|
2980
|
+
_attachDrag(el) {
|
|
2981
|
+
el.addEventListener("mousedown", (ev) => {
|
|
2982
|
+
ev.preventDefault();
|
|
2983
|
+
ev.stopPropagation();
|
|
2984
|
+
const svg = el.ownerSVGElement;
|
|
2985
|
+
if (!svg) return;
|
|
2986
|
+
const toUser = /* @__PURE__ */ __name((cx, cy) => {
|
|
2987
|
+
const p = svg.createSVGPoint();
|
|
2988
|
+
p.x = cx;
|
|
2989
|
+
p.y = cy;
|
|
2990
|
+
const m = svg.getScreenCTM();
|
|
2991
|
+
return m ? p.matrixTransform(m.inverse()) : { x: 0, y: 0 };
|
|
2992
|
+
}, "toUser");
|
|
2993
|
+
const start = toUser(ev.clientX, ev.clientY);
|
|
2994
|
+
const baseX = this._offsetX, baseY = this._offsetY;
|
|
2995
|
+
el.style.cursor = "grabbing";
|
|
2996
|
+
const onMove = /* @__PURE__ */ __name((e) => {
|
|
2997
|
+
const p = toUser(e.clientX, e.clientY);
|
|
2998
|
+
this._offsetX = baseX + (p.x - start.x);
|
|
2999
|
+
this._offsetY = baseY + (p.y - start.y);
|
|
3000
|
+
this.pinned = true;
|
|
3001
|
+
this._visible = true;
|
|
3002
|
+
this.svg_element.setAttribute("display", "block");
|
|
3003
|
+
this.update();
|
|
3004
|
+
}, "onMove");
|
|
3005
|
+
const onUp = /* @__PURE__ */ __name(() => {
|
|
3006
|
+
document.removeEventListener("mousemove", onMove);
|
|
3007
|
+
document.removeEventListener("mouseup", onUp);
|
|
3008
|
+
el.style.cursor = "grab";
|
|
3009
|
+
this.graph.dispatchEvent(new CustomEvent("qe-label-move", {
|
|
3010
|
+
detail: { id: this.point.ID, transform: { x: this._offsetX, y: this._offsetY } },
|
|
3011
|
+
bubbles: true,
|
|
3012
|
+
composed: true
|
|
3013
|
+
}));
|
|
3014
|
+
}, "onUp");
|
|
3015
|
+
document.addEventListener("mousemove", onMove);
|
|
3016
|
+
document.addEventListener("mouseup", onUp);
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
2863
3019
|
/**
|
|
2864
3020
|
* Recompute position and font-size from current graph extents.
|
|
2865
3021
|
* No-ops when the label is hidden (AUTO/CLICK modes while not hovered/clicked).
|
|
@@ -2908,6 +3064,9 @@ const _QELabel = class _QELabel {
|
|
|
2908
3064
|
}
|
|
2909
3065
|
this.svg_element.setAttribute("x", x.toString());
|
|
2910
3066
|
this.svg_element.setAttribute("y", y.toString());
|
|
3067
|
+
if (this._offsetX || this._offsetY) {
|
|
3068
|
+
this.svg_element.setAttribute("transform", `translate(${this._offsetX}, ${this._offsetY})`);
|
|
3069
|
+
}
|
|
2911
3070
|
this.svg_element.setAttribute("font-size", fontSize.toString());
|
|
2912
3071
|
this.svg_element.setAttribute("font-family", "system-ui, sans-serif");
|
|
2913
3072
|
this.svg_element.setAttribute("font-weight", "500");
|
|
@@ -2921,6 +3080,7 @@ const _QELabel = class _QELabel {
|
|
|
2921
3080
|
this.update();
|
|
2922
3081
|
}
|
|
2923
3082
|
hide() {
|
|
3083
|
+
if (this.pinned) return;
|
|
2924
3084
|
this._visible = false;
|
|
2925
3085
|
this.svg_element.setAttribute("display", "none");
|
|
2926
3086
|
}
|
|
@@ -3003,6 +3163,14 @@ function computeCIRect(varX, varY, n, pointScale) {
|
|
|
3003
3163
|
return { halfW: t95 * seX, halfH: t95 * seY };
|
|
3004
3164
|
}
|
|
3005
3165
|
__name(computeCIRect, "computeCIRect");
|
|
3166
|
+
const MEAN_MAX_FRACTION_OF_INTERVAL = 0.5;
|
|
3167
|
+
function capMeanToInterval(meanPt, halfW, halfH) {
|
|
3168
|
+
if (!meanPt) return;
|
|
3169
|
+
const half = Math.min(Math.abs(halfW), Math.abs(halfH));
|
|
3170
|
+
if (!(half > 0)) return;
|
|
3171
|
+
meanPt.meta = { ...meanPt.meta ?? {}, maxRadius: half * MEAN_MAX_FRACTION_OF_INTERVAL };
|
|
3172
|
+
}
|
|
3173
|
+
__name(capMeanToInterval, "capMeanToInterval");
|
|
3006
3174
|
function readIntervalBounds(row, dim0, dim1) {
|
|
3007
3175
|
const get = /* @__PURE__ */ __name((col) => {
|
|
3008
3176
|
const i = row.names.indexOf(col);
|
|
@@ -3016,6 +3184,11 @@ function readIntervalBounds(row, dim0, dim1) {
|
|
|
3016
3184
|
return { x_low: xLow, x_high: xHigh, y_low: yLow, y_high: yHigh };
|
|
3017
3185
|
}
|
|
3018
3186
|
__name(readIntervalBounds, "readIntervalBounds");
|
|
3187
|
+
function parseColorScale(attr) {
|
|
3188
|
+
const a = attr == null ? void 0 : attr.trim().toLowerCase();
|
|
3189
|
+
return a === "plot" || a === "absolute" ? a : "model";
|
|
3190
|
+
}
|
|
3191
|
+
__name(parseColorScale, "parseColorScale");
|
|
3019
3192
|
function parseLabelMode(attr, defaultMode) {
|
|
3020
3193
|
switch (attr == null ? void 0 : attr.toLowerCase()) {
|
|
3021
3194
|
case "on":
|
|
@@ -3049,6 +3222,13 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3049
3222
|
this._axisXText = null;
|
|
3050
3223
|
this._axisYText = null;
|
|
3051
3224
|
this._axisLabelEls = /* @__PURE__ */ new Map();
|
|
3225
|
+
this._magnifyText = null;
|
|
3226
|
+
this._magnifyEl = null;
|
|
3227
|
+
this._animateMs = 0;
|
|
3228
|
+
this._tweenRaf = null;
|
|
3229
|
+
this._highlight = { edges: /* @__PURE__ */ new Set(), nodes: /* @__PURE__ */ new Set() };
|
|
3230
|
+
this._highlightGroup = null;
|
|
3231
|
+
this._tooltipEl = null;
|
|
3052
3232
|
this.graph = this;
|
|
3053
3233
|
this.parent = null;
|
|
3054
3234
|
this.__data__ = new Graph(DefaultGraphOptions);
|
|
@@ -3060,6 +3240,8 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3060
3240
|
if (this._vizena && e.target !== this._vizena) return;
|
|
3061
3241
|
const model = e.detail;
|
|
3062
3242
|
if (!model) return;
|
|
3243
|
+
this._animateMs = this.hasAttribute("animate") ? parseInt(this.getAttribute("animate") || "", 10) || 300 : 0;
|
|
3244
|
+
const tweenFrom = this._animateMs > 0 && this._hasModel ? this._captureTween() : null;
|
|
3063
3245
|
this._hasModel = true;
|
|
3064
3246
|
const dims = model.dimensions;
|
|
3065
3247
|
const unitIdCol = model.unit_id_col;
|
|
@@ -3068,6 +3250,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3068
3250
|
const config = this._readLayerConfig();
|
|
3069
3251
|
this._showAxes = config.showAxes;
|
|
3070
3252
|
this._axisXText = config.axisXLabel === null ? null : config.axisXLabel || String((dims == null ? void 0 : dims[0]) ?? "x");
|
|
3253
|
+
this._magnifyText = config.edgeMagnify == null ? null : `(scaled ${config.edgeMagnify.toFixed(1)}x)`;
|
|
3071
3254
|
this._axisYText = config.axisYLabel === null ? null : config.axisYLabel || String((dims == null ? void 0 : dims[1]) ?? "y");
|
|
3072
3255
|
const groupColorMap = {};
|
|
3073
3256
|
if (groupColName && ((_a = model.points) == null ? void 0 : _a.names)) {
|
|
@@ -3136,8 +3319,16 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3136
3319
|
const colorStr = group ? groupColorMap[group] ?? "#888888" : "#888888";
|
|
3137
3320
|
if (showUnitPoints) {
|
|
3138
3321
|
const pt = rowToPoint(row, unitIdCol, colorStr, pointScale);
|
|
3139
|
-
pt.size = 2.5;
|
|
3140
|
-
pt.meta = {
|
|
3322
|
+
pt.size = config.pointSize ?? 2.5;
|
|
3323
|
+
pt.meta = {
|
|
3324
|
+
...pt.meta ?? {},
|
|
3325
|
+
stroke: "#ffffff",
|
|
3326
|
+
kind: "point",
|
|
3327
|
+
name: pt.ID,
|
|
3328
|
+
group: group ?? void 0,
|
|
3329
|
+
unit: pt.ID,
|
|
3330
|
+
fixedSize: config.pointFixedSize
|
|
3331
|
+
};
|
|
3141
3332
|
this.__data__.points.set(pt.ID, pt);
|
|
3142
3333
|
pointsLayer.__data__.objects.set(pt.ID, pt);
|
|
3143
3334
|
}
|
|
@@ -3187,19 +3378,26 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3187
3378
|
const meanPt = new Point({
|
|
3188
3379
|
ID: `mean-${group}`,
|
|
3189
3380
|
dimensions: [mx, my],
|
|
3190
|
-
meta: { group }
|
|
3381
|
+
meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
|
|
3191
3382
|
});
|
|
3192
3383
|
meanPt.color = new Color(color);
|
|
3193
3384
|
meanPt.size = 4;
|
|
3194
3385
|
meanPt.shape = "rect";
|
|
3195
3386
|
this.__data__.points.set(meanPt.ID, meanPt);
|
|
3196
3387
|
pointsLayer.__data__.objects.set(meanPt.ID, meanPt);
|
|
3197
|
-
const bounds = readIntervalBounds(row, dims[0], dims[1]);
|
|
3198
|
-
if (bounds) {
|
|
3388
|
+
const bounds = config.showCI ? readIntervalBounds(row, dims[0], dims[1]) : null;
|
|
3389
|
+
if (!bounds) {
|
|
3390
|
+
const stale = this._ciRects.get(group);
|
|
3391
|
+
if (stale) {
|
|
3392
|
+
stale.remove();
|
|
3393
|
+
this._ciRects.delete(group);
|
|
3394
|
+
}
|
|
3395
|
+
} else {
|
|
3199
3396
|
const rect_x = bounds.x_low * pointScale;
|
|
3200
3397
|
const rect_y = -bounds.y_high * pointScale;
|
|
3201
3398
|
const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
|
|
3202
3399
|
const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
|
|
3400
|
+
capMeanToInterval(meanPt, rect_width / 2, rect_height / 2);
|
|
3203
3401
|
let el = this._ciRects.get(group);
|
|
3204
3402
|
if (!el) {
|
|
3205
3403
|
el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
@@ -3225,7 +3423,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3225
3423
|
const meanPt = new Point({
|
|
3226
3424
|
ID: `mean-${group}`,
|
|
3227
3425
|
dimensions: [x / n * pointScale, -(y / n) * pointScale],
|
|
3228
|
-
meta: { group }
|
|
3426
|
+
meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
|
|
3229
3427
|
});
|
|
3230
3428
|
meanPt.color = new Color(color);
|
|
3231
3429
|
meanPt.size = 4;
|
|
@@ -3253,6 +3451,11 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3253
3451
|
const rect_y = -bounds.y_high * pointScale;
|
|
3254
3452
|
const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
|
|
3255
3453
|
const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
|
|
3454
|
+
capMeanToInterval(
|
|
3455
|
+
this.__data__.points.get(`mean-${group}`),
|
|
3456
|
+
rect_width / 2,
|
|
3457
|
+
rect_height / 2
|
|
3458
|
+
);
|
|
3256
3459
|
let el = this._ciRects.get(group);
|
|
3257
3460
|
if (!el) {
|
|
3258
3461
|
el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
@@ -3280,6 +3483,11 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3280
3483
|
const rawVarY = y2 / n - μy * μy;
|
|
3281
3484
|
const { halfW, halfH } = computeCIRect(rawVarX, rawVarY, n, pointScale);
|
|
3282
3485
|
const cx = μx * pointScale, cy = -μy * pointScale;
|
|
3486
|
+
capMeanToInterval(
|
|
3487
|
+
this.__data__.points.get(`mean-${group}`),
|
|
3488
|
+
halfW,
|
|
3489
|
+
halfH
|
|
3490
|
+
);
|
|
3283
3491
|
let el = this._ciRects.get(group);
|
|
3284
3492
|
if (!el) {
|
|
3285
3493
|
el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
@@ -3452,6 +3660,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3452
3660
|
model.nodes.rows().forEach((row) => {
|
|
3453
3661
|
const pt = rowToPoint(row, nodeIdCol, "#444444");
|
|
3454
3662
|
pt.size = 2;
|
|
3663
|
+
pt.meta = { ...pt.meta ?? {}, kind: "node", name: pt.ID };
|
|
3455
3664
|
nodeLayer.__data__.objects.set(pt.ID, pt);
|
|
3456
3665
|
});
|
|
3457
3666
|
if (edgesEnabled && edgeLayer && ((_l = model.edges) == null ? void 0 : _l.rows)) {
|
|
@@ -3550,21 +3759,46 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3550
3759
|
});
|
|
3551
3760
|
}
|
|
3552
3761
|
const isDirected = !!model.directed;
|
|
3553
|
-
const
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3762
|
+
const widthFrac = widthPerWeight(config.edgeWidth) * (config.edgeMagnify ?? 1);
|
|
3763
|
+
const rangeOf = /* @__PURE__ */ __name((ws) => {
|
|
3764
|
+
const nz = ws.map(Math.abs).filter((v) => v > 0);
|
|
3765
|
+
return nz.length ? [Math.min(...nz), Math.max(...nz)] : [0, 0];
|
|
3766
|
+
}, "rangeOf");
|
|
3767
|
+
const modelRange = /* @__PURE__ */ __name((self) => {
|
|
3768
|
+
const vals = [];
|
|
3769
|
+
edgePairs.forEach(({ col, pair }) => {
|
|
3770
|
+
if (pair[0] === pair[1] !== self) return;
|
|
3771
|
+
const column = model.edges.col(col);
|
|
3772
|
+
if (column) for (const v of column) vals.push(Number(v));
|
|
3773
|
+
});
|
|
3774
|
+
return rangeOf(vals);
|
|
3775
|
+
}, "modelRange");
|
|
3776
|
+
const plotRange = /* @__PURE__ */ __name((self) => rangeOf(drawEdges.filter((e2) => e2.pair[0] === e2.pair[1] === self).map((e2) => e2.weight)), "plotRange");
|
|
3777
|
+
const cscale = config.edgeColorScale;
|
|
3778
|
+
const colourRange = /* @__PURE__ */ __name((self) => cscale === "plot" ? plotRange(self) : modelRange(self), "colourRange");
|
|
3779
|
+
const lineRange = colourRange(false), selfRange = colourRange(true);
|
|
3780
|
+
const colourOf = /* @__PURE__ */ __name((w, self) => {
|
|
3781
|
+
if (cscale === "absolute") {
|
|
3782
|
+
const a = Math.min(1, Math.abs(w));
|
|
3783
|
+
return { saturation: a, opacity: a };
|
|
3784
|
+
}
|
|
3785
|
+
const [lo, hi] = self ? selfRange : lineRange;
|
|
3786
|
+
return { saturation: rampFor(w, lo, hi, SATURATION_RANGE), opacity: rampFor(w, lo, hi, OPACITY_RANGE) };
|
|
3787
|
+
}, "colourOf");
|
|
3557
3788
|
const nodeWeightSum = {};
|
|
3558
3789
|
drawEdges.forEach(({ pair, weight }) => {
|
|
3559
3790
|
const isSelf = pair[0] === pair[1];
|
|
3560
3791
|
if (isSelf && !isDirected) return;
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3792
|
+
const add = /* @__PURE__ */ __name((id) => {
|
|
3793
|
+
nodeWeightSum[id] = (nodeWeightSum[id] ?? 0) + Math.abs(weight);
|
|
3794
|
+
}, "add");
|
|
3795
|
+
if (isDirected) {
|
|
3796
|
+
add(pair[1]);
|
|
3797
|
+
return;
|
|
3564
3798
|
}
|
|
3799
|
+
add(pair[0]);
|
|
3800
|
+
add(pair[1]);
|
|
3565
3801
|
});
|
|
3566
|
-
const maxNodeWeight = Math.max(1, ...Object.values(nodeWeightSum));
|
|
3567
|
-
const nodeSizes = {};
|
|
3568
3802
|
model.nodes.rows().forEach((row) => {
|
|
3569
3803
|
const codeIdx = row.names.indexOf(nodeIdCol);
|
|
3570
3804
|
const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
|
|
@@ -3572,8 +3806,12 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3572
3806
|
const pt = nodeLayer.__data__.objects.get(id);
|
|
3573
3807
|
if (!pt) return;
|
|
3574
3808
|
const w = nodeWeightSum[id] ?? 0;
|
|
3575
|
-
|
|
3576
|
-
|
|
3809
|
+
const unconnected = !(w > 0);
|
|
3810
|
+
pt.size = unconnected ? config.hideUnconnectedNodes ? 0 : UNCONNECTED_NODE_SIZE : nodeSizeForEdges(w, widthFrac);
|
|
3811
|
+
pt.meta = {
|
|
3812
|
+
...pt.meta ?? {},
|
|
3813
|
+
hidden: unconnected && config.hideUnconnectedNodes
|
|
3814
|
+
};
|
|
3577
3815
|
});
|
|
3578
3816
|
const totalWeightByPair = {};
|
|
3579
3817
|
if (isDirected) {
|
|
@@ -3588,8 +3826,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3588
3826
|
if (!fromNode || !toNode) return;
|
|
3589
3827
|
const isSelf = pair[0] === pair[1];
|
|
3590
3828
|
if (isSelf && !isDirected) return;
|
|
3591
|
-
const
|
|
3592
|
-
const opacity = Math.min(1, weight / classMax);
|
|
3829
|
+
const { saturation, opacity } = colourOf(weight, isSelf);
|
|
3593
3830
|
const fromPt = new Point({ ID: `${pair[0]}-src`, dimensions: [fromNode.x, fromNode.y], meta: {} });
|
|
3594
3831
|
const toPt = new Point({ ID: `${pair[1]}-dst`, dimensions: [toNode.x, toNode.y], meta: {} });
|
|
3595
3832
|
let edgeLine;
|
|
@@ -3613,16 +3850,17 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3613
3850
|
edgeLine.meta = {
|
|
3614
3851
|
isMeanEdge: true,
|
|
3615
3852
|
weight,
|
|
3616
|
-
//
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
maxWeight: isSelf ? maxSelfWeight : maxEdgeWeight,
|
|
3853
|
+
// Width per unit weight (extent units), shared by all marks.
|
|
3854
|
+
widthFrac,
|
|
3855
|
+
saturation,
|
|
3620
3856
|
opacity,
|
|
3621
3857
|
isDirected,
|
|
3622
3858
|
isSelf,
|
|
3623
|
-
//
|
|
3624
|
-
|
|
3625
|
-
|
|
3859
|
+
// Identity for the qe-edge-* interaction events.
|
|
3860
|
+
source: pair[0],
|
|
3861
|
+
target: pair[1],
|
|
3862
|
+
group: groupAttr ?? void 0,
|
|
3863
|
+
unit: unitAttr ?? void 0
|
|
3626
3864
|
};
|
|
3627
3865
|
const targetLayer = isSelf ? edgenodeLayer ?? edgeLayer : edgeLayer;
|
|
3628
3866
|
targetLayer.__data__.objects.set(edgeLine.ID, edgeLine);
|
|
@@ -3630,7 +3868,15 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3630
3868
|
edgeLayer.__data__.__state__.setVariable("layer_updated", edgeLayer.__data__);
|
|
3631
3869
|
if (edgenodeLayer) edgenodeLayer.__data__.__state__.setVariable("layer_updated", edgenodeLayer.__data__);
|
|
3632
3870
|
}
|
|
3633
|
-
|
|
3871
|
+
const nodesVisible = config.showNodes;
|
|
3872
|
+
if (nodeLayer.svg_element) {
|
|
3873
|
+
nodeLayer.svg_element.style.display = nodesVisible ? "" : "none";
|
|
3874
|
+
}
|
|
3875
|
+
if (!nodesVisible) {
|
|
3876
|
+
this._nodeLabels.forEach((lbl) => lbl.remove());
|
|
3877
|
+
this._nodeLabels.clear();
|
|
3878
|
+
}
|
|
3879
|
+
if (labelSvg && labelNodes !== 0 && nodesVisible) {
|
|
3634
3880
|
this._nodeLabels.forEach((lbl, id) => {
|
|
3635
3881
|
if (!nodeLayer.__data__.objects.get(id)) {
|
|
3636
3882
|
lbl.remove();
|
|
@@ -3638,11 +3884,17 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3638
3884
|
}
|
|
3639
3885
|
});
|
|
3640
3886
|
model.nodes.rows().forEach((row) => {
|
|
3887
|
+
var _a2, _b2;
|
|
3641
3888
|
const codeIdx = row.names.indexOf(nodeIdCol);
|
|
3642
3889
|
const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
|
|
3643
3890
|
if (!id) return;
|
|
3644
3891
|
const pt = nodeLayer.__data__.objects.get(id);
|
|
3645
3892
|
if (!pt) return;
|
|
3893
|
+
if ((_a2 = pt.meta) == null ? void 0 : _a2.hidden) {
|
|
3894
|
+
(_b2 = this._nodeLabels.get(id)) == null ? void 0 : _b2.remove();
|
|
3895
|
+
this._nodeLabels.delete(id);
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3646
3898
|
let lbl = this._nodeLabels.get(id);
|
|
3647
3899
|
if (!lbl) {
|
|
3648
3900
|
lbl = new QELabel(id, pt, labelSvg, this, labelNodes);
|
|
@@ -3659,6 +3911,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3659
3911
|
if (axisPointsLayer) this.__data__.__state__.setVariable("graph_updated_axis_points", axisPointsLayer.__data__.objects);
|
|
3660
3912
|
if (axisLinesLayer) this.__data__.__state__.setVariable("graph_updated_axis_lines", axisLinesLayer.__data__.objects);
|
|
3661
3913
|
this.update();
|
|
3914
|
+
if (tweenFrom) this._runTween(tweenFrom);
|
|
3662
3915
|
});
|
|
3663
3916
|
}
|
|
3664
3917
|
static get observedAttributes() {
|
|
@@ -3699,6 +3952,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3699
3952
|
// should render. Called once at the top of every model-updated handler so
|
|
3700
3953
|
// all rendering decisions are derived from a single snapshot.
|
|
3701
3954
|
_readLayerConfig() {
|
|
3955
|
+
var _a;
|
|
3702
3956
|
const nodesEl = this.querySelector("qe-nodes");
|
|
3703
3957
|
const meansEl = this.querySelector("qe-means");
|
|
3704
3958
|
const pointsEl = this.querySelector("qe-points");
|
|
@@ -3707,11 +3961,18 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3707
3961
|
const axesEl = this.querySelector("qe-axes");
|
|
3708
3962
|
const labelsShorthand = this.getAttribute("labels");
|
|
3709
3963
|
return {
|
|
3710
|
-
// Nodes — always
|
|
3964
|
+
// Nodes — always placed from the model (they anchor the coordinate
|
|
3965
|
+
// space); <qe-nodes> presence draws them, like every other layer.
|
|
3711
3966
|
labelNodes: parseLabelMode(
|
|
3712
3967
|
(nodesEl == null ? void 0 : nodesEl.getAttribute("label")) ?? this.getAttribute("label-nodes") ?? labelsShorthand,
|
|
3713
3968
|
Label.DISPLAY_TYPES.ON
|
|
3714
3969
|
),
|
|
3970
|
+
showNodes: !!nodesEl,
|
|
3971
|
+
edgeWidth: (() => {
|
|
3972
|
+
const v = Number(this.getAttribute("edge-width"));
|
|
3973
|
+
return Number.isFinite(v) && v > 0 && v <= 1 ? v : DEFAULT_EDGE_WIDTH;
|
|
3974
|
+
})(),
|
|
3975
|
+
hideUnconnectedNodes: ((_a = nodesEl == null ? void 0 : nodesEl.getAttribute("unconnected")) == null ? void 0 : _a.toLowerCase()) === "hide",
|
|
3715
3976
|
scalePoints: this.getAttribute("scale-points") !== "false",
|
|
3716
3977
|
// Means — presence of <qe-means> = show; absence = hide.
|
|
3717
3978
|
showMeans: !!meansEl,
|
|
@@ -3721,18 +3982,28 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3721
3982
|
(meansEl == null ? void 0 : meansEl.getAttribute("label")) ?? this.getAttribute("label-means") ?? labelsShorthand,
|
|
3722
3983
|
Label.DISPLAY_TYPES.ON
|
|
3723
3984
|
),
|
|
3985
|
+
// fixed-size keeps mean markers a constant on-screen px size (webENA parity).
|
|
3986
|
+
meanFixedSize: !!meansEl && meansEl.hasAttribute("fixed-size"),
|
|
3724
3987
|
// Points — presence of <qe-points> = show unit dots.
|
|
3725
3988
|
showPoints: !!pointsEl,
|
|
3726
3989
|
labelPoints: parseLabelMode(
|
|
3727
3990
|
(pointsEl == null ? void 0 : pointsEl.getAttribute("label")) ?? this.getAttribute("label-points") ?? labelsShorthand,
|
|
3728
3991
|
Label.DISPLAY_TYPES.OFF
|
|
3729
3992
|
),
|
|
3993
|
+
pointSize: (pointsEl == null ? void 0 : pointsEl.getAttribute("size")) ? parseFloat(pointsEl.getAttribute("size")) : null,
|
|
3994
|
+
// fixed-size keeps unit dots a constant on-screen px size (webENA parity).
|
|
3995
|
+
pointFixedSize: !!pointsEl && pointsEl.hasAttribute("fixed-size"),
|
|
3730
3996
|
// Edges — presence of <qe-edges> = draw a network.
|
|
3731
3997
|
showEdges: !!edgesEl,
|
|
3732
3998
|
edgeGroup: (edgesEl == null ? void 0 : edgesEl.getAttribute("group")) ?? null,
|
|
3733
3999
|
edgeUnit: (edgesEl == null ? void 0 : edgesEl.getAttribute("unit")) ?? null,
|
|
3734
4000
|
edgeCompare: (edgesEl == null ? void 0 : edgesEl.getAttribute("compare")) ?? null,
|
|
3735
4001
|
edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null,
|
|
4002
|
+
edgeColorScale: parseColorScale((edgesEl == null ? void 0 : edgesEl.getAttribute("color-scale")) ?? null),
|
|
4003
|
+
edgeMagnify: (edgesEl == null ? void 0 : edgesEl.hasAttribute("magnify")) ? (() => {
|
|
4004
|
+
const v = Number(edgesEl.getAttribute("magnify"));
|
|
4005
|
+
return Number.isFinite(v) && v > 0 ? v : 1;
|
|
4006
|
+
})() : null,
|
|
3736
4007
|
// Trajectories — presence of <qe-trajectories> = draw cognitive trajectories
|
|
3737
4008
|
showTrajectories: !!trajectoriesEl,
|
|
3738
4009
|
trajectoryUnits: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("units")) ?? null,
|
|
@@ -3773,6 +4044,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3773
4044
|
if (w) this.style.width = isNaN(Number(w)) ? w : `${w}px`;
|
|
3774
4045
|
if (h) this.style.height = isNaN(Number(h)) ? h : `${h}px`;
|
|
3775
4046
|
this.appendChild(this.svg_element);
|
|
4047
|
+
_QEGraphElement._instances.add(this);
|
|
3776
4048
|
this._childObserver = new MutationObserver(() => this._scheduleRerender());
|
|
3777
4049
|
this._childObserver.observe(this, {
|
|
3778
4050
|
childList: true,
|
|
@@ -3783,10 +4055,14 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3783
4055
|
"unit",
|
|
3784
4056
|
"compare",
|
|
3785
4057
|
"also",
|
|
4058
|
+
"color-scale",
|
|
4059
|
+
"magnify",
|
|
3786
4060
|
"groups",
|
|
3787
4061
|
"confidence",
|
|
3788
4062
|
"label",
|
|
4063
|
+
"unconnected",
|
|
3789
4064
|
"scale-points",
|
|
4065
|
+
"edge-width",
|
|
3790
4066
|
"labels",
|
|
3791
4067
|
"label-nodes",
|
|
3792
4068
|
"label-means",
|
|
@@ -3802,7 +4078,9 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3802
4078
|
"stroke-width-range",
|
|
3803
4079
|
"opacity-range",
|
|
3804
4080
|
"x",
|
|
3805
|
-
"y"
|
|
4081
|
+
"y",
|
|
4082
|
+
"size",
|
|
4083
|
+
"fixed-size"
|
|
3806
4084
|
]
|
|
3807
4085
|
});
|
|
3808
4086
|
const forId = this.getAttribute("for");
|
|
@@ -3822,9 +4100,275 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3822
4100
|
}
|
|
3823
4101
|
}
|
|
3824
4102
|
disconnectedCallback() {
|
|
3825
|
-
var _a;
|
|
4103
|
+
var _a, _b;
|
|
3826
4104
|
(_a = this._childObserver) == null ? void 0 : _a.disconnect();
|
|
3827
4105
|
this._childObserver = null;
|
|
4106
|
+
_QEGraphElement._instances.delete(this);
|
|
4107
|
+
(_b = this._tooltipEl) == null ? void 0 : _b.remove();
|
|
4108
|
+
this._tooltipEl = null;
|
|
4109
|
+
}
|
|
4110
|
+
/** All connected <qe-graph> instances (newest membership order). */
|
|
4111
|
+
static instances() {
|
|
4112
|
+
return [..._QEGraphElement._instances];
|
|
4113
|
+
}
|
|
4114
|
+
/** Look up a graph by its `id` or `plot-group` attribute. */
|
|
4115
|
+
static getGraph(key) {
|
|
4116
|
+
for (const g of _QEGraphElement._instances) {
|
|
4117
|
+
if (g.id === key || g.getAttribute("plot-group") === key) return g;
|
|
4118
|
+
}
|
|
4119
|
+
return null;
|
|
4120
|
+
}
|
|
4121
|
+
/** Resolved code nodes with their current graph-space coordinates. */
|
|
4122
|
+
getNodes() {
|
|
4123
|
+
const layer = this.__children__.search("__children__", "nodes");
|
|
4124
|
+
if (!layer) return [];
|
|
4125
|
+
return [...layer.__data__.objects.values()].map((o) => {
|
|
4126
|
+
var _a;
|
|
4127
|
+
return {
|
|
4128
|
+
id: o.ID,
|
|
4129
|
+
name: ((_a = o.meta) == null ? void 0 : _a.name) ?? o.ID,
|
|
4130
|
+
x: o.x,
|
|
4131
|
+
y: o.y
|
|
4132
|
+
};
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4135
|
+
/** Resolved edges of the current network with weights and direction. */
|
|
4136
|
+
getEdges() {
|
|
4137
|
+
const collect = /* @__PURE__ */ __name((name) => {
|
|
4138
|
+
const l = this.__children__.search("__children__", name);
|
|
4139
|
+
return l ? [...l.__data__.objects.values()] : [];
|
|
4140
|
+
}, "collect");
|
|
4141
|
+
return [...collect("edges"), ...collect("edgenodes")].map((o) => {
|
|
4142
|
+
const m = o.meta ?? {};
|
|
4143
|
+
return {
|
|
4144
|
+
source: m.source,
|
|
4145
|
+
target: m.target,
|
|
4146
|
+
weight: m.weight ?? 0,
|
|
4147
|
+
directed: !!m.isDirected,
|
|
4148
|
+
self: !!m.isSelf
|
|
4149
|
+
};
|
|
4150
|
+
}).filter((e) => e.source != null && e.target != null);
|
|
4151
|
+
}
|
|
4152
|
+
/** Marks (circles/rects), undirected edge lines, and directed / self-edge
|
|
4153
|
+
* triangle paths all participate in tweening. */
|
|
4154
|
+
_tweenTargets() {
|
|
4155
|
+
return [...this.svg_element.querySelectorAll(
|
|
4156
|
+
"circle[data-point-id], rect[data-point-id], line[data-edge-source], path[data-edge-source]"
|
|
4157
|
+
)];
|
|
4158
|
+
}
|
|
4159
|
+
_readCoords(el) {
|
|
4160
|
+
const tag = el.tagName.toLowerCase();
|
|
4161
|
+
const n = /* @__PURE__ */ __name((a) => parseFloat(el.getAttribute(a) || "0"), "n");
|
|
4162
|
+
if (tag === "circle") return { t: "c", cx: n("cx"), cy: n("cy") };
|
|
4163
|
+
if (tag === "rect") return { t: "r", x: n("x"), y: n("y") };
|
|
4164
|
+
if (tag === "line") return { t: "l", x1: n("x1"), y1: n("y1"), x2: n("x2"), y2: n("y2") };
|
|
4165
|
+
if (tag === "path") {
|
|
4166
|
+
const d = el.getAttribute("d") || "";
|
|
4167
|
+
return { t: "p", d, nums: (d.match(_QEGraphElement._PATH_NUM_RE) || []).map(Number) };
|
|
4168
|
+
}
|
|
4169
|
+
return null;
|
|
4170
|
+
}
|
|
4171
|
+
/** Snapshot current tween-target positions keyed by element. */
|
|
4172
|
+
_captureTween() {
|
|
4173
|
+
const m = /* @__PURE__ */ new Map();
|
|
4174
|
+
this._tweenTargets().forEach((el) => {
|
|
4175
|
+
const c = this._readCoords(el);
|
|
4176
|
+
if (c) m.set(el, c);
|
|
4177
|
+
});
|
|
4178
|
+
return m;
|
|
4179
|
+
}
|
|
4180
|
+
/** Animate each still-present element from its snapshot to its new position. */
|
|
4181
|
+
_runTween(from) {
|
|
4182
|
+
if (this._tweenRaf !== null) {
|
|
4183
|
+
cancelAnimationFrame(this._tweenRaf);
|
|
4184
|
+
this._tweenRaf = null;
|
|
4185
|
+
}
|
|
4186
|
+
const to = /* @__PURE__ */ new Map();
|
|
4187
|
+
this._tweenTargets().forEach((el) => {
|
|
4188
|
+
if (from.has(el)) {
|
|
4189
|
+
const c = this._readCoords(el);
|
|
4190
|
+
if (c) to.set(el, c);
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
if (!to.size) return;
|
|
4194
|
+
const dur = this._animateMs;
|
|
4195
|
+
const t0 = performance.now();
|
|
4196
|
+
const ease = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2, "ease");
|
|
4197
|
+
const lerp = /* @__PURE__ */ __name((a, b, k) => a + (b - a) * k, "lerp");
|
|
4198
|
+
const step = /* @__PURE__ */ __name((now) => {
|
|
4199
|
+
const t = Math.min(1, (now - t0) / dur);
|
|
4200
|
+
const k = ease(t);
|
|
4201
|
+
to.forEach((nc, el) => {
|
|
4202
|
+
const oc = from.get(el);
|
|
4203
|
+
if (!oc || oc.t !== nc.t) return;
|
|
4204
|
+
if (nc.t === "c") {
|
|
4205
|
+
el.setAttribute("cx", String(lerp(oc.cx, nc.cx, k)));
|
|
4206
|
+
el.setAttribute("cy", String(lerp(oc.cy, nc.cy, k)));
|
|
4207
|
+
} else if (nc.t === "r") {
|
|
4208
|
+
el.setAttribute("x", String(lerp(oc.x, nc.x, k)));
|
|
4209
|
+
el.setAttribute("y", String(lerp(oc.y, nc.y, k)));
|
|
4210
|
+
} else if (nc.t === "l") {
|
|
4211
|
+
el.setAttribute("x1", String(lerp(oc.x1, nc.x1, k)));
|
|
4212
|
+
el.setAttribute("y1", String(lerp(oc.y1, nc.y1, k)));
|
|
4213
|
+
el.setAttribute("x2", String(lerp(oc.x2, nc.x2, k)));
|
|
4214
|
+
el.setAttribute("y2", String(lerp(oc.y2, nc.y2, k)));
|
|
4215
|
+
} else {
|
|
4216
|
+
if (!oc.nums || oc.nums.length !== nc.nums.length) return;
|
|
4217
|
+
let i = 0;
|
|
4218
|
+
const d = nc.d.replace(
|
|
4219
|
+
_QEGraphElement._PATH_NUM_RE,
|
|
4220
|
+
() => String(lerp(oc.nums[i], nc.nums[i++], k))
|
|
4221
|
+
);
|
|
4222
|
+
el.setAttribute("d", d);
|
|
4223
|
+
}
|
|
4224
|
+
});
|
|
4225
|
+
this._tweenRaf = t < 1 ? requestAnimationFrame(step) : null;
|
|
4226
|
+
}, "step");
|
|
4227
|
+
this._tweenRaf = requestAnimationFrame(step);
|
|
4228
|
+
}
|
|
4229
|
+
/** Serialize the current plot as a standalone SVG string (webENA P2.3). */
|
|
4230
|
+
toSVG() {
|
|
4231
|
+
const svg = this.svg_element.cloneNode(true);
|
|
4232
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
4233
|
+
svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
4234
|
+
const rect = this.svg_element.getBoundingClientRect();
|
|
4235
|
+
if (rect.width) svg.setAttribute("width", String(Math.round(rect.width)));
|
|
4236
|
+
if (rect.height) svg.setAttribute("height", String(Math.round(rect.height)));
|
|
4237
|
+
return new XMLSerializer().serializeToString(svg);
|
|
4238
|
+
}
|
|
4239
|
+
/** Rasterize the current plot to a PNG data URL (webENA P2.3). `scale`
|
|
4240
|
+
* multiplies the pixel dimensions for a higher-resolution image. */
|
|
4241
|
+
toPNG(scale = 2) {
|
|
4242
|
+
const svgStr = this.toSVG();
|
|
4243
|
+
const rect = this.svg_element.getBoundingClientRect();
|
|
4244
|
+
const w = Math.max(1, Math.round(rect.width));
|
|
4245
|
+
const h = Math.max(1, Math.round(rect.height));
|
|
4246
|
+
return new Promise((resolve, reject) => {
|
|
4247
|
+
const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
|
|
4248
|
+
const url = URL.createObjectURL(blob);
|
|
4249
|
+
const img = new Image();
|
|
4250
|
+
img.onload = () => {
|
|
4251
|
+
try {
|
|
4252
|
+
const canvas = document.createElement("canvas");
|
|
4253
|
+
canvas.width = w * scale;
|
|
4254
|
+
canvas.height = h * scale;
|
|
4255
|
+
const ctx = canvas.getContext("2d");
|
|
4256
|
+
if (!ctx) {
|
|
4257
|
+
URL.revokeObjectURL(url);
|
|
4258
|
+
reject(new Error("no 2d context"));
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
ctx.scale(scale, scale);
|
|
4262
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
4263
|
+
URL.revokeObjectURL(url);
|
|
4264
|
+
resolve(canvas.toDataURL("image/png"));
|
|
4265
|
+
} catch (err) {
|
|
4266
|
+
URL.revokeObjectURL(url);
|
|
4267
|
+
reject(err);
|
|
4268
|
+
}
|
|
4269
|
+
};
|
|
4270
|
+
img.onerror = (e) => {
|
|
4271
|
+
URL.revokeObjectURL(url);
|
|
4272
|
+
reject(e);
|
|
4273
|
+
};
|
|
4274
|
+
img.src = url;
|
|
4275
|
+
});
|
|
4276
|
+
}
|
|
4277
|
+
/** Unit points with their current graph-space coordinates. */
|
|
4278
|
+
getPoints() {
|
|
4279
|
+
const layer = this.__children__.search("__children__", "points");
|
|
4280
|
+
if (!layer) return [];
|
|
4281
|
+
return [...layer.__data__.objects.values()].filter((o) => {
|
|
4282
|
+
var _a;
|
|
4283
|
+
return ((_a = o.meta) == null ? void 0 : _a.kind) === "point";
|
|
4284
|
+
}).map((o) => {
|
|
4285
|
+
var _a;
|
|
4286
|
+
return { id: o.ID, group: (_a = o.meta) == null ? void 0 : _a.group, x: o.x, y: o.y };
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
/** Ids of unit points inside a graph-space rectangle (for drag/lasso select). */
|
|
4290
|
+
pointsInRect(rect) {
|
|
4291
|
+
return this.getPoints().filter((p) => p.x >= rect.x && p.x <= rect.x + rect.w && p.y >= rect.y && p.y <= rect.y + rect.h).map((p) => p.id);
|
|
4292
|
+
}
|
|
4293
|
+
/** Order-independent key so an undirected pair matches either direction. */
|
|
4294
|
+
_edgeKey(a, b) {
|
|
4295
|
+
return a <= b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
4296
|
+
}
|
|
4297
|
+
/** Mark edges/nodes as highlighted. Replaces the previous selection. */
|
|
4298
|
+
setHighlight(sel) {
|
|
4299
|
+
this._highlight.edges = new Set((sel.edges ?? []).map((e) => this._edgeKey(e.source, e.target)));
|
|
4300
|
+
this._highlight.nodes = new Set((sel.nodes ?? []).map(String));
|
|
4301
|
+
this._applyHighlights();
|
|
4302
|
+
}
|
|
4303
|
+
/** Clear all highlighting. */
|
|
4304
|
+
clearHighlight() {
|
|
4305
|
+
this._highlight.edges.clear();
|
|
4306
|
+
this._highlight.nodes.clear();
|
|
4307
|
+
this._applyHighlights();
|
|
4308
|
+
}
|
|
4309
|
+
_applyHighlights() {
|
|
4310
|
+
if (!this.svg_element) return;
|
|
4311
|
+
if (!this._highlightGroup) {
|
|
4312
|
+
this._highlightGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
4313
|
+
this._highlightGroup.setAttribute("data-layer-name", "highlight");
|
|
4314
|
+
this._highlightGroup.setAttribute("pointer-events", "none");
|
|
4315
|
+
}
|
|
4316
|
+
this.svg_element.appendChild(this._highlightGroup);
|
|
4317
|
+
const g = this._highlightGroup;
|
|
4318
|
+
g.textContent = "";
|
|
4319
|
+
const extent = this.__data__.extremes.x_max || 1;
|
|
4320
|
+
const HL = "#f5a623";
|
|
4321
|
+
const mk = /* @__PURE__ */ __name((tag, attrs) => {
|
|
4322
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
4323
|
+
for (const k in attrs) el.setAttribute(k, String(attrs[k]));
|
|
4324
|
+
return el;
|
|
4325
|
+
}, "mk");
|
|
4326
|
+
if (this._highlight.edges.size) {
|
|
4327
|
+
const xy = {};
|
|
4328
|
+
this.getNodes().forEach((n) => {
|
|
4329
|
+
xy[n.id] = { x: n.x, y: n.y };
|
|
4330
|
+
});
|
|
4331
|
+
this._highlight.edges.forEach((key) => {
|
|
4332
|
+
const [a, b] = key.split("\0");
|
|
4333
|
+
const A = xy[a], B = xy[b];
|
|
4334
|
+
if (!A || !B) return;
|
|
4335
|
+
g.appendChild(mk("line", {
|
|
4336
|
+
x1: A.x,
|
|
4337
|
+
y1: A.y,
|
|
4338
|
+
x2: B.x,
|
|
4339
|
+
y2: B.y,
|
|
4340
|
+
stroke: HL,
|
|
4341
|
+
"stroke-width": extent * 0.02,
|
|
4342
|
+
"stroke-opacity": 0.6,
|
|
4343
|
+
"stroke-linecap": "round"
|
|
4344
|
+
}));
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
this._highlight.nodes.forEach((id) => {
|
|
4348
|
+
var _a;
|
|
4349
|
+
const el = this.svg_element.querySelector(`[data-point-id="${((_a = window.CSS) == null ? void 0 : _a.escape) ? CSS.escape(id) : id}"]`);
|
|
4350
|
+
if (!el) return;
|
|
4351
|
+
const tag = el.tagName.toLowerCase();
|
|
4352
|
+
let cx, cy, r;
|
|
4353
|
+
if (tag === "rect") {
|
|
4354
|
+
const w = parseFloat(el.getAttribute("width") || "0");
|
|
4355
|
+
cx = parseFloat(el.getAttribute("x") || "0") + w / 2;
|
|
4356
|
+
cy = parseFloat(el.getAttribute("y") || "0") + parseFloat(el.getAttribute("height") || "0") / 2;
|
|
4357
|
+
r = w / 2;
|
|
4358
|
+
} else {
|
|
4359
|
+
cx = parseFloat(el.getAttribute("cx") || "0");
|
|
4360
|
+
cy = parseFloat(el.getAttribute("cy") || "0");
|
|
4361
|
+
r = parseFloat(el.getAttribute("r") || "0");
|
|
4362
|
+
}
|
|
4363
|
+
g.appendChild(mk("circle", {
|
|
4364
|
+
cx,
|
|
4365
|
+
cy,
|
|
4366
|
+
r: r * 1.7 + extent * 4e-3,
|
|
4367
|
+
fill: "none",
|
|
4368
|
+
stroke: HL,
|
|
4369
|
+
"stroke-width": extent * 6e-3
|
|
4370
|
+
}));
|
|
4371
|
+
});
|
|
3828
4372
|
}
|
|
3829
4373
|
create() {
|
|
3830
4374
|
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
@@ -3942,6 +4486,138 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3942
4486
|
const lbl = getAutoLabel(id);
|
|
3943
4487
|
if ((lbl == null ? void 0 : lbl.displayMode) === Label.DISPLAY_TYPES.AUTO) lbl.hide();
|
|
3944
4488
|
});
|
|
4489
|
+
this._attachInteractionEvents();
|
|
4490
|
+
}
|
|
4491
|
+
/** Emit a namespaced CustomEvent on the public element (bubbles + composed). */
|
|
4492
|
+
_emit(name, detail) {
|
|
4493
|
+
this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
|
|
4494
|
+
}
|
|
4495
|
+
/** Walk up from a hit element to the nearest ancestor carrying `attr`. */
|
|
4496
|
+
_closestWith(el, attr) {
|
|
4497
|
+
var _a, _b;
|
|
4498
|
+
while (el && el !== this.svg_element) {
|
|
4499
|
+
if (((_a = el.getAttribute) == null ? void 0 : _a.call(el, attr)) != null && ((_b = el.hasAttribute) == null ? void 0 : _b.call(el, attr))) return el;
|
|
4500
|
+
el = el.parentElement;
|
|
4501
|
+
}
|
|
4502
|
+
return null;
|
|
4503
|
+
}
|
|
4504
|
+
/** Resolve an edge hit element to a qe-edge-* detail, or null. */
|
|
4505
|
+
_edgeDetail(el) {
|
|
4506
|
+
const e = this._closestWith(el, "data-edge-source");
|
|
4507
|
+
if (!e) return null;
|
|
4508
|
+
const d = {
|
|
4509
|
+
source: e.getAttribute("data-edge-source"),
|
|
4510
|
+
target: e.getAttribute("data-edge-target"),
|
|
4511
|
+
weight: parseFloat(e.getAttribute("data-edge-weight") || "0"),
|
|
4512
|
+
directed: e.getAttribute("data-edge-directed") === "true",
|
|
4513
|
+
self: e.getAttribute("data-edge-self") === "true"
|
|
4514
|
+
};
|
|
4515
|
+
if (e.hasAttribute("data-edge-group")) d.group = e.getAttribute("data-edge-group");
|
|
4516
|
+
if (e.hasAttribute("data-edge-unit")) d.unit = e.getAttribute("data-edge-unit");
|
|
4517
|
+
return d;
|
|
4518
|
+
}
|
|
4519
|
+
/** Resolve a node/mean/point hit element to a qe-node-* detail, or null. */
|
|
4520
|
+
_nodeDetail(el) {
|
|
4521
|
+
const n = this._closestWith(el, "data-point-id");
|
|
4522
|
+
if (!n) return null;
|
|
4523
|
+
const tag = n.tagName.toLowerCase();
|
|
4524
|
+
let x, y;
|
|
4525
|
+
if (tag === "rect") {
|
|
4526
|
+
x = parseFloat(n.getAttribute("x") || "0") + parseFloat(n.getAttribute("width") || "0") / 2;
|
|
4527
|
+
y = parseFloat(n.getAttribute("y") || "0") + parseFloat(n.getAttribute("height") || "0") / 2;
|
|
4528
|
+
} else {
|
|
4529
|
+
x = parseFloat(n.getAttribute("cx") || "0");
|
|
4530
|
+
y = parseFloat(n.getAttribute("cy") || "0");
|
|
4531
|
+
}
|
|
4532
|
+
const id = n.getAttribute("data-point-id");
|
|
4533
|
+
const d = {
|
|
4534
|
+
id,
|
|
4535
|
+
kind: n.getAttribute("data-kind") || "point",
|
|
4536
|
+
name: n.getAttribute("data-name") || id,
|
|
4537
|
+
x,
|
|
4538
|
+
y
|
|
4539
|
+
};
|
|
4540
|
+
if (n.hasAttribute("data-group")) d.group = n.getAttribute("data-group");
|
|
4541
|
+
if (n.hasAttribute("data-unit")) d.unit = n.getAttribute("data-unit");
|
|
4542
|
+
return d;
|
|
4543
|
+
}
|
|
4544
|
+
_attachInteractionEvents() {
|
|
4545
|
+
const svg = this.svg_element;
|
|
4546
|
+
svg.addEventListener("click", (ev) => {
|
|
4547
|
+
const target = ev.target;
|
|
4548
|
+
const edge = this._edgeDetail(target);
|
|
4549
|
+
if (edge) {
|
|
4550
|
+
this._emit("qe-edge-click", { ...edge, originalEvent: ev });
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
const node = this._nodeDetail(target);
|
|
4554
|
+
if (node) this._emit("qe-node-click", { ...node, originalEvent: ev });
|
|
4555
|
+
});
|
|
4556
|
+
let hovered = null;
|
|
4557
|
+
const keyOf = /* @__PURE__ */ __name((t, d) => t === "edge" ? `${d.source}\0${d.target}` : String(d.id), "keyOf");
|
|
4558
|
+
svg.addEventListener("mousemove", (ev) => {
|
|
4559
|
+
const target = ev.target;
|
|
4560
|
+
const edge = this._edgeDetail(target);
|
|
4561
|
+
const node = edge ? null : this._nodeDetail(target);
|
|
4562
|
+
const next = edge ? { type: "edge", detail: edge } : node ? { type: "node", detail: node } : null;
|
|
4563
|
+
const nextKey = next ? keyOf(next.type, next.detail) : null;
|
|
4564
|
+
if (hovered && (!next || hovered.key !== nextKey)) {
|
|
4565
|
+
this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
|
|
4566
|
+
hovered = null;
|
|
4567
|
+
}
|
|
4568
|
+
if (next && (!hovered || hovered.key !== nextKey)) {
|
|
4569
|
+
this._emit(next.type === "edge" ? "qe-edge-hover" : "qe-node-hover", { ...next.detail, originalEvent: ev });
|
|
4570
|
+
hovered = { type: next.type, key: nextKey };
|
|
4571
|
+
}
|
|
4572
|
+
if (this.hasAttribute("tooltip") && next) {
|
|
4573
|
+
this._showTooltip(this._tooltipText(next.type, next.detail), ev.clientX, ev.clientY);
|
|
4574
|
+
} else {
|
|
4575
|
+
this._hideTooltip();
|
|
4576
|
+
}
|
|
4577
|
+
});
|
|
4578
|
+
svg.addEventListener("mouseleave", () => {
|
|
4579
|
+
if (hovered) {
|
|
4580
|
+
this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
|
|
4581
|
+
hovered = null;
|
|
4582
|
+
}
|
|
4583
|
+
this._hideTooltip();
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
/** "source – target (weight)" for edges; the mark name for nodes/means/points. */
|
|
4587
|
+
_tooltipText(type, d) {
|
|
4588
|
+
if (type === "edge") {
|
|
4589
|
+
const w = Number(d.weight);
|
|
4590
|
+
const wStr = Number.isFinite(w) ? ` (${+w.toFixed(3)})` : "";
|
|
4591
|
+
return `${d.source} – ${d.target}${wStr}`;
|
|
4592
|
+
}
|
|
4593
|
+
return String(d.name ?? d.id);
|
|
4594
|
+
}
|
|
4595
|
+
_showTooltip(text, clientX, clientY) {
|
|
4596
|
+
if (!this._tooltipEl) {
|
|
4597
|
+
const el = document.createElement("div");
|
|
4598
|
+
el.setAttribute("data-qe-tooltip", "");
|
|
4599
|
+
Object.assign(el.style, {
|
|
4600
|
+
position: "fixed",
|
|
4601
|
+
zIndex: "2147483647",
|
|
4602
|
+
pointerEvents: "none",
|
|
4603
|
+
background: "rgba(20,20,20,0.92)",
|
|
4604
|
+
color: "#fff",
|
|
4605
|
+
font: "12px/1.4 system-ui, sans-serif",
|
|
4606
|
+
padding: "3px 7px",
|
|
4607
|
+
borderRadius: "4px",
|
|
4608
|
+
whiteSpace: "nowrap",
|
|
4609
|
+
transform: "translate(-50%, calc(-100% - 8px))"
|
|
4610
|
+
});
|
|
4611
|
+
document.body.appendChild(el);
|
|
4612
|
+
this._tooltipEl = el;
|
|
4613
|
+
}
|
|
4614
|
+
this._tooltipEl.textContent = text;
|
|
4615
|
+
this._tooltipEl.style.left = `${clientX}px`;
|
|
4616
|
+
this._tooltipEl.style.top = `${clientY}px`;
|
|
4617
|
+
this._tooltipEl.style.display = "block";
|
|
4618
|
+
}
|
|
4619
|
+
_hideTooltip() {
|
|
4620
|
+
if (this._tooltipEl) this._tooltipEl.style.display = "none";
|
|
3945
4621
|
}
|
|
3946
4622
|
draw() {
|
|
3947
4623
|
var _a;
|
|
@@ -3960,6 +4636,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3960
4636
|
this.update_labels();
|
|
3961
4637
|
this.update_extremes();
|
|
3962
4638
|
this.update_axes();
|
|
4639
|
+
this._applyHighlights();
|
|
3963
4640
|
}
|
|
3964
4641
|
/**
|
|
3965
4642
|
* Axes (<qe-axes>): toggle the axis-line cross by presence, and draw/position
|
|
@@ -3967,6 +4644,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3967
4644
|
* cached SVG <text> nodes in the labels group; null text removes them.
|
|
3968
4645
|
*/
|
|
3969
4646
|
update_axes() {
|
|
4647
|
+
var _a;
|
|
3970
4648
|
const axesLayer = this.__children__.search("__children__", "axes");
|
|
3971
4649
|
if (axesLayer == null ? void 0 : axesLayer.svg_element) {
|
|
3972
4650
|
axesLayer.svg_element.style.display = this._showAxes ? "" : "none";
|
|
@@ -3998,17 +4676,37 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
|
|
|
3998
4676
|
el.setAttribute("font-weight", "600");
|
|
3999
4677
|
el.setAttribute("fill", "#556");
|
|
4000
4678
|
if (axis === "x") {
|
|
4001
|
-
el.setAttribute("x", (extent
|
|
4002
|
-
el.setAttribute("y", (
|
|
4003
|
-
el.setAttribute("text-anchor", "
|
|
4679
|
+
el.setAttribute("x", (extent - gap).toString());
|
|
4680
|
+
el.setAttribute("y", (-gap).toString());
|
|
4681
|
+
el.setAttribute("text-anchor", "end");
|
|
4004
4682
|
} else {
|
|
4005
|
-
el.setAttribute("x",
|
|
4006
|
-
el.setAttribute("y", (-extent
|
|
4007
|
-
el.setAttribute("text-anchor", "
|
|
4683
|
+
el.setAttribute("x", gap.toString());
|
|
4684
|
+
el.setAttribute("y", (-extent + fontSize + gap).toString());
|
|
4685
|
+
el.setAttribute("text-anchor", "start");
|
|
4008
4686
|
}
|
|
4009
4687
|
}, "place");
|
|
4010
4688
|
place("x", this._axisXText);
|
|
4011
4689
|
place("y", this._axisYText);
|
|
4690
|
+
if (this._magnifyText == null) {
|
|
4691
|
+
(_a = this._magnifyEl) == null ? void 0 : _a.remove();
|
|
4692
|
+
this._magnifyEl = null;
|
|
4693
|
+
} else {
|
|
4694
|
+
if (!this._magnifyEl) {
|
|
4695
|
+
this._magnifyEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
4696
|
+
this._magnifyEl.setAttribute("data-magnify-label", "");
|
|
4697
|
+
this._magnifyEl.setAttribute("pointer-events", "none");
|
|
4698
|
+
}
|
|
4699
|
+
if (!this._magnifyEl.parentElement) parent.appendChild(this._magnifyEl);
|
|
4700
|
+
const el = this._magnifyEl;
|
|
4701
|
+
el.textContent = this._magnifyText;
|
|
4702
|
+
el.setAttribute("font-size", (fontSize * 0.85).toString());
|
|
4703
|
+
el.setAttribute("font-family", "system-ui, sans-serif");
|
|
4704
|
+
el.setAttribute("font-style", "italic");
|
|
4705
|
+
el.setAttribute("fill", "#556");
|
|
4706
|
+
el.setAttribute("x", (-extent + gap).toString());
|
|
4707
|
+
el.setAttribute("y", (-extent + fontSize + gap).toString());
|
|
4708
|
+
el.setAttribute("text-anchor", "start");
|
|
4709
|
+
}
|
|
4012
4710
|
}
|
|
4013
4711
|
update_labels() {
|
|
4014
4712
|
this._nodeLabels.forEach((lbl) => lbl.update());
|
|
@@ -4072,8 +4770,12 @@ _QEGraphElement.style = `
|
|
|
4072
4770
|
background-color: #FFF;
|
|
4073
4771
|
}
|
|
4074
4772
|
`;
|
|
4773
|
+
_QEGraphElement._instances = /* @__PURE__ */ new Set();
|
|
4774
|
+
_QEGraphElement._PATH_NUM_RE = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
|
|
4075
4775
|
let QEGraphElement = _QEGraphElement;
|
|
4076
4776
|
customElements.define(QEGraphElement.tagName, QEGraphElement);
|
|
4777
|
+
const getGraph = /* @__PURE__ */ __name((key) => QEGraphElement.getGraph(key), "getGraph");
|
|
4778
|
+
const getGraphs = /* @__PURE__ */ __name(() => QEGraphElement.instances(), "getGraphs");
|
|
4077
4779
|
export {
|
|
4078
4780
|
Color,
|
|
4079
4781
|
DataFrame,
|
|
@@ -4098,6 +4800,8 @@ export {
|
|
|
4098
4800
|
Trajectory,
|
|
4099
4801
|
WeightedColor,
|
|
4100
4802
|
fitTrajectory,
|
|
4803
|
+
getGraph,
|
|
4804
|
+
getGraphs,
|
|
4101
4805
|
nearOriginFilter,
|
|
4102
4806
|
sampleCurve,
|
|
4103
4807
|
selectDegree,
|