@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.umd.js CHANGED
@@ -164,7 +164,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
164
164
  }
165
165
  });
166
166
  __privateSet(this, _names2, names);
167
- if (rowNames && rowNames.length !== this[0].length) {
167
+ if (rowNames && this[0] && rowNames.length !== this[0].length) {
168
168
  throw new Error("Row names length must match the number of rows in the DataFrame.");
169
169
  }
170
170
  if (rowNames && rowNames.length > 0) {
@@ -228,6 +228,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
228
228
  rows(...wh) {
229
229
  let selectors, selectedRows = [], selectedRowNames = [];
230
230
  if (wh.length === 0) {
231
+ if (!this[0] || this.length === 0) {
232
+ return [];
233
+ }
231
234
  const numRows = this[0].length;
232
235
  selectedRows = Array.from({ length: numRows }, (_, i) => this.getRowByIndex(i));
233
236
  selectedRowNames = __privateGet(this, _rowNames2);
@@ -286,7 +289,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
286
289
  return __privateGet(this, _rowNames2);
287
290
  }
288
291
  get nrow() {
289
- return this[0].length;
292
+ return this[0] ? this[0].length : 0;
290
293
  }
291
294
  get ncol() {
292
295
  return this.length;
@@ -353,6 +356,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
353
356
  set groups(v) {
354
357
  this._groups = v;
355
358
  }
359
+ get trajectories() {
360
+ return this._trajectories;
361
+ }
362
+ set trajectories(v) {
363
+ this._trajectories = v;
364
+ }
356
365
  get dimensions() {
357
366
  return this._dimensions;
358
367
  }
@@ -465,7 +474,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
465
474
  */
466
475
  setModelData(data) {
467
476
  var _a, _b, _c, _d;
468
- const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier"];
477
+ const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier", "trajectories"];
469
478
  frameKeys.forEach((name) => {
470
479
  var _a2;
471
480
  const raw = data[name];
@@ -474,7 +483,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
474
483
  let idCol;
475
484
  if (name === "nodes") {
476
485
  idCol = data.node_id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
477
- } else if (name === "edges" || name === "points") {
486
+ } else if (name === "edges" || name === "points" || name === "trajectories") {
478
487
  idCol = data.id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
479
488
  } else {
480
489
  idCol = data.group_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
@@ -544,8 +553,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
544
553
  __name(_QEVisualElement, "QEVisualElement");
545
554
  _QEVisualElement.tagName = "qe-visual";
546
555
  _QEVisualElement.styles = `
547
- :host { display: inline-block; }
556
+ :host { display: block; width: 100%; }
548
557
  :host(.hidden) { display: none !important; }
558
+ /* The output wrapper and the slotted graph fill the host so the plot
559
+ stretches to its container instead of collapsing to intrinsic size. */
560
+ div { display: block; width: 100%; height: 100%; }
561
+ ::slotted(qe-graph) { display: block; width: 100%; height: 100%; }
549
562
  `;
550
563
  let QEVisualElement = _QEVisualElement;
551
564
  customElements.define(QEVisualElement.tagName, QEVisualElement);
@@ -586,9 +599,10 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
586
599
  __name(_Color, "Color");
587
600
  let Color = _Color;
588
601
  const _WeightedColor = class _WeightedColor extends Color {
589
- constructor(base, weight) {
602
+ constructor(base, weight, alpha) {
590
603
  super(base);
591
604
  this.weight = weight;
605
+ this.alpha = alpha;
592
606
  }
593
607
  // Parse hex → HSV (h in [0, 360), s/v in [0, 1]).
594
608
  static toHSV(hex) {
@@ -659,7 +673,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
659
673
  toString() {
660
674
  const { h, v } = _WeightedColor.toHSV(this.base);
661
675
  const w = Math.min(1, Math.abs(this.weight));
662
- return _WeightedColor.hsvToRgba(h, w, v, w);
676
+ const a = this.alpha === void 0 ? w : Math.min(1, Math.max(0, this.alpha));
677
+ return _WeightedColor.hsvToRgba(h, w, v, a);
663
678
  }
664
679
  };
665
680
  __name(_WeightedColor, "WeightedColor");
@@ -816,7 +831,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
816
831
  __name(slopeIntercept, "slopeIntercept");
817
832
  Line2.slopeIntercept = slopeIntercept;
818
833
  function point_along(line, distance = 0) {
819
- 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];
834
+ const ratio = distance;
835
+ const Xt = (1 - ratio) * line[0][0] + ratio * line[1][0];
836
+ const Yt = (1 - ratio) * line[0][1] + ratio * line[1][1];
820
837
  return [Xt, Yt];
821
838
  }
822
839
  __name(point_along, "point_along");
@@ -1663,6 +1680,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
1663
1680
  this.points = /* @__PURE__ */ new Map();
1664
1681
  this.nodes = /* @__PURE__ */ new Map();
1665
1682
  this.edges = /* @__PURE__ */ new Map();
1683
+ this.trajectories = /* @__PURE__ */ new Map();
1666
1684
  this.labels = /* @__PURE__ */ new Map();
1667
1685
  this.transform = new Transform();
1668
1686
  this.scale_x = 1;
@@ -1705,6 +1723,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
1705
1723
  { name: "edgenodes", layers: [], tabindex: 0 }
1706
1724
  ]
1707
1725
  },
1726
+ { name: "trajectories", layers: [], tabindex: 0 },
1708
1727
  { name: "points", layers: [], tabindex: 0 },
1709
1728
  { name: "labels", layers: [], tabindex: 0 }
1710
1729
  ] }
@@ -1830,9 +1849,13 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
1830
1849
  if (search_layers.length === 0) {
1831
1850
  this.extremes = new Extremes(1, 1);
1832
1851
  } else {
1833
- 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;
1834
- console.log("Found max for extremes:", found_max);
1835
- this.extremes = new Extremes(Math.ceil(found_max), Math.ceil(found_max));
1852
+ const content_max = Math.max(
1853
+ ...search_layers.map((l) => Math.max(
1854
+ ...[...l.objects.values()].map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))
1855
+ )).filter((a) => Math.abs(a) !== Infinity)
1856
+ );
1857
+ const found_max = (Number.isFinite(content_max) && content_max > 0 ? content_max : 1) * 1.3;
1858
+ this.extremes = new Extremes(found_max, found_max);
1836
1859
  }
1837
1860
  }
1838
1861
  base_point_size(scale = 8) {
@@ -1872,6 +1895,251 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
1872
1895
  }
1873
1896
  `;
1874
1897
  let DrawableElement = _DrawableElement;
1898
+ const TRAJECTORY_ZERO_EPS = 1e-8;
1899
+ function nearOriginFilter(pts, eps = TRAJECTORY_ZERO_EPS) {
1900
+ return pts.filter((p) => Math.abs(p.x) > eps || Math.abs(p.y) > eps);
1901
+ }
1902
+ __name(nearOriginFilter, "nearOriginFilter");
1903
+ function normalizedT(n) {
1904
+ if (n <= 0) return [];
1905
+ if (n === 1) return [0];
1906
+ const t = new Array(n);
1907
+ for (let i = 0; i < n; i++) t[i] = i / (n - 1);
1908
+ return t;
1909
+ }
1910
+ __name(normalizedT, "normalizedT");
1911
+ function design(t, degree) {
1912
+ return t.map((ti) => {
1913
+ const row = new Array(degree + 1);
1914
+ let p = 1;
1915
+ for (let k = 0; k <= degree; k++) {
1916
+ row[k] = p;
1917
+ p *= ti;
1918
+ }
1919
+ return row;
1920
+ });
1921
+ }
1922
+ __name(design, "design");
1923
+ function qr(A) {
1924
+ const n = A.length;
1925
+ const m = A[0].length;
1926
+ const Q = A.map((row) => row.slice());
1927
+ const R = Array.from({ length: m }, () => new Array(m).fill(0));
1928
+ for (let j = 0; j < m; j++) {
1929
+ let norm = 0;
1930
+ for (let i = 0; i < n; i++) norm += Q[i][j] * Q[i][j];
1931
+ norm = Math.sqrt(norm);
1932
+ R[j][j] = norm;
1933
+ if (norm > 1e-300) {
1934
+ for (let i = 0; i < n; i++) Q[i][j] /= norm;
1935
+ }
1936
+ for (let k = j + 1; k < m; k++) {
1937
+ let dot = 0;
1938
+ for (let i = 0; i < n; i++) dot += Q[i][j] * Q[i][k];
1939
+ R[j][k] = dot;
1940
+ for (let i = 0; i < n; i++) Q[i][k] -= dot * Q[i][j];
1941
+ }
1942
+ }
1943
+ return { Q, R };
1944
+ }
1945
+ __name(qr, "qr");
1946
+ function qtY(Q, y) {
1947
+ const n = Q.length;
1948
+ const m = Q[0].length;
1949
+ const out = new Array(m).fill(0);
1950
+ for (let j = 0; j < m; j++) {
1951
+ let s = 0;
1952
+ for (let i = 0; i < n; i++) s += Q[i][j] * y[i];
1953
+ out[j] = s;
1954
+ }
1955
+ return out;
1956
+ }
1957
+ __name(qtY, "qtY");
1958
+ function backSolve(R, b) {
1959
+ const m = b.length;
1960
+ const c = new Array(m).fill(0);
1961
+ for (let i = m - 1; i >= 0; i--) {
1962
+ let s = b[i];
1963
+ for (let j = i + 1; j < m; j++) s -= R[i][j] * c[j];
1964
+ c[i] = Math.abs(R[i][i]) > 1e-12 ? s / R[i][i] : 0;
1965
+ }
1966
+ return c;
1967
+ }
1968
+ __name(backSolve, "backSolve");
1969
+ function leverages(Q) {
1970
+ return Q.map((row) => {
1971
+ let s = 0;
1972
+ for (let j = 0; j < row.length; j++) s += row[j] * row[j];
1973
+ return s;
1974
+ });
1975
+ }
1976
+ __name(leverages, "leverages");
1977
+ function fitCoeffs(t, y, degree) {
1978
+ const X = design(t, degree);
1979
+ const { Q, R } = qr(X);
1980
+ return backSolve(R, qtY(Q, y));
1981
+ }
1982
+ __name(fitCoeffs, "fitCoeffs");
1983
+ function evalPoly(coeffs, t) {
1984
+ let v = 0;
1985
+ let p = 1;
1986
+ for (let k = 0; k < coeffs.length; k++) {
1987
+ v += coeffs[k] * p;
1988
+ p *= t;
1989
+ }
1990
+ return v;
1991
+ }
1992
+ __name(evalPoly, "evalPoly");
1993
+ function loocv2d(t, x, y, degree) {
1994
+ const X = design(t, degree);
1995
+ const { Q, R } = qr(X);
1996
+ const h = leverages(Q);
1997
+ const cx = backSolve(R, qtY(Q, x));
1998
+ const cy = backSolve(R, qtY(Q, y));
1999
+ let press = 0;
2000
+ for (let i = 0; i < t.length; i++) {
2001
+ const denom = 1 - h[i];
2002
+ if (denom <= 0) return Infinity;
2003
+ const ex = x[i] - evalPoly(cx, t[i]);
2004
+ const ey = y[i] - evalPoly(cy, t[i]);
2005
+ press += (ex / denom) ** 2 + (ey / denom) ** 2;
2006
+ }
2007
+ return press;
2008
+ }
2009
+ __name(loocv2d, "loocv2d");
2010
+ function selectDegree(fitPts, maxDegree, mode) {
2011
+ const n = fitPts.length;
2012
+ const cap = Math.min(Math.max(1, Math.floor(maxDegree)), Math.max(1, n - 1));
2013
+ if (mode === "fixed" || n < 3) return cap;
2014
+ const t = normalizedT(n);
2015
+ const x = fitPts.map((p) => p.x);
2016
+ const y = fitPts.map((p) => p.y);
2017
+ let best = 1;
2018
+ let bestVal = Infinity;
2019
+ for (let deg = 1; deg <= cap; deg++) {
2020
+ const v = loocv2d(t, x, y, deg);
2021
+ if (v < bestVal) {
2022
+ bestVal = v;
2023
+ best = deg;
2024
+ }
2025
+ }
2026
+ return best;
2027
+ }
2028
+ __name(selectDegree, "selectDegree");
2029
+ function fitTrajectory(activePts, opts) {
2030
+ const fitPts = nearOriginFilter(activePts);
2031
+ if (fitPts.length < 3) return null;
2032
+ const selBasis = opts.selectionSet ? nearOriginFilter(opts.selectionSet) : fitPts;
2033
+ const basis = selBasis.length >= 3 ? selBasis : fitPts;
2034
+ let degree = selectDegree(basis, opts.maxDegree, opts.degreeSelection);
2035
+ degree = Math.min(degree, Math.max(1, fitPts.length - 1));
2036
+ const t = normalizedT(fitPts.length);
2037
+ const coeffsX = fitCoeffs(t, fitPts.map((p) => p.x), degree);
2038
+ const coeffsY = fitCoeffs(t, fitPts.map((p) => p.y), degree);
2039
+ return { degree, coeffsX, coeffsY };
2040
+ }
2041
+ __name(fitTrajectory, "fitTrajectory");
2042
+ function sampleCurve(fit, steps = 100) {
2043
+ const out = [];
2044
+ for (let s = 0; s <= steps; s++) {
2045
+ const t = s / steps;
2046
+ out.push({ x: evalPoly(fit.coeffsX, t), y: evalPoly(fit.coeffsY, t) });
2047
+ }
2048
+ return out;
2049
+ }
2050
+ __name(sampleCurve, "sampleCurve");
2051
+ const TRAJECTORY_CURVE_STEPS = 100;
2052
+ const _Trajectory = class _Trajectory extends PlottedObject {
2053
+ constructor(options = {}) {
2054
+ const defaultColor = options.color instanceof Color ? options.color : new Color(typeof options.color === "string" ? options.color : "#000000");
2055
+ super([], {
2056
+ ID: options.ID || `traj-${Math.random().toString(36).substring(2, 9)}`,
2057
+ size: options.size || 1,
2058
+ color: defaultColor,
2059
+ highlight: null,
2060
+ active: options.active || false,
2061
+ meta: options.meta || {},
2062
+ ...options
2063
+ });
2064
+ this._typeName = "Trajectory";
2065
+ this.trajPoints = [];
2066
+ this.smooth = "none";
2067
+ this.maxDegree = 3;
2068
+ this.degreeSelection = "loocv_2d";
2069
+ this.selectedDegree = -1;
2070
+ this.polyCoeffsX = [];
2071
+ this.polyCoeffsY = [];
2072
+ this.currentTime = Infinity;
2073
+ this.showPoints = true;
2074
+ this.showTips = true;
2075
+ this.lineStyle = "uniform";
2076
+ this.strokeWidth = 2;
2077
+ this.strokeWidthRange = [1, 6];
2078
+ this.opacityRange = [0.3, 1];
2079
+ this.smooth = options.smooth || "none";
2080
+ this.maxDegree = options.maxDegree !== void 0 ? options.maxDegree : 3;
2081
+ this.degreeSelection = options.degreeSelection || "loocv_2d";
2082
+ this.polyCoeffsX = options.polyCoeffsX || [];
2083
+ this.polyCoeffsY = options.polyCoeffsY || [];
2084
+ this.currentTime = options.currentTime !== void 0 ? options.currentTime : Infinity;
2085
+ this.showPoints = options.showPoints !== void 0 ? options.showPoints : true;
2086
+ this.showTips = options.showTips !== void 0 ? options.showTips : true;
2087
+ this.lineStyle = options.lineStyle || "uniform";
2088
+ this.strokeWidth = options.strokeWidth || 2;
2089
+ this.strokeWidthRange = options.strokeWidthRange || [1, 6];
2090
+ this.opacityRange = options.opacityRange || [0.3, 1];
2091
+ if (options.points && Array.isArray(options.points)) {
2092
+ this.trajPoints = options.points.map((p) => {
2093
+ if (p instanceof Point) return p;
2094
+ const pt = new Point({ dimensions: [p.x, p.y], meta: { time: p.time, label: p.label } });
2095
+ pt.x = p.x;
2096
+ pt.y = p.y;
2097
+ return pt;
2098
+ });
2099
+ }
2100
+ }
2101
+ get activePoints() {
2102
+ if (this.currentTime === Infinity) {
2103
+ return this.trajPoints;
2104
+ }
2105
+ return this.trajPoints.filter((p) => {
2106
+ const t = p.meta && typeof p.meta.time === "number" ? p.meta.time : 0;
2107
+ return t <= this.currentTime;
2108
+ });
2109
+ }
2110
+ get path() {
2111
+ return this.pathPoints.reduce((acc, pt, idx) => {
2112
+ return idx === 0 ? `M${pt.x},${pt.y}` : `${acc} L${pt.x},${pt.y}`;
2113
+ }, "");
2114
+ }
2115
+ get pathPoints() {
2116
+ const pts = this.activePoints;
2117
+ if (pts.length < 2) return [];
2118
+ if (this.smooth === "poly") {
2119
+ if (this.polyCoeffsX.length > 0 && this.polyCoeffsY.length > 0) {
2120
+ return sampleCurve(
2121
+ { degree: this.polyCoeffsX.length - 1, coeffsX: this.polyCoeffsX, coeffsY: this.polyCoeffsY },
2122
+ TRAJECTORY_CURVE_STEPS
2123
+ );
2124
+ }
2125
+ const fit = fitTrajectory(
2126
+ pts.map((p) => ({ x: p.x, y: p.y })),
2127
+ {
2128
+ maxDegree: this.maxDegree,
2129
+ degreeSelection: this.degreeSelection,
2130
+ selectionSet: this.trajPoints.map((p) => ({ x: p.x, y: p.y }))
2131
+ }
2132
+ );
2133
+ if (fit) {
2134
+ this.selectedDegree = fit.degree;
2135
+ return sampleCurve(fit, TRAJECTORY_CURVE_STEPS);
2136
+ }
2137
+ }
2138
+ return pts.map((pt) => ({ x: pt.x, y: pt.y }));
2139
+ }
2140
+ };
2141
+ __name(_Trajectory, "Trajectory");
2142
+ let Trajectory = _Trajectory;
1875
2143
  const zoom_level = 1;
1876
2144
  const _Edge = class _Edge extends Line {
1877
2145
  constructor(data, from, to, weight = 0) {
@@ -1918,9 +2186,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
1918
2186
  return __privateGet(this, _edge);
1919
2187
  }
1920
2188
  get path() {
1921
- let _this = __privateGet(this, _edge);
1922
- 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);
1923
- let p = `M${head.join(" ")} ${mid_int0.join(" ")} ${nock_int0.join(" ")} ${mid.join(" ")} ${nock_int1.join(" ")} ${mid_int1.join(" ")}z`;
2189
+ const _this = __privateGet(this, _edge);
2190
+ const T = [_this.to.x, _this.to.y];
2191
+ const A = _this.from_point;
2192
+ const [B0, B1] = _this.base_pts;
2193
+ const L = Math.hypot(A[0] - T[0], A[1] - T[1]);
2194
+ const W = Math.hypot(B0[0] - B1[0], B0[1] - B1[1]);
2195
+ if (!(L > 0) || !(W > 0)) return "";
2196
+ const u = [(A[0] - T[0]) / L, (A[1] - T[1]) / L];
2197
+ const b = Math.hypot(B0[0] - T[0], B0[1] - T[1]);
2198
+ const v = [(B0[0] - T[0]) / b, (B0[1] - T[1]) / b];
2199
+ const halfW = /* @__PURE__ */ __name((d) => W / 2 * Math.max(0, 1 - d / L), "halfW");
2200
+ const at = /* @__PURE__ */ __name((d, s) => `${T[0] + u[0] * d + v[0] * s} ${T[1] + u[1] * d + v[1] * s}`, "at");
2201
+ const dh = 0.2 * L;
2202
+ const c = Math.min(0.15 * L, 0.8 * 2 * halfW(dh));
2203
+ const dm = dh + c / 2, dn = dh + c;
2204
+ const p = `M${at(dh, 0)} L${at(dm, halfW(dm))} L${at(dn, halfW(dn))} L${at(dm, 0)} L${at(dn, -halfW(dn))} L${at(dm, -halfW(dm))}z`;
1924
2205
  return p;
1925
2206
  }
1926
2207
  };
@@ -2063,6 +2344,46 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2063
2344
  };
2064
2345
  __name(_Drawable, "Drawable");
2065
2346
  let Drawable = _Drawable;
2347
+ const DEFAULT_EDGE_WIDTH = 0.04;
2348
+ const SATURATION_RANGE = [0.25, 1];
2349
+ const OPACITY_RANGE = [0.3, 1];
2350
+ function rampFor(w, lo, hi, range) {
2351
+ const a = Math.abs(w);
2352
+ if (!(a > 0)) return 0;
2353
+ if (!(hi > lo)) return range[1];
2354
+ const t = Math.min(1, Math.max(0, (a - lo) / (hi - lo)));
2355
+ return range[0] + t * (range[1] - range[0]);
2356
+ }
2357
+ __name(rampFor, "rampFor");
2358
+ const EDGE_WIDTH_PER_EXTENT = 0.011;
2359
+ const TAPERED_OUTLINE_PX = 0.75;
2360
+ const CHEVRON_DARKEN = 0.8;
2361
+ const CHEVRON_HALO_PX = 0.75;
2362
+ const POINT_RADIUS_PER_EXTENT = 6e-3;
2363
+ const UNCONNECTED_NODE_SIZE = 0.6;
2364
+ function widthPerWeight(edgeWidth) {
2365
+ return 2 * edgeWidth;
2366
+ }
2367
+ __name(widthPerWeight, "widthPerWeight");
2368
+ function nodeSizeForEdges(weightSum, widthFrac) {
2369
+ return weightSum * widthFrac / (2 * POINT_RADIUS_PER_EXTENT);
2370
+ }
2371
+ __name(nodeSizeForEdges, "nodeSizeForEdges");
2372
+ function flattenOnWhite(col) {
2373
+ const m = /rgba?\(([^)]+)\)/.exec(col);
2374
+ if (!m) return col;
2375
+ const [r, g, b, a = 1] = m[1].split(",").map(Number);
2376
+ const f = /* @__PURE__ */ __name((c) => Math.round(255 + (c - 255) * a), "f");
2377
+ return `rgb(${f(r)},${f(g)},${f(b)})`;
2378
+ }
2379
+ __name(flattenOnWhite, "flattenOnWhite");
2380
+ function darkenRgba(col, k) {
2381
+ const m = /rgba?\(([^)]+)\)/.exec(col);
2382
+ if (!m) return col;
2383
+ const [r, g, b, a = 1] = m[1].split(",").map(Number);
2384
+ return `rgba(${Math.round(r * k)},${Math.round(g * k)},${Math.round(b * k)},${a})`;
2385
+ }
2386
+ __name(darkenRgba, "darkenRgba");
2066
2387
  const _QELine = class _QELine extends Drawable {
2067
2388
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
2068
2389
  super(__data__, parent, graph, __children__);
@@ -2072,8 +2393,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2072
2393
  this.__children__ = __children__;
2073
2394
  this.__name__ = "unnamed-line";
2074
2395
  this._arrow_el = null;
2396
+ this._hit_el = null;
2075
2397
  this.svg_element = this.create();
2076
2398
  }
2399
+ /** Stamp edge identity onto an element so <qe-graph> delegation can resolve
2400
+ * qe-edge-* events. No-op for non-edges (e.g. axis lines). */
2401
+ _stampEdge(el) {
2402
+ const meta = this.__data__.meta ?? {};
2403
+ if (!meta.isMeanEdge) return;
2404
+ el.setAttribute("data-edge-source", String(meta.source ?? ""));
2405
+ el.setAttribute("data-edge-target", String(meta.target ?? ""));
2406
+ el.setAttribute("data-edge-weight", String(meta.weight ?? 0));
2407
+ el.setAttribute("data-edge-directed", meta.isDirected ? "true" : "false");
2408
+ if (meta.isSelf) el.setAttribute("data-edge-self", "true");
2409
+ if (meta.group != null) el.setAttribute("data-edge-group", String(meta.group));
2410
+ if (meta.unit != null) el.setAttribute("data-edge-unit", String(meta.unit));
2411
+ }
2077
2412
  create() {
2078
2413
  if (this.__data__ instanceof NodeEdge) {
2079
2414
  const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -2110,24 +2445,28 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2110
2445
  const extent = ((_a = this.graph.__data__.extremes) == null ? void 0 : _a.x_max) ?? 1;
2111
2446
  const meta = this.__data__.meta ?? {};
2112
2447
  if (meta.isMeanEdge) {
2448
+ this._stampEdge(this.svg_element);
2113
2449
  const weight = typeof meta.weight === "number" ? meta.weight : 0;
2114
2450
  const maxW = typeof meta.maxWeight === "number" ? meta.maxWeight : 1;
2115
2451
  const opacity = typeof meta.opacity === "number" ? meta.opacity : Math.min(1, weight / (maxW || 1));
2116
- const sw = (extent > 0 ? extent : 1) * 0.011 * (weight / (maxW || 1));
2452
+ const ext = extent > 0 ? extent : 1;
2453
+ const sw = typeof meta.widthFrac === "number" ? ext * meta.widthFrac * weight : ext * EDGE_WIDTH_PER_EXTENT * (weight / (maxW || 1));
2117
2454
  const baseColor = this.__data__._color || new Color("#000000");
2118
- const edgeColor = new WeightedColor(baseColor, opacity).toString();
2455
+ const saturation = typeof meta.saturation === "number" ? meta.saturation : opacity;
2456
+ const edgeColor = new WeightedColor(baseColor, saturation, opacity).toString();
2119
2457
  if (this.__data__ instanceof NodeEdge) {
2120
- const nodeSize = typeof meta.nodeSize === "number" ? meta.nodeSize : 2;
2121
- const nodeR = extent * 6e-3 * nodeSize;
2122
- this.__data__.weight = 2 * nodeR * 0.55 * opacity;
2458
+ this.__data__.weight = sw;
2123
2459
  this.svg_element.setAttribute("d", this.__data__.path);
2124
2460
  this.svg_element.setAttribute("fill", edgeColor);
2125
2461
  this.svg_element.setAttribute("stroke", "none");
2126
2462
  } else if (this.__data__ instanceof TaperedEdge) {
2127
- this.__data__.weight = sw * 4;
2463
+ this.__data__.weight = sw;
2128
2464
  this.svg_element.setAttribute("d", this.__data__.path);
2129
2465
  this.svg_element.setAttribute("fill", edgeColor);
2130
- this.svg_element.setAttribute("stroke", "none");
2466
+ this.svg_element.setAttribute("stroke", edgeColor);
2467
+ this.svg_element.setAttribute("stroke-width", String(TAPERED_OUTLINE_PX));
2468
+ this.svg_element.setAttribute("vector-effect", "non-scaling-stroke");
2469
+ this.svg_element.setAttribute("stroke-linejoin", "round");
2131
2470
  if (meta.isDirected) {
2132
2471
  try {
2133
2472
  const arrow = new EdgeArrow(this.__data__);
@@ -2141,20 +2480,39 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2141
2480
  this.parent.appendChild(this._arrow_el);
2142
2481
  }
2143
2482
  this._arrow_el.setAttribute("d", arrowPath);
2144
- const arrowColor = baseColor.toString();
2145
- this._arrow_el.setAttribute("fill", arrowColor);
2483
+ this._arrow_el.setAttribute("fill", darkenRgba(flattenOnWhite(edgeColor), CHEVRON_DARKEN));
2484
+ this._arrow_el.setAttribute("stroke", "#ffffff");
2485
+ this._arrow_el.setAttribute("stroke-width", String(CHEVRON_HALO_PX));
2486
+ this._arrow_el.setAttribute("vector-effect", "non-scaling-stroke");
2487
+ this._arrow_el.setAttribute("stroke-linejoin", "round");
2146
2488
  }
2147
2489
  } catch (_) {
2148
2490
  }
2149
2491
  }
2150
2492
  } else {
2151
- this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
2152
- this.svg_element.setAttribute("y1", this.__data__.from.y.toString());
2153
- this.svg_element.setAttribute("x2", this.__data__.to.x.toString());
2154
- this.svg_element.setAttribute("y2", this.__data__.to.y.toString());
2493
+ const x1 = this.__data__.from.x, y1 = this.__data__.from.y;
2494
+ const x2 = this.__data__.to.x, y2 = this.__data__.to.y;
2495
+ this.svg_element.setAttribute("x1", x1.toString());
2496
+ this.svg_element.setAttribute("y1", y1.toString());
2497
+ this.svg_element.setAttribute("x2", x2.toString());
2498
+ this.svg_element.setAttribute("y2", y2.toString());
2155
2499
  this.svg_element.setAttribute("stroke-width", sw.toString());
2156
2500
  this.svg_element.setAttribute("stroke", edgeColor);
2501
+ this.svg_element.setAttribute("pointer-events", "stroke");
2157
2502
  this.svg_element.removeAttribute("marker-end");
2503
+ if (!this._hit_el) {
2504
+ this._hit_el = document.createElementNS("http://www.w3.org/2000/svg", "line");
2505
+ this._hit_el.setAttribute("stroke", "transparent");
2506
+ this._hit_el.setAttribute("stroke-linecap", "round");
2507
+ this._hit_el.setAttribute("pointer-events", "stroke");
2508
+ }
2509
+ if (!this._hit_el.parentElement) this.parent.appendChild(this._hit_el);
2510
+ this._hit_el.setAttribute("x1", x1.toString());
2511
+ this._hit_el.setAttribute("y1", y1.toString());
2512
+ this._hit_el.setAttribute("x2", x2.toString());
2513
+ this._hit_el.setAttribute("y2", y2.toString());
2514
+ this._hit_el.setAttribute("stroke-width", Math.max(sw, extent * 0.03).toString());
2515
+ this._stampEdge(this._hit_el);
2158
2516
  }
2159
2517
  } else {
2160
2518
  this.svg_element.setAttribute("x1", this.__data__.from.x.toString());
@@ -2168,10 +2526,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2168
2526
  }
2169
2527
  }
2170
2528
  draw() {
2171
- var _a, _b;
2172
- (_a = this.parent) == null ? void 0 : _a.appendChild(this.svg_element);
2529
+ var _a, _b, _c;
2530
+ if (this._hit_el) (_a = this.parent) == null ? void 0 : _a.appendChild(this._hit_el);
2531
+ (_b = this.parent) == null ? void 0 : _b.appendChild(this.svg_element);
2173
2532
  if (this._arrow_el) {
2174
- (_b = this.parent) == null ? void 0 : _b.appendChild(this._arrow_el);
2533
+ (_c = this.parent) == null ? void 0 : _c.appendChild(this._arrow_el);
2175
2534
  }
2176
2535
  }
2177
2536
  };
@@ -2201,14 +2560,45 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2201
2560
  * (≈ 1–2 % of the coordinate half-extent for a typical ENA network).
2202
2561
  */
2203
2562
  computeRadius() {
2204
- const extent = this.graph.__data__.extremes.x_max;
2205
- return (extent > 0 ? extent : 1) * 6e-3 * this.__data__.size;
2563
+ var _a;
2564
+ const r = this.baseRadius();
2565
+ const cap = (_a = this.__data__.meta) == null ? void 0 : _a.maxRadius;
2566
+ return typeof cap === "number" && cap > 0 ? Math.min(r, cap) : r;
2567
+ }
2568
+ baseRadius() {
2569
+ var _a, _b, _c, _d;
2570
+ const extent = this.graph.__data__.extremes.x_max || 1;
2571
+ const sizeScale = ((_a = this.__data__.meta) == null ? void 0 : _a.sizeScale) ?? 1;
2572
+ if ((_b = this.__data__.meta) == null ? void 0 : _b.fixedSize) {
2573
+ const rect = (_d = (_c = this.graph.svg_element) == null ? void 0 : _c.getBoundingClientRect) == null ? void 0 : _d.call(_c);
2574
+ const zoom = this.graph.zoom_level || 1;
2575
+ const pxPerUnit = rect && rect.width > 0 ? rect.width / (2 * extent) : 1;
2576
+ const targetPx = this.__data__.size * 2 * sizeScale;
2577
+ return targetPx / pxPerUnit / zoom;
2578
+ }
2579
+ return extent * POINT_RADIUS_PER_EXTENT * this.__data__.size * sizeScale;
2580
+ }
2581
+ /** Optional halo stroke (e.g. white outline on unit points to separate
2582
+ * overlapping dots in dense clusters). Width scales with the dot so it holds
2583
+ * up at any zoom. No-op unless the object requests it via meta.stroke. */
2584
+ applyStroke(el) {
2585
+ var _a;
2586
+ const stroke = (_a = this.__data__.meta) == null ? void 0 : _a.stroke;
2587
+ if (!stroke) return;
2588
+ el.setAttribute("stroke", stroke);
2589
+ el.setAttribute("stroke-width", (this.computeRadius() * 0.22).toString());
2206
2590
  }
2207
2591
  create() {
2208
2592
  const tag = this.isSquare ? "rect" : "circle";
2209
2593
  const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
2210
2594
  el.setAttribute("data-point-id", this.__data__.ID);
2595
+ const meta = this.__data__.meta ?? {};
2596
+ if (meta.kind) el.setAttribute("data-kind", meta.kind);
2597
+ if (meta.name != null) el.setAttribute("data-name", String(meta.name));
2598
+ if (meta.group != null) el.setAttribute("data-group", String(meta.group));
2599
+ if (meta.unit != null) el.setAttribute("data-unit", String(meta.unit));
2211
2600
  el.setAttribute("fill", this.__data__.color.toString());
2601
+ this.applyStroke(el);
2212
2602
  const r = this.computeRadius();
2213
2603
  if (this.isSquare) {
2214
2604
  el.setAttribute("x", (this.x - r).toString());
@@ -2221,9 +2611,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2221
2611
  el.setAttribute("r", r.toString());
2222
2612
  }
2223
2613
  this.svg_element = el;
2224
- el.addEventListener("click", (event) => {
2225
- console.log("Point clicked: ", this.__data__);
2226
- event.stopPropagation();
2614
+ el.addEventListener("click", () => {
2227
2615
  this.__data__.active = !this.__data__.active;
2228
2616
  this.update();
2229
2617
  const pointEvent = new CustomEvent("point-clicked", {
@@ -2246,6 +2634,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2246
2634
  }
2247
2635
  const r = this.computeRadius();
2248
2636
  this.svg_element.setAttribute("fill", this.__data__.color.toString());
2637
+ this.applyStroke(this.svg_element);
2249
2638
  if (this.isSquare) {
2250
2639
  this.svg_element.setAttribute("x", (this.x - r).toString());
2251
2640
  this.svg_element.setAttribute("y", (this.y - r).toString());
@@ -2264,6 +2653,141 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2264
2653
  };
2265
2654
  __name(_QEPoint, "QEPoint");
2266
2655
  let QEPoint = _QEPoint;
2656
+ const _QETrajectory = class _QETrajectory extends Drawable {
2657
+ constructor(__data__, parent, graph, __children__ = new QEArray()) {
2658
+ super(__data__, parent, graph, __children__);
2659
+ this.__data__ = __data__;
2660
+ this.parent = parent;
2661
+ this.graph = graph;
2662
+ this.__children__ = __children__;
2663
+ this.__name__ = "trajectory";
2664
+ this._path_el = null;
2665
+ this._segments_group = null;
2666
+ this._tip_el = null;
2667
+ this._points_group = null;
2668
+ this.svg_element = this.create();
2669
+ }
2670
+ // Trajectory styling is specified in display pixels, while SVG paths use
2671
+ // graph coordinates. Convert at render time so marks stay legible at any scale.
2672
+ graphUnitsForPixels(pixels) {
2673
+ var _a;
2674
+ const extent = this.graph.__data__.extremes.x_max || 1;
2675
+ const width = ((_a = this.graph.svg_element) == null ? void 0 : _a.getBoundingClientRect().width) || 0;
2676
+ return width > 0 ? pixels * (2 * extent / width) : pixels * extent * 6e-3;
2677
+ }
2678
+ create() {
2679
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
2680
+ group.setAttribute("class", "qe-trajectory-group");
2681
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2682
+ path.setAttribute("fill", "none");
2683
+ path.setAttribute("stroke", this.__data__.color.toString());
2684
+ path.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2685
+ group.appendChild(path);
2686
+ this._path_el = path;
2687
+ const segmentsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2688
+ segmentsGroup.setAttribute("class", "qe-trajectory-segments");
2689
+ group.appendChild(segmentsGroup);
2690
+ this._segments_group = segmentsGroup;
2691
+ const pointsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2692
+ pointsGroup.setAttribute("class", "qe-trajectory-points");
2693
+ group.appendChild(pointsGroup);
2694
+ this._points_group = pointsGroup;
2695
+ if (this.__data__.showTips) {
2696
+ const tip = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2697
+ tip.setAttribute("fill", this.__data__.color.toString());
2698
+ tip.setAttribute("r", this.graphUnitsForPixels(4).toString());
2699
+ tip.setAttribute("stroke", "#ffffff");
2700
+ tip.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2701
+ group.appendChild(tip);
2702
+ this._tip_el = tip;
2703
+ }
2704
+ this.svg_element = group;
2705
+ return group;
2706
+ }
2707
+ update() {
2708
+ if (!this.svg_element) {
2709
+ this.create();
2710
+ }
2711
+ if (!this.svg_element.parentElement) {
2712
+ this.parent.appendChild(this.svg_element);
2713
+ }
2714
+ const dStr = this.__data__.path;
2715
+ if (this._path_el) {
2716
+ this._path_el.setAttribute("d", dStr);
2717
+ this._path_el.setAttribute("stroke", this.__data__.color.toString());
2718
+ this._path_el.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2719
+ this._path_el.style.display = this.__data__.lineStyle === "tapered" ? "none" : "block";
2720
+ }
2721
+ if (this._segments_group) {
2722
+ this._segments_group.innerHTML = "";
2723
+ if (this.__data__.lineStyle === "tapered") {
2724
+ const pathPts = this.__data__.pathPoints;
2725
+ const nSegments = pathPts.length - 1;
2726
+ const [minWidth, maxWidth] = this.__data__.strokeWidthRange;
2727
+ const [minOpacity, maxOpacity] = this.__data__.opacityRange;
2728
+ for (let i = 0; i < nSegments; i++) {
2729
+ const start = pathPts[i];
2730
+ const end = pathPts[i + 1];
2731
+ const progress = nSegments <= 1 ? 1 : i / (nSegments - 1);
2732
+ const width = minWidth + (maxWidth - minWidth) * progress;
2733
+ const opacity = minOpacity + (maxOpacity - minOpacity) * progress;
2734
+ const segment = document.createElementNS("http://www.w3.org/2000/svg", "path");
2735
+ segment.setAttribute("d", `M${start.x},${start.y} L${end.x},${end.y}`);
2736
+ segment.setAttribute("fill", "none");
2737
+ segment.setAttribute("stroke", this.__data__.color.toString());
2738
+ segment.setAttribute("stroke-linecap", "round");
2739
+ segment.setAttribute("stroke-linejoin", "round");
2740
+ segment.setAttribute("stroke-width", this.graphUnitsForPixels(width).toString());
2741
+ segment.setAttribute("opacity", opacity.toString());
2742
+ this._segments_group.appendChild(segment);
2743
+ }
2744
+ this._segments_group.style.display = "block";
2745
+ } else {
2746
+ this._segments_group.style.display = "none";
2747
+ }
2748
+ }
2749
+ const pts = this.__data__.activePoints;
2750
+ if (this._points_group) {
2751
+ if (this.__data__.showPoints && pts.length > 0) {
2752
+ this._points_group.innerHTML = "";
2753
+ pts.forEach((pt) => {
2754
+ var _a;
2755
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2756
+ dot.setAttribute("cx", pt.x.toString());
2757
+ dot.setAttribute("cy", pt.y.toString());
2758
+ dot.setAttribute("r", this.graphUnitsForPixels(2.5).toString());
2759
+ dot.setAttribute("fill", this.__data__.color.toString());
2760
+ dot.setAttribute("opacity", "0.7");
2761
+ (_a = this._points_group) == null ? void 0 : _a.appendChild(dot);
2762
+ });
2763
+ this._points_group.style.display = "block";
2764
+ } else {
2765
+ this._points_group.style.display = "none";
2766
+ }
2767
+ }
2768
+ if (this._tip_el && this.__data__.showTips) {
2769
+ if (pts.length > 0) {
2770
+ const lastPt = pts[pts.length - 1];
2771
+ this._tip_el.setAttribute("cx", lastPt.x.toString());
2772
+ this._tip_el.setAttribute("cy", lastPt.y.toString());
2773
+ this._tip_el.setAttribute("fill", this.__data__.color.toString());
2774
+ this._tip_el.setAttribute("r", this.graphUnitsForPixels(4).toString());
2775
+ this._tip_el.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2776
+ this._tip_el.style.display = "block";
2777
+ } else {
2778
+ this._tip_el.style.display = "none";
2779
+ }
2780
+ }
2781
+ }
2782
+ draw() {
2783
+ if (this.svg_element && !this.svg_element.parentElement) {
2784
+ this.parent.appendChild(this.svg_element);
2785
+ }
2786
+ this.update();
2787
+ }
2788
+ };
2789
+ __name(_QETrajectory, "QETrajectory");
2790
+ let QETrajectory = _QETrajectory;
2267
2791
  const { ArrayTools } = Tools;
2268
2792
  const _QEArray = class _QEArray extends Array {
2269
2793
  constructor(...args) {
@@ -2323,7 +2847,8 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2323
2847
  "Edge": QELine,
2324
2848
  "TaperedEdge": QELine,
2325
2849
  "TaperedWeightedMidpointEdge": QELine,
2326
- "NodeEdge": QELine
2850
+ "NodeEdge": QELine,
2851
+ "Trajectory": QETrajectory
2327
2852
  };
2328
2853
  const _QELayer = class _QELayer {
2329
2854
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
@@ -2347,8 +2872,14 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2347
2872
  (obj) => !this.__children__.some((child) => childId(child) === obj.ID)
2348
2873
  );
2349
2874
  adding.forEach((obj) => {
2350
- const obj_name = obj.constructor.name;
2351
- const ena_element = new QEElementConstructors[obj_name](obj, this.svg_element, this.graph);
2875
+ const obj_name = obj._typeName || obj.constructor.name;
2876
+ let constructor = QEElementConstructors[obj_name];
2877
+ if (!constructor) {
2878
+ if (obj instanceof Trajectory) constructor = QETrajectory;
2879
+ else if (obj instanceof Point) constructor = QEPoint;
2880
+ else constructor = QELine;
2881
+ }
2882
+ const ena_element = new constructor(obj, this.svg_element, this.graph);
2352
2883
  this.__children__.push(ena_element);
2353
2884
  });
2354
2885
  updating.forEach((child) => {
@@ -2427,6 +2958,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2427
2958
  this.customX = customX;
2428
2959
  this.customY = customY;
2429
2960
  this._visible = false;
2961
+ this._offsetX = 0;
2962
+ this._offsetY = 0;
2963
+ this.pinned = false;
2430
2964
  this.svg_element = this._create();
2431
2965
  if (displayMode === Label.DISPLAY_TYPES.ON) {
2432
2966
  this._visible = true;
@@ -2440,10 +2974,52 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2440
2974
  const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
2441
2975
  el.textContent = this.text;
2442
2976
  el.setAttribute("data-label-for", this.point.ID);
2443
- el.setAttribute("pointer-events", "none");
2977
+ el.setAttribute("pointer-events", "auto");
2978
+ el.style.cursor = "grab";
2979
+ this._attachDrag(el);
2444
2980
  this.parent.appendChild(el);
2445
2981
  return el;
2446
2982
  }
2983
+ /** Drag to reposition; pins the label open and emits qe-label-move on drop. */
2984
+ _attachDrag(el) {
2985
+ el.addEventListener("mousedown", (ev) => {
2986
+ ev.preventDefault();
2987
+ ev.stopPropagation();
2988
+ const svg = el.ownerSVGElement;
2989
+ if (!svg) return;
2990
+ const toUser = /* @__PURE__ */ __name((cx, cy) => {
2991
+ const p = svg.createSVGPoint();
2992
+ p.x = cx;
2993
+ p.y = cy;
2994
+ const m = svg.getScreenCTM();
2995
+ return m ? p.matrixTransform(m.inverse()) : { x: 0, y: 0 };
2996
+ }, "toUser");
2997
+ const start = toUser(ev.clientX, ev.clientY);
2998
+ const baseX = this._offsetX, baseY = this._offsetY;
2999
+ el.style.cursor = "grabbing";
3000
+ const onMove = /* @__PURE__ */ __name((e) => {
3001
+ const p = toUser(e.clientX, e.clientY);
3002
+ this._offsetX = baseX + (p.x - start.x);
3003
+ this._offsetY = baseY + (p.y - start.y);
3004
+ this.pinned = true;
3005
+ this._visible = true;
3006
+ this.svg_element.setAttribute("display", "block");
3007
+ this.update();
3008
+ }, "onMove");
3009
+ const onUp = /* @__PURE__ */ __name(() => {
3010
+ document.removeEventListener("mousemove", onMove);
3011
+ document.removeEventListener("mouseup", onUp);
3012
+ el.style.cursor = "grab";
3013
+ this.graph.dispatchEvent(new CustomEvent("qe-label-move", {
3014
+ detail: { id: this.point.ID, transform: { x: this._offsetX, y: this._offsetY } },
3015
+ bubbles: true,
3016
+ composed: true
3017
+ }));
3018
+ }, "onUp");
3019
+ document.addEventListener("mousemove", onMove);
3020
+ document.addEventListener("mouseup", onUp);
3021
+ });
3022
+ }
2447
3023
  /**
2448
3024
  * Recompute position and font-size from current graph extents.
2449
3025
  * No-ops when the label is hidden (AUTO/CLICK modes while not hovered/clicked).
@@ -2492,6 +3068,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2492
3068
  }
2493
3069
  this.svg_element.setAttribute("x", x.toString());
2494
3070
  this.svg_element.setAttribute("y", y.toString());
3071
+ if (this._offsetX || this._offsetY) {
3072
+ this.svg_element.setAttribute("transform", `translate(${this._offsetX}, ${this._offsetY})`);
3073
+ }
2495
3074
  this.svg_element.setAttribute("font-size", fontSize.toString());
2496
3075
  this.svg_element.setAttribute("font-family", "system-ui, sans-serif");
2497
3076
  this.svg_element.setAttribute("font-weight", "500");
@@ -2505,6 +3084,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2505
3084
  this.update();
2506
3085
  }
2507
3086
  hide() {
3087
+ if (this.pinned) return;
2508
3088
  this._visible = false;
2509
3089
  this.svg_element.setAttribute("display", "none");
2510
3090
  }
@@ -2540,10 +3120,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2540
3120
  __name(_QEEdgesElement, "QEEdgesElement");
2541
3121
  _QEEdgesElement.tagName = "qe-edges";
2542
3122
  let QEEdgesElement = _QEEdgesElement;
3123
+ const _QETrajectoriesElement = class _QETrajectoriesElement extends HTMLElement {
3124
+ };
3125
+ __name(_QETrajectoriesElement, "QETrajectoriesElement");
3126
+ _QETrajectoriesElement.tagName = "qe-trajectories";
3127
+ let QETrajectoriesElement = _QETrajectoriesElement;
3128
+ const _QEAxesElement = class _QEAxesElement extends HTMLElement {
3129
+ };
3130
+ __name(_QEAxesElement, "QEAxesElement");
3131
+ _QEAxesElement.tagName = "qe-axes";
3132
+ let QEAxesElement = _QEAxesElement;
2543
3133
  customElements.define(QENodesElement.tagName, QENodesElement);
2544
3134
  customElements.define(QEMeansElement.tagName, QEMeansElement);
2545
3135
  customElements.define(QEPointsElement.tagName, QEPointsElement);
2546
3136
  customElements.define(QEEdgesElement.tagName, QEEdgesElement);
3137
+ customElements.define(QETrajectoriesElement.tagName, QETrajectoriesElement);
3138
+ customElements.define(QEAxesElement.tagName, QEAxesElement);
2547
3139
  const GROUP_PALETTE = [
2548
3140
  "#4477AA",
2549
3141
  "#EE6677",
@@ -2575,6 +3167,14 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2575
3167
  return { halfW: t95 * seX, halfH: t95 * seY };
2576
3168
  }
2577
3169
  __name(computeCIRect, "computeCIRect");
3170
+ const MEAN_MAX_FRACTION_OF_INTERVAL = 0.5;
3171
+ function capMeanToInterval(meanPt, halfW, halfH) {
3172
+ if (!meanPt) return;
3173
+ const half = Math.min(Math.abs(halfW), Math.abs(halfH));
3174
+ if (!(half > 0)) return;
3175
+ meanPt.meta = { ...meanPt.meta ?? {}, maxRadius: half * MEAN_MAX_FRACTION_OF_INTERVAL };
3176
+ }
3177
+ __name(capMeanToInterval, "capMeanToInterval");
2578
3178
  function readIntervalBounds(row, dim0, dim1) {
2579
3179
  const get = /* @__PURE__ */ __name((col) => {
2580
3180
  const i = row.names.indexOf(col);
@@ -2588,6 +3188,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2588
3188
  return { x_low: xLow, x_high: xHigh, y_low: yLow, y_high: yHigh };
2589
3189
  }
2590
3190
  __name(readIntervalBounds, "readIntervalBounds");
3191
+ function parseColorScale(attr) {
3192
+ const a = attr == null ? void 0 : attr.trim().toLowerCase();
3193
+ return a === "plot" || a === "absolute" ? a : "model";
3194
+ }
3195
+ __name(parseColorScale, "parseColorScale");
2591
3196
  function parseLabelMode(attr, defaultMode) {
2592
3197
  switch (attr == null ? void 0 : attr.toLowerCase()) {
2593
3198
  case "on":
@@ -2617,6 +3222,17 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2617
3222
  this._nodeLabels = /* @__PURE__ */ new Map();
2618
3223
  this._meanLabels = /* @__PURE__ */ new Map();
2619
3224
  this._pointLabels = /* @__PURE__ */ new Map();
3225
+ this._showAxes = false;
3226
+ this._axisXText = null;
3227
+ this._axisYText = null;
3228
+ this._axisLabelEls = /* @__PURE__ */ new Map();
3229
+ this._magnifyText = null;
3230
+ this._magnifyEl = null;
3231
+ this._animateMs = 0;
3232
+ this._tweenRaf = null;
3233
+ this._highlight = { edges: /* @__PURE__ */ new Set(), nodes: /* @__PURE__ */ new Set() };
3234
+ this._highlightGroup = null;
3235
+ this._tooltipEl = null;
2620
3236
  this.graph = this;
2621
3237
  this.parent = null;
2622
3238
  this.__data__ = new Graph(DefaultGraphOptions);
@@ -2624,16 +3240,22 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2624
3240
  this.svg_element = this.create();
2625
3241
  console.log("Created ena-graph svg element: ", this.svg_element);
2626
3242
  document.addEventListener("model-updated", (e) => {
2627
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
3243
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2628
3244
  if (this._vizena && e.target !== this._vizena) return;
2629
3245
  const model = e.detail;
2630
3246
  if (!model) return;
3247
+ this._animateMs = this.hasAttribute("animate") ? parseInt(this.getAttribute("animate") || "", 10) || 300 : 0;
3248
+ const tweenFrom = this._animateMs > 0 && this._hasModel ? this._captureTween() : null;
2631
3249
  this._hasModel = true;
2632
3250
  const dims = model.dimensions;
2633
3251
  const unitIdCol = model.unit_id_col;
2634
3252
  const nodeIdCol = model.node_id_col;
2635
3253
  const groupColName = model.group_col;
2636
3254
  const config = this._readLayerConfig();
3255
+ this._showAxes = config.showAxes;
3256
+ this._axisXText = config.axisXLabel === null ? null : config.axisXLabel || String((dims == null ? void 0 : dims[0]) ?? "x");
3257
+ this._magnifyText = config.edgeMagnify == null ? null : `(scaled ${config.edgeMagnify.toFixed(1)}x)`;
3258
+ this._axisYText = config.axisYLabel === null ? null : config.axisYLabel || String((dims == null ? void 0 : dims[1]) ?? "y");
2637
3259
  const groupColorMap = {};
2638
3260
  if (groupColName && ((_a = model.points) == null ? void 0 : _a.names)) {
2639
3261
  const groupCol = model.points.col(groupColName);
@@ -2667,7 +3289,9 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2667
3289
  }, "getExtent");
2668
3290
  const maxNodeExtent = ((_c = model.nodes) == null ? void 0 : _c.rows) ? getExtent(model.nodes.rows()) : 0;
2669
3291
  const maxPointExtent = ((_d = model.points) == null ? void 0 : _d.rows) ? getExtent(model.points.rows()) : 0;
2670
- const pointScale = scalePointsEnabled && maxPointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / maxPointExtent : 1;
3292
+ const maxTrajExtent = ((_e = model.trajectories) == null ? void 0 : _e.rows) ? getExtent(model.trajectories.rows()) : 0;
3293
+ const effectivePointExtent = maxPointExtent > 0 ? maxPointExtent : maxTrajExtent;
3294
+ const pointScale = scalePointsEnabled && effectivePointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / effectivePointExtent : 1;
2671
3295
  const labelNodes = config.labelNodes;
2672
3296
  const labelMeans = config.labelMeans;
2673
3297
  const labelPoints = config.labelPoints;
@@ -2690,7 +3314,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2690
3314
  }, "rowToPoint");
2691
3315
  const groupSums = {};
2692
3316
  const showUnitPoints = config.showPoints;
2693
- if ((_e = model.points) == null ? void 0 : _e.rows) {
3317
+ if ((_f = model.points) == null ? void 0 : _f.rows) {
2694
3318
  const pointsLayer = this.__children__.search("__children__", "points");
2695
3319
  if (pointsLayer) {
2696
3320
  model.points.rows().forEach((row) => {
@@ -2699,6 +3323,16 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2699
3323
  const colorStr = group ? groupColorMap[group] ?? "#888888" : "#888888";
2700
3324
  if (showUnitPoints) {
2701
3325
  const pt = rowToPoint(row, unitIdCol, colorStr, pointScale);
3326
+ pt.size = config.pointSize ?? 2.5;
3327
+ pt.meta = {
3328
+ ...pt.meta ?? {},
3329
+ stroke: "#ffffff",
3330
+ kind: "point",
3331
+ name: pt.ID,
3332
+ group: group ?? void 0,
3333
+ unit: pt.ID,
3334
+ fixedSize: config.pointFixedSize
3335
+ };
2702
3336
  this.__data__.points.set(pt.ID, pt);
2703
3337
  pointsLayer.__data__.objects.set(pt.ID, pt);
2704
3338
  }
@@ -2716,7 +3350,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2716
3350
  groupSums[group].n += 1;
2717
3351
  }
2718
3352
  });
2719
- const hasGroupsFrame = !!(((_f = model.groups) == null ? void 0 : _f.rows) && model.groups.rows().length > 0);
3353
+ const hasGroupsFrame = !!(((_g = model.groups) == null ? void 0 : _g.rows) && model.groups.rows().length > 0);
2720
3354
  const activeGroupRows = config.showMeans && hasGroupsFrame ? model.groups.rows().filter((r) => {
2721
3355
  const g = r.rowName ?? String(r[0]);
2722
3356
  return activeMeanGroups.size === 0 || activeMeanGroups.has(g);
@@ -2748,19 +3382,26 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2748
3382
  const meanPt = new Point({
2749
3383
  ID: `mean-${group}`,
2750
3384
  dimensions: [mx, my],
2751
- meta: { group }
3385
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
2752
3386
  });
2753
3387
  meanPt.color = new Color(color);
2754
3388
  meanPt.size = 4;
2755
3389
  meanPt.shape = "rect";
2756
3390
  this.__data__.points.set(meanPt.ID, meanPt);
2757
3391
  pointsLayer.__data__.objects.set(meanPt.ID, meanPt);
2758
- const bounds = readIntervalBounds(row, dims[0], dims[1]);
2759
- if (bounds) {
3392
+ const bounds = config.showCI ? readIntervalBounds(row, dims[0], dims[1]) : null;
3393
+ if (!bounds) {
3394
+ const stale = this._ciRects.get(group);
3395
+ if (stale) {
3396
+ stale.remove();
3397
+ this._ciRects.delete(group);
3398
+ }
3399
+ } else {
2760
3400
  const rect_x = bounds.x_low * pointScale;
2761
3401
  const rect_y = -bounds.y_high * pointScale;
2762
3402
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
2763
3403
  const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
3404
+ capMeanToInterval(meanPt, rect_width / 2, rect_height / 2);
2764
3405
  let el = this._ciRects.get(group);
2765
3406
  if (!el) {
2766
3407
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2786,7 +3427,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2786
3427
  const meanPt = new Point({
2787
3428
  ID: `mean-${group}`,
2788
3429
  dimensions: [x / n * pointScale, -(y / n) * pointScale],
2789
- meta: { group }
3430
+ meta: { kind: "mean", name: group, group, fixedSize: config.meanFixedSize }
2790
3431
  });
2791
3432
  meanPt.color = new Color(color);
2792
3433
  meanPt.size = 4;
@@ -2802,7 +3443,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2802
3443
  this._ciRects.delete(g);
2803
3444
  }
2804
3445
  });
2805
- if ((_g = model.confidence) == null ? void 0 : _g.rows) {
3446
+ if ((_h = model.confidence) == null ? void 0 : _h.rows) {
2806
3447
  model.confidence.rows().forEach((row) => {
2807
3448
  const groupIdx = row.names.indexOf(groupColName ?? "group");
2808
3449
  const group = groupIdx >= 0 ? String(row[groupIdx]) : row.rowName ?? null;
@@ -2814,6 +3455,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2814
3455
  const rect_y = -bounds.y_high * pointScale;
2815
3456
  const rect_width = (bounds.x_high - bounds.x_low) * pointScale;
2816
3457
  const rect_height = (bounds.y_high - bounds.y_low) * pointScale;
3458
+ capMeanToInterval(
3459
+ this.__data__.points.get(`mean-${group}`),
3460
+ rect_width / 2,
3461
+ rect_height / 2
3462
+ );
2817
3463
  let el = this._ciRects.get(group);
2818
3464
  if (!el) {
2819
3465
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2841,6 +3487,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2841
3487
  const rawVarY = y2 / n - μy * μy;
2842
3488
  const { halfW, halfH } = computeCIRect(rawVarX, rawVarY, n, pointScale);
2843
3489
  const cx = μx * pointScale, cy = -μy * pointScale;
3490
+ capMeanToInterval(
3491
+ this.__data__.points.get(`mean-${group}`),
3492
+ halfW,
3493
+ halfH
3494
+ );
2844
3495
  let el = this._ciRects.get(group);
2845
3496
  if (!el) {
2846
3497
  el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
@@ -2862,7 +3513,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2862
3513
  });
2863
3514
  }
2864
3515
  }
2865
- if ((_h = model.outlier) == null ? void 0 : _h.rows) {
3516
+ if ((_i = model.outlier) == null ? void 0 : _i.rows) {
2866
3517
  this._outlierRects.forEach((el, g) => {
2867
3518
  if (!groupColorMap[g]) {
2868
3519
  el.remove();
@@ -2951,20 +3602,74 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
2951
3602
  pointsLayer.__data__.__state__.setVariable("layer_updated", pointsLayer.__data__);
2952
3603
  }
2953
3604
  }
3605
+ const trajectoriesLayer = this.__children__.search("__children__", "trajectories");
3606
+ const trajectoriesEnabled = config.showTrajectories;
3607
+ if (trajectoriesEnabled && trajectoriesLayer) {
3608
+ trajectoriesLayer.__data__.objects.clear();
3609
+ this.__data__.trajectories.clear();
3610
+ const trajSource = ((_j = model.trajectories) == null ? void 0 : _j.rows) ? model.trajectories : model.points;
3611
+ if (trajSource == null ? void 0 : trajSource.rows) {
3612
+ const unitRows = {};
3613
+ const selectedUnits = config.trajectoryUnits ? config.trajectoryUnits.split(",").map((s) => s.trim()) : null;
3614
+ trajSource.rows().forEach((row) => {
3615
+ 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;
3616
+ const unit = uIdx >= 0 ? String(row[uIdx]) : row.rowName || "unit-1";
3617
+ if (selectedUnits && !selectedUnits.includes(unit)) return;
3618
+ const xIdx = row.names.indexOf(dims[0]);
3619
+ const yIdx = dims.length > 1 ? row.names.indexOf(dims[1]) : -1;
3620
+ 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;
3621
+ const time = tIdx >= 0 ? Number(row[tIdx]) : unitRows[unit] ? unitRows[unit].length + 1 : 1;
3622
+ if (xIdx >= 0 && yIdx >= 0) {
3623
+ if (!unitRows[unit]) unitRows[unit] = [];
3624
+ unitRows[unit].push({
3625
+ x: Number(row[xIdx]) * pointScale,
3626
+ y: -Number(row[yIdx]) * pointScale,
3627
+ time
3628
+ });
3629
+ }
3630
+ });
3631
+ const unitKeys = Object.keys(unitRows);
3632
+ unitKeys.forEach((unitKey, uIdx) => {
3633
+ const rawPts = unitRows[unitKey];
3634
+ if (rawPts.length === 0) return;
3635
+ const colorStr = GROUP_PALETTE[uIdx % GROUP_PALETTE.length];
3636
+ const trajObj = new Trajectory({
3637
+ ID: `traj-${unitKey}`,
3638
+ unitId: unitKey,
3639
+ color: new Color(colorStr),
3640
+ lineStyle: config.trajectoryLineStyle,
3641
+ strokeWidth: config.trajectoryStrokeWidth,
3642
+ strokeWidthRange: config.trajectoryStrokeWidthRange,
3643
+ opacityRange: config.trajectoryOpacityRange,
3644
+ smooth: config.trajectorySmooth,
3645
+ maxDegree: config.trajectoryMaxDegree,
3646
+ degreeSelection: config.trajectoryDegreeSelection,
3647
+ currentTime: config.trajectoryPlayhead ?? Infinity,
3648
+ showPoints: config.trajectoryShowPoints,
3649
+ showTips: config.trajectoryShowTips,
3650
+ points: rawPts.map((p) => ({ x: p.x, y: p.y, time: p.time }))
3651
+ });
3652
+ this.__data__.trajectories.set(unitKey, trajObj);
3653
+ trajectoriesLayer.__data__.objects.set(trajObj.ID, trajObj);
3654
+ });
3655
+ }
3656
+ trajectoriesLayer.__data__.__state__.setVariable("layer_updated", trajectoriesLayer.__data__);
3657
+ }
2954
3658
  const networkLayer = this.__children__.search("__children__", "network");
2955
3659
  const edgeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edges");
2956
3660
  const nodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "nodes");
2957
3661
  const edgenodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edgenodes");
2958
3662
  const edgesEnabled = config.showEdges;
2959
- if (((_i = model.nodes) == null ? void 0 : _i.rows) && nodeLayer) {
3663
+ if (((_k = model.nodes) == null ? void 0 : _k.rows) && nodeLayer) {
2960
3664
  model.nodes.rows().forEach((row) => {
2961
3665
  const pt = rowToPoint(row, nodeIdCol, "#444444");
2962
3666
  pt.size = 2;
3667
+ pt.meta = { ...pt.meta ?? {}, kind: "node", name: pt.ID };
2963
3668
  nodeLayer.__data__.objects.set(pt.ID, pt);
2964
3669
  });
2965
- if (edgesEnabled && edgeLayer && ((_j = model.edges) == null ? void 0 : _j.rows)) {
3670
+ if (edgesEnabled && edgeLayer && ((_l = model.edges) == null ? void 0 : _l.rows)) {
2966
3671
  const unitGroupMap = {};
2967
- if (groupColName && ((_k = model.points) == null ? void 0 : _k.rows)) {
3672
+ if (groupColName && ((_m = model.points) == null ? void 0 : _m.rows)) {
2968
3673
  model.points.rows().forEach((row) => {
2969
3674
  const qIdx = row.names.indexOf(unitIdCol);
2970
3675
  const gIdx = row.names.indexOf(groupColName);
@@ -3058,21 +3763,46 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3058
3763
  });
3059
3764
  }
3060
3765
  const isDirected = !!model.directed;
3061
- const nonSelfWeights = drawEdges.filter((e2) => e2.pair[0] !== e2.pair[1]).map((e2) => e2.weight);
3062
- const maxEdgeWeight = nonSelfWeights.length > 0 ? Math.max(...nonSelfWeights) : 1e-9;
3063
- const selfWeights = drawEdges.filter((e2) => e2.pair[0] === e2.pair[1]).map((e2) => e2.weight);
3064
- const maxSelfWeight = selfWeights.length > 0 ? Math.max(...selfWeights) : 0;
3766
+ const widthFrac = widthPerWeight(config.edgeWidth) * (config.edgeMagnify ?? 1);
3767
+ const rangeOf = /* @__PURE__ */ __name((ws) => {
3768
+ const nz = ws.map(Math.abs).filter((v) => v > 0);
3769
+ return nz.length ? [Math.min(...nz), Math.max(...nz)] : [0, 0];
3770
+ }, "rangeOf");
3771
+ const modelRange = /* @__PURE__ */ __name((self2) => {
3772
+ const vals = [];
3773
+ edgePairs.forEach(({ col, pair }) => {
3774
+ if (pair[0] === pair[1] !== self2) return;
3775
+ const column = model.edges.col(col);
3776
+ if (column) for (const v of column) vals.push(Number(v));
3777
+ });
3778
+ return rangeOf(vals);
3779
+ }, "modelRange");
3780
+ const plotRange = /* @__PURE__ */ __name((self2) => rangeOf(drawEdges.filter((e2) => e2.pair[0] === e2.pair[1] === self2).map((e2) => e2.weight)), "plotRange");
3781
+ const cscale = config.edgeColorScale;
3782
+ const colourRange = /* @__PURE__ */ __name((self2) => cscale === "plot" ? plotRange(self2) : modelRange(self2), "colourRange");
3783
+ const lineRange = colourRange(false), selfRange = colourRange(true);
3784
+ const colourOf = /* @__PURE__ */ __name((w, self2) => {
3785
+ if (cscale === "absolute") {
3786
+ const a = Math.min(1, Math.abs(w));
3787
+ return { saturation: a, opacity: a };
3788
+ }
3789
+ const [lo, hi] = self2 ? selfRange : lineRange;
3790
+ return { saturation: rampFor(w, lo, hi, SATURATION_RANGE), opacity: rampFor(w, lo, hi, OPACITY_RANGE) };
3791
+ }, "colourOf");
3065
3792
  const nodeWeightSum = {};
3066
3793
  drawEdges.forEach(({ pair, weight }) => {
3067
3794
  const isSelf = pair[0] === pair[1];
3068
3795
  if (isSelf && !isDirected) return;
3069
- nodeWeightSum[pair[0]] = (nodeWeightSum[pair[0]] ?? 0) + weight;
3070
- if (!isSelf) {
3071
- nodeWeightSum[pair[1]] = (nodeWeightSum[pair[1]] ?? 0) + weight;
3796
+ const add = /* @__PURE__ */ __name((id) => {
3797
+ nodeWeightSum[id] = (nodeWeightSum[id] ?? 0) + Math.abs(weight);
3798
+ }, "add");
3799
+ if (isDirected) {
3800
+ add(pair[1]);
3801
+ return;
3072
3802
  }
3803
+ add(pair[0]);
3804
+ add(pair[1]);
3073
3805
  });
3074
- const maxNodeWeight = Math.max(1, ...Object.values(nodeWeightSum));
3075
- const nodeSizes = {};
3076
3806
  model.nodes.rows().forEach((row) => {
3077
3807
  const codeIdx = row.names.indexOf(nodeIdCol);
3078
3808
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
@@ -3080,8 +3810,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3080
3810
  const pt = nodeLayer.__data__.objects.get(id);
3081
3811
  if (!pt) return;
3082
3812
  const w = nodeWeightSum[id] ?? 0;
3083
- pt.size = 2 + w / maxNodeWeight * 3;
3084
- nodeSizes[id] = pt.size;
3813
+ const unconnected = !(w > 0);
3814
+ pt.size = unconnected ? config.hideUnconnectedNodes ? 0 : UNCONNECTED_NODE_SIZE : nodeSizeForEdges(w, widthFrac);
3815
+ pt.meta = {
3816
+ ...pt.meta ?? {},
3817
+ hidden: unconnected && config.hideUnconnectedNodes
3818
+ };
3085
3819
  });
3086
3820
  const totalWeightByPair = {};
3087
3821
  if (isDirected) {
@@ -3090,15 +3824,13 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3090
3824
  totalWeightByPair[key] = (totalWeightByPair[key] ?? 0) + weight;
3091
3825
  });
3092
3826
  }
3093
- ((_l = this.__data__.extremes) == null ? void 0 : _l.x_max) ?? 1;
3094
3827
  drawEdges.forEach(({ col, pair, weight, color }) => {
3095
3828
  const fromNode = nodeLayer.__data__.objects.get(pair[0]);
3096
3829
  const toNode = nodeLayer.__data__.objects.get(pair[1]);
3097
3830
  if (!fromNode || !toNode) return;
3098
3831
  const isSelf = pair[0] === pair[1];
3099
3832
  if (isSelf && !isDirected) return;
3100
- const classMax = isSelf ? Math.max(maxSelfWeight, 1e-9) : maxEdgeWeight;
3101
- const opacity = Math.min(1, weight / classMax);
3833
+ const { saturation, opacity } = colourOf(weight, isSelf);
3102
3834
  const fromPt = new Point({ ID: `${pair[0]}-src`, dimensions: [fromNode.x, fromNode.y], meta: {} });
3103
3835
  const toPt = new Point({ ID: `${pair[1]}-dst`, dimensions: [toNode.x, toNode.y], meta: {} });
3104
3836
  let edgeLine;
@@ -3122,16 +3854,17 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3122
3854
  edgeLine.meta = {
3123
3855
  isMeanEdge: true,
3124
3856
  weight,
3125
- // NodeEdge compares within the self-loop class; cross-pair edges
3126
- // compare within their own class. Store the correct denominator
3127
- // so QELine.update() can compute opacity and display size properly.
3128
- maxWeight: isSelf ? maxSelfWeight : maxEdgeWeight,
3857
+ // Width per unit weight (extent units), shared by all marks.
3858
+ widthFrac,
3859
+ saturation,
3129
3860
  opacity,
3130
3861
  isDirected,
3131
3862
  isSelf,
3132
- // nodeSize is needed by QELine.update() to compute the NodeEdge
3133
- // display radius at update time (when extremes.x_max is available).
3134
- nodeSize: isSelf ? nodeSizes[pair[0]] ?? 2 : void 0
3863
+ // Identity for the qe-edge-* interaction events.
3864
+ source: pair[0],
3865
+ target: pair[1],
3866
+ group: groupAttr ?? void 0,
3867
+ unit: unitAttr ?? void 0
3135
3868
  };
3136
3869
  const targetLayer = isSelf ? edgenodeLayer ?? edgeLayer : edgeLayer;
3137
3870
  targetLayer.__data__.objects.set(edgeLine.ID, edgeLine);
@@ -3139,7 +3872,15 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3139
3872
  edgeLayer.__data__.__state__.setVariable("layer_updated", edgeLayer.__data__);
3140
3873
  if (edgenodeLayer) edgenodeLayer.__data__.__state__.setVariable("layer_updated", edgenodeLayer.__data__);
3141
3874
  }
3142
- if (labelSvg && labelNodes !== 0) {
3875
+ const nodesVisible = config.showNodes;
3876
+ if (nodeLayer.svg_element) {
3877
+ nodeLayer.svg_element.style.display = nodesVisible ? "" : "none";
3878
+ }
3879
+ if (!nodesVisible) {
3880
+ this._nodeLabels.forEach((lbl) => lbl.remove());
3881
+ this._nodeLabels.clear();
3882
+ }
3883
+ if (labelSvg && labelNodes !== 0 && nodesVisible) {
3143
3884
  this._nodeLabels.forEach((lbl, id) => {
3144
3885
  if (!nodeLayer.__data__.objects.get(id)) {
3145
3886
  lbl.remove();
@@ -3147,11 +3888,17 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3147
3888
  }
3148
3889
  });
3149
3890
  model.nodes.rows().forEach((row) => {
3891
+ var _a2, _b2;
3150
3892
  const codeIdx = row.names.indexOf(nodeIdCol);
3151
3893
  const id = codeIdx >= 0 ? String(row[codeIdx]) : "";
3152
3894
  if (!id) return;
3153
3895
  const pt = nodeLayer.__data__.objects.get(id);
3154
3896
  if (!pt) return;
3897
+ if ((_a2 = pt.meta) == null ? void 0 : _a2.hidden) {
3898
+ (_b2 = this._nodeLabels.get(id)) == null ? void 0 : _b2.remove();
3899
+ this._nodeLabels.delete(id);
3900
+ return;
3901
+ }
3155
3902
  let lbl = this._nodeLabels.get(id);
3156
3903
  if (!lbl) {
3157
3904
  lbl = new QELabel(id, pt, labelSvg, this, labelNodes);
@@ -3168,6 +3915,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3168
3915
  if (axisPointsLayer) this.__data__.__state__.setVariable("graph_updated_axis_points", axisPointsLayer.__data__.objects);
3169
3916
  if (axisLinesLayer) this.__data__.__state__.setVariable("graph_updated_axis_lines", axisLinesLayer.__data__.objects);
3170
3917
  this.update();
3918
+ if (tweenFrom) this._runTween(tweenFrom);
3171
3919
  });
3172
3920
  }
3173
3921
  static get observedAttributes() {
@@ -3208,17 +3956,27 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3208
3956
  // should render. Called once at the top of every model-updated handler so
3209
3957
  // all rendering decisions are derived from a single snapshot.
3210
3958
  _readLayerConfig() {
3959
+ var _a;
3211
3960
  const nodesEl = this.querySelector("qe-nodes");
3212
3961
  const meansEl = this.querySelector("qe-means");
3213
3962
  const pointsEl = this.querySelector("qe-points");
3214
3963
  const edgesEl = this.querySelector("qe-edges");
3964
+ const trajectoriesEl = this.querySelector("qe-trajectories");
3965
+ const axesEl = this.querySelector("qe-axes");
3215
3966
  const labelsShorthand = this.getAttribute("labels");
3216
3967
  return {
3217
- // Nodes — always rendered (coordinate anchors); <qe-nodes> controls labels.
3968
+ // Nodes — always placed from the model (they anchor the coordinate
3969
+ // space); <qe-nodes> presence draws them, like every other layer.
3218
3970
  labelNodes: parseLabelMode(
3219
3971
  (nodesEl == null ? void 0 : nodesEl.getAttribute("label")) ?? this.getAttribute("label-nodes") ?? labelsShorthand,
3220
3972
  Label.DISPLAY_TYPES.ON
3221
3973
  ),
3974
+ showNodes: !!nodesEl,
3975
+ edgeWidth: (() => {
3976
+ const v = Number(this.getAttribute("edge-width"));
3977
+ return Number.isFinite(v) && v > 0 && v <= 1 ? v : DEFAULT_EDGE_WIDTH;
3978
+ })(),
3979
+ hideUnconnectedNodes: ((_a = nodesEl == null ? void 0 : nodesEl.getAttribute("unconnected")) == null ? void 0 : _a.toLowerCase()) === "hide",
3222
3980
  scalePoints: this.getAttribute("scale-points") !== "false",
3223
3981
  // Means — presence of <qe-means> = show; absence = hide.
3224
3982
  showMeans: !!meansEl,
@@ -3228,20 +3986,56 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3228
3986
  (meansEl == null ? void 0 : meansEl.getAttribute("label")) ?? this.getAttribute("label-means") ?? labelsShorthand,
3229
3987
  Label.DISPLAY_TYPES.ON
3230
3988
  ),
3989
+ // fixed-size keeps mean markers a constant on-screen px size (webENA parity).
3990
+ meanFixedSize: !!meansEl && meansEl.hasAttribute("fixed-size"),
3231
3991
  // Points — presence of <qe-points> = show unit dots.
3232
3992
  showPoints: !!pointsEl,
3233
3993
  labelPoints: parseLabelMode(
3234
3994
  (pointsEl == null ? void 0 : pointsEl.getAttribute("label")) ?? this.getAttribute("label-points") ?? labelsShorthand,
3235
3995
  Label.DISPLAY_TYPES.OFF
3236
3996
  ),
3997
+ pointSize: (pointsEl == null ? void 0 : pointsEl.getAttribute("size")) ? parseFloat(pointsEl.getAttribute("size")) : null,
3998
+ // fixed-size keeps unit dots a constant on-screen px size (webENA parity).
3999
+ pointFixedSize: !!pointsEl && pointsEl.hasAttribute("fixed-size"),
3237
4000
  // Edges — presence of <qe-edges> = draw a network.
3238
4001
  showEdges: !!edgesEl,
3239
4002
  edgeGroup: (edgesEl == null ? void 0 : edgesEl.getAttribute("group")) ?? null,
3240
4003
  edgeUnit: (edgesEl == null ? void 0 : edgesEl.getAttribute("unit")) ?? null,
3241
4004
  edgeCompare: (edgesEl == null ? void 0 : edgesEl.getAttribute("compare")) ?? null,
3242
- edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null
4005
+ edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null,
4006
+ edgeColorScale: parseColorScale((edgesEl == null ? void 0 : edgesEl.getAttribute("color-scale")) ?? null),
4007
+ edgeMagnify: (edgesEl == null ? void 0 : edgesEl.hasAttribute("magnify")) ? (() => {
4008
+ const v = Number(edgesEl.getAttribute("magnify"));
4009
+ return Number.isFinite(v) && v > 0 ? v : 1;
4010
+ })() : null,
4011
+ // Trajectories — presence of <qe-trajectories> = draw cognitive trajectories
4012
+ showTrajectories: !!trajectoriesEl,
4013
+ trajectoryUnits: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("units")) ?? null,
4014
+ trajectorySmooth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("smooth")) ?? "none",
4015
+ trajectoryMaxDegree: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("max-degree")) ? parseInt(trajectoriesEl.getAttribute("max-degree"), 10) : 3,
4016
+ trajectoryDegreeSelection: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("degree-selection")) ?? "loocv_2d",
4017
+ trajectoryShowPoints: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("points")) !== "false",
4018
+ trajectoryShowTips: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("tips")) !== "false",
4019
+ trajectoryPlayhead: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("playhead")) ? parseFloat(trajectoriesEl.getAttribute("playhead")) : null,
4020
+ trajectoryLineStyle: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("line-style")) ?? "uniform",
4021
+ trajectoryStrokeWidth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width")) ? parseFloat(trajectoriesEl.getAttribute("stroke-width")) : 2,
4022
+ trajectoryStrokeWidthRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width-range"), [1, 6]),
4023
+ trajectoryOpacityRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("opacity-range"), [0.3, 1]),
4024
+ // Axes — presence of <qe-axes> = draw axis lines. The x / y attributes add
4025
+ // a per-axis label: a value is used verbatim, an empty attribute falls back
4026
+ // to the dimension name (resolved later, where dims are known). null = no
4027
+ // label for that axis.
4028
+ showAxes: !!axesEl,
4029
+ axisXLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("x")) ? axesEl.getAttribute("x") ?? "" : null,
4030
+ axisYLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("y")) ? axesEl.getAttribute("y") ?? "" : null
3243
4031
  };
3244
4032
  }
4033
+ _parseNumberPair(value, fallback) {
4034
+ if (!value) return fallback;
4035
+ const parts = value.split(",").map((part) => Number(part.trim()));
4036
+ if (parts.length < 2 || !parts.every(Number.isFinite)) return fallback;
4037
+ return [parts[0], parts[1]];
4038
+ }
3245
4039
  attributeChangedCallback(_name2, oldValue, newValue) {
3246
4040
  if (oldValue === newValue) return;
3247
4041
  this._scheduleRerender();
@@ -3250,9 +4044,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3250
4044
  var _a;
3251
4045
  const w = this.getAttribute("width");
3252
4046
  const h = this.getAttribute("height");
4047
+ this.style.display = "block";
3253
4048
  if (w) this.style.width = isNaN(Number(w)) ? w : `${w}px`;
3254
4049
  if (h) this.style.height = isNaN(Number(h)) ? h : `${h}px`;
3255
4050
  this.appendChild(this.svg_element);
4051
+ _QEGraphElement._instances.add(this);
3256
4052
  this._childObserver = new MutationObserver(() => this._scheduleRerender());
3257
4053
  this._childObserver.observe(this, {
3258
4054
  childList: true,
@@ -3263,14 +4059,32 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3263
4059
  "unit",
3264
4060
  "compare",
3265
4061
  "also",
4062
+ "color-scale",
4063
+ "magnify",
3266
4064
  "groups",
3267
4065
  "confidence",
3268
4066
  "label",
4067
+ "unconnected",
3269
4068
  "scale-points",
4069
+ "edge-width",
3270
4070
  "labels",
3271
4071
  "label-nodes",
3272
4072
  "label-means",
3273
- "label-points"
4073
+ "label-points",
4074
+ "units",
4075
+ "smooth",
4076
+ "degree",
4077
+ "points",
4078
+ "tips",
4079
+ "playhead",
4080
+ "line-style",
4081
+ "stroke-width",
4082
+ "stroke-width-range",
4083
+ "opacity-range",
4084
+ "x",
4085
+ "y",
4086
+ "size",
4087
+ "fixed-size"
3274
4088
  ]
3275
4089
  });
3276
4090
  const forId = this.getAttribute("for");
@@ -3290,13 +4104,283 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3290
4104
  }
3291
4105
  }
3292
4106
  disconnectedCallback() {
3293
- var _a;
4107
+ var _a, _b;
3294
4108
  (_a = this._childObserver) == null ? void 0 : _a.disconnect();
3295
4109
  this._childObserver = null;
4110
+ _QEGraphElement._instances.delete(this);
4111
+ (_b = this._tooltipEl) == null ? void 0 : _b.remove();
4112
+ this._tooltipEl = null;
4113
+ }
4114
+ /** All connected <qe-graph> instances (newest membership order). */
4115
+ static instances() {
4116
+ return [..._QEGraphElement._instances];
4117
+ }
4118
+ /** Look up a graph by its `id` or `plot-group` attribute. */
4119
+ static getGraph(key) {
4120
+ for (const g of _QEGraphElement._instances) {
4121
+ if (g.id === key || g.getAttribute("plot-group") === key) return g;
4122
+ }
4123
+ return null;
4124
+ }
4125
+ /** Resolved code nodes with their current graph-space coordinates. */
4126
+ getNodes() {
4127
+ const layer = this.__children__.search("__children__", "nodes");
4128
+ if (!layer) return [];
4129
+ return [...layer.__data__.objects.values()].map((o) => {
4130
+ var _a;
4131
+ return {
4132
+ id: o.ID,
4133
+ name: ((_a = o.meta) == null ? void 0 : _a.name) ?? o.ID,
4134
+ x: o.x,
4135
+ y: o.y
4136
+ };
4137
+ });
4138
+ }
4139
+ /** Resolved edges of the current network with weights and direction. */
4140
+ getEdges() {
4141
+ const collect = /* @__PURE__ */ __name((name) => {
4142
+ const l = this.__children__.search("__children__", name);
4143
+ return l ? [...l.__data__.objects.values()] : [];
4144
+ }, "collect");
4145
+ return [...collect("edges"), ...collect("edgenodes")].map((o) => {
4146
+ const m = o.meta ?? {};
4147
+ return {
4148
+ source: m.source,
4149
+ target: m.target,
4150
+ weight: m.weight ?? 0,
4151
+ directed: !!m.isDirected,
4152
+ self: !!m.isSelf
4153
+ };
4154
+ }).filter((e) => e.source != null && e.target != null);
4155
+ }
4156
+ /** Marks (circles/rects), undirected edge lines, and directed / self-edge
4157
+ * triangle paths all participate in tweening. */
4158
+ _tweenTargets() {
4159
+ return [...this.svg_element.querySelectorAll(
4160
+ "circle[data-point-id], rect[data-point-id], line[data-edge-source], path[data-edge-source]"
4161
+ )];
4162
+ }
4163
+ _readCoords(el) {
4164
+ const tag = el.tagName.toLowerCase();
4165
+ const n = /* @__PURE__ */ __name((a) => parseFloat(el.getAttribute(a) || "0"), "n");
4166
+ if (tag === "circle") return { t: "c", cx: n("cx"), cy: n("cy") };
4167
+ if (tag === "rect") return { t: "r", x: n("x"), y: n("y") };
4168
+ if (tag === "line") return { t: "l", x1: n("x1"), y1: n("y1"), x2: n("x2"), y2: n("y2") };
4169
+ if (tag === "path") {
4170
+ const d = el.getAttribute("d") || "";
4171
+ return { t: "p", d, nums: (d.match(_QEGraphElement._PATH_NUM_RE) || []).map(Number) };
4172
+ }
4173
+ return null;
4174
+ }
4175
+ /** Snapshot current tween-target positions keyed by element. */
4176
+ _captureTween() {
4177
+ const m = /* @__PURE__ */ new Map();
4178
+ this._tweenTargets().forEach((el) => {
4179
+ const c = this._readCoords(el);
4180
+ if (c) m.set(el, c);
4181
+ });
4182
+ return m;
4183
+ }
4184
+ /** Animate each still-present element from its snapshot to its new position. */
4185
+ _runTween(from) {
4186
+ if (this._tweenRaf !== null) {
4187
+ cancelAnimationFrame(this._tweenRaf);
4188
+ this._tweenRaf = null;
4189
+ }
4190
+ const to = /* @__PURE__ */ new Map();
4191
+ this._tweenTargets().forEach((el) => {
4192
+ if (from.has(el)) {
4193
+ const c = this._readCoords(el);
4194
+ if (c) to.set(el, c);
4195
+ }
4196
+ });
4197
+ if (!to.size) return;
4198
+ const dur = this._animateMs;
4199
+ const t0 = performance.now();
4200
+ const ease = /* @__PURE__ */ __name((t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2, "ease");
4201
+ const lerp = /* @__PURE__ */ __name((a, b, k) => a + (b - a) * k, "lerp");
4202
+ const step = /* @__PURE__ */ __name((now) => {
4203
+ const t = Math.min(1, (now - t0) / dur);
4204
+ const k = ease(t);
4205
+ to.forEach((nc, el) => {
4206
+ const oc = from.get(el);
4207
+ if (!oc || oc.t !== nc.t) return;
4208
+ if (nc.t === "c") {
4209
+ el.setAttribute("cx", String(lerp(oc.cx, nc.cx, k)));
4210
+ el.setAttribute("cy", String(lerp(oc.cy, nc.cy, k)));
4211
+ } else if (nc.t === "r") {
4212
+ el.setAttribute("x", String(lerp(oc.x, nc.x, k)));
4213
+ el.setAttribute("y", String(lerp(oc.y, nc.y, k)));
4214
+ } else if (nc.t === "l") {
4215
+ el.setAttribute("x1", String(lerp(oc.x1, nc.x1, k)));
4216
+ el.setAttribute("y1", String(lerp(oc.y1, nc.y1, k)));
4217
+ el.setAttribute("x2", String(lerp(oc.x2, nc.x2, k)));
4218
+ el.setAttribute("y2", String(lerp(oc.y2, nc.y2, k)));
4219
+ } else {
4220
+ if (!oc.nums || oc.nums.length !== nc.nums.length) return;
4221
+ let i = 0;
4222
+ const d = nc.d.replace(
4223
+ _QEGraphElement._PATH_NUM_RE,
4224
+ () => String(lerp(oc.nums[i], nc.nums[i++], k))
4225
+ );
4226
+ el.setAttribute("d", d);
4227
+ }
4228
+ });
4229
+ this._tweenRaf = t < 1 ? requestAnimationFrame(step) : null;
4230
+ }, "step");
4231
+ this._tweenRaf = requestAnimationFrame(step);
4232
+ }
4233
+ /** Serialize the current plot as a standalone SVG string (webENA P2.3). */
4234
+ toSVG() {
4235
+ const svg = this.svg_element.cloneNode(true);
4236
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
4237
+ svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
4238
+ const rect = this.svg_element.getBoundingClientRect();
4239
+ if (rect.width) svg.setAttribute("width", String(Math.round(rect.width)));
4240
+ if (rect.height) svg.setAttribute("height", String(Math.round(rect.height)));
4241
+ return new XMLSerializer().serializeToString(svg);
4242
+ }
4243
+ /** Rasterize the current plot to a PNG data URL (webENA P2.3). `scale`
4244
+ * multiplies the pixel dimensions for a higher-resolution image. */
4245
+ toPNG(scale = 2) {
4246
+ const svgStr = this.toSVG();
4247
+ const rect = this.svg_element.getBoundingClientRect();
4248
+ const w = Math.max(1, Math.round(rect.width));
4249
+ const h = Math.max(1, Math.round(rect.height));
4250
+ return new Promise((resolve, reject) => {
4251
+ const blob = new Blob([svgStr], { type: "image/svg+xml;charset=utf-8" });
4252
+ const url = URL.createObjectURL(blob);
4253
+ const img = new Image();
4254
+ img.onload = () => {
4255
+ try {
4256
+ const canvas = document.createElement("canvas");
4257
+ canvas.width = w * scale;
4258
+ canvas.height = h * scale;
4259
+ const ctx = canvas.getContext("2d");
4260
+ if (!ctx) {
4261
+ URL.revokeObjectURL(url);
4262
+ reject(new Error("no 2d context"));
4263
+ return;
4264
+ }
4265
+ ctx.scale(scale, scale);
4266
+ ctx.drawImage(img, 0, 0, w, h);
4267
+ URL.revokeObjectURL(url);
4268
+ resolve(canvas.toDataURL("image/png"));
4269
+ } catch (err) {
4270
+ URL.revokeObjectURL(url);
4271
+ reject(err);
4272
+ }
4273
+ };
4274
+ img.onerror = (e) => {
4275
+ URL.revokeObjectURL(url);
4276
+ reject(e);
4277
+ };
4278
+ img.src = url;
4279
+ });
4280
+ }
4281
+ /** Unit points with their current graph-space coordinates. */
4282
+ getPoints() {
4283
+ const layer = this.__children__.search("__children__", "points");
4284
+ if (!layer) return [];
4285
+ return [...layer.__data__.objects.values()].filter((o) => {
4286
+ var _a;
4287
+ return ((_a = o.meta) == null ? void 0 : _a.kind) === "point";
4288
+ }).map((o) => {
4289
+ var _a;
4290
+ return { id: o.ID, group: (_a = o.meta) == null ? void 0 : _a.group, x: o.x, y: o.y };
4291
+ });
4292
+ }
4293
+ /** Ids of unit points inside a graph-space rectangle (for drag/lasso select). */
4294
+ pointsInRect(rect) {
4295
+ return this.getPoints().filter((p) => p.x >= rect.x && p.x <= rect.x + rect.w && p.y >= rect.y && p.y <= rect.y + rect.h).map((p) => p.id);
4296
+ }
4297
+ /** Order-independent key so an undirected pair matches either direction. */
4298
+ _edgeKey(a, b) {
4299
+ return a <= b ? `${a}\0${b}` : `${b}\0${a}`;
4300
+ }
4301
+ /** Mark edges/nodes as highlighted. Replaces the previous selection. */
4302
+ setHighlight(sel) {
4303
+ this._highlight.edges = new Set((sel.edges ?? []).map((e) => this._edgeKey(e.source, e.target)));
4304
+ this._highlight.nodes = new Set((sel.nodes ?? []).map(String));
4305
+ this._applyHighlights();
4306
+ }
4307
+ /** Clear all highlighting. */
4308
+ clearHighlight() {
4309
+ this._highlight.edges.clear();
4310
+ this._highlight.nodes.clear();
4311
+ this._applyHighlights();
4312
+ }
4313
+ _applyHighlights() {
4314
+ if (!this.svg_element) return;
4315
+ if (!this._highlightGroup) {
4316
+ this._highlightGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
4317
+ this._highlightGroup.setAttribute("data-layer-name", "highlight");
4318
+ this._highlightGroup.setAttribute("pointer-events", "none");
4319
+ }
4320
+ this.svg_element.appendChild(this._highlightGroup);
4321
+ const g = this._highlightGroup;
4322
+ g.textContent = "";
4323
+ const extent = this.__data__.extremes.x_max || 1;
4324
+ const HL = "#f5a623";
4325
+ const mk = /* @__PURE__ */ __name((tag, attrs) => {
4326
+ const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
4327
+ for (const k in attrs) el.setAttribute(k, String(attrs[k]));
4328
+ return el;
4329
+ }, "mk");
4330
+ if (this._highlight.edges.size) {
4331
+ const xy = {};
4332
+ this.getNodes().forEach((n) => {
4333
+ xy[n.id] = { x: n.x, y: n.y };
4334
+ });
4335
+ this._highlight.edges.forEach((key) => {
4336
+ const [a, b] = key.split("\0");
4337
+ const A = xy[a], B = xy[b];
4338
+ if (!A || !B) return;
4339
+ g.appendChild(mk("line", {
4340
+ x1: A.x,
4341
+ y1: A.y,
4342
+ x2: B.x,
4343
+ y2: B.y,
4344
+ stroke: HL,
4345
+ "stroke-width": extent * 0.02,
4346
+ "stroke-opacity": 0.6,
4347
+ "stroke-linecap": "round"
4348
+ }));
4349
+ });
4350
+ }
4351
+ this._highlight.nodes.forEach((id) => {
4352
+ var _a;
4353
+ const el = this.svg_element.querySelector(`[data-point-id="${((_a = window.CSS) == null ? void 0 : _a.escape) ? CSS.escape(id) : id}"]`);
4354
+ if (!el) return;
4355
+ const tag = el.tagName.toLowerCase();
4356
+ let cx, cy, r;
4357
+ if (tag === "rect") {
4358
+ const w = parseFloat(el.getAttribute("width") || "0");
4359
+ cx = parseFloat(el.getAttribute("x") || "0") + w / 2;
4360
+ cy = parseFloat(el.getAttribute("y") || "0") + parseFloat(el.getAttribute("height") || "0") / 2;
4361
+ r = w / 2;
4362
+ } else {
4363
+ cx = parseFloat(el.getAttribute("cx") || "0");
4364
+ cy = parseFloat(el.getAttribute("cy") || "0");
4365
+ r = parseFloat(el.getAttribute("r") || "0");
4366
+ }
4367
+ g.appendChild(mk("circle", {
4368
+ cx,
4369
+ cy,
4370
+ r: r * 1.7 + extent * 4e-3,
4371
+ fill: "none",
4372
+ stroke: HL,
4373
+ "stroke-width": extent * 6e-3
4374
+ }));
4375
+ });
3296
4376
  }
3297
4377
  create() {
3298
4378
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3299
4379
  svg.classList.add("graph");
4380
+ svg.style.display = "block";
4381
+ svg.style.width = "100%";
4382
+ svg.style.height = "100%";
4383
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
3300
4384
  svg.setAttribute("viewBox", this.__data__.extremes.as_viewBox());
3301
4385
  svg.setAttribute("transform", this.__data__.transform.toString());
3302
4386
  svg.setAttribute("role", "figure");
@@ -3406,6 +4490,138 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3406
4490
  const lbl = getAutoLabel(id);
3407
4491
  if ((lbl == null ? void 0 : lbl.displayMode) === Label.DISPLAY_TYPES.AUTO) lbl.hide();
3408
4492
  });
4493
+ this._attachInteractionEvents();
4494
+ }
4495
+ /** Emit a namespaced CustomEvent on the public element (bubbles + composed). */
4496
+ _emit(name, detail) {
4497
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
4498
+ }
4499
+ /** Walk up from a hit element to the nearest ancestor carrying `attr`. */
4500
+ _closestWith(el, attr) {
4501
+ var _a, _b;
4502
+ while (el && el !== this.svg_element) {
4503
+ if (((_a = el.getAttribute) == null ? void 0 : _a.call(el, attr)) != null && ((_b = el.hasAttribute) == null ? void 0 : _b.call(el, attr))) return el;
4504
+ el = el.parentElement;
4505
+ }
4506
+ return null;
4507
+ }
4508
+ /** Resolve an edge hit element to a qe-edge-* detail, or null. */
4509
+ _edgeDetail(el) {
4510
+ const e = this._closestWith(el, "data-edge-source");
4511
+ if (!e) return null;
4512
+ const d = {
4513
+ source: e.getAttribute("data-edge-source"),
4514
+ target: e.getAttribute("data-edge-target"),
4515
+ weight: parseFloat(e.getAttribute("data-edge-weight") || "0"),
4516
+ directed: e.getAttribute("data-edge-directed") === "true",
4517
+ self: e.getAttribute("data-edge-self") === "true"
4518
+ };
4519
+ if (e.hasAttribute("data-edge-group")) d.group = e.getAttribute("data-edge-group");
4520
+ if (e.hasAttribute("data-edge-unit")) d.unit = e.getAttribute("data-edge-unit");
4521
+ return d;
4522
+ }
4523
+ /** Resolve a node/mean/point hit element to a qe-node-* detail, or null. */
4524
+ _nodeDetail(el) {
4525
+ const n = this._closestWith(el, "data-point-id");
4526
+ if (!n) return null;
4527
+ const tag = n.tagName.toLowerCase();
4528
+ let x, y;
4529
+ if (tag === "rect") {
4530
+ x = parseFloat(n.getAttribute("x") || "0") + parseFloat(n.getAttribute("width") || "0") / 2;
4531
+ y = parseFloat(n.getAttribute("y") || "0") + parseFloat(n.getAttribute("height") || "0") / 2;
4532
+ } else {
4533
+ x = parseFloat(n.getAttribute("cx") || "0");
4534
+ y = parseFloat(n.getAttribute("cy") || "0");
4535
+ }
4536
+ const id = n.getAttribute("data-point-id");
4537
+ const d = {
4538
+ id,
4539
+ kind: n.getAttribute("data-kind") || "point",
4540
+ name: n.getAttribute("data-name") || id,
4541
+ x,
4542
+ y
4543
+ };
4544
+ if (n.hasAttribute("data-group")) d.group = n.getAttribute("data-group");
4545
+ if (n.hasAttribute("data-unit")) d.unit = n.getAttribute("data-unit");
4546
+ return d;
4547
+ }
4548
+ _attachInteractionEvents() {
4549
+ const svg = this.svg_element;
4550
+ svg.addEventListener("click", (ev) => {
4551
+ const target = ev.target;
4552
+ const edge = this._edgeDetail(target);
4553
+ if (edge) {
4554
+ this._emit("qe-edge-click", { ...edge, originalEvent: ev });
4555
+ return;
4556
+ }
4557
+ const node = this._nodeDetail(target);
4558
+ if (node) this._emit("qe-node-click", { ...node, originalEvent: ev });
4559
+ });
4560
+ let hovered = null;
4561
+ const keyOf = /* @__PURE__ */ __name((t, d) => t === "edge" ? `${d.source}\0${d.target}` : String(d.id), "keyOf");
4562
+ svg.addEventListener("mousemove", (ev) => {
4563
+ const target = ev.target;
4564
+ const edge = this._edgeDetail(target);
4565
+ const node = edge ? null : this._nodeDetail(target);
4566
+ const next = edge ? { type: "edge", detail: edge } : node ? { type: "node", detail: node } : null;
4567
+ const nextKey = next ? keyOf(next.type, next.detail) : null;
4568
+ if (hovered && (!next || hovered.key !== nextKey)) {
4569
+ this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
4570
+ hovered = null;
4571
+ }
4572
+ if (next && (!hovered || hovered.key !== nextKey)) {
4573
+ this._emit(next.type === "edge" ? "qe-edge-hover" : "qe-node-hover", { ...next.detail, originalEvent: ev });
4574
+ hovered = { type: next.type, key: nextKey };
4575
+ }
4576
+ if (this.hasAttribute("tooltip") && next) {
4577
+ this._showTooltip(this._tooltipText(next.type, next.detail), ev.clientX, ev.clientY);
4578
+ } else {
4579
+ this._hideTooltip();
4580
+ }
4581
+ });
4582
+ svg.addEventListener("mouseleave", () => {
4583
+ if (hovered) {
4584
+ this._emit(hovered.type === "edge" ? "qe-edge-unhover" : "qe-node-unhover", { key: hovered.key });
4585
+ hovered = null;
4586
+ }
4587
+ this._hideTooltip();
4588
+ });
4589
+ }
4590
+ /** "source – target (weight)" for edges; the mark name for nodes/means/points. */
4591
+ _tooltipText(type, d) {
4592
+ if (type === "edge") {
4593
+ const w = Number(d.weight);
4594
+ const wStr = Number.isFinite(w) ? ` (${+w.toFixed(3)})` : "";
4595
+ return `${d.source} – ${d.target}${wStr}`;
4596
+ }
4597
+ return String(d.name ?? d.id);
4598
+ }
4599
+ _showTooltip(text, clientX, clientY) {
4600
+ if (!this._tooltipEl) {
4601
+ const el = document.createElement("div");
4602
+ el.setAttribute("data-qe-tooltip", "");
4603
+ Object.assign(el.style, {
4604
+ position: "fixed",
4605
+ zIndex: "2147483647",
4606
+ pointerEvents: "none",
4607
+ background: "rgba(20,20,20,0.92)",
4608
+ color: "#fff",
4609
+ font: "12px/1.4 system-ui, sans-serif",
4610
+ padding: "3px 7px",
4611
+ borderRadius: "4px",
4612
+ whiteSpace: "nowrap",
4613
+ transform: "translate(-50%, calc(-100% - 8px))"
4614
+ });
4615
+ document.body.appendChild(el);
4616
+ this._tooltipEl = el;
4617
+ }
4618
+ this._tooltipEl.textContent = text;
4619
+ this._tooltipEl.style.left = `${clientX}px`;
4620
+ this._tooltipEl.style.top = `${clientY}px`;
4621
+ this._tooltipEl.style.display = "block";
4622
+ }
4623
+ _hideTooltip() {
4624
+ if (this._tooltipEl) this._tooltipEl.style.display = "none";
3409
4625
  }
3410
4626
  draw() {
3411
4627
  var _a;
@@ -3423,6 +4639,78 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3423
4639
  this.update_layers();
3424
4640
  this.update_labels();
3425
4641
  this.update_extremes();
4642
+ this.update_axes();
4643
+ this._applyHighlights();
4644
+ }
4645
+ /**
4646
+ * Axes (<qe-axes>): toggle the axis-line cross by presence, and draw/position
4647
+ * per-axis labels at the positive ends using the current extent. Labels are
4648
+ * cached SVG <text> nodes in the labels group; null text removes them.
4649
+ */
4650
+ update_axes() {
4651
+ var _a;
4652
+ const axesLayer = this.__children__.search("__children__", "axes");
4653
+ if (axesLayer == null ? void 0 : axesLayer.svg_element) {
4654
+ axesLayer.svg_element.style.display = this._showAxes ? "" : "none";
4655
+ }
4656
+ const parent = this._labelsGroup;
4657
+ const extent = this.__data__.extremes.x_max;
4658
+ if (!parent || extent <= 0) return;
4659
+ const fontSize = extent * 0.06;
4660
+ const gap = fontSize * 0.6;
4661
+ const place = /* @__PURE__ */ __name((axis, text) => {
4662
+ let el = this._axisLabelEls.get(axis);
4663
+ if (!this._showAxes || text == null) {
4664
+ if (el) {
4665
+ el.remove();
4666
+ this._axisLabelEls.delete(axis);
4667
+ }
4668
+ return;
4669
+ }
4670
+ if (!el) {
4671
+ el = document.createElementNS("http://www.w3.org/2000/svg", "text");
4672
+ el.setAttribute("data-axis-label", axis);
4673
+ el.setAttribute("pointer-events", "none");
4674
+ parent.appendChild(el);
4675
+ this._axisLabelEls.set(axis, el);
4676
+ }
4677
+ el.textContent = text;
4678
+ el.setAttribute("font-size", fontSize.toString());
4679
+ el.setAttribute("font-family", "system-ui, sans-serif");
4680
+ el.setAttribute("font-weight", "600");
4681
+ el.setAttribute("fill", "#556");
4682
+ if (axis === "x") {
4683
+ el.setAttribute("x", (extent - gap).toString());
4684
+ el.setAttribute("y", (-gap).toString());
4685
+ el.setAttribute("text-anchor", "end");
4686
+ } else {
4687
+ el.setAttribute("x", gap.toString());
4688
+ el.setAttribute("y", (-extent + fontSize + gap).toString());
4689
+ el.setAttribute("text-anchor", "start");
4690
+ }
4691
+ }, "place");
4692
+ place("x", this._axisXText);
4693
+ place("y", this._axisYText);
4694
+ if (this._magnifyText == null) {
4695
+ (_a = this._magnifyEl) == null ? void 0 : _a.remove();
4696
+ this._magnifyEl = null;
4697
+ } else {
4698
+ if (!this._magnifyEl) {
4699
+ this._magnifyEl = document.createElementNS("http://www.w3.org/2000/svg", "text");
4700
+ this._magnifyEl.setAttribute("data-magnify-label", "");
4701
+ this._magnifyEl.setAttribute("pointer-events", "none");
4702
+ }
4703
+ if (!this._magnifyEl.parentElement) parent.appendChild(this._magnifyEl);
4704
+ const el = this._magnifyEl;
4705
+ el.textContent = this._magnifyText;
4706
+ el.setAttribute("font-size", (fontSize * 0.85).toString());
4707
+ el.setAttribute("font-family", "system-ui, sans-serif");
4708
+ el.setAttribute("font-style", "italic");
4709
+ el.setAttribute("fill", "#556");
4710
+ el.setAttribute("x", (-extent + gap).toString());
4711
+ el.setAttribute("y", (-extent + fontSize + gap).toString());
4712
+ el.setAttribute("text-anchor", "start");
4713
+ }
3426
4714
  }
3427
4715
  update_labels() {
3428
4716
  this._nodeLabels.forEach((lbl) => lbl.update());
@@ -3486,8 +4774,12 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3486
4774
  background-color: #FFF;
3487
4775
  }
3488
4776
  `;
4777
+ _QEGraphElement._instances = /* @__PURE__ */ new Set();
4778
+ _QEGraphElement._PATH_NUM_RE = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
3489
4779
  let QEGraphElement = _QEGraphElement;
3490
4780
  customElements.define(QEGraphElement.tagName, QEGraphElement);
4781
+ const getGraph = /* @__PURE__ */ __name((key) => QEGraphElement.getGraph(key), "getGraph");
4782
+ const getGraphs = /* @__PURE__ */ __name(() => QEGraphElement.instances(), "getGraphs");
3491
4783
  exports2.Color = Color;
3492
4784
  exports2.DataFrame = DataFrame;
3493
4785
  exports2.DataFrameColumn = DataFrameColumn;
@@ -3495,6 +4787,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3495
4787
  exports2.DataFrameRow = DataFrameRow;
3496
4788
  exports2.Edge = Edge;
3497
4789
  exports2.Graph = Graph;
4790
+ exports2.QEAxesElement = QEAxesElement;
3498
4791
  exports2.QEEdgesElement = QEEdgesElement;
3499
4792
  exports2.QEGraphElement = QEGraphElement;
3500
4793
  exports2.QELayer = QELayer;
@@ -3503,8 +4796,18 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
3503
4796
  exports2.QENodesElement = QENodesElement;
3504
4797
  exports2.QEPoint = QEPoint;
3505
4798
  exports2.QEPointsElement = QEPointsElement;
4799
+ exports2.QETrajectoriesElement = QETrajectoriesElement;
4800
+ exports2.QETrajectory = QETrajectory;
3506
4801
  exports2.QEVisualElement = QEVisualElement;
4802
+ exports2.TRAJECTORY_ZERO_EPS = TRAJECTORY_ZERO_EPS;
4803
+ exports2.Trajectory = Trajectory;
3507
4804
  exports2.WeightedColor = WeightedColor;
4805
+ exports2.fitTrajectory = fitTrajectory;
4806
+ exports2.getGraph = getGraph;
4807
+ exports2.getGraphs = getGraphs;
4808
+ exports2.nearOriginFilter = nearOriginFilter;
4809
+ exports2.sampleCurve = sampleCurve;
4810
+ exports2.selectDegree = selectDegree;
3508
4811
  exports2.vizENA = vizENA;
3509
4812
  Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
3510
4813
  }));