@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/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
- return _WeightedColor.hsvToRgba(h, w, v, w);
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
- let _this = __privateGet(this, _edge);
2184
- let _mid = _this.from_point, head = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.2), nock = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.35), mid = Tools.Geometry.Line.midpoint(head, nock), rot_mid = Tools.Geometry.Line.rotate(_this.line, mid), rot_nock = Tools.Geometry.Line.rotate(_this.line, nock), mid_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_mid), mid_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_mid), nock_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_nock), nock_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_nock);
2185
- let p = `M${head.join(" ")} ${mid_int0.join(" ")} ${nock_int0.join(" ")} ${mid.join(" ")} ${nock_int1.join(" ")} ${mid_int1.join(" ")}z`;
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 sw = (extent > 0 ? extent : 1) * 0.011 * (weight / (maxW || 1));
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 edgeColor = new WeightedColor(baseColor, opacity).toString();
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
- const nodeSize = typeof meta.nodeSize === "number" ? meta.nodeSize : 2;
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 * 4;
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", "none");
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
- const arrowColor = baseColor.toString();
2407
- this._arrow_el.setAttribute("fill", arrowColor);
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.svg_element.setAttribute("x1", this.__data__.from.x.toString());
2414
- this.svg_element.setAttribute("y1", this.__data__.from.y.toString());
2415
- this.svg_element.setAttribute("x2", this.__data__.to.x.toString());
2416
- this.svg_element.setAttribute("y2", this.__data__.to.y.toString());
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.svg_element);
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
- (_b = this.parent) == null ? void 0 : _b.appendChild(this._arrow_el);
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
- const extent = this.graph.__data__.extremes.x_max;
2467
- return (extent > 0 ? extent : 1) * 6e-3 * this.__data__.size;
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", (event) => {
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", "none");
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 = { ...pt.meta ?? {}, stroke: "#ffffff" };
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 nonSelfWeights = drawEdges.filter((e2) => e2.pair[0] !== e2.pair[1]).map((e2) => e2.weight);
3554
- const maxEdgeWeight = nonSelfWeights.length > 0 ? Math.max(...nonSelfWeights) : 1e-9;
3555
- const selfWeights = drawEdges.filter((e2) => e2.pair[0] === e2.pair[1]).map((e2) => e2.weight);
3556
- const maxSelfWeight = selfWeights.length > 0 ? Math.max(...selfWeights) : 0;
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
- nodeWeightSum[pair[0]] = (nodeWeightSum[pair[0]] ?? 0) + weight;
3562
- if (!isSelf) {
3563
- nodeWeightSum[pair[1]] = (nodeWeightSum[pair[1]] ?? 0) + weight;
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
- pt.size = 2 + w / maxNodeWeight * 3;
3576
- nodeSizes[id] = pt.size;
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 classMax = isSelf ? Math.max(maxSelfWeight, 1e-9) : maxEdgeWeight;
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
- // NodeEdge compares within the self-loop class; cross-pair edges
3617
- // compare within their own class. Store the correct denominator
3618
- // so QELine.update() can compute opacity and display size properly.
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
- // nodeSize is needed by QELine.update() to compute the NodeEdge
3624
- // display radius at update time (when extremes.x_max is available).
3625
- nodeSize: isSelf ? nodeSizes[pair[0]] ?? 2 : void 0
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
- if (labelSvg && labelNodes !== 0) {
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 rendered (coordinate anchors); <qe-nodes> controls labels.
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 + gap).toString());
4002
- el.setAttribute("y", (fontSize * 0.35).toString());
4003
- el.setAttribute("text-anchor", "start");
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", "0");
4006
- el.setAttribute("y", (-extent - gap).toString());
4007
- el.setAttribute("text-anchor", "middle");
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,