@qe-libs/qeviz 0.1.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
@@ -160,7 +160,7 @@ const _DataFrame = class _DataFrame extends Array {
160
160
  }
161
161
  });
162
162
  __privateSet(this, _names2, names);
163
- if (rowNames && rowNames.length !== this[0].length) {
163
+ if (rowNames && this[0] && rowNames.length !== this[0].length) {
164
164
  throw new Error("Row names length must match the number of rows in the DataFrame.");
165
165
  }
166
166
  if (rowNames && rowNames.length > 0) {
@@ -224,6 +224,9 @@ const _DataFrame = class _DataFrame extends Array {
224
224
  rows(...wh) {
225
225
  let selectors, selectedRows = [], selectedRowNames = [];
226
226
  if (wh.length === 0) {
227
+ if (!this[0] || this.length === 0) {
228
+ return [];
229
+ }
227
230
  const numRows = this[0].length;
228
231
  selectedRows = Array.from({ length: numRows }, (_, i) => this.getRowByIndex(i));
229
232
  selectedRowNames = __privateGet(this, _rowNames2);
@@ -282,7 +285,7 @@ const _DataFrame = class _DataFrame extends Array {
282
285
  return __privateGet(this, _rowNames2);
283
286
  }
284
287
  get nrow() {
285
- return this[0].length;
288
+ return this[0] ? this[0].length : 0;
286
289
  }
287
290
  get ncol() {
288
291
  return this.length;
@@ -349,6 +352,12 @@ const _Model = class _Model {
349
352
  set groups(v) {
350
353
  this._groups = v;
351
354
  }
355
+ get trajectories() {
356
+ return this._trajectories;
357
+ }
358
+ set trajectories(v) {
359
+ this._trajectories = v;
360
+ }
352
361
  get dimensions() {
353
362
  return this._dimensions;
354
363
  }
@@ -461,7 +470,7 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
461
470
  */
462
471
  setModelData(data) {
463
472
  var _a, _b, _c, _d;
464
- const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier"];
473
+ const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier", "trajectories"];
465
474
  frameKeys.forEach((name) => {
466
475
  var _a2;
467
476
  const raw = data[name];
@@ -470,7 +479,7 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
470
479
  let idCol;
471
480
  if (name === "nodes") {
472
481
  idCol = data.node_id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
473
- } else if (name === "edges" || name === "points") {
482
+ } else if (name === "edges" || name === "points" || name === "trajectories") {
474
483
  idCol = data.id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
475
484
  } else {
476
485
  idCol = data.group_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
@@ -540,8 +549,12 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
540
549
  __name(_QEVisualElement, "QEVisualElement");
541
550
  _QEVisualElement.tagName = "qe-visual";
542
551
  _QEVisualElement.styles = `
543
- :host { display: inline-block; }
552
+ :host { display: block; width: 100%; }
544
553
  :host(.hidden) { display: none !important; }
554
+ /* The output wrapper and the slotted graph fill the host so the plot
555
+ stretches to its container instead of collapsing to intrinsic size. */
556
+ div { display: block; width: 100%; height: 100%; }
557
+ ::slotted(qe-graph) { display: block; width: 100%; height: 100%; }
545
558
  `;
546
559
  let QEVisualElement = _QEVisualElement;
547
560
  customElements.define(QEVisualElement.tagName, QEVisualElement);
@@ -582,9 +595,10 @@ const _Color = class _Color {
582
595
  __name(_Color, "Color");
583
596
  let Color = _Color;
584
597
  const _WeightedColor = class _WeightedColor extends Color {
585
- constructor(base, weight) {
598
+ constructor(base, weight, alpha) {
586
599
  super(base);
587
600
  this.weight = weight;
601
+ this.alpha = alpha;
588
602
  }
589
603
  // Parse hex → HSV (h in [0, 360), s/v in [0, 1]).
590
604
  static toHSV(hex) {
@@ -655,7 +669,8 @@ const _WeightedColor = class _WeightedColor extends Color {
655
669
  toString() {
656
670
  const { h, v } = _WeightedColor.toHSV(this.base);
657
671
  const w = Math.min(1, Math.abs(this.weight));
658
- 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);
659
674
  }
660
675
  };
661
676
  __name(_WeightedColor, "WeightedColor");
@@ -812,7 +827,9 @@ var Tools;
812
827
  __name(slopeIntercept, "slopeIntercept");
813
828
  Line2.slopeIntercept = slopeIntercept;
814
829
  function point_along(line, distance = 0) {
815
- let len = Tools2.Geometry.Line.length(line[0], line[1]), ratio = distance / len, Xt = (1 - ratio) * line[0][0] + ratio * line[1][0], Yt = (1 - ratio) * line[0][1] + ratio * line[1][1];
830
+ const ratio = distance;
831
+ const Xt = (1 - ratio) * line[0][0] + ratio * line[1][0];
832
+ const Yt = (1 - ratio) * line[0][1] + ratio * line[1][1];
816
833
  return [Xt, Yt];
817
834
  }
818
835
  __name(point_along, "point_along");
@@ -1659,6 +1676,7 @@ const _Graph = class _Graph {
1659
1676
  this.points = /* @__PURE__ */ new Map();
1660
1677
  this.nodes = /* @__PURE__ */ new Map();
1661
1678
  this.edges = /* @__PURE__ */ new Map();
1679
+ this.trajectories = /* @__PURE__ */ new Map();
1662
1680
  this.labels = /* @__PURE__ */ new Map();
1663
1681
  this.transform = new Transform();
1664
1682
  this.scale_x = 1;
@@ -1701,6 +1719,7 @@ const _Graph = class _Graph {
1701
1719
  { name: "edgenodes", layers: [], tabindex: 0 }
1702
1720
  ]
1703
1721
  },
1722
+ { name: "trajectories", layers: [], tabindex: 0 },
1704
1723
  { name: "points", layers: [], tabindex: 0 },
1705
1724
  { name: "labels", layers: [], tabindex: 0 }
1706
1725
  ] }
@@ -1826,9 +1845,13 @@ const _Graph = class _Graph {
1826
1845
  if (search_layers.length === 0) {
1827
1846
  this.extremes = new Extremes(1, 1);
1828
1847
  } else {
1829
- const found_max = Math.max(...search_layers.map((l) => Math.max(...[...l.objects.values()].map((o) => Math.max(Math.abs(o.x), Math.abs(o.y))))).filter((a) => Math.abs(a) !== Infinity)) * 1.5;
1830
- console.log("Found max for extremes:", found_max);
1831
- this.extremes = new Extremes(Math.ceil(found_max), Math.ceil(found_max));
1848
+ const content_max = Math.max(
1849
+ ...search_layers.map((l) => Math.max(
1850
+ ...[...l.objects.values()].map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))
1851
+ )).filter((a) => Math.abs(a) !== Infinity)
1852
+ );
1853
+ const found_max = (Number.isFinite(content_max) && content_max > 0 ? content_max : 1) * 1.3;
1854
+ this.extremes = new Extremes(found_max, found_max);
1832
1855
  }
1833
1856
  }
1834
1857
  base_point_size(scale = 8) {
@@ -1868,6 +1891,251 @@ _DrawableElement.style = `
1868
1891
  }
1869
1892
  `;
1870
1893
  let DrawableElement = _DrawableElement;
1894
+ const TRAJECTORY_ZERO_EPS = 1e-8;
1895
+ function nearOriginFilter(pts, eps = TRAJECTORY_ZERO_EPS) {
1896
+ return pts.filter((p) => Math.abs(p.x) > eps || Math.abs(p.y) > eps);
1897
+ }
1898
+ __name(nearOriginFilter, "nearOriginFilter");
1899
+ function normalizedT(n) {
1900
+ if (n <= 0) return [];
1901
+ if (n === 1) return [0];
1902
+ const t = new Array(n);
1903
+ for (let i = 0; i < n; i++) t[i] = i / (n - 1);
1904
+ return t;
1905
+ }
1906
+ __name(normalizedT, "normalizedT");
1907
+ function design(t, degree) {
1908
+ return t.map((ti) => {
1909
+ const row = new Array(degree + 1);
1910
+ let p = 1;
1911
+ for (let k = 0; k <= degree; k++) {
1912
+ row[k] = p;
1913
+ p *= ti;
1914
+ }
1915
+ return row;
1916
+ });
1917
+ }
1918
+ __name(design, "design");
1919
+ function qr(A) {
1920
+ const n = A.length;
1921
+ const m = A[0].length;
1922
+ const Q = A.map((row) => row.slice());
1923
+ const R = Array.from({ length: m }, () => new Array(m).fill(0));
1924
+ for (let j = 0; j < m; j++) {
1925
+ let norm = 0;
1926
+ for (let i = 0; i < n; i++) norm += Q[i][j] * Q[i][j];
1927
+ norm = Math.sqrt(norm);
1928
+ R[j][j] = norm;
1929
+ if (norm > 1e-300) {
1930
+ for (let i = 0; i < n; i++) Q[i][j] /= norm;
1931
+ }
1932
+ for (let k = j + 1; k < m; k++) {
1933
+ let dot = 0;
1934
+ for (let i = 0; i < n; i++) dot += Q[i][j] * Q[i][k];
1935
+ R[j][k] = dot;
1936
+ for (let i = 0; i < n; i++) Q[i][k] -= dot * Q[i][j];
1937
+ }
1938
+ }
1939
+ return { Q, R };
1940
+ }
1941
+ __name(qr, "qr");
1942
+ function qtY(Q, y) {
1943
+ const n = Q.length;
1944
+ const m = Q[0].length;
1945
+ const out = new Array(m).fill(0);
1946
+ for (let j = 0; j < m; j++) {
1947
+ let s = 0;
1948
+ for (let i = 0; i < n; i++) s += Q[i][j] * y[i];
1949
+ out[j] = s;
1950
+ }
1951
+ return out;
1952
+ }
1953
+ __name(qtY, "qtY");
1954
+ function backSolve(R, b) {
1955
+ const m = b.length;
1956
+ const c = new Array(m).fill(0);
1957
+ for (let i = m - 1; i >= 0; i--) {
1958
+ let s = b[i];
1959
+ for (let j = i + 1; j < m; j++) s -= R[i][j] * c[j];
1960
+ c[i] = Math.abs(R[i][i]) > 1e-12 ? s / R[i][i] : 0;
1961
+ }
1962
+ return c;
1963
+ }
1964
+ __name(backSolve, "backSolve");
1965
+ function leverages(Q) {
1966
+ return Q.map((row) => {
1967
+ let s = 0;
1968
+ for (let j = 0; j < row.length; j++) s += row[j] * row[j];
1969
+ return s;
1970
+ });
1971
+ }
1972
+ __name(leverages, "leverages");
1973
+ function fitCoeffs(t, y, degree) {
1974
+ const X = design(t, degree);
1975
+ const { Q, R } = qr(X);
1976
+ return backSolve(R, qtY(Q, y));
1977
+ }
1978
+ __name(fitCoeffs, "fitCoeffs");
1979
+ function evalPoly(coeffs, t) {
1980
+ let v = 0;
1981
+ let p = 1;
1982
+ for (let k = 0; k < coeffs.length; k++) {
1983
+ v += coeffs[k] * p;
1984
+ p *= t;
1985
+ }
1986
+ return v;
1987
+ }
1988
+ __name(evalPoly, "evalPoly");
1989
+ function loocv2d(t, x, y, degree) {
1990
+ const X = design(t, degree);
1991
+ const { Q, R } = qr(X);
1992
+ const h = leverages(Q);
1993
+ const cx = backSolve(R, qtY(Q, x));
1994
+ const cy = backSolve(R, qtY(Q, y));
1995
+ let press = 0;
1996
+ for (let i = 0; i < t.length; i++) {
1997
+ const denom = 1 - h[i];
1998
+ if (denom <= 0) return Infinity;
1999
+ const ex = x[i] - evalPoly(cx, t[i]);
2000
+ const ey = y[i] - evalPoly(cy, t[i]);
2001
+ press += (ex / denom) ** 2 + (ey / denom) ** 2;
2002
+ }
2003
+ return press;
2004
+ }
2005
+ __name(loocv2d, "loocv2d");
2006
+ function selectDegree(fitPts, maxDegree, mode) {
2007
+ const n = fitPts.length;
2008
+ const cap = Math.min(Math.max(1, Math.floor(maxDegree)), Math.max(1, n - 1));
2009
+ if (mode === "fixed" || n < 3) return cap;
2010
+ const t = normalizedT(n);
2011
+ const x = fitPts.map((p) => p.x);
2012
+ const y = fitPts.map((p) => p.y);
2013
+ let best = 1;
2014
+ let bestVal = Infinity;
2015
+ for (let deg = 1; deg <= cap; deg++) {
2016
+ const v = loocv2d(t, x, y, deg);
2017
+ if (v < bestVal) {
2018
+ bestVal = v;
2019
+ best = deg;
2020
+ }
2021
+ }
2022
+ return best;
2023
+ }
2024
+ __name(selectDegree, "selectDegree");
2025
+ function fitTrajectory(activePts, opts) {
2026
+ const fitPts = nearOriginFilter(activePts);
2027
+ if (fitPts.length < 3) return null;
2028
+ const selBasis = opts.selectionSet ? nearOriginFilter(opts.selectionSet) : fitPts;
2029
+ const basis = selBasis.length >= 3 ? selBasis : fitPts;
2030
+ let degree = selectDegree(basis, opts.maxDegree, opts.degreeSelection);
2031
+ degree = Math.min(degree, Math.max(1, fitPts.length - 1));
2032
+ const t = normalizedT(fitPts.length);
2033
+ const coeffsX = fitCoeffs(t, fitPts.map((p) => p.x), degree);
2034
+ const coeffsY = fitCoeffs(t, fitPts.map((p) => p.y), degree);
2035
+ return { degree, coeffsX, coeffsY };
2036
+ }
2037
+ __name(fitTrajectory, "fitTrajectory");
2038
+ function sampleCurve(fit, steps = 100) {
2039
+ const out = [];
2040
+ for (let s = 0; s <= steps; s++) {
2041
+ const t = s / steps;
2042
+ out.push({ x: evalPoly(fit.coeffsX, t), y: evalPoly(fit.coeffsY, t) });
2043
+ }
2044
+ return out;
2045
+ }
2046
+ __name(sampleCurve, "sampleCurve");
2047
+ const TRAJECTORY_CURVE_STEPS = 100;
2048
+ const _Trajectory = class _Trajectory extends PlottedObject {
2049
+ constructor(options = {}) {
2050
+ const defaultColor = options.color instanceof Color ? options.color : new Color(typeof options.color === "string" ? options.color : "#000000");
2051
+ super([], {
2052
+ ID: options.ID || `traj-${Math.random().toString(36).substring(2, 9)}`,
2053
+ size: options.size || 1,
2054
+ color: defaultColor,
2055
+ highlight: null,
2056
+ active: options.active || false,
2057
+ meta: options.meta || {},
2058
+ ...options
2059
+ });
2060
+ this._typeName = "Trajectory";
2061
+ this.trajPoints = [];
2062
+ this.smooth = "none";
2063
+ this.maxDegree = 3;
2064
+ this.degreeSelection = "loocv_2d";
2065
+ this.selectedDegree = -1;
2066
+ this.polyCoeffsX = [];
2067
+ this.polyCoeffsY = [];
2068
+ this.currentTime = Infinity;
2069
+ this.showPoints = true;
2070
+ this.showTips = true;
2071
+ this.lineStyle = "uniform";
2072
+ this.strokeWidth = 2;
2073
+ this.strokeWidthRange = [1, 6];
2074
+ this.opacityRange = [0.3, 1];
2075
+ this.smooth = options.smooth || "none";
2076
+ this.maxDegree = options.maxDegree !== void 0 ? options.maxDegree : 3;
2077
+ this.degreeSelection = options.degreeSelection || "loocv_2d";
2078
+ this.polyCoeffsX = options.polyCoeffsX || [];
2079
+ this.polyCoeffsY = options.polyCoeffsY || [];
2080
+ this.currentTime = options.currentTime !== void 0 ? options.currentTime : Infinity;
2081
+ this.showPoints = options.showPoints !== void 0 ? options.showPoints : true;
2082
+ this.showTips = options.showTips !== void 0 ? options.showTips : true;
2083
+ this.lineStyle = options.lineStyle || "uniform";
2084
+ this.strokeWidth = options.strokeWidth || 2;
2085
+ this.strokeWidthRange = options.strokeWidthRange || [1, 6];
2086
+ this.opacityRange = options.opacityRange || [0.3, 1];
2087
+ if (options.points && Array.isArray(options.points)) {
2088
+ this.trajPoints = options.points.map((p) => {
2089
+ if (p instanceof Point) return p;
2090
+ const pt = new Point({ dimensions: [p.x, p.y], meta: { time: p.time, label: p.label } });
2091
+ pt.x = p.x;
2092
+ pt.y = p.y;
2093
+ return pt;
2094
+ });
2095
+ }
2096
+ }
2097
+ get activePoints() {
2098
+ if (this.currentTime === Infinity) {
2099
+ return this.trajPoints;
2100
+ }
2101
+ return this.trajPoints.filter((p) => {
2102
+ const t = p.meta && typeof p.meta.time === "number" ? p.meta.time : 0;
2103
+ return t <= this.currentTime;
2104
+ });
2105
+ }
2106
+ get path() {
2107
+ return this.pathPoints.reduce((acc, pt, idx) => {
2108
+ return idx === 0 ? `M${pt.x},${pt.y}` : `${acc} L${pt.x},${pt.y}`;
2109
+ }, "");
2110
+ }
2111
+ get pathPoints() {
2112
+ const pts = this.activePoints;
2113
+ if (pts.length < 2) return [];
2114
+ if (this.smooth === "poly") {
2115
+ if (this.polyCoeffsX.length > 0 && this.polyCoeffsY.length > 0) {
2116
+ return sampleCurve(
2117
+ { degree: this.polyCoeffsX.length - 1, coeffsX: this.polyCoeffsX, coeffsY: this.polyCoeffsY },
2118
+ TRAJECTORY_CURVE_STEPS
2119
+ );
2120
+ }
2121
+ const fit = fitTrajectory(
2122
+ pts.map((p) => ({ x: p.x, y: p.y })),
2123
+ {
2124
+ maxDegree: this.maxDegree,
2125
+ degreeSelection: this.degreeSelection,
2126
+ selectionSet: this.trajPoints.map((p) => ({ x: p.x, y: p.y }))
2127
+ }
2128
+ );
2129
+ if (fit) {
2130
+ this.selectedDegree = fit.degree;
2131
+ return sampleCurve(fit, TRAJECTORY_CURVE_STEPS);
2132
+ }
2133
+ }
2134
+ return pts.map((pt) => ({ x: pt.x, y: pt.y }));
2135
+ }
2136
+ };
2137
+ __name(_Trajectory, "Trajectory");
2138
+ let Trajectory = _Trajectory;
1871
2139
  const zoom_level = 1;
1872
2140
  const _Edge = class _Edge extends Line {
1873
2141
  constructor(data, from, to, weight = 0) {
@@ -1914,9 +2182,22 @@ const _EdgeArrow = class _EdgeArrow extends PlottedObject {
1914
2182
  return __privateGet(this, _edge);
1915
2183
  }
1916
2184
  get path() {
1917
- let _this = __privateGet(this, _edge);
1918
- let _mid = _this.from_point, head = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.7), nock = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.85), 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);
1919
- 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`;
1920
2201
  return p;
1921
2202
  }
1922
2203
  };
@@ -2059,6 +2340,46 @@ const _Drawable = class _Drawable {
2059
2340
  };
2060
2341
  __name(_Drawable, "Drawable");
2061
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");
2062
2383
  const _QELine = class _QELine extends Drawable {
2063
2384
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
2064
2385
  super(__data__, parent, graph, __children__);
@@ -2068,8 +2389,22 @@ const _QELine = class _QELine extends Drawable {
2068
2389
  this.__children__ = __children__;
2069
2390
  this.__name__ = "unnamed-line";
2070
2391
  this._arrow_el = null;
2392
+ this._hit_el = null;
2071
2393
  this.svg_element = this.create();
2072
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
+ }
2073
2408
  create() {
2074
2409
  if (this.__data__ instanceof NodeEdge) {
2075
2410
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -2106,24 +2441,28 @@ const _QELine = class _QELine extends Drawable {
2106
2441
  const extent = ((_a = this.graph.__data__.extremes) == null ? void 0 : _a.x_max) ?? 1;
2107
2442
  const meta = this.__data__.meta ?? {};
2108
2443
  if (meta.isMeanEdge) {
2444
+ this._stampEdge(this.svg_element);
2109
2445
  const weight = typeof meta.weight === "number" ? meta.weight : 0;
2110
2446
  const maxW = typeof meta.maxWeight === "number" ? meta.maxWeight : 1;
2111
2447
  const opacity = typeof meta.opacity === "number" ? meta.opacity : Math.min(1, weight / (maxW || 1));
2112
- 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));
2113
2450
  const baseColor = this.__data__._color || new Color("#000000");
2114
- 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();
2115
2453
  if (this.__data__ instanceof NodeEdge) {
2116
- const nodeSize = typeof meta.nodeSize === "number" ? meta.nodeSize : 2;
2117
- const nodeR = extent * 6e-3 * nodeSize;
2118
- this.__data__.weight = 2 * nodeR * 0.55 * opacity;
2454
+ this.__data__.weight = sw;
2119
2455
  this.svg_element.setAttribute("d", this.__data__.path);
2120
2456
  this.svg_element.setAttribute("fill", edgeColor);
2121
2457
  this.svg_element.setAttribute("stroke", "none");
2122
2458
  } else if (this.__data__ instanceof TaperedEdge) {
2123
- this.__data__.weight = sw * 4;
2459
+ this.__data__.weight = sw;
2124
2460
  this.svg_element.setAttribute("d", this.__data__.path);
2125
2461
  this.svg_element.setAttribute("fill", edgeColor);
2126
- 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");
2127
2466
  if (meta.isDirected) {
2128
2467
  try {
2129
2468
  const arrow = new EdgeArrow(this.__data__);
@@ -2137,20 +2476,39 @@ const _QELine = class _QELine extends Drawable {
2137
2476
  this.parent.appendChild(this._arrow_el);
2138
2477
  }
2139
2478
  this._arrow_el.setAttribute("d", arrowPath);
2140
- const arrowColor = baseColor.toString();
2141
- 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");
2142
2484
  }
2143
2485
  } catch (_) {
2144
2486
  }
2145
2487
  }
2146
2488
  } else {
2147
- this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
2148
- this.svg_element.setAttribute("y1", this.__data__.from.y.toString());
2149
- this.svg_element.setAttribute("x2", this.__data__.to.x.toString());
2150
- 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());
2151
2495
  this.svg_element.setAttribute("stroke-width", sw.toString());
2152
2496
  this.svg_element.setAttribute("stroke", edgeColor);
2497
+ this.svg_element.setAttribute("pointer-events", "stroke");
2153
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);
2154
2512
  }
2155
2513
  } else {
2156
2514
  this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
@@ -2164,10 +2522,11 @@ const _QELine = class _QELine extends Drawable {
2164
2522
  }
2165
2523
  }
2166
2524
  draw() {
2167
- var _a, _b;
2168
- (_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);
2169
2528
  if (this._arrow_el) {
2170
- (_b = this.parent) == null ? void 0 : _b.appendChild(this._arrow_el);
2529
+ (_c = this.parent) == null ? void 0 : _c.appendChild(this._arrow_el);
2171
2530
  }
2172
2531
  }
2173
2532
  };
@@ -2197,14 +2556,45 @@ const _QEPoint = class _QEPoint extends Drawable {
2197
2556
  * (≈ 1–2 % of the coordinate half-extent for a typical ENA network).
2198
2557
  */
2199
2558
  computeRadius() {
2200
- const extent = this.graph.__data__.extremes.x_max;
2201
- 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;
2576
+ }
2577
+ /** Optional halo stroke (e.g. white outline on unit points to separate
2578
+ * overlapping dots in dense clusters). Width scales with the dot so it holds
2579
+ * up at any zoom. No-op unless the object requests it via meta.stroke. */
2580
+ applyStroke(el) {
2581
+ var _a;
2582
+ const stroke = (_a = this.__data__.meta) == null ? void 0 : _a.stroke;
2583
+ if (!stroke) return;
2584
+ el.setAttribute("stroke", stroke);
2585
+ el.setAttribute("stroke-width", (this.computeRadius() * 0.22).toString());
2202
2586
  }
2203
2587
  create() {
2204
2588
  const tag = this.isSquare ? "rect" : "circle";
2205
2589
  const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
2206
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));
2207
2596
  el.setAttribute("fill", this.__data__.color.toString());
2597
+ this.applyStroke(el);
2208
2598
  const r = this.computeRadius();
2209
2599
  if (this.isSquare) {
2210
2600
  el.setAttribute("x", (this.x - r).toString());
@@ -2217,9 +2607,7 @@ const _QEPoint = class _QEPoint extends Drawable {
2217
2607
  el.setAttribute("r", r.toString());
2218
2608
  }
2219
2609
  this.svg_element = el;
2220
- el.addEventListener("click", (event) => {
2221
- console.log("Point clicked: ", this.__data__);
2222
- event.stopPropagation();
2610
+ el.addEventListener("click", () => {
2223
2611
  this.__data__.active = !this.__data__.active;
2224
2612
  this.update();
2225
2613
  const pointEvent = new CustomEvent("point-clicked", {
@@ -2242,6 +2630,7 @@ const _QEPoint = class _QEPoint extends Drawable {
2242
2630
  }
2243
2631
  const r = this.computeRadius();
2244
2632
  this.svg_element.setAttribute("fill", this.__data__.color.toString());
2633
+ this.applyStroke(this.svg_element);
2245
2634
  if (this.isSquare) {
2246
2635
  this.svg_element.setAttribute("x", (this.x - r).toString());
2247
2636
  this.svg_element.setAttribute("y", (this.y - r).toString());
@@ -2260,6 +2649,141 @@ const _QEPoint = class _QEPoint extends Drawable {
2260
2649
  };
2261
2650
  __name(_QEPoint, "QEPoint");
2262
2651
  let QEPoint = _QEPoint;
2652
+ const _QETrajectory = class _QETrajectory extends Drawable {
2653
+ constructor(__data__, parent, graph, __children__ = new QEArray()) {
2654
+ super(__data__, parent, graph, __children__);
2655
+ this.__data__ = __data__;
2656
+ this.parent = parent;
2657
+ this.graph = graph;
2658
+ this.__children__ = __children__;
2659
+ this.__name__ = "trajectory";
2660
+ this._path_el = null;
2661
+ this._segments_group = null;
2662
+ this._tip_el = null;
2663
+ this._points_group = null;
2664
+ this.svg_element = this.create();
2665
+ }
2666
+ // Trajectory styling is specified in display pixels, while SVG paths use
2667
+ // graph coordinates. Convert at render time so marks stay legible at any scale.
2668
+ graphUnitsForPixels(pixels) {
2669
+ var _a;
2670
+ const extent = this.graph.__data__.extremes.x_max || 1;
2671
+ const width = ((_a = this.graph.svg_element) == null ? void 0 : _a.getBoundingClientRect().width) || 0;
2672
+ return width > 0 ? pixels * (2 * extent / width) : pixels * extent * 6e-3;
2673
+ }
2674
+ create() {
2675
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
2676
+ group.setAttribute("class", "qe-trajectory-group");
2677
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2678
+ path.setAttribute("fill", "none");
2679
+ path.setAttribute("stroke", this.__data__.color.toString());
2680
+ path.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2681
+ group.appendChild(path);
2682
+ this._path_el = path;
2683
+ const segmentsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2684
+ segmentsGroup.setAttribute("class", "qe-trajectory-segments");
2685
+ group.appendChild(segmentsGroup);
2686
+ this._segments_group = segmentsGroup;
2687
+ const pointsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2688
+ pointsGroup.setAttribute("class", "qe-trajectory-points");
2689
+ group.appendChild(pointsGroup);
2690
+ this._points_group = pointsGroup;
2691
+ if (this.__data__.showTips) {
2692
+ const tip = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2693
+ tip.setAttribute("fill", this.__data__.color.toString());
2694
+ tip.setAttribute("r", this.graphUnitsForPixels(4).toString());
2695
+ tip.setAttribute("stroke", "#ffffff");
2696
+ tip.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2697
+ group.appendChild(tip);
2698
+ this._tip_el = tip;
2699
+ }
2700
+ this.svg_element = group;
2701
+ return group;
2702
+ }
2703
+ update() {
2704
+ if (!this.svg_element) {
2705
+ this.create();
2706
+ }
2707
+ if (!this.svg_element.parentElement) {
2708
+ this.parent.appendChild(this.svg_element);
2709
+ }
2710
+ const dStr = this.__data__.path;
2711
+ if (this._path_el) {
2712
+ this._path_el.setAttribute("d", dStr);
2713
+ this._path_el.setAttribute("stroke", this.__data__.color.toString());
2714
+ this._path_el.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2715
+ this._path_el.style.display = this.__data__.lineStyle === "tapered" ? "none" : "block";
2716
+ }
2717
+ if (this._segments_group) {
2718
+ this._segments_group.innerHTML = "";
2719
+ if (this.__data__.lineStyle === "tapered") {
2720
+ const pathPts = this.__data__.pathPoints;
2721
+ const nSegments = pathPts.length - 1;
2722
+ const [minWidth, maxWidth] = this.__data__.strokeWidthRange;
2723
+ const [minOpacity, maxOpacity] = this.__data__.opacityRange;
2724
+ for (let i = 0; i < nSegments; i++) {
2725
+ const start = pathPts[i];
2726
+ const end = pathPts[i + 1];
2727
+ const progress = nSegments <= 1 ? 1 : i / (nSegments - 1);
2728
+ const width = minWidth + (maxWidth - minWidth) * progress;
2729
+ const opacity = minOpacity + (maxOpacity - minOpacity) * progress;
2730
+ const segment = document.createElementNS("http://www.w3.org/2000/svg", "path");
2731
+ segment.setAttribute("d", `M${start.x},${start.y} L${end.x},${end.y}`);
2732
+ segment.setAttribute("fill", "none");
2733
+ segment.setAttribute("stroke", this.__data__.color.toString());
2734
+ segment.setAttribute("stroke-linecap", "round");
2735
+ segment.setAttribute("stroke-linejoin", "round");
2736
+ segment.setAttribute("stroke-width", this.graphUnitsForPixels(width).toString());
2737
+ segment.setAttribute("opacity", opacity.toString());
2738
+ this._segments_group.appendChild(segment);
2739
+ }
2740
+ this._segments_group.style.display = "block";
2741
+ } else {
2742
+ this._segments_group.style.display = "none";
2743
+ }
2744
+ }
2745
+ const pts = this.__data__.activePoints;
2746
+ if (this._points_group) {
2747
+ if (this.__data__.showPoints && pts.length > 0) {
2748
+ this._points_group.innerHTML = "";
2749
+ pts.forEach((pt) => {
2750
+ var _a;
2751
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2752
+ dot.setAttribute("cx", pt.x.toString());
2753
+ dot.setAttribute("cy", pt.y.toString());
2754
+ dot.setAttribute("r", this.graphUnitsForPixels(2.5).toString());
2755
+ dot.setAttribute("fill", this.__data__.color.toString());
2756
+ dot.setAttribute("opacity", "0.7");
2757
+ (_a = this._points_group) == null ? void 0 : _a.appendChild(dot);
2758
+ });
2759
+ this._points_group.style.display = "block";
2760
+ } else {
2761
+ this._points_group.style.display = "none";
2762
+ }
2763
+ }
2764
+ if (this._tip_el && this.__data__.showTips) {
2765
+ if (pts.length > 0) {
2766
+ const lastPt = pts[pts.length - 1];
2767
+ this._tip_el.setAttribute("cx", lastPt.x.toString());
2768
+ this._tip_el.setAttribute("cy", lastPt.y.toString());
2769
+ this._tip_el.setAttribute("fill", this.__data__.color.toString());
2770
+ this._tip_el.setAttribute("r", this.graphUnitsForPixels(4).toString());
2771
+ this._tip_el.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2772
+ this._tip_el.style.display = "block";
2773
+ } else {
2774
+ this._tip_el.style.display = "none";
2775
+ }
2776
+ }
2777
+ }
2778
+ draw() {
2779
+ if (this.svg_element && !this.svg_element.parentElement) {
2780
+ this.parent.appendChild(this.svg_element);
2781
+ }
2782
+ this.update();
2783
+ }
2784
+ };
2785
+ __name(_QETrajectory, "QETrajectory");
2786
+ let QETrajectory = _QETrajectory;
2263
2787
  const { ArrayTools } = Tools;
2264
2788
  const _QEArray = class _QEArray extends Array {
2265
2789
  constructor(...args) {
@@ -2319,7 +2843,8 @@ const QEElementConstructors = {
2319
2843
  "Edge": QELine,
2320
2844
  "TaperedEdge": QELine,
2321
2845
  "TaperedWeightedMidpointEdge": QELine,
2322
- "NodeEdge": QELine
2846
+ "NodeEdge": QELine,
2847
+ "Trajectory": QETrajectory
2323
2848
  };
2324
2849
  const _QELayer = class _QELayer {
2325
2850
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
@@ -2343,8 +2868,14 @@ const _QELayer = class _QELayer {
2343
2868
  (obj) => !this.__children__.some((child) => childId(child) === obj.ID)
2344
2869
  );
2345
2870
  adding.forEach((obj) => {
2346
- const obj_name = obj.constructor.name;
2347
- const ena_element = new QEElementConstructors[obj_name](obj, this.svg_element, this.graph);
2871
+ const obj_name = obj._typeName || obj.constructor.name;
2872
+ let constructor = QEElementConstructors[obj_name];
2873
+ if (!constructor) {
2874
+ if (obj instanceof Trajectory) constructor = QETrajectory;
2875
+ else if (obj instanceof Point) constructor = QEPoint;
2876
+ else constructor = QELine;
2877
+ }
2878
+ const ena_element = new constructor(obj, this.svg_element, this.graph);
2348
2879
  this.__children__.push(ena_element);
2349
2880
  });
2350
2881
  updating.forEach((child) => {
@@ -2423,6 +2954,9 @@ const _QELabel = class _QELabel {
2423
2954
  this.customX = customX;
2424
2955
  this.customY = customY;
2425
2956
  this._visible = false;
2957
+ this._offsetX = 0;
2958
+ this._offsetY = 0;
2959
+ this.pinned = false;
2426
2960
  this.svg_element = this._create();
2427
2961
  if (displayMode === Label.DISPLAY_TYPES.ON) {
2428
2962
  this._visible = true;
@@ -2436,10 +2970,52 @@ const _QELabel = class _QELabel {
2436
2970
  const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
2437
2971
  el.textContent = this.text;
2438
2972
  el.setAttribute("data-label-for", this.point.ID);
2439
- el.setAttribute("pointer-events", "none");
2973
+ el.setAttribute("pointer-events", "auto");
2974
+ el.style.cursor = "grab";
2975
+ this._attachDrag(el);
2440
2976
  this.parent.appendChild(el);
2441
2977
  return el;
2442
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
+ }
2443
3019
  /**
2444
3020
  * Recompute position and font-size from current graph extents.
2445
3021
  * No-ops when the label is hidden (AUTO/CLICK modes while not hovered/clicked).
@@ -2488,6 +3064,9 @@ const _QELabel = class _QELabel {
2488
3064
  }
2489
3065
  this.svg_element.setAttribute("x", x.toString());
2490
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
+ }
2491
3070
  this.svg_element.setAttribute("font-size", fontSize.toString());
2492
3071
  this.svg_element.setAttribute("font-family", "system-ui, sans-serif");
2493
3072
  this.svg_element.setAttribute("font-weight", "500");
@@ -2501,6 +3080,7 @@ const _QELabel = class _QELabel {
2501
3080
  this.update();
2502
3081
  }
2503
3082
  hide() {
3083
+ if (this.pinned) return;
2504
3084
  this._visible = false;
2505
3085
  this.svg_element.setAttribute("display", "none");
2506
3086
  }
@@ -2536,10 +3116,22 @@ const _QEEdgesElement = class _QEEdgesElement extends HTMLElement {
2536
3116
  __name(_QEEdgesElement, "QEEdgesElement");
2537
3117
  _QEEdgesElement.tagName = "qe-edges";
2538
3118
  let QEEdgesElement = _QEEdgesElement;
3119
+ const _QETrajectoriesElement = class _QETrajectoriesElement extends HTMLElement {
3120
+ };
3121
+ __name(_QETrajectoriesElement, "QETrajectoriesElement");
3122
+ _QETrajectoriesElement.tagName = "qe-trajectories";
3123
+ let QETrajectoriesElement = _QETrajectoriesElement;
3124
+ const _QEAxesElement = class _QEAxesElement extends HTMLElement {
3125
+ };
3126
+ __name(_QEAxesElement, "QEAxesElement");
3127
+ _QEAxesElement.tagName = "qe-axes";
3128
+ let QEAxesElement = _QEAxesElement;
2539
3129
  customElements.define(QENodesElement.tagName, QENodesElement);
2540
3130
  customElements.define(QEMeansElement.tagName, QEMeansElement);
2541
3131
  customElements.define(QEPointsElement.tagName, QEPointsElement);
2542
3132
  customElements.define(QEEdgesElement.tagName, QEEdgesElement);
3133
+ customElements.define(QETrajectoriesElement.tagName, QETrajectoriesElement);
3134
+ customElements.define(QEAxesElement.tagName, QEAxesElement);
2543
3135
  const GROUP_PALETTE = [
2544
3136
  "#4477AA",
2545
3137
  "#EE6677",
@@ -2571,6 +3163,14 @@ function computeCIRect(varX, varY, n, pointScale) {
2571
3163
  return { halfW: t95 * seX, halfH: t95 * seY };
2572
3164
  }
2573
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");
2574
3174
  function readIntervalBounds(row, dim0, dim1) {
2575
3175
  const get = /* @__PURE__ */ __name((col) => {
2576
3176
  const i = row.names.indexOf(col);
@@ -2584,6 +3184,11 @@ function readIntervalBounds(row, dim0, dim1) {
2584
3184
  return { x_low: xLow, x_high: xHigh, y_low: yLow, y_high: yHigh };
2585
3185
  }
2586
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");
2587
3192
  function parseLabelMode(attr, defaultMode) {
2588
3193
  switch (attr == null ? void 0 : attr.toLowerCase()) {
2589
3194
  case "on":
@@ -2613,6 +3218,17 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2613
3218
  this._nodeLabels = /* @__PURE__ */ new Map();
2614
3219
  this._meanLabels = /* @__PURE__ */ new Map();
2615
3220
  this._pointLabels = /* @__PURE__ */ new Map();
3221
+ this._showAxes = false;
3222
+ this._axisXText = null;
3223
+ this._axisYText = null;
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;
2616
3232
  this.graph = this;
2617
3233
  this.parent = null;
2618
3234
  this.__data__ = new Graph(DefaultGraphOptions);
@@ -2620,16 +3236,22 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2620
3236
  this.svg_element = this.create();
2621
3237
  console.log("Created ena-graph svg element: ", this.svg_element);
2622
3238
  document.addEventListener("model-updated", (e) => {
2623
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
3239
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2624
3240
  if (this._vizena && e.target !== this._vizena) return;
2625
3241
  const model = e.detail;
2626
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;
2627
3245
  this._hasModel = true;
2628
3246
  const dims = model.dimensions;
2629
3247
  const unitIdCol = model.unit_id_col;
2630
3248
  const nodeIdCol = model.node_id_col;
2631
3249
  const groupColName = model.group_col;
2632
3250
  const config = this._readLayerConfig();
3251
+ this._showAxes = config.showAxes;
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)`;
3254
+ this._axisYText = config.axisYLabel === null ? null : config.axisYLabel || String((dims == null ? void 0 : dims[1]) ?? "y");
2633
3255
  const groupColorMap = {};
2634
3256
  if (groupColName && ((_a = model.points) == null ? void 0 : _a.names)) {
2635
3257
  const groupCol = model.points.col(groupColName);
@@ -2663,7 +3285,9 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2663
3285
  }, "getExtent");
2664
3286
  const maxNodeExtent = ((_c = model.nodes) == null ? void 0 : _c.rows) ? getExtent(model.nodes.rows()) : 0;
2665
3287
  const maxPointExtent = ((_d = model.points) == null ? void 0 : _d.rows) ? getExtent(model.points.rows()) : 0;
2666
- const pointScale = scalePointsEnabled && maxPointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / maxPointExtent : 1;
3288
+ const maxTrajExtent = ((_e = model.trajectories) == null ? void 0 : _e.rows) ? getExtent(model.trajectories.rows()) : 0;
3289
+ const effectivePointExtent = maxPointExtent > 0 ? maxPointExtent : maxTrajExtent;
3290
+ const pointScale = scalePointsEnabled && effectivePointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / effectivePointExtent : 1;
2667
3291
  const labelNodes = config.labelNodes;
2668
3292
  const labelMeans = config.labelMeans;
2669
3293
  const labelPoints = config.labelPoints;
@@ -2686,7 +3310,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2686
3310
  }, "rowToPoint");
2687
3311
  const groupSums = {};
2688
3312
  const showUnitPoints = config.showPoints;
2689
- if ((_e = model.points) == null ? void 0 : _e.rows) {
3313
+ if ((_f = model.points) == null ? void 0 : _f.rows) {
2690
3314
  const pointsLayer = this.__children__.search("__children__", "points");
2691
3315
  if (pointsLayer) {
2692
3316
  model.points.rows().forEach((row) => {
@@ -2695,6 +3319,16 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2695
3319
  const colorStr = group ? groupColorMap[group] ?? "#888888" : "#888888";
2696
3320
  if (showUnitPoints) {
2697
3321
  const pt = rowToPoint(row, unitIdCol, colorStr, pointScale);
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
+ };
2698
3332
  this.__data__.points.set(pt.ID, pt);
2699
3333
  pointsLayer.__data__.objects.set(pt.ID, pt);
2700
3334
  }
@@ -2712,7 +3346,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2712
3346
  groupSums[group].n += 1;
2713
3347
  }
2714
3348
  });
2715
- const hasGroupsFrame = !!(((_f = model.groups) == null ? void 0 : _f.rows) && model.groups.rows().length > 0);
3349
+ const hasGroupsFrame = !!(((_g = model.groups) == null ? void 0 : _g.rows) && model.groups.rows().length > 0);
2716
3350
  const activeGroupRows = config.showMeans && hasGroupsFrame ? model.groups.rows().filter((r) => {
2717
3351
  const g = r.rowName ?? String(r[0]);
2718
3352
  return activeMeanGroups.size === 0 || activeMeanGroups.has(g);
@@ -2744,19 +3378,26 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2744
3378
  const meanPt = new Point({
2745
3379
  ID: `mean-${group}`,
2746
3380
  dimensions: [mx, my],
2747
- meta: { group }
3381
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
2748
3382
  });
2749
3383
  meanPt.color = new Color(color);
2750
3384
  meanPt.size = 4;
2751
3385
  meanPt.shape = "rect";
2752
3386
  this.__data__.points.set(meanPt.ID, meanPt);
2753
3387
  pointsLayer.__data__.objects.set(meanPt.ID, meanPt);
2754
- const bounds = readIntervalBounds(row, dims[0], dims[1]);
2755
- 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 {
2756
3396
  const rect_x = bounds.x_low * pointScale;
2757
3397
  const rect_y = -bounds.y_high * pointScale;
2758
3398
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
2759
3399
  const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
3400
+ capMeanToInterval(meanPt, rect_width / 2, rect_height / 2);
2760
3401
  let el = this._ciRects.get(group);
2761
3402
  if (!el) {
2762
3403
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2782,7 +3423,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2782
3423
  const meanPt = new Point({
2783
3424
  ID: `mean-${group}`,
2784
3425
  dimensions: [x / n * pointScale, -(y / n) * pointScale],
2785
- meta: { group }
3426
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
2786
3427
  });
2787
3428
  meanPt.color = new Color(color);
2788
3429
  meanPt.size = 4;
@@ -2798,7 +3439,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2798
3439
  this._ciRects.delete(g);
2799
3440
  }
2800
3441
  });
2801
- if ((_g = model.confidence) == null ? void 0 : _g.rows) {
3442
+ if ((_h = model.confidence) == null ? void 0 : _h.rows) {
2802
3443
  model.confidence.rows().forEach((row) => {
2803
3444
  const groupIdx = row.names.indexOf(groupColName ?? "group");
2804
3445
  const group = groupIdx >= 0 ? String(row[groupIdx]) : row.rowName ?? null;
@@ -2810,6 +3451,11 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2810
3451
  const rect_y = -bounds.y_high * pointScale;
2811
3452
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
2812
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
+ );
2813
3459
  let el = this._ciRects.get(group);
2814
3460
  if (!el) {
2815
3461
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2837,6 +3483,11 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2837
3483
  const rawVarY = y2 / n - μy * μy;
2838
3484
  const { halfW, halfH } = computeCIRect(rawVarX, rawVarY, n, pointScale);
2839
3485
  const cx = μx * pointScale, cy = -μy * pointScale;
3486
+ capMeanToInterval(
3487
+ this.__data__.points.get(`mean-${group}`),
3488
+ halfW,
3489
+ halfH
3490
+ );
2840
3491
  let el = this._ciRects.get(group);
2841
3492
  if (!el) {
2842
3493
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2858,7 +3509,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2858
3509
  });
2859
3510
  }
2860
3511
  }
2861
- if ((_h = model.outlier) == null ? void 0 : _h.rows) {
3512
+ if ((_i = model.outlier) == null ? void 0 : _i.rows) {
2862
3513
  this._outlierRects.forEach((el, g) => {
2863
3514
  if (!groupColorMap[g]) {
2864
3515
  el.remove();
@@ -2947,20 +3598,74 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2947
3598
  pointsLayer.__data__.__state__.setVariable("layer_updated", pointsLayer.__data__);
2948
3599
  }
2949
3600
  }
3601
+ const trajectoriesLayer = this.__children__.search("__children__", "trajectories");
3602
+ const trajectoriesEnabled = config.showTrajectories;
3603
+ if (trajectoriesEnabled && trajectoriesLayer) {
3604
+ trajectoriesLayer.__data__.objects.clear();
3605
+ this.__data__.trajectories.clear();
3606
+ const trajSource = ((_j = model.trajectories) == null ? void 0 : _j.rows) ? model.trajectories : model.points;
3607
+ if (trajSource == null ? void 0 : trajSource.rows) {
3608
+ const unitRows = {};
3609
+ const selectedUnits = config.trajectoryUnits ? config.trajectoryUnits.split(",").map((s) => s.trim()) : null;
3610
+ trajSource.rows().forEach((row) => {
3611
+ const uIdx = row.names.indexOf(unitIdCol) >= 0 ? row.names.indexOf(unitIdCol) : row.names.indexOf("unit") >= 0 ? row.names.indexOf("unit") : row.names.indexOf("ENA_UNIT") >= 0 ? row.names.indexOf("ENA_UNIT") : -1;
3612
+ const unit = uIdx >= 0 ? String(row[uIdx]) : row.rowName || "unit-1";
3613
+ if (selectedUnits && !selectedUnits.includes(unit)) return;
3614
+ const xIdx = row.names.indexOf(dims[0]);
3615
+ const yIdx = dims.length > 1 ? row.names.indexOf(dims[1]) : -1;
3616
+ const tIdx = row.names.indexOf("time") >= 0 ? row.names.indexOf("time") : row.names.indexOf("ActivityNumber") >= 0 ? row.names.indexOf("ActivityNumber") : row.names.indexOf("RowID") >= 0 ? row.names.indexOf("RowID") : -1;
3617
+ const time = tIdx >= 0 ? Number(row[tIdx]) : unitRows[unit] ? unitRows[unit].length + 1 : 1;
3618
+ if (xIdx >= 0 && yIdx >= 0) {
3619
+ if (!unitRows[unit]) unitRows[unit] = [];
3620
+ unitRows[unit].push({
3621
+ x: Number(row[xIdx]) * pointScale,
3622
+ y: -Number(row[yIdx]) * pointScale,
3623
+ time
3624
+ });
3625
+ }
3626
+ });
3627
+ const unitKeys = Object.keys(unitRows);
3628
+ unitKeys.forEach((unitKey, uIdx) => {
3629
+ const rawPts = unitRows[unitKey];
3630
+ if (rawPts.length === 0) return;
3631
+ const colorStr = GROUP_PALETTE[uIdx % GROUP_PALETTE.length];
3632
+ const trajObj = new Trajectory({
3633
+ ID: `traj-${unitKey}`,
3634
+ unitId: unitKey,
3635
+ color: new Color(colorStr),
3636
+ lineStyle: config.trajectoryLineStyle,
3637
+ strokeWidth: config.trajectoryStrokeWidth,
3638
+ strokeWidthRange: config.trajectoryStrokeWidthRange,
3639
+ opacityRange: config.trajectoryOpacityRange,
3640
+ smooth: config.trajectorySmooth,
3641
+ maxDegree: config.trajectoryMaxDegree,
3642
+ degreeSelection: config.trajectoryDegreeSelection,
3643
+ currentTime: config.trajectoryPlayhead ?? Infinity,
3644
+ showPoints: config.trajectoryShowPoints,
3645
+ showTips: config.trajectoryShowTips,
3646
+ points: rawPts.map((p) => ({ x: p.x, y: p.y, time: p.time }))
3647
+ });
3648
+ this.__data__.trajectories.set(unitKey, trajObj);
3649
+ trajectoriesLayer.__data__.objects.set(trajObj.ID, trajObj);
3650
+ });
3651
+ }
3652
+ trajectoriesLayer.__data__.__state__.setVariable("layer_updated", trajectoriesLayer.__data__);
3653
+ }
2950
3654
  const networkLayer = this.__children__.search("__children__", "network");
2951
3655
  const edgeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edges");
2952
3656
  const nodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "nodes");
2953
3657
  const edgenodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edgenodes");
2954
3658
  const edgesEnabled = config.showEdges;
2955
- if (((_i = model.nodes) == null ? void 0 : _i.rows) && nodeLayer) {
3659
+ if (((_k = model.nodes) == null ? void 0 : _k.rows) && nodeLayer) {
2956
3660
  model.nodes.rows().forEach((row) => {
2957
3661
  const pt = rowToPoint(row, nodeIdCol, "#444444");
2958
3662
  pt.size = 2;
3663
+ pt.meta = { ...pt.meta ?? {}, kind: "node", name: pt.ID };
2959
3664
  nodeLayer.__data__.objects.set(pt.ID, pt);
2960
3665
  });
2961
- if (edgesEnabled && edgeLayer && ((_j = model.edges) == null ? void 0 : _j.rows)) {
3666
+ if (edgesEnabled && edgeLayer && ((_l = model.edges) == null ? void 0 : _l.rows)) {
2962
3667
  const unitGroupMap = {};
2963
- if (groupColName && ((_k = model.points) == null ? void 0 : _k.rows)) {
3668
+ if (groupColName && ((_m = model.points) == null ? void 0 : _m.rows)) {
2964
3669
  model.points.rows().forEach((row) => {
2965
3670
  const qIdx = row.names.indexOf(unitIdCol);
2966
3671
  const gIdx = row.names.indexOf(groupColName);
@@ -3054,21 +3759,46 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3054
3759
  });
3055
3760
  }
3056
3761
  const isDirected = !!model.directed;
3057
- const nonSelfWeights = drawEdges.filter((e2) => e2.pair[0] !== e2.pair[1]).map((e2) => e2.weight);
3058
- const maxEdgeWeight = nonSelfWeights.length > 0 ? Math.max(...nonSelfWeights) : 1e-9;
3059
- const selfWeights = drawEdges.filter((e2) => e2.pair[0] === e2.pair[1]).map((e2) => e2.weight);
3060
- 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");
3061
3788
  const nodeWeightSum = {};
3062
3789
  drawEdges.forEach(({ pair, weight }) => {
3063
3790
  const isSelf = pair[0] === pair[1];
3064
3791
  if (isSelf && !isDirected) return;
3065
- nodeWeightSum[pair[0]] = (nodeWeightSum[pair[0]] ?? 0) + weight;
3066
- if (!isSelf) {
3067
- 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;
3068
3798
  }
3799
+ add(pair[0]);
3800
+ add(pair[1]);
3069
3801
  });
3070
- const maxNodeWeight = Math.max(1, ...Object.values(nodeWeightSum));
3071
- const nodeSizes = {};
3072
3802
  model.nodes.rows().forEach((row) => {
3073
3803
  const codeIdx = row.names.indexOf(nodeIdCol);
3074
3804
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
@@ -3076,8 +3806,12 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3076
3806
  const pt = nodeLayer.__data__.objects.get(id);
3077
3807
  if (!pt) return;
3078
3808
  const w = nodeWeightSum[id] ?? 0;
3079
- pt.size = 2 + w / maxNodeWeight * 3;
3080
- 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
+ };
3081
3815
  });
3082
3816
  const totalWeightByPair = {};
3083
3817
  if (isDirected) {
@@ -3086,15 +3820,13 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3086
3820
  totalWeightByPair[key] = (totalWeightByPair[key] ?? 0) + weight;
3087
3821
  });
3088
3822
  }
3089
- ((_l = this.__data__.extremes) == null ? void 0 : _l.x_max) ?? 1;
3090
3823
  drawEdges.forEach(({ col, pair, weight, color }) => {
3091
3824
  const fromNode = nodeLayer.__data__.objects.get(pair[0]);
3092
3825
  const toNode = nodeLayer.__data__.objects.get(pair[1]);
3093
3826
  if (!fromNode || !toNode) return;
3094
3827
  const isSelf = pair[0] === pair[1];
3095
3828
  if (isSelf && !isDirected) return;
3096
- const classMax = isSelf ? Math.max(maxSelfWeight, 1e-9) : maxEdgeWeight;
3097
- const opacity = Math.min(1, weight / classMax);
3829
+ const { saturation, opacity } = colourOf(weight, isSelf);
3098
3830
  const fromPt = new Point({ ID: `${pair[0]}-src`, dimensions: [fromNode.x, fromNode.y], meta: {} });
3099
3831
  const toPt = new Point({ ID: `${pair[1]}-dst`, dimensions: [toNode.x, toNode.y], meta: {} });
3100
3832
  let edgeLine;
@@ -3118,16 +3850,17 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3118
3850
  edgeLine.meta = {
3119
3851
  isMeanEdge: true,
3120
3852
  weight,
3121
- // NodeEdge compares within the self-loop class; cross-pair edges
3122
- // compare within their own class. Store the correct denominator
3123
- // so QELine.update() can compute opacity and display size properly.
3124
- maxWeight: isSelf ? maxSelfWeight : maxEdgeWeight,
3853
+ // Width per unit weight (extent units), shared by all marks.
3854
+ widthFrac,
3855
+ saturation,
3125
3856
  opacity,
3126
3857
  isDirected,
3127
3858
  isSelf,
3128
- // nodeSize is needed by QELine.update() to compute the NodeEdge
3129
- // display radius at update time (when extremes.x_max is available).
3130
- 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
3131
3864
  };
3132
3865
  const targetLayer = isSelf ? edgenodeLayer ?? edgeLayer : edgeLayer;
3133
3866
  targetLayer.__data__.objects.set(edgeLine.ID, edgeLine);
@@ -3135,7 +3868,15 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3135
3868
  edgeLayer.__data__.__state__.setVariable("layer_updated", edgeLayer.__data__);
3136
3869
  if (edgenodeLayer) edgenodeLayer.__data__.__state__.setVariable("layer_updated", edgenodeLayer.__data__);
3137
3870
  }
3138
- 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) {
3139
3880
  this._nodeLabels.forEach((lbl, id) => {
3140
3881
  if (!nodeLayer.__data__.objects.get(id)) {
3141
3882
  lbl.remove();
@@ -3143,11 +3884,17 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3143
3884
  }
3144
3885
  });
3145
3886
  model.nodes.rows().forEach((row) => {
3887
+ var _a2, _b2;
3146
3888
  const codeIdx = row.names.indexOf(nodeIdCol);
3147
3889
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
3148
3890
  if (!id) return;
3149
3891
  const pt = nodeLayer.__data__.objects.get(id);
3150
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
+ }
3151
3898
  let lbl = this._nodeLabels.get(id);
3152
3899
  if (!lbl) {
3153
3900
  lbl = new QELabel(id, pt, labelSvg, this, labelNodes);
@@ -3164,6 +3911,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3164
3911
  if (axisPointsLayer) this.__data__.__state__.setVariable("graph_updated_axis_points", axisPointsLayer.__data__.objects);
3165
3912
  if (axisLinesLayer) this.__data__.__state__.setVariable("graph_updated_axis_lines", axisLinesLayer.__data__.objects);
3166
3913
  this.update();
3914
+ if (tweenFrom) this._runTween(tweenFrom);
3167
3915
  });
3168
3916
  }
3169
3917
  static get observedAttributes() {
@@ -3204,17 +3952,27 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3204
3952
  // should render. Called once at the top of every model-updated handler so
3205
3953
  // all rendering decisions are derived from a single snapshot.
3206
3954
  _readLayerConfig() {
3955
+ var _a;
3207
3956
  const nodesEl = this.querySelector("qe-nodes");
3208
3957
  const meansEl = this.querySelector("qe-means");
3209
3958
  const pointsEl = this.querySelector("qe-points");
3210
3959
  const edgesEl = this.querySelector("qe-edges");
3960
+ const trajectoriesEl = this.querySelector("qe-trajectories");
3961
+ const axesEl = this.querySelector("qe-axes");
3211
3962
  const labelsShorthand = this.getAttribute("labels");
3212
3963
  return {
3213
- // 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.
3214
3966
  labelNodes: parseLabelMode(
3215
3967
  (nodesEl == null ? void 0 : nodesEl.getAttribute("label")) ?? this.getAttribute("label-nodes") ?? labelsShorthand,
3216
3968
  Label.DISPLAY_TYPES.ON
3217
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",
3218
3976
  scalePoints: this.getAttribute("scale-points") !== "false",
3219
3977
  // Means — presence of <qe-means> = show; absence = hide.
3220
3978
  showMeans: !!meansEl,
@@ -3224,20 +3982,56 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3224
3982
  (meansEl == null ? void 0 : meansEl.getAttribute("label")) ?? this.getAttribute("label-means") ?? labelsShorthand,
3225
3983
  Label.DISPLAY_TYPES.ON
3226
3984
  ),
3985
+ // fixed-size keeps mean markers a constant on-screen px size (webENA parity).
3986
+ meanFixedSize: !!meansEl && meansEl.hasAttribute("fixed-size"),
3227
3987
  // Points — presence of <qe-points> = show unit dots.
3228
3988
  showPoints: !!pointsEl,
3229
3989
  labelPoints: parseLabelMode(
3230
3990
  (pointsEl == null ? void 0 : pointsEl.getAttribute("label")) ?? this.getAttribute("label-points") ?? labelsShorthand,
3231
3991
  Label.DISPLAY_TYPES.OFF
3232
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"),
3233
3996
  // Edges — presence of <qe-edges> = draw a network.
3234
3997
  showEdges: !!edgesEl,
3235
3998
  edgeGroup: (edgesEl == null ? void 0 : edgesEl.getAttribute("group")) ?? null,
3236
3999
  edgeUnit: (edgesEl == null ? void 0 : edgesEl.getAttribute("unit")) ?? null,
3237
4000
  edgeCompare: (edgesEl == null ? void 0 : edgesEl.getAttribute("compare")) ?? null,
3238
- edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null
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,
4007
+ // Trajectories — presence of <qe-trajectories> = draw cognitive trajectories
4008
+ showTrajectories: !!trajectoriesEl,
4009
+ trajectoryUnits: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("units")) ?? null,
4010
+ trajectorySmooth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("smooth")) ?? "none",
4011
+ trajectoryMaxDegree: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("max-degree")) ? parseInt(trajectoriesEl.getAttribute("max-degree"), 10) : 3,
4012
+ trajectoryDegreeSelection: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("degree-selection")) ?? "loocv_2d",
4013
+ trajectoryShowPoints: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("points")) !== "false",
4014
+ trajectoryShowTips: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("tips")) !== "false",
4015
+ trajectoryPlayhead: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("playhead")) ? parseFloat(trajectoriesEl.getAttribute("playhead")) : null,
4016
+ trajectoryLineStyle: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("line-style")) ?? "uniform",
4017
+ trajectoryStrokeWidth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width")) ? parseFloat(trajectoriesEl.getAttribute("stroke-width")) : 2,
4018
+ trajectoryStrokeWidthRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width-range"), [1, 6]),
4019
+ trajectoryOpacityRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("opacity-range"), [0.3, 1]),
4020
+ // Axes — presence of <qe-axes> = draw axis lines. The x / y attributes add
4021
+ // a per-axis label: a value is used verbatim, an empty attribute falls back
4022
+ // to the dimension name (resolved later, where dims are known). null = no
4023
+ // label for that axis.
4024
+ showAxes: !!axesEl,
4025
+ axisXLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("x")) ? axesEl.getAttribute("x") ?? "" : null,
4026
+ axisYLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("y")) ? axesEl.getAttribute("y") ?? "" : null
3239
4027
  };
3240
4028
  }
4029
+ _parseNumberPair(value, fallback) {
4030
+ if (!value) return fallback;
4031
+ const parts = value.split(",").map((part) => Number(part.trim()));
4032
+ if (parts.length < 2 || !parts.every(Number.isFinite)) return fallback;
4033
+ return [parts[0], parts[1]];
4034
+ }
3241
4035
  attributeChangedCallback(_name2, oldValue, newValue) {
3242
4036
  if (oldValue === newValue) return;
3243
4037
  this._scheduleRerender();
@@ -3246,9 +4040,11 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3246
4040
  var _a;
3247
4041
  const w = this.getAttribute("width");
3248
4042
  const h = this.getAttribute("height");
4043
+ this.style.display = "block";
3249
4044
  if (w) this.style.width = isNaN(Number(w)) ? w : `${w}px`;
3250
4045
  if (h) this.style.height = isNaN(Number(h)) ? h : `${h}px`;
3251
4046
  this.appendChild(this.svg_element);
4047
+ _QEGraphElement._instances.add(this);
3252
4048
  this._childObserver = new MutationObserver(() => this._scheduleRerender());
3253
4049
  this._childObserver.observe(this, {
3254
4050
  childList: true,
@@ -3259,14 +4055,32 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3259
4055
  "unit",
3260
4056
  "compare",
3261
4057
  "also",
4058
+ "color-scale",
4059
+ "magnify",
3262
4060
  "groups",
3263
4061
  "confidence",
3264
4062
  "label",
4063
+ "unconnected",
3265
4064
  "scale-points",
4065
+ "edge-width",
3266
4066
  "labels",
3267
4067
  "label-nodes",
3268
4068
  "label-means",
3269
- "label-points"
4069
+ "label-points",
4070
+ "units",
4071
+ "smooth",
4072
+ "degree",
4073
+ "points",
4074
+ "tips",
4075
+ "playhead",
4076
+ "line-style",
4077
+ "stroke-width",
4078
+ "stroke-width-range",
4079
+ "opacity-range",
4080
+ "x",
4081
+ "y",
4082
+ "size",
4083
+ "fixed-size"
3270
4084
  ]
3271
4085
  });
3272
4086
  const forId = this.getAttribute("for");
@@ -3286,13 +4100,283 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3286
4100
  }
3287
4101
  }
3288
4102
  disconnectedCallback() {
3289
- var _a;
4103
+ var _a, _b;
3290
4104
  (_a = this._childObserver) == null ? void 0 : _a.disconnect();
3291
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
+ });
3292
4372
  }
3293
4373
  create() {
3294
4374
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3295
4375
  svg.classList.add("graph");
4376
+ svg.style.display = "block";
4377
+ svg.style.width = "100%";
4378
+ svg.style.height = "100%";
4379
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
3296
4380
  svg.setAttribute("viewBox", this.__data__.extremes.as_viewBox());
3297
4381
  svg.setAttribute("transform", this.__data__.transform.toString());
3298
4382
  svg.setAttribute("role", "figure");
@@ -3402,6 +4486,138 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3402
4486
  const lbl = getAutoLabel(id);
3403
4487
  if ((lbl == null ? void 0 : lbl.displayMode) === Label.DISPLAY_TYPES.AUTO) lbl.hide();
3404
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";
3405
4621
  }
3406
4622
  draw() {
3407
4623
  var _a;
@@ -3419,6 +4635,78 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3419
4635
  this.update_layers();
3420
4636
  this.update_labels();
3421
4637
  this.update_extremes();
4638
+ this.update_axes();
4639
+ this._applyHighlights();
4640
+ }
4641
+ /**
4642
+ * Axes (<qe-axes>): toggle the axis-line cross by presence, and draw/position
4643
+ * per-axis labels at the positive ends using the current extent. Labels are
4644
+ * cached SVG <text> nodes in the labels group; null text removes them.
4645
+ */
4646
+ update_axes() {
4647
+ var _a;
4648
+ const axesLayer = this.__children__.search("__children__", "axes");
4649
+ if (axesLayer == null ? void 0 : axesLayer.svg_element) {
4650
+ axesLayer.svg_element.style.display = this._showAxes ? "" : "none";
4651
+ }
4652
+ const parent = this._labelsGroup;
4653
+ const extent = this.__data__.extremes.x_max;
4654
+ if (!parent || extent <= 0) return;
4655
+ const fontSize = extent * 0.06;
4656
+ const gap = fontSize * 0.6;
4657
+ const place = /* @__PURE__ */ __name((axis, text) => {
4658
+ let el = this._axisLabelEls.get(axis);
4659
+ if (!this._showAxes || text == null) {
4660
+ if (el) {
4661
+ el.remove();
4662
+ this._axisLabelEls.delete(axis);
4663
+ }
4664
+ return;
4665
+ }
4666
+ if (!el) {
4667
+ el = document.createElementNS("http://www.w3.org/2000/svg", "text");
4668
+ el.setAttribute("data-axis-label", axis);
4669
+ el.setAttribute("pointer-events", "none");
4670
+ parent.appendChild(el);
4671
+ this._axisLabelEls.set(axis, el);
4672
+ }
4673
+ el.textContent = text;
4674
+ el.setAttribute("font-size", fontSize.toString());
4675
+ el.setAttribute("font-family", "system-ui, sans-serif");
4676
+ el.setAttribute("font-weight", "600");
4677
+ el.setAttribute("fill", "#556");
4678
+ if (axis === "x") {
4679
+ el.setAttribute("x", (extent - gap).toString());
4680
+ el.setAttribute("y", (-gap).toString());
4681
+ el.setAttribute("text-anchor", "end");
4682
+ } else {
4683
+ el.setAttribute("x", gap.toString());
4684
+ el.setAttribute("y", (-extent + fontSize + gap).toString());
4685
+ el.setAttribute("text-anchor", "start");
4686
+ }
4687
+ }, "place");
4688
+ place("x", this._axisXText);
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
+ }
3422
4710
  }
3423
4711
  update_labels() {
3424
4712
  this._nodeLabels.forEach((lbl) => lbl.update());
@@ -3482,8 +4770,12 @@ _QEGraphElement.style = `
3482
4770
  background-color: #FFF;
3483
4771
  }
3484
4772
  `;
4773
+ _QEGraphElement._instances = /* @__PURE__ */ new Set();
4774
+ _QEGraphElement._PATH_NUM_RE = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
3485
4775
  let QEGraphElement = _QEGraphElement;
3486
4776
  customElements.define(QEGraphElement.tagName, QEGraphElement);
4777
+ const getGraph = /* @__PURE__ */ __name((key) => QEGraphElement.getGraph(key), "getGraph");
4778
+ const getGraphs = /* @__PURE__ */ __name(() => QEGraphElement.instances(), "getGraphs");
3487
4779
  export {
3488
4780
  Color,
3489
4781
  DataFrame,
@@ -3492,6 +4784,7 @@ export {
3492
4784
  DataFrameRow,
3493
4785
  Edge,
3494
4786
  Graph,
4787
+ QEAxesElement,
3495
4788
  QEEdgesElement,
3496
4789
  QEGraphElement,
3497
4790
  QELayer,
@@ -3500,8 +4793,18 @@ export {
3500
4793
  QENodesElement,
3501
4794
  QEPoint,
3502
4795
  QEPointsElement,
4796
+ QETrajectoriesElement,
4797
+ QETrajectory,
3503
4798
  QEVisualElement,
4799
+ TRAJECTORY_ZERO_EPS,
4800
+ Trajectory,
3504
4801
  WeightedColor,
4802
+ fitTrajectory,
4803
+ getGraph,
4804
+ getGraphs,
4805
+ nearOriginFilter,
4806
+ sampleCurve,
4807
+ selectDegree,
3505
4808
  vizENA
3506
4809
  };
3507
4810
  //# sourceMappingURL=qeviz.es.js.map