@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.umd.js CHANGED
@@ -599,9 +599,10 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
599
599
  __name(_Color, "Color");
600
600
  let Color = _Color;
601
601
  const _WeightedColor = class _WeightedColor extends Color {
602
- constructor(base, weight) {
602
+ constructor(base, weight, alpha) {
603
603
  super(base);
604
604
  this.weight = weight;
605
+ this.alpha = alpha;
605
606
  }
606
607
  // Parse hex → HSV (h in [0, 360), s/v in [0, 1]).
607
608
  static toHSV(hex) {
@@ -672,7 +673,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
672
673
  toString() {
673
674
  const { h, v } = _WeightedColor.toHSV(this.base);
674
675
  const w = Math.min(1, Math.abs(this.weight));
675
- return _WeightedColor.hsvToRgba(h, w, v, w);
676
+ const a = this.alpha === void 0 ? w : Math.min(1, Math.max(0, this.alpha));
677
+ return _WeightedColor.hsvToRgba(h, w, v, a);
676
678
  }
677
679
  };
678
680
  __name(_WeightedColor, "WeightedColor");
@@ -2184,9 +2186,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2184
2186
  return __privateGet(this, _edge);
2185
2187
  }
2186
2188
  get path() {
2187
- let _this = __privateGet(this, _edge);
2188
- 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);
2189
- let p = `M${head.join(" ")} ${mid_int0.join(" ")} ${nock_int0.join(" ")} ${mid.join(" ")} ${nock_int1.join(" ")} ${mid_int1.join(" ")}z`;
2189
+ const _this = __privateGet(this, _edge);
2190
+ const T = [_this.to.x, _this.to.y];
2191
+ const A = _this.from_point;
2192
+ const [B0, B1] = _this.base_pts;
2193
+ const L = Math.hypot(A[0] - T[0], A[1] - T[1]);
2194
+ const W = Math.hypot(B0[0] - B1[0], B0[1] - B1[1]);
2195
+ if (!(L > 0) || !(W > 0)) return "";
2196
+ const u = [(A[0] - T[0]) / L, (A[1] - T[1]) / L];
2197
+ const b = Math.hypot(B0[0] - T[0], B0[1] - T[1]);
2198
+ const v = [(B0[0] - T[0]) / b, (B0[1] - T[1]) / b];
2199
+ const halfW = /* @__PURE__ */ __name((d) => W / 2 * Math.max(0, 1 - d / L), "halfW");
2200
+ const at = /* @__PURE__ */ __name((d, s) => `${T[0] + u[0] * d + v[0] * s} ${T[1] + u[1] * d + v[1] * s}`, "at");
2201
+ const dh = 0.2 * L;
2202
+ const c = Math.min(0.15 * L, 0.8 * 2 * halfW(dh));
2203
+ const dm = dh + c / 2, dn = dh + c;
2204
+ 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`;
2190
2205
  return p;
2191
2206
  }
2192
2207
  };
@@ -2329,6 +2344,46 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2329
2344
  };
2330
2345
  __name(_Drawable, "Drawable");
2331
2346
  let Drawable = _Drawable;
2347
+ const DEFAULT_EDGE_WIDTH = 0.04;
2348
+ const SATURATION_RANGE = [0.25, 1];
2349
+ const OPACITY_RANGE = [0.3, 1];
2350
+ function rampFor(w, lo, hi, range) {
2351
+ const a = Math.abs(w);
2352
+ if (!(a > 0)) return 0;
2353
+ if (!(hi > lo)) return range[1];
2354
+ const t = Math.min(1, Math.max(0, (a - lo) / (hi - lo)));
2355
+ return range[0] + t * (range[1] - range[0]);
2356
+ }
2357
+ __name(rampFor, "rampFor");
2358
+ const EDGE_WIDTH_PER_EXTENT = 0.011;
2359
+ const TAPERED_OUTLINE_PX = 0.75;
2360
+ const CHEVRON_DARKEN = 0.8;
2361
+ const CHEVRON_HALO_PX = 0.75;
2362
+ const POINT_RADIUS_PER_EXTENT = 6e-3;
2363
+ const UNCONNECTED_NODE_SIZE = 0.6;
2364
+ function widthPerWeight(edgeWidth) {
2365
+ return 2 * edgeWidth;
2366
+ }
2367
+ __name(widthPerWeight, "widthPerWeight");
2368
+ function nodeSizeForEdges(weightSum, widthFrac) {
2369
+ return weightSum * widthFrac / (2 * POINT_RADIUS_PER_EXTENT);
2370
+ }
2371
+ __name(nodeSizeForEdges, "nodeSizeForEdges");
2372
+ function flattenOnWhite(col) {
2373
+ const m = /rgba?\(([^)]+)\)/.exec(col);
2374
+ if (!m) return col;
2375
+ const [r, g, b, a = 1] = m[1].split(",").map(Number);
2376
+ const f = /* @__PURE__ */ __name((c) => Math.round(255 + (c - 255) * a), "f");
2377
+ return `rgb(${f(r)},${f(g)},${f(b)})`;
2378
+ }
2379
+ __name(flattenOnWhite, "flattenOnWhite");
2380
+ function darkenRgba(col, k) {
2381
+ const m = /rgba?\(([^)]+)\)/.exec(col);
2382
+ if (!m) return col;
2383
+ const [r, g, b, a = 1] = m[1].split(",").map(Number);
2384
+ return `rgba(${Math.round(r * k)},${Math.round(g * k)},${Math.round(b * k)},${a})`;
2385
+ }
2386
+ __name(darkenRgba, "darkenRgba");
2332
2387
  const _QELine = class _QELine extends Drawable {
2333
2388
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
2334
2389
  super(__data__, parent, graph, __children__);
@@ -2338,8 +2393,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2338
2393
  this.__children__ = __children__;
2339
2394
  this.__name__ = "unnamed-line";
2340
2395
  this._arrow_el = null;
2396
+ this._hit_el = null;
2341
2397
  this.svg_element = this.create();
2342
2398
  }
2399
+ /** Stamp edge identity onto an element so <qe-graph> delegation can resolve
2400
+ * qe-edge-* events. No-op for non-edges (e.g. axis lines). */
2401
+ _stampEdge(el) {
2402
+ const meta = this.__data__.meta ?? {};
2403
+ if (!meta.isMeanEdge) return;
2404
+ el.setAttribute("data-edge-source", String(meta.source ?? ""));
2405
+ el.setAttribute("data-edge-target", String(meta.target ?? ""));
2406
+ el.setAttribute("data-edge-weight", String(meta.weight ?? 0));
2407
+ el.setAttribute("data-edge-directed", meta.isDirected ? "true" : "false");
2408
+ if (meta.isSelf) el.setAttribute("data-edge-self", "true");
2409
+ if (meta.group != null) el.setAttribute("data-edge-group", String(meta.group));
2410
+ if (meta.unit != null) el.setAttribute("data-edge-unit", String(meta.unit));
2411
+ }
2343
2412
  create() {
2344
2413
  if (this.__data__ instanceof NodeEdge) {
2345
2414
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -2376,24 +2445,28 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2376
2445
  const extent = ((_a = this.graph.__data__.extremes) == null ? void 0 : _a.x_max) ?? 1;
2377
2446
  const meta = this.__data__.meta ?? {};
2378
2447
  if (meta.isMeanEdge) {
2448
+ this._stampEdge(this.svg_element);
2379
2449
  const weight = typeof meta.weight === "number" ? meta.weight : 0;
2380
2450
  const maxW = typeof meta.maxWeight === "number" ? meta.maxWeight : 1;
2381
2451
  const opacity = typeof meta.opacity === "number" ? meta.opacity : Math.min(1, weight / (maxW || 1));
2382
- const sw = (extent > 0 ? extent : 1) * 0.011 * (weight / (maxW || 1));
2452
+ const ext = extent > 0 ? extent : 1;
2453
+ const sw = typeof meta.widthFrac === "number" ? ext * meta.widthFrac * weight : ext * EDGE_WIDTH_PER_EXTENT * (weight / (maxW || 1));
2383
2454
  const baseColor = this.__data__._color || new Color("#000000");
2384
- const edgeColor = new WeightedColor(baseColor, opacity).toString();
2455
+ const saturation = typeof meta.saturation === "number" ? meta.saturation : opacity;
2456
+ const edgeColor = new WeightedColor(baseColor, saturation, opacity).toString();
2385
2457
  if (this.__data__ instanceof NodeEdge) {
2386
- const nodeSize = typeof meta.nodeSize === "number" ? meta.nodeSize : 2;
2387
- const nodeR = extent * 6e-3 * nodeSize;
2388
- this.__data__.weight = 2 * nodeR * 0.55 * opacity;
2458
+ this.__data__.weight = sw;
2389
2459
  this.svg_element.setAttribute("d", this.__data__.path);
2390
2460
  this.svg_element.setAttribute("fill", edgeColor);
2391
2461
  this.svg_element.setAttribute("stroke", "none");
2392
2462
  } else if (this.__data__ instanceof TaperedEdge) {
2393
- this.__data__.weight = sw * 4;
2463
+ this.__data__.weight = sw;
2394
2464
  this.svg_element.setAttribute("d", this.__data__.path);
2395
2465
  this.svg_element.setAttribute("fill", edgeColor);
2396
- this.svg_element.setAttribute("stroke", "none");
2466
+ this.svg_element.setAttribute("stroke", edgeColor);
2467
+ this.svg_element.setAttribute("stroke-width", String(TAPERED_OUTLINE_PX));
2468
+ this.svg_element.setAttribute("vector-effect", "non-scaling-stroke");
2469
+ this.svg_element.setAttribute("stroke-linejoin", "round");
2397
2470
  if (meta.isDirected) {
2398
2471
  try {
2399
2472
  const arrow = new EdgeArrow(this.__data__);
@@ -2407,20 +2480,39 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2407
2480
  this.parent.appendChild(this._arrow_el);
2408
2481
  }
2409
2482
  this._arrow_el.setAttribute("d", arrowPath);
2410
- const arrowColor = baseColor.toString();
2411
- this._arrow_el.setAttribute("fill", arrowColor);
2483
+ this._arrow_el.setAttribute("fill", darkenRgba(flattenOnWhite(edgeColor), CHEVRON_DARKEN));
2484
+ this._arrow_el.setAttribute("stroke", "#ffffff");
2485
+ this._arrow_el.setAttribute("stroke-width", String(CHEVRON_HALO_PX));
2486
+ this._arrow_el.setAttribute("vector-effect", "non-scaling-stroke");
2487
+ this._arrow_el.setAttribute("stroke-linejoin", "round");
2412
2488
  }
2413
2489
  } catch (_) {
2414
2490
  }
2415
2491
  }
2416
2492
  } else {
2417
- this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
2418
- this.svg_element.setAttribute("y1", this.__data__.from.y.toString());
2419
- this.svg_element.setAttribute("x2", this.__data__.to.x.toString());
2420
- this.svg_element.setAttribute("y2", this.__data__.to.y.toString());
2493
+ const x1 = this.__data__.from.x, y1 = this.__data__.from.y;
2494
+ const x2 = this.__data__.to.x, y2 = this.__data__.to.y;
2495
+ this.svg_element.setAttribute("x1", x1.toString());
2496
+ this.svg_element.setAttribute("y1", y1.toString());
2497
+ this.svg_element.setAttribute("x2", x2.toString());
2498
+ this.svg_element.setAttribute("y2", y2.toString());
2421
2499
  this.svg_element.setAttribute("stroke-width", sw.toString());
2422
2500
  this.svg_element.setAttribute("stroke", edgeColor);
2501
+ this.svg_element.setAttribute("pointer-events", "stroke");
2423
2502
  this.svg_element.removeAttribute("marker-end");
2503
+ if (!this._hit_el) {
2504
+ this._hit_el = document.createElementNS("http://www.w3.org/2000/svg", "line");
2505
+ this._hit_el.setAttribute("stroke", "transparent");
2506
+ this._hit_el.setAttribute("stroke-linecap", "round");
2507
+ this._hit_el.setAttribute("pointer-events", "stroke");
2508
+ }
2509
+ if (!this._hit_el.parentElement) this.parent.appendChild(this._hit_el);
2510
+ this._hit_el.setAttribute("x1", x1.toString());
2511
+ this._hit_el.setAttribute("y1", y1.toString());
2512
+ this._hit_el.setAttribute("x2", x2.toString());
2513
+ this._hit_el.setAttribute("y2", y2.toString());
2514
+ this._hit_el.setAttribute("stroke-width", Math.max(sw, extent * 0.03).toString());
2515
+ this._stampEdge(this._hit_el);
2424
2516
  }
2425
2517
  } else {
2426
2518
  this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
@@ -2434,10 +2526,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2434
2526
  }
2435
2527
  }
2436
2528
  draw() {
2437
- var _a, _b;
2438
- (_a = this.parent) == null ? void 0 : _a.appendChild(this.svg_element);
2529
+ var _a, _b, _c;
2530
+ if (this._hit_el) (_a = this.parent) == null ? void 0 : _a.appendChild(this._hit_el);
2531
+ (_b = this.parent) == null ? void 0 : _b.appendChild(this.svg_element);
2439
2532
  if (this._arrow_el) {
2440
- (_b = this.parent) == null ? void 0 : _b.appendChild(this._arrow_el);
2533
+ (_c = this.parent) == null ? void 0 : _c.appendChild(this._arrow_el);
2441
2534
  }
2442
2535
  }
2443
2536
  };
@@ -2467,8 +2560,23 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2467
2560
  * (≈ 1–2 % of the coordinate half-extent for a typical ENA network).
2468
2561
  */
2469
2562
  computeRadius() {
2470
- const extent = this.graph.__data__.extremes.x_max;
2471
- return (extent > 0 ? extent : 1) * 6e-3 * this.__data__.size;
2563
+ var _a;
2564
+ const r = this.baseRadius();
2565
+ const cap = (_a = this.__data__.meta) == null ? void 0 : _a.maxRadius;
2566
+ return typeof cap === "number" && cap > 0 ? Math.min(r, cap) : r;
2567
+ }
2568
+ baseRadius() {
2569
+ var _a, _b, _c, _d;
2570
+ const extent = this.graph.__data__.extremes.x_max || 1;
2571
+ const sizeScale = ((_a = this.__data__.meta) == null ? void 0 : _a.sizeScale) ?? 1;
2572
+ if ((_b = this.__data__.meta) == null ? void 0 : _b.fixedSize) {
2573
+ const rect = (_d = (_c = this.graph.svg_element) == null ? void 0 : _c.getBoundingClientRect) == null ? void 0 : _d.call(_c);
2574
+ const zoom = this.graph.zoom_level || 1;
2575
+ const pxPerUnit = rect && rect.width > 0 ? rect.width / (2 * extent) : 1;
2576
+ const targetPx = this.__data__.size * 2 * sizeScale;
2577
+ return targetPx / pxPerUnit / zoom;
2578
+ }
2579
+ return extent * POINT_RADIUS_PER_EXTENT * this.__data__.size * sizeScale;
2472
2580
  }
2473
2581
  /** Optional halo stroke (e.g. white outline on unit points to separate
2474
2582
  * overlapping dots in dense clusters). Width scales with the dot so it holds
@@ -2484,6 +2592,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2484
2592
  const tag = this.isSquare ? "rect" : "circle";
2485
2593
  const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
2486
2594
  el.setAttribute("data-point-id", this.__data__.ID);
2595
+ const meta = this.__data__.meta ?? {};
2596
+ if (meta.kind) el.setAttribute("data-kind", meta.kind);
2597
+ if (meta.name != null) el.setAttribute("data-name", String(meta.name));
2598
+ if (meta.group != null) el.setAttribute("data-group", String(meta.group));
2599
+ if (meta.unit != null) el.setAttribute("data-unit", String(meta.unit));
2487
2600
  el.setAttribute("fill", this.__data__.color.toString());
2488
2601
  this.applyStroke(el);
2489
2602
  const r = this.computeRadius();
@@ -2498,9 +2611,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2498
2611
  el.setAttribute("r", r.toString());
2499
2612
  }
2500
2613
  this.svg_element = el;
2501
- el.addEventListener("click", (event) => {
2502
- console.log("Point clicked: ", this.__data__);
2503
- event.stopPropagation();
2614
+ el.addEventListener("click", () => {
2504
2615
  this.__data__.active = !this.__data__.active;
2505
2616
  this.update();
2506
2617
  const pointEvent = new CustomEvent("point-clicked", {
@@ -2847,6 +2958,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2847
2958
  this.customX = customX;
2848
2959
  this.customY = customY;
2849
2960
  this._visible = false;
2961
+ this._offsetX = 0;
2962
+ this._offsetY = 0;
2963
+ this.pinned = false;
2850
2964
  this.svg_element = this._create();
2851
2965
  if (displayMode === Label.DISPLAY_TYPES.ON) {
2852
2966
  this._visible = true;
@@ -2860,10 +2974,52 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2860
2974
  const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
2861
2975
  el.textContent = this.text;
2862
2976
  el.setAttribute("data-label-for", this.point.ID);
2863
- el.setAttribute("pointer-events", "none");
2977
+ el.setAttribute("pointer-events", "auto");
2978
+ el.style.cursor = "grab";
2979
+ this._attachDrag(el);
2864
2980
  this.parent.appendChild(el);
2865
2981
  return el;
2866
2982
  }
2983
+ /** Drag to reposition; pins the label open and emits qe-label-move on drop. */
2984
+ _attachDrag(el) {
2985
+ el.addEventListener("mousedown", (ev) => {
2986
+ ev.preventDefault();
2987
+ ev.stopPropagation();
2988
+ const svg = el.ownerSVGElement;
2989
+ if (!svg) return;
2990
+ const toUser = /* @__PURE__ */ __name((cx, cy) => {
2991
+ const p = svg.createSVGPoint();
2992
+ p.x = cx;
2993
+ p.y = cy;
2994
+ const m = svg.getScreenCTM();
2995
+ return m ? p.matrixTransform(m.inverse()) : { x: 0, y: 0 };
2996
+ }, "toUser");
2997
+ const start = toUser(ev.clientX, ev.clientY);
2998
+ const baseX = this._offsetX, baseY = this._offsetY;
2999
+ el.style.cursor = "grabbing";
3000
+ const onMove = /* @__PURE__ */ __name((e) => {
3001
+ const p = toUser(e.clientX, e.clientY);
3002
+ this._offsetX = baseX + (p.x - start.x);
3003
+ this._offsetY = baseY + (p.y - start.y);
3004
+ this.pinned = true;
3005
+ this._visible = true;
3006
+ this.svg_element.setAttribute("display", "block");
3007
+ this.update();
3008
+ }, "onMove");
3009
+ const onUp = /* @__PURE__ */ __name(() => {
3010
+ document.removeEventListener("mousemove", onMove);
3011
+ document.removeEventListener("mouseup", onUp);
3012
+ el.style.cursor = "grab";
3013
+ this.graph.dispatchEvent(new CustomEvent("qe-label-move", {
3014
+ detail: { id: this.point.ID, transform: { x: this._offsetX, y: this._offsetY } },
3015
+ bubbles: true,
3016
+ composed: true
3017
+ }));
3018
+ }, "onUp");
3019
+ document.addEventListener("mousemove", onMove);
3020
+ document.addEventListener("mouseup", onUp);
3021
+ });
3022
+ }
2867
3023
  /**
2868
3024
  * Recompute position and font-size from current graph extents.
2869
3025
  * No-ops when the label is hidden (AUTO/CLICK modes while not hovered/clicked).
@@ -2912,6 +3068,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2912
3068
  }
2913
3069
  this.svg_element.setAttribute("x", x.toString());
2914
3070
  this.svg_element.setAttribute("y", y.toString());
3071
+ if (this._offsetX || this._offsetY) {
3072
+ this.svg_element.setAttribute("transform", `translate(${this._offsetX}, ${this._offsetY})`);
3073
+ }
2915
3074
  this.svg_element.setAttribute("font-size", fontSize.toString());
2916
3075
  this.svg_element.setAttribute("font-family", "system-ui, sans-serif");
2917
3076
  this.svg_element.setAttribute("font-weight", "500");
@@ -2925,6 +3084,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2925
3084
  this.update();
2926
3085
  }
2927
3086
  hide() {
3087
+ if (this.pinned) return;
2928
3088
  this._visible = false;
2929
3089
  this.svg_element.setAttribute("display", "none");
2930
3090
  }
@@ -3007,6 +3167,14 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3007
3167
  return { halfW: t95 * seX, halfH: t95 * seY };
3008
3168
  }
3009
3169
  __name(computeCIRect, "computeCIRect");
3170
+ const MEAN_MAX_FRACTION_OF_INTERVAL = 0.5;
3171
+ function capMeanToInterval(meanPt, halfW, halfH) {
3172
+ if (!meanPt) return;
3173
+ const half = Math.min(Math.abs(halfW), Math.abs(halfH));
3174
+ if (!(half > 0)) return;
3175
+ meanPt.meta = { ...meanPt.meta ?? {}, maxRadius: half * MEAN_MAX_FRACTION_OF_INTERVAL };
3176
+ }
3177
+ __name(capMeanToInterval, "capMeanToInterval");
3010
3178
  function readIntervalBounds(row, dim0, dim1) {
3011
3179
  const get = /* @__PURE__ */ __name((col) => {
3012
3180
  const i = row.names.indexOf(col);
@@ -3020,6 +3188,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3020
3188
  return { x_low: xLow, x_high: xHigh, y_low: yLow, y_high: yHigh };
3021
3189
  }
3022
3190
  __name(readIntervalBounds, "readIntervalBounds");
3191
+ function parseColorScale(attr) {
3192
+ const a = attr == null ? void 0 : attr.trim().toLowerCase();
3193
+ return a === "plot" || a === "absolute" ? a : "model";
3194
+ }
3195
+ __name(parseColorScale, "parseColorScale");
3023
3196
  function parseLabelMode(attr, defaultMode) {
3024
3197
  switch (attr == null ? void 0 : attr.toLowerCase()) {
3025
3198
  case "on":
@@ -3053,6 +3226,13 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3053
3226
  this._axisXText = null;
3054
3227
  this._axisYText = null;
3055
3228
  this._axisLabelEls = /* @__PURE__ */ new Map();
3229
+ this._magnifyText = null;
3230
+ this._magnifyEl = null;
3231
+ this._animateMs = 0;
3232
+ this._tweenRaf = null;
3233
+ this._highlight = { edges: /* @__PURE__ */ new Set(), nodes: /* @__PURE__ */ new Set() };
3234
+ this._highlightGroup = null;
3235
+ this._tooltipEl = null;
3056
3236
  this.graph = this;
3057
3237
  this.parent = null;
3058
3238
  this.__data__ = new Graph(DefaultGraphOptions);
@@ -3064,6 +3244,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3064
3244
  if (this._vizena && e.target !== this._vizena) return;
3065
3245
  const model = e.detail;
3066
3246
  if (!model) return;
3247
+ this._animateMs = this.hasAttribute("animate") ? parseInt(this.getAttribute("animate") || "", 10) || 300 : 0;
3248
+ const tweenFrom = this._animateMs > 0 && this._hasModel ? this._captureTween() : null;
3067
3249
  this._hasModel = true;
3068
3250
  const dims = model.dimensions;
3069
3251
  const unitIdCol = model.unit_id_col;
@@ -3072,6 +3254,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3072
3254
  const config = this._readLayerConfig();
3073
3255
  this._showAxes = config.showAxes;
3074
3256
  this._axisXText = config.axisXLabel === null ? null : config.axisXLabel || String((dims == null ? void 0 : dims[0]) ?? "x");
3257
+ this._magnifyText = config.edgeMagnify == null ? null : `(scaled ${config.edgeMagnify.toFixed(1)}x)`;
3075
3258
  this._axisYText = config.axisYLabel === null ? null : config.axisYLabel || String((dims == null ? void 0 : dims[1]) ?? "y");
3076
3259
  const groupColorMap = {};
3077
3260
  if (groupColName && ((_a = model.points) == null ? void 0 : _a.names)) {
@@ -3140,8 +3323,16 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3140
3323
  const colorStr = group ? groupColorMap[group] ?? "#888888" : "#888888";
3141
3324
  if (showUnitPoints) {
3142
3325
  const pt = rowToPoint(row, unitIdCol, colorStr, pointScale);
3143
- pt.size = 2.5;
3144
- pt.meta = { ...pt.meta ?? {}, stroke: "#ffffff" };
3326
+ pt.size = config.pointSize ?? 2.5;
3327
+ pt.meta = {
3328
+ ...pt.meta ?? {},
3329
+ stroke: "#ffffff",
3330
+ kind: "point",
3331
+ name: pt.ID,
3332
+ group: group ?? void 0,
3333
+ unit: pt.ID,
3334
+ fixedSize: config.pointFixedSize
3335
+ };
3145
3336
  this.__data__.points.set(pt.ID, pt);
3146
3337
  pointsLayer.__data__.objects.set(pt.ID, pt);
3147
3338
  }
@@ -3191,19 +3382,26 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3191
3382
  const meanPt = new Point({
3192
3383
  ID: `mean-${group}`,
3193
3384
  dimensions: [mx, my],
3194
- meta: { group }
3385
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
3195
3386
  });
3196
3387
  meanPt.color = new Color(color);
3197
3388
  meanPt.size = 4;
3198
3389
  meanPt.shape = "rect";
3199
3390
  this.__data__.points.set(meanPt.ID, meanPt);
3200
3391
  pointsLayer.__data__.objects.set(meanPt.ID, meanPt);
3201
- const bounds = readIntervalBounds(row, dims[0], dims[1]);
3202
- if (bounds) {
3392
+ const bounds = config.showCI ? readIntervalBounds(row, dims[0], dims[1]) : null;
3393
+ if (!bounds) {
3394
+ const stale = this._ciRects.get(group);
3395
+ if (stale) {
3396
+ stale.remove();
3397
+ this._ciRects.delete(group);
3398
+ }
3399
+ } else {
3203
3400
  const rect_x = bounds.x_low * pointScale;
3204
3401
  const rect_y = -bounds.y_high * pointScale;
3205
3402
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
3206
3403
  const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
3404
+ capMeanToInterval(meanPt, rect_width / 2, rect_height / 2);
3207
3405
  let el = this._ciRects.get(group);
3208
3406
  if (!el) {
3209
3407
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -3229,7 +3427,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3229
3427
  const meanPt = new Point({
3230
3428
  ID: `mean-${group}`,
3231
3429
  dimensions: [x / n * pointScale, -(y / n) * pointScale],
3232
- meta: { group }
3430
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
3233
3431
  });
3234
3432
  meanPt.color = new Color(color);
3235
3433
  meanPt.size = 4;
@@ -3257,6 +3455,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3257
3455
  const rect_y = -bounds.y_high * pointScale;
3258
3456
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
3259
3457
  const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
3458
+ capMeanToInterval(
3459
+ this.__data__.points.get(`mean-${group}`),
3460
+ rect_width / 2,
3461
+ rect_height / 2
3462
+ );
3260
3463
  let el = this._ciRects.get(group);
3261
3464
  if (!el) {
3262
3465
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -3284,6 +3487,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3284
3487
  const rawVarY = y2 / n - μy * μy;
3285
3488
  const { halfW, halfH } = computeCIRect(rawVarX, rawVarY, n, pointScale);
3286
3489
  const cx = μx * pointScale, cy = -μy * pointScale;
3490
+ capMeanToInterval(
3491
+ this.__data__.points.get(`mean-${group}`),
3492
+ halfW,
3493
+ halfH
3494
+ );
3287
3495
  let el = this._ciRects.get(group);
3288
3496
  if (!el) {
3289
3497
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -3456,6 +3664,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3456
3664
  model.nodes.rows().forEach((row) => {
3457
3665
  const pt = rowToPoint(row, nodeIdCol, "#444444");
3458
3666
  pt.size = 2;
3667
+ pt.meta = { ...pt.meta ?? {}, kind: "node", name: pt.ID };
3459
3668
  nodeLayer.__data__.objects.set(pt.ID, pt);
3460
3669
  });
3461
3670
  if (edgesEnabled && edgeLayer && ((_l = model.edges) == null ? void 0 : _l.rows)) {
@@ -3554,21 +3763,46 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3554
3763
  });
3555
3764
  }
3556
3765
  const isDirected = !!model.directed;
3557
- const nonSelfWeights = drawEdges.filter((e2) => e2.pair[0] !== e2.pair[1]).map((e2) => e2.weight);
3558
- const maxEdgeWeight = nonSelfWeights.length > 0 ? Math.max(...nonSelfWeights) : 1e-9;
3559
- const selfWeights = drawEdges.filter((e2) => e2.pair[0] === e2.pair[1]).map((e2) => e2.weight);
3560
- const maxSelfWeight = selfWeights.length > 0 ? Math.max(...selfWeights) : 0;
3766
+ const widthFrac = widthPerWeight(config.edgeWidth) * (config.edgeMagnify ?? 1);
3767
+ const rangeOf = /* @__PURE__ */ __name((ws) => {
3768
+ const nz = ws.map(Math.abs).filter((v) => v > 0);
3769
+ return nz.length ? [Math.min(...nz), Math.max(...nz)] : [0, 0];
3770
+ }, "rangeOf");
3771
+ const modelRange = /* @__PURE__ */ __name((self2) => {
3772
+ const vals = [];
3773
+ edgePairs.forEach(({ col, pair }) => {
3774
+ if (pair[0] === pair[1] !== self2) return;
3775
+ const column = model.edges.col(col);
3776
+ if (column) for (const v of column) vals.push(Number(v));
3777
+ });
3778
+ return rangeOf(vals);
3779
+ }, "modelRange");
3780
+ const plotRange = /* @__PURE__ */ __name((self2) => rangeOf(drawEdges.filter((e2) => e2.pair[0] === e2.pair[1] === self2).map((e2) => e2.weight)), "plotRange");
3781
+ const cscale = config.edgeColorScale;
3782
+ const colourRange = /* @__PURE__ */ __name((self2) => cscale === "plot" ? plotRange(self2) : modelRange(self2), "colourRange");
3783
+ const lineRange = colourRange(false), selfRange = colourRange(true);
3784
+ const colourOf = /* @__PURE__ */ __name((w, self2) => {
3785
+ if (cscale === "absolute") {
3786
+ const a = Math.min(1, Math.abs(w));
3787
+ return { saturation: a, opacity: a };
3788
+ }
3789
+ const [lo, hi] = self2 ? selfRange : lineRange;
3790
+ return { saturation: rampFor(w, lo, hi, SATURATION_RANGE), opacity: rampFor(w, lo, hi, OPACITY_RANGE) };
3791
+ }, "colourOf");
3561
3792
  const nodeWeightSum = {};
3562
3793
  drawEdges.forEach(({ pair, weight }) => {
3563
3794
  const isSelf = pair[0] === pair[1];
3564
3795
  if (isSelf && !isDirected) return;
3565
- nodeWeightSum[pair[0]] = (nodeWeightSum[pair[0]] ?? 0) + weight;
3566
- if (!isSelf) {
3567
- nodeWeightSum[pair[1]] = (nodeWeightSum[pair[1]] ?? 0) + weight;
3796
+ const add = /* @__PURE__ */ __name((id) => {
3797
+ nodeWeightSum[id] = (nodeWeightSum[id] ?? 0) + Math.abs(weight);
3798
+ }, "add");
3799
+ if (isDirected) {
3800
+ add(pair[1]);
3801
+ return;
3568
3802
  }
3803
+ add(pair[0]);
3804
+ add(pair[1]);
3569
3805
  });
3570
- const maxNodeWeight = Math.max(1, ...Object.values(nodeWeightSum));
3571
- const nodeSizes = {};
3572
3806
  model.nodes.rows().forEach((row) => {
3573
3807
  const codeIdx = row.names.indexOf(nodeIdCol);
3574
3808
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
@@ -3576,8 +3810,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3576
3810
  const pt = nodeLayer.__data__.objects.get(id);
3577
3811
  if (!pt) return;
3578
3812
  const w = nodeWeightSum[id] ?? 0;
3579
- pt.size = 2 + w / maxNodeWeight * 3;
3580
- nodeSizes[id] = pt.size;
3813
+ const unconnected = !(w > 0);
3814
+ pt.size = unconnected ? config.hideUnconnectedNodes ? 0 : UNCONNECTED_NODE_SIZE : nodeSizeForEdges(w, widthFrac);
3815
+ pt.meta = {
3816
+ ...pt.meta ?? {},
3817
+ hidden: unconnected && config.hideUnconnectedNodes
3818
+ };
3581
3819
  });
3582
3820
  const totalWeightByPair = {};
3583
3821
  if (isDirected) {
@@ -3592,8 +3830,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3592
3830
  if (!fromNode || !toNode) return;
3593
3831
  const isSelf = pair[0] === pair[1];
3594
3832
  if (isSelf && !isDirected) return;
3595
- const classMax = isSelf ? Math.max(maxSelfWeight, 1e-9) : maxEdgeWeight;
3596
- const opacity = Math.min(1, weight / classMax);
3833
+ const { saturation, opacity } = colourOf(weight, isSelf);
3597
3834
  const fromPt = new Point({ ID: `${pair[0]}-src`, dimensions: [fromNode.x, fromNode.y], meta: {} });
3598
3835
  const toPt = new Point({ ID: `${pair[1]}-dst`, dimensions: [toNode.x, toNode.y], meta: {} });
3599
3836
  let edgeLine;
@@ -3617,16 +3854,17 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3617
3854
  edgeLine.meta = {
3618
3855
  isMeanEdge: true,
3619
3856
  weight,
3620
- // NodeEdge compares within the self-loop class; cross-pair edges
3621
- // compare within their own class. Store the correct denominator
3622
- // so QELine.update() can compute opacity and display size properly.
3623
- maxWeight: isSelf ? maxSelfWeight : maxEdgeWeight,
3857
+ // Width per unit weight (extent units), shared by all marks.
3858
+ widthFrac,
3859
+ saturation,
3624
3860
  opacity,
3625
3861
  isDirected,
3626
3862
  isSelf,
3627
- // nodeSize is needed by QELine.update() to compute the NodeEdge
3628
- // display radius at update time (when extremes.x_max is available).
3629
- nodeSize: isSelf ? nodeSizes[pair[0]] ?? 2 : void 0
3863
+ // Identity for the qe-edge-* interaction events.
3864
+ source: pair[0],
3865
+ target: pair[1],
3866
+ group: groupAttr ?? void 0,
3867
+ unit: unitAttr ?? void 0
3630
3868
  };
3631
3869
  const targetLayer = isSelf ? edgenodeLayer ?? edgeLayer : edgeLayer;
3632
3870
  targetLayer.__data__.objects.set(edgeLine.ID, edgeLine);
@@ -3634,7 +3872,15 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3634
3872
  edgeLayer.__data__.__state__.setVariable("layer_updated", edgeLayer.__data__);
3635
3873
  if (edgenodeLayer) edgenodeLayer.__data__.__state__.setVariable("layer_updated", edgenodeLayer.__data__);
3636
3874
  }
3637
- if (labelSvg && labelNodes !== 0) {
3875
+ const nodesVisible = config.showNodes;
3876
+ if (nodeLayer.svg_element) {
3877
+ nodeLayer.svg_element.style.display = nodesVisible ? "" : "none";
3878
+ }
3879
+ if (!nodesVisible) {
3880
+ this._nodeLabels.forEach((lbl) => lbl.remove());
3881
+ this._nodeLabels.clear();
3882
+ }
3883
+ if (labelSvg && labelNodes !== 0 && nodesVisible) {
3638
3884
  this._nodeLabels.forEach((lbl, id) => {
3639
3885
  if (!nodeLayer.__data__.objects.get(id)) {
3640
3886
  lbl.remove();
@@ -3642,11 +3888,17 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3642
3888
  }
3643
3889
  });
3644
3890
  model.nodes.rows().forEach((row) => {
3891
+ var _a2, _b2;
3645
3892
  const codeIdx = row.names.indexOf(nodeIdCol);
3646
3893
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
3647
3894
  if (!id) return;
3648
3895
  const pt = nodeLayer.__data__.objects.get(id);
3649
3896
  if (!pt) return;
3897
+ if ((_a2 = pt.meta) == null ? void 0 : _a2.hidden) {
3898
+ (_b2 = this._nodeLabels.get(id)) == null ? void 0 : _b2.remove();
3899
+ this._nodeLabels.delete(id);
3900
+ return;
3901
+ }
3650
3902
  let lbl = this._nodeLabels.get(id);
3651
3903
  if (!lbl) {
3652
3904
  lbl = new QELabel(id, pt, labelSvg, this, labelNodes);
@@ -3663,6 +3915,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3663
3915
  if (axisPointsLayer) this.__data__.__state__.setVariable("graph_updated_axis_points", axisPointsLayer.__data__.objects);
3664
3916
  if (axisLinesLayer) this.__data__.__state__.setVariable("graph_updated_axis_lines", axisLinesLayer.__data__.objects);
3665
3917
  this.update();
3918
+ if (tweenFrom) this._runTween(tweenFrom);
3666
3919
  });
3667
3920
  }
3668
3921
  static get observedAttributes() {
@@ -3703,6 +3956,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3703
3956
  // should render. Called once at the top of every model-updated handler so
3704
3957
  // all rendering decisions are derived from a single snapshot.
3705
3958
  _readLayerConfig() {
3959
+ var _a;
3706
3960
  const nodesEl = this.querySelector("qe-nodes");
3707
3961
  const meansEl = this.querySelector("qe-means");
3708
3962
  const pointsEl = this.querySelector("qe-points");
@@ -3711,11 +3965,18 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3711
3965
  const axesEl = this.querySelector("qe-axes");
3712
3966
  const labelsShorthand = this.getAttribute("labels");
3713
3967
  return {
3714
- // Nodes — always rendered (coordinate anchors); <qe-nodes> controls labels.
3968
+ // Nodes — always placed from the model (they anchor the coordinate
3969
+ // space); <qe-nodes> presence draws them, like every other layer.
3715
3970
  labelNodes: parseLabelMode(
3716
3971
  (nodesEl == null ? void 0 : nodesEl.getAttribute("label")) ?? this.getAttribute("label-nodes") ?? labelsShorthand,
3717
3972
  Label.DISPLAY_TYPES.ON
3718
3973
  ),
3974
+ showNodes: !!nodesEl,
3975
+ edgeWidth: (() => {
3976
+ const v = Number(this.getAttribute("edge-width"));
3977
+ return Number.isFinite(v) && v > 0 && v <= 1 ? v : DEFAULT_EDGE_WIDTH;
3978
+ })(),
3979
+ hideUnconnectedNodes: ((_a = nodesEl == null ? void 0 : nodesEl.getAttribute("unconnected")) == null ? void 0 : _a.toLowerCase()) === "hide",
3719
3980
  scalePoints: this.getAttribute("scale-points") !== "false",
3720
3981
  // Means — presence of <qe-means> = show; absence = hide.
3721
3982
  showMeans: !!meansEl,
@@ -3725,18 +3986,28 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3725
3986
  (meansEl == null ? void 0 : meansEl.getAttribute("label")) ?? this.getAttribute("label-means") ?? labelsShorthand,
3726
3987
  Label.DISPLAY_TYPES.ON
3727
3988
  ),
3989
+ // fixed-size keeps mean markers a constant on-screen px size (webENA parity).
3990
+ meanFixedSize: !!meansEl && meansEl.hasAttribute("fixed-size"),
3728
3991
  // Points — presence of <qe-points> = show unit dots.
3729
3992
  showPoints: !!pointsEl,
3730
3993
  labelPoints: parseLabelMode(
3731
3994
  (pointsEl == null ? void 0 : pointsEl.getAttribute("label")) ?? this.getAttribute("label-points") ?? labelsShorthand,
3732
3995
  Label.DISPLAY_TYPES.OFF
3733
3996
  ),
3997
+ pointSize: (pointsEl == null ? void 0 : pointsEl.getAttribute("size")) ? parseFloat(pointsEl.getAttribute("size")) : null,
3998
+ // fixed-size keeps unit dots a constant on-screen px size (webENA parity).
3999
+ pointFixedSize: !!pointsEl && pointsEl.hasAttribute("fixed-size"),
3734
4000
  // Edges — presence of <qe-edges> = draw a network.
3735
4001
  showEdges: !!edgesEl,
3736
4002
  edgeGroup: (edgesEl == null ? void 0 : edgesEl.getAttribute("group")) ?? null,
3737
4003
  edgeUnit: (edgesEl == null ? void 0 : edgesEl.getAttribute("unit")) ?? null,
3738
4004
  edgeCompare: (edgesEl == null ? void 0 : edgesEl.getAttribute("compare")) ?? null,
3739
4005
  edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null,
4006
+ edgeColorScale: parseColorScale((edgesEl == null ? void 0 : edgesEl.getAttribute("color-scale")) ?? null),
4007
+ edgeMagnify: (edgesEl == null ? void 0 : edgesEl.hasAttribute("magnify")) ? (() => {
4008
+ const v = Number(edgesEl.getAttribute("magnify"));
4009
+ return Number.isFinite(v) && v > 0 ? v : 1;
4010
+ })() : null,
3740
4011
  // Trajectories — presence of <qe-trajectories> = draw cognitive trajectories
3741
4012
  showTrajectories: !!trajectoriesEl,
3742
4013
  trajectoryUnits: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("units")) ?? null,
@@ -3777,6 +4048,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3777
4048
  if (w) this.style.width = isNaN(Number(w)) ? w : `${w}px`;
3778
4049
  if (h) this.style.height = isNaN(Number(h)) ? h : `${h}px`;
3779
4050
  this.appendChild(this.svg_element);
4051
+ _QEGraphElement._instances.add(this);
3780
4052
  this._childObserver = new MutationObserver(() => this._scheduleRerender());
3781
4053
  this._childObserver.observe(this, {
3782
4054
  childList: true,
@@ -3787,10 +4059,14 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3787
4059
  "unit",
3788
4060
  "compare",
3789
4061
  "also",
4062
+ "color-scale",
4063
+ "magnify",
3790
4064
  "groups",
3791
4065
  "confidence",
3792
4066
  "label",
4067
+ "unconnected",
3793
4068
  "scale-points",
4069
+ "edge-width",
3794
4070
  "labels",
3795
4071
  "label-nodes",
3796
4072
  "label-means",
@@ -3806,7 +4082,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3806
4082
  "stroke-width-range",
3807
4083
  "opacity-range",
3808
4084
  "x",
3809
- "y"
4085
+ "y",
4086
+ "size",
4087
+ "fixed-size"
3810
4088
  ]
3811
4089
  });
3812
4090
  const forId = this.getAttribute("for");
@@ -3826,9 +4104,275 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3826
4104
  }
3827
4105
  }
3828
4106
  disconnectedCallback() {
3829
- var _a;
4107
+ var _a, _b;
3830
4108
  (_a = this._childObserver) == null ? void 0 : _a.disconnect();
3831
4109
  this._childObserver = null;
4110
+ _QEGraphElement._instances.delete(this);
4111
+ (_b = this._tooltipEl) == null ? void 0 : _b.remove();
4112
+ this._tooltipEl = null;
4113
+ }
4114
+ /** All connected <qe-graph> instances (newest membership order). */
4115
+ static instances() {
4116
+ return [..._QEGraphElement._instances];
4117
+ }
4118
+ /** Look up a graph by its `id` or `plot-group` attribute. */
4119
+ static getGraph(key) {
4120
+ for (const g of _QEGraphElement._instances) {
4121
+ if (g.id === key || g.getAttribute("plot-group") === key) return g;
4122
+ }
4123
+ return null;
4124
+ }
4125
+ /** Resolved code nodes with their current graph-space coordinates. */
4126
+ getNodes() {
4127
+ const layer = this.__children__.search("__children__", "nodes");
4128
+ if (!layer) return [];
4129
+ return [...layer.__data__.objects.values()].map((o) => {
4130
+ var _a;
4131
+ return {
4132
+ id: o.ID,
4133
+ name: ((_a = o.meta) == null ? void 0 : _a.name) ?? o.ID,
4134
+ x: o.x,
4135
+ y: o.y
4136
+ };
4137
+ });
4138
+ }
4139
+ /** Resolved edges of the current network with weights and direction. */
4140
+ getEdges() {
4141
+ const collect = /* @__PURE__ */ __name((name) => {
4142
+ const l = this.__children__.search("__children__", name);
4143
+ return l ? [...l.__data__.objects.values()] : [];
4144
+ }, "collect");
4145
+ return [...collect("edges"), ...collect("edgenodes")].map((o) => {
4146
+ const m = o.meta ?? {};
4147
+ return {
4148
+ source: m.source,
4149
+ target: m.target,
4150
+ weight: m.weight ?? 0,
4151
+ directed: !!m.isDirected,
4152
+ self: !!m.isSelf
4153
+ };
4154
+ }).filter((e) => e.source != null && e.target != null);
4155
+ }
4156
+ /** Marks (circles/rects), undirected edge lines, and directed / self-edge
4157
+ * triangle paths all participate in tweening. */
4158
+ _tweenTargets() {
4159
+ return [...this.svg_element.querySelectorAll(
4160
+ "circle[data-point-id], rect[data-point-id], line[data-edge-source], path[data-edge-source]"
4161
+ )];
4162
+ }
4163
+ _readCoords(el) {
4164
+ const tag = el.tagName.toLowerCase();
4165
+ const n = /* @__PURE__ */ __name((a) => parseFloat(el.getAttribute(a) || "0"), "n");
4166
+ if (tag === "circle") return { t: "c", cx: n("cx"), cy: n("cy") };
4167
+ if (tag === "rect") return { t: "r", x: n("x"), y: n("y") };
4168
+ if (tag === "line") return { t: "l", x1: n("x1"), y1: n("y1"), x2: n("x2"), y2: n("y2") };
4169
+ if (tag === "path") {
4170
+ const d = el.getAttribute("d") || "";
4171
+ return { t: "p", d, nums: (d.match(_QEGraphElement._PATH_NUM_RE) || []).map(Number) };
4172
+ }
4173
+ return null;
4174
+ }
4175
+ /** Snapshot current tween-target positions keyed by element. */
4176
+ _captureTween() {
4177
+ const m = /* @__PURE__ */ new Map();
4178
+ this._tweenTargets().forEach((el) => {
4179
+ const c = this._readCoords(el);
4180
+ if (c) m.set(el, c);
4181
+ });
4182
+ return m;
4183
+ }
4184
+ /** Animate each still-present element from its snapshot to its new position. */
4185
+ _runTween(from) {
4186
+ if (this._tweenRaf !== null) {
4187
+ cancelAnimationFrame(this._tweenRaf);
4188
+ this._tweenRaf = null;
4189
+ }
4190
+ const to = /* @__PURE__ */ new Map();
4191
+ this._tweenTargets().forEach((el) => {
4192
+ if (from.has(el)) {
4193
+ const c = this._readCoords(el);
4194
+ if (c) to.set(el, c);
4195
+ }
4196
+ });
4197
+ if (!to.size) return;
4198
+ const dur = this._animateMs;
4199
+ const t0 = performance.now();
4200
+ const ease = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2, "ease");
4201
+ const lerp = /* @__PURE__ */ __name((a, b, k) => a + (b - a) * k, "lerp");
4202
+ const step = /* @__PURE__ */ __name((now) => {
4203
+ const t = Math.min(1, (now - t0) / dur);
4204
+ const k = ease(t);
4205
+ to.forEach((nc, el) => {
4206
+ const oc = from.get(el);
4207
+ if (!oc || oc.t !== nc.t) return;
4208
+ if (nc.t === "c") {
4209
+ el.setAttribute("cx", String(lerp(oc.cx, nc.cx, k)));
4210
+ el.setAttribute("cy", String(lerp(oc.cy, nc.cy, k)));
4211
+ } else if (nc.t === "r") {
4212
+ el.setAttribute("x", String(lerp(oc.x, nc.x, k)));
4213
+ el.setAttribute("y", String(lerp(oc.y, nc.y, k)));
4214
+ } else if (nc.t === "l") {
4215
+ el.setAttribute("x1", String(lerp(oc.x1, nc.x1, k)));
4216
+ el.setAttribute("y1", String(lerp(oc.y1, nc.y1, k)));
4217
+ el.setAttribute("x2", String(lerp(oc.x2, nc.x2, k)));
4218
+ el.setAttribute("y2", String(lerp(oc.y2, nc.y2, k)));
4219
+ } else {
4220
+ if (!oc.nums || oc.nums.length !== nc.nums.length) return;
4221
+ let i = 0;
4222
+ const d = nc.d.replace(
4223
+ _QEGraphElement._PATH_NUM_RE,
4224
+ () => String(lerp(oc.nums[i], nc.nums[i++], k))
4225
+ );
4226
+ el.setAttribute("d", d);
4227
+ }
4228
+ });
4229
+ this._tweenRaf = t < 1 ? requestAnimationFrame(step) : null;
4230
+ }, "step");
4231
+ this._tweenRaf = requestAnimationFrame(step);
4232
+ }
4233
+ /** Serialize the current plot as a standalone SVG string (webENA P2.3). */
4234
+ toSVG() {
4235
+ const svg = this.svg_element.cloneNode(true);
4236
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
4237
+ svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
4238
+ const rect = this.svg_element.getBoundingClientRect();
4239
+ if (rect.width) svg.setAttribute("width", String(Math.round(rect.width)));
4240
+ if (rect.height) svg.setAttribute("height", String(Math.round(rect.height)));
4241
+ return new XMLSerializer().serializeToString(svg);
4242
+ }
4243
+ /** Rasterize the current plot to a PNG data URL (webENA P2.3). `scale`
4244
+ * multiplies the pixel dimensions for a higher-resolution image. */
4245
+ toPNG(scale = 2) {
4246
+ const svgStr = this.toSVG();
4247
+ const rect = this.svg_element.getBoundingClientRect();
4248
+ const w = Math.max(1, Math.round(rect.width));
4249
+ const h = Math.max(1, Math.round(rect.height));
4250
+ return new Promise((resolve, reject) => {
4251
+ const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
4252
+ const url = URL.createObjectURL(blob);
4253
+ const img = new Image();
4254
+ img.onload = () => {
4255
+ try {
4256
+ const canvas = document.createElement("canvas");
4257
+ canvas.width = w * scale;
4258
+ canvas.height = h * scale;
4259
+ const ctx = canvas.getContext("2d");
4260
+ if (!ctx) {
4261
+ URL.revokeObjectURL(url);
4262
+ reject(new Error("no 2d context"));
4263
+ return;
4264
+ }
4265
+ ctx.scale(scale, scale);
4266
+ ctx.drawImage(img, 0, 0, w, h);
4267
+ URL.revokeObjectURL(url);
4268
+ resolve(canvas.toDataURL("image/png"));
4269
+ } catch (err) {
4270
+ URL.revokeObjectURL(url);
4271
+ reject(err);
4272
+ }
4273
+ };
4274
+ img.onerror = (e) => {
4275
+ URL.revokeObjectURL(url);
4276
+ reject(e);
4277
+ };
4278
+ img.src = url;
4279
+ });
4280
+ }
4281
+ /** Unit points with their current graph-space coordinates. */
4282
+ getPoints() {
4283
+ const layer = this.__children__.search("__children__", "points");
4284
+ if (!layer) return [];
4285
+ return [...layer.__data__.objects.values()].filter((o) => {
4286
+ var _a;
4287
+ return ((_a = o.meta) == null ? void 0 : _a.kind) === "point";
4288
+ }).map((o) => {
4289
+ var _a;
4290
+ return { id: o.ID, group: (_a = o.meta) == null ? void 0 : _a.group, x: o.x, y: o.y };
4291
+ });
4292
+ }
4293
+ /** Ids of unit points inside a graph-space rectangle (for drag/lasso select). */
4294
+ pointsInRect(rect) {
4295
+ 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);
4296
+ }
4297
+ /** Order-independent key so an undirected pair matches either direction. */
4298
+ _edgeKey(a, b) {
4299
+ return a <= b ? `${a}\0${b}` : `${b}\0${a}`;
4300
+ }
4301
+ /** Mark edges/nodes as highlighted. Replaces the previous selection. */
4302
+ setHighlight(sel) {
4303
+ this._highlight.edges = new Set((sel.edges ?? []).map((e) => this._edgeKey(e.source, e.target)));
4304
+ this._highlight.nodes = new Set((sel.nodes ?? []).map(String));
4305
+ this._applyHighlights();
4306
+ }
4307
+ /** Clear all highlighting. */
4308
+ clearHighlight() {
4309
+ this._highlight.edges.clear();
4310
+ this._highlight.nodes.clear();
4311
+ this._applyHighlights();
4312
+ }
4313
+ _applyHighlights() {
4314
+ if (!this.svg_element) return;
4315
+ if (!this._highlightGroup) {
4316
+ this._highlightGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
4317
+ this._highlightGroup.setAttribute("data-layer-name", "highlight");
4318
+ this._highlightGroup.setAttribute("pointer-events", "none");
4319
+ }
4320
+ this.svg_element.appendChild(this._highlightGroup);
4321
+ const g = this._highlightGroup;
4322
+ g.textContent = "";
4323
+ const extent = this.__data__.extremes.x_max || 1;
4324
+ const HL = "#f5a623";
4325
+ const mk = /* @__PURE__ */ __name((tag, attrs) => {
4326
+ const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
4327
+ for (const k in attrs) el.setAttribute(k, String(attrs[k]));
4328
+ return el;
4329
+ }, "mk");
4330
+ if (this._highlight.edges.size) {
4331
+ const xy = {};
4332
+ this.getNodes().forEach((n) => {
4333
+ xy[n.id] = { x: n.x, y: n.y };
4334
+ });
4335
+ this._highlight.edges.forEach((key) => {
4336
+ const [a, b] = key.split("\0");
4337
+ const A = xy[a], B = xy[b];
4338
+ if (!A || !B) return;
4339
+ g.appendChild(mk("line", {
4340
+ x1: A.x,
4341
+ y1: A.y,
4342
+ x2: B.x,
4343
+ y2: B.y,
4344
+ stroke: HL,
4345
+ "stroke-width": extent * 0.02,
4346
+ "stroke-opacity": 0.6,
4347
+ "stroke-linecap": "round"
4348
+ }));
4349
+ });
4350
+ }
4351
+ this._highlight.nodes.forEach((id) => {
4352
+ var _a;
4353
+ const el = this.svg_element.querySelector(`[data-point-id="${((_a = window.CSS) == null ? void 0 : _a.escape) ? CSS.escape(id) : id}"]`);
4354
+ if (!el) return;
4355
+ const tag = el.tagName.toLowerCase();
4356
+ let cx, cy, r;
4357
+ if (tag === "rect") {
4358
+ const w = parseFloat(el.getAttribute("width") || "0");
4359
+ cx = parseFloat(el.getAttribute("x") || "0") + w / 2;
4360
+ cy = parseFloat(el.getAttribute("y") || "0") + parseFloat(el.getAttribute("height") || "0") / 2;
4361
+ r = w / 2;
4362
+ } else {
4363
+ cx = parseFloat(el.getAttribute("cx") || "0");
4364
+ cy = parseFloat(el.getAttribute("cy") || "0");
4365
+ r = parseFloat(el.getAttribute("r") || "0");
4366
+ }
4367
+ g.appendChild(mk("circle", {
4368
+ cx,
4369
+ cy,
4370
+ r: r * 1.7 + extent * 4e-3,
4371
+ fill: "none",
4372
+ stroke: HL,
4373
+ "stroke-width": extent * 6e-3
4374
+ }));
4375
+ });
3832
4376
  }
3833
4377
  create() {
3834
4378
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
@@ -3946,6 +4490,138 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3946
4490
  const lbl = getAutoLabel(id);
3947
4491
  if ((lbl == null ? void 0 : lbl.displayMode) === Label.DISPLAY_TYPES.AUTO) lbl.hide();
3948
4492
  });
4493
+ this._attachInteractionEvents();
4494
+ }
4495
+ /** Emit a namespaced CustomEvent on the public element (bubbles + composed). */
4496
+ _emit(name, detail) {
4497
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
4498
+ }
4499
+ /** Walk up from a hit element to the nearest ancestor carrying `attr`. */
4500
+ _closestWith(el, attr) {
4501
+ var _a, _b;
4502
+ while (el && el !== this.svg_element) {
4503
+ if (((_a = el.getAttribute) == null ? void 0 : _a.call(el, attr)) != null && ((_b = el.hasAttribute) == null ? void 0 : _b.call(el, attr))) return el;
4504
+ el = el.parentElement;
4505
+ }
4506
+ return null;
4507
+ }
4508
+ /** Resolve an edge hit element to a qe-edge-* detail, or null. */
4509
+ _edgeDetail(el) {
4510
+ const e = this._closestWith(el, "data-edge-source");
4511
+ if (!e) return null;
4512
+ const d = {
4513
+ source: e.getAttribute("data-edge-source"),
4514
+ target: e.getAttribute("data-edge-target"),
4515
+ weight: parseFloat(e.getAttribute("data-edge-weight") || "0"),
4516
+ directed: e.getAttribute("data-edge-directed") === "true",
4517
+ self: e.getAttribute("data-edge-self") === "true"
4518
+ };
4519
+ if (e.hasAttribute("data-edge-group")) d.group = e.getAttribute("data-edge-group");
4520
+ if (e.hasAttribute("data-edge-unit")) d.unit = e.getAttribute("data-edge-unit");
4521
+ return d;
4522
+ }
4523
+ /** Resolve a node/mean/point hit element to a qe-node-* detail, or null. */
4524
+ _nodeDetail(el) {
4525
+ const n = this._closestWith(el, "data-point-id");
4526
+ if (!n) return null;
4527
+ const tag = n.tagName.toLowerCase();
4528
+ let x, y;
4529
+ if (tag === "rect") {
4530
+ x = parseFloat(n.getAttribute("x") || "0") + parseFloat(n.getAttribute("width") || "0") / 2;
4531
+ y = parseFloat(n.getAttribute("y") || "0") + parseFloat(n.getAttribute("height") || "0") / 2;
4532
+ } else {
4533
+ x = parseFloat(n.getAttribute("cx") || "0");
4534
+ y = parseFloat(n.getAttribute("cy") || "0");
4535
+ }
4536
+ const id = n.getAttribute("data-point-id");
4537
+ const d = {
4538
+ id,
4539
+ kind: n.getAttribute("data-kind") || "point",
4540
+ name: n.getAttribute("data-name") || id,
4541
+ x,
4542
+ y
4543
+ };
4544
+ if (n.hasAttribute("data-group")) d.group = n.getAttribute("data-group");
4545
+ if (n.hasAttribute("data-unit")) d.unit = n.getAttribute("data-unit");
4546
+ return d;
4547
+ }
4548
+ _attachInteractionEvents() {
4549
+ const svg = this.svg_element;
4550
+ svg.addEventListener("click", (ev) => {
4551
+ const target = ev.target;
4552
+ const edge = this._edgeDetail(target);
4553
+ if (edge) {
4554
+ this._emit("qe-edge-click", { ...edge, originalEvent: ev });
4555
+ return;
4556
+ }
4557
+ const node = this._nodeDetail(target);
4558
+ if (node) this._emit("qe-node-click", { ...node, originalEvent: ev });
4559
+ });
4560
+ let hovered = null;
4561
+ const keyOf = /* @__PURE__ */ __name((t, d) => t === "edge" ? `${d.source}\0${d.target}` : String(d.id), "keyOf");
4562
+ svg.addEventListener("mousemove", (ev) => {
4563
+ const target = ev.target;
4564
+ const edge = this._edgeDetail(target);
4565
+ const node = edge ? null : this._nodeDetail(target);
4566
+ const next = edge ? { type: "edge", detail: edge } : node ? { type: "node", detail: node } : null;
4567
+ const nextKey = next ? keyOf(next.type, next.detail) : null;
4568
+ if (hovered && (!next || hovered.key !== nextKey)) {
4569
+ this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
4570
+ hovered = null;
4571
+ }
4572
+ if (next && (!hovered || hovered.key !== nextKey)) {
4573
+ this._emit(next.type === "edge" ? "qe-edge-hover" : "qe-node-hover", { ...next.detail, originalEvent: ev });
4574
+ hovered = { type: next.type, key: nextKey };
4575
+ }
4576
+ if (this.hasAttribute("tooltip") && next) {
4577
+ this._showTooltip(this._tooltipText(next.type, next.detail), ev.clientX, ev.clientY);
4578
+ } else {
4579
+ this._hideTooltip();
4580
+ }
4581
+ });
4582
+ svg.addEventListener("mouseleave", () => {
4583
+ if (hovered) {
4584
+ this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
4585
+ hovered = null;
4586
+ }
4587
+ this._hideTooltip();
4588
+ });
4589
+ }
4590
+ /** "source – target (weight)" for edges; the mark name for nodes/means/points. */
4591
+ _tooltipText(type, d) {
4592
+ if (type === "edge") {
4593
+ const w = Number(d.weight);
4594
+ const wStr = Number.isFinite(w) ? ` (${+w.toFixed(3)})` : "";
4595
+ return `${d.source} – ${d.target}${wStr}`;
4596
+ }
4597
+ return String(d.name ?? d.id);
4598
+ }
4599
+ _showTooltip(text, clientX, clientY) {
4600
+ if (!this._tooltipEl) {
4601
+ const el = document.createElement("div");
4602
+ el.setAttribute("data-qe-tooltip", "");
4603
+ Object.assign(el.style, {
4604
+ position: "fixed",
4605
+ zIndex: "2147483647",
4606
+ pointerEvents: "none",
4607
+ background: "rgba(20,20,20,0.92)",
4608
+ color: "#fff",
4609
+ font: "12px/1.4 system-ui, sans-serif",
4610
+ padding: "3px 7px",
4611
+ borderRadius: "4px",
4612
+ whiteSpace: "nowrap",
4613
+ transform: "translate(-50%, calc(-100% - 8px))"
4614
+ });
4615
+ document.body.appendChild(el);
4616
+ this._tooltipEl = el;
4617
+ }
4618
+ this._tooltipEl.textContent = text;
4619
+ this._tooltipEl.style.left = `${clientX}px`;
4620
+ this._tooltipEl.style.top = `${clientY}px`;
4621
+ this._tooltipEl.style.display = "block";
4622
+ }
4623
+ _hideTooltip() {
4624
+ if (this._tooltipEl) this._tooltipEl.style.display = "none";
3949
4625
  }
3950
4626
  draw() {
3951
4627
  var _a;
@@ -3964,6 +4640,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3964
4640
  this.update_labels();
3965
4641
  this.update_extremes();
3966
4642
  this.update_axes();
4643
+ this._applyHighlights();
3967
4644
  }
3968
4645
  /**
3969
4646
  * Axes (<qe-axes>): toggle the axis-line cross by presence, and draw/position
@@ -3971,6 +4648,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3971
4648
  * cached SVG <text> nodes in the labels group; null text removes them.
3972
4649
  */
3973
4650
  update_axes() {
4651
+ var _a;
3974
4652
  const axesLayer = this.__children__.search("__children__", "axes");
3975
4653
  if (axesLayer == null ? void 0 : axesLayer.svg_element) {
3976
4654
  axesLayer.svg_element.style.display = this._showAxes ? "" : "none";
@@ -4002,17 +4680,37 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
4002
4680
  el.setAttribute("font-weight", "600");
4003
4681
  el.setAttribute("fill", "#556");
4004
4682
  if (axis === "x") {
4005
- el.setAttribute("x", (extent + gap).toString());
4006
- el.setAttribute("y", (fontSize * 0.35).toString());
4007
- el.setAttribute("text-anchor", "start");
4683
+ el.setAttribute("x", (extent - gap).toString());
4684
+ el.setAttribute("y", (-gap).toString());
4685
+ el.setAttribute("text-anchor", "end");
4008
4686
  } else {
4009
- el.setAttribute("x", "0");
4010
- el.setAttribute("y", (-extent - gap).toString());
4011
- el.setAttribute("text-anchor", "middle");
4687
+ el.setAttribute("x", gap.toString());
4688
+ el.setAttribute("y", (-extent + fontSize + gap).toString());
4689
+ el.setAttribute("text-anchor", "start");
4012
4690
  }
4013
4691
  }, "place");
4014
4692
  place("x", this._axisXText);
4015
4693
  place("y", this._axisYText);
4694
+ if (this._magnifyText == null) {
4695
+ (_a = this._magnifyEl) == null ? void 0 : _a.remove();
4696
+ this._magnifyEl = null;
4697
+ } else {
4698
+ if (!this._magnifyEl) {
4699
+ this._magnifyEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
4700
+ this._magnifyEl.setAttribute("data-magnify-label", "");
4701
+ this._magnifyEl.setAttribute("pointer-events", "none");
4702
+ }
4703
+ if (!this._magnifyEl.parentElement) parent.appendChild(this._magnifyEl);
4704
+ const el = this._magnifyEl;
4705
+ el.textContent = this._magnifyText;
4706
+ el.setAttribute("font-size", (fontSize * 0.85).toString());
4707
+ el.setAttribute("font-family", "system-ui, sans-serif");
4708
+ el.setAttribute("font-style", "italic");
4709
+ el.setAttribute("fill", "#556");
4710
+ el.setAttribute("x", (-extent + gap).toString());
4711
+ el.setAttribute("y", (-extent + fontSize + gap).toString());
4712
+ el.setAttribute("text-anchor", "start");
4713
+ }
4016
4714
  }
4017
4715
  update_labels() {
4018
4716
  this._nodeLabels.forEach((lbl) => lbl.update());
@@ -4076,8 +4774,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
4076
4774
  background-color: #FFF;
4077
4775
  }
4078
4776
  `;
4777
+ _QEGraphElement._instances = /* @__PURE__ */ new Set();
4778
+ _QEGraphElement._PATH_NUM_RE = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
4079
4779
  let QEGraphElement = _QEGraphElement;
4080
4780
  customElements.define(QEGraphElement.tagName, QEGraphElement);
4781
+ const getGraph = /* @__PURE__ */ __name((key) => QEGraphElement.getGraph(key), "getGraph");
4782
+ const getGraphs = /* @__PURE__ */ __name(() => QEGraphElement.instances(), "getGraphs");
4081
4783
  exports2.Color = Color;
4082
4784
  exports2.DataFrame = DataFrame;
4083
4785
  exports2.DataFrameColumn = DataFrameColumn;
@@ -4101,6 +4803,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
4101
4803
  exports2.Trajectory = Trajectory;
4102
4804
  exports2.WeightedColor = WeightedColor;
4103
4805
  exports2.fitTrajectory = fitTrajectory;
4806
+ exports2.getGraph = getGraph;
4807
+ exports2.getGraphs = getGraphs;
4104
4808
  exports2.nearOriginFilter = nearOriginFilter;
4105
4809
  exports2.sampleCurve = sampleCurve;
4106
4810
  exports2.selectDegree = selectDegree;