@qe-libs/qeviz 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/qeviz.es.js CHANGED
@@ -160,7 +160,7 @@ const _DataFrame = class _DataFrame extends Array {
160
160
  }
161
161
  });
162
162
  __privateSet(this, _names2, names);
163
- if (rowNames && rowNames.length !== this[0].length) {
163
+ if (rowNames && this[0] && rowNames.length !== this[0].length) {
164
164
  throw new Error("Row names length must match the number of rows in the DataFrame.");
165
165
  }
166
166
  if (rowNames && rowNames.length > 0) {
@@ -224,6 +224,9 @@ const _DataFrame = class _DataFrame extends Array {
224
224
  rows(...wh) {
225
225
  let selectors, selectedRows = [], selectedRowNames = [];
226
226
  if (wh.length === 0) {
227
+ if (!this[0] || this.length === 0) {
228
+ return [];
229
+ }
227
230
  const numRows = this[0].length;
228
231
  selectedRows = Array.from({ length: numRows }, (_, i) => this.getRowByIndex(i));
229
232
  selectedRowNames = __privateGet(this, _rowNames2);
@@ -282,7 +285,7 @@ const _DataFrame = class _DataFrame extends Array {
282
285
  return __privateGet(this, _rowNames2);
283
286
  }
284
287
  get nrow() {
285
- return this[0].length;
288
+ return this[0] ? this[0].length : 0;
286
289
  }
287
290
  get ncol() {
288
291
  return this.length;
@@ -349,6 +352,12 @@ const _Model = class _Model {
349
352
  set groups(v) {
350
353
  this._groups = v;
351
354
  }
355
+ get trajectories() {
356
+ return this._trajectories;
357
+ }
358
+ set trajectories(v) {
359
+ this._trajectories = v;
360
+ }
352
361
  get dimensions() {
353
362
  return this._dimensions;
354
363
  }
@@ -461,7 +470,7 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
461
470
  */
462
471
  setModelData(data) {
463
472
  var _a, _b, _c, _d;
464
- const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier"];
473
+ const frameKeys = ["nodes", "edges", "points", "groups", "confidence", "outlier", "trajectories"];
465
474
  frameKeys.forEach((name) => {
466
475
  var _a2;
467
476
  const raw = data[name];
@@ -470,7 +479,7 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
470
479
  let idCol;
471
480
  if (name === "nodes") {
472
481
  idCol = data.node_id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
473
- } else if (name === "edges" || name === "points") {
482
+ } else if (name === "edges" || name === "points" || name === "trajectories") {
474
483
  idCol = data.id_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
475
484
  } else {
476
485
  idCol = data.group_col ?? colNames.find((c) => (types[c] ?? "character") === "character");
@@ -540,8 +549,12 @@ const _QEVisualElement = class _QEVisualElement extends HTMLElement {
540
549
  __name(_QEVisualElement, "QEVisualElement");
541
550
  _QEVisualElement.tagName = "qe-visual";
542
551
  _QEVisualElement.styles = `
543
- :host { display: inline-block; }
552
+ :host { display: block; width: 100%; }
544
553
  :host(.hidden) { display: none !important; }
554
+ /* The output wrapper and the slotted graph fill the host so the plot
555
+ stretches to its container instead of collapsing to intrinsic size. */
556
+ div { display: block; width: 100%; height: 100%; }
557
+ ::slotted(qe-graph) { display: block; width: 100%; height: 100%; }
545
558
  `;
546
559
  let QEVisualElement = _QEVisualElement;
547
560
  customElements.define(QEVisualElement.tagName, QEVisualElement);
@@ -812,7 +825,9 @@ var Tools;
812
825
  __name(slopeIntercept, "slopeIntercept");
813
826
  Line2.slopeIntercept = slopeIntercept;
814
827
  function point_along(line, distance = 0) {
815
- let len = Tools2.Geometry.Line.length(line[0], line[1]), ratio = distance / len, Xt = (1 - ratio) * line[0][0] + ratio * line[1][0], Yt = (1 - ratio) * line[0][1] + ratio * line[1][1];
828
+ const ratio = distance;
829
+ const Xt = (1 - ratio) * line[0][0] + ratio * line[1][0];
830
+ const Yt = (1 - ratio) * line[0][1] + ratio * line[1][1];
816
831
  return [Xt, Yt];
817
832
  }
818
833
  __name(point_along, "point_along");
@@ -1659,6 +1674,7 @@ const _Graph = class _Graph {
1659
1674
  this.points = /* @__PURE__ */ new Map();
1660
1675
  this.nodes = /* @__PURE__ */ new Map();
1661
1676
  this.edges = /* @__PURE__ */ new Map();
1677
+ this.trajectories = /* @__PURE__ */ new Map();
1662
1678
  this.labels = /* @__PURE__ */ new Map();
1663
1679
  this.transform = new Transform();
1664
1680
  this.scale_x = 1;
@@ -1701,6 +1717,7 @@ const _Graph = class _Graph {
1701
1717
  { name: "edgenodes", layers: [], tabindex: 0 }
1702
1718
  ]
1703
1719
  },
1720
+ { name: "trajectories", layers: [], tabindex: 0 },
1704
1721
  { name: "points", layers: [], tabindex: 0 },
1705
1722
  { name: "labels", layers: [], tabindex: 0 }
1706
1723
  ] }
@@ -1826,9 +1843,13 @@ const _Graph = class _Graph {
1826
1843
  if (search_layers.length === 0) {
1827
1844
  this.extremes = new Extremes(1, 1);
1828
1845
  } else {
1829
- const found_max = Math.max(...search_layers.map((l) => Math.max(...[...l.objects.values()].map((o) => Math.max(Math.abs(o.x), Math.abs(o.y))))).filter((a) => Math.abs(a) !== Infinity)) * 1.5;
1830
- console.log("Found max for extremes:", found_max);
1831
- this.extremes = new Extremes(Math.ceil(found_max), Math.ceil(found_max));
1846
+ const content_max = Math.max(
1847
+ ...search_layers.map((l) => Math.max(
1848
+ ...[...l.objects.values()].map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))
1849
+ )).filter((a) => Math.abs(a) !== Infinity)
1850
+ );
1851
+ const found_max = (Number.isFinite(content_max) && content_max > 0 ? content_max : 1) * 1.3;
1852
+ this.extremes = new Extremes(found_max, found_max);
1832
1853
  }
1833
1854
  }
1834
1855
  base_point_size(scale = 8) {
@@ -1868,6 +1889,251 @@ _DrawableElement.style = `
1868
1889
  }
1869
1890
  `;
1870
1891
  let DrawableElement = _DrawableElement;
1892
+ const TRAJECTORY_ZERO_EPS = 1e-8;
1893
+ function nearOriginFilter(pts, eps = TRAJECTORY_ZERO_EPS) {
1894
+ return pts.filter((p) => Math.abs(p.x) > eps || Math.abs(p.y) > eps);
1895
+ }
1896
+ __name(nearOriginFilter, "nearOriginFilter");
1897
+ function normalizedT(n) {
1898
+ if (n <= 0) return [];
1899
+ if (n === 1) return [0];
1900
+ const t = new Array(n);
1901
+ for (let i = 0; i < n; i++) t[i] = i / (n - 1);
1902
+ return t;
1903
+ }
1904
+ __name(normalizedT, "normalizedT");
1905
+ function design(t, degree) {
1906
+ return t.map((ti) => {
1907
+ const row = new Array(degree + 1);
1908
+ let p = 1;
1909
+ for (let k = 0; k <= degree; k++) {
1910
+ row[k] = p;
1911
+ p *= ti;
1912
+ }
1913
+ return row;
1914
+ });
1915
+ }
1916
+ __name(design, "design");
1917
+ function qr(A) {
1918
+ const n = A.length;
1919
+ const m = A[0].length;
1920
+ const Q = A.map((row) => row.slice());
1921
+ const R = Array.from({ length: m }, () => new Array(m).fill(0));
1922
+ for (let j = 0; j < m; j++) {
1923
+ let norm = 0;
1924
+ for (let i = 0; i < n; i++) norm += Q[i][j] * Q[i][j];
1925
+ norm = Math.sqrt(norm);
1926
+ R[j][j] = norm;
1927
+ if (norm > 1e-300) {
1928
+ for (let i = 0; i < n; i++) Q[i][j] /= norm;
1929
+ }
1930
+ for (let k = j + 1; k < m; k++) {
1931
+ let dot = 0;
1932
+ for (let i = 0; i < n; i++) dot += Q[i][j] * Q[i][k];
1933
+ R[j][k] = dot;
1934
+ for (let i = 0; i < n; i++) Q[i][k] -= dot * Q[i][j];
1935
+ }
1936
+ }
1937
+ return { Q, R };
1938
+ }
1939
+ __name(qr, "qr");
1940
+ function qtY(Q, y) {
1941
+ const n = Q.length;
1942
+ const m = Q[0].length;
1943
+ const out = new Array(m).fill(0);
1944
+ for (let j = 0; j < m; j++) {
1945
+ let s = 0;
1946
+ for (let i = 0; i < n; i++) s += Q[i][j] * y[i];
1947
+ out[j] = s;
1948
+ }
1949
+ return out;
1950
+ }
1951
+ __name(qtY, "qtY");
1952
+ function backSolve(R, b) {
1953
+ const m = b.length;
1954
+ const c = new Array(m).fill(0);
1955
+ for (let i = m - 1; i >= 0; i--) {
1956
+ let s = b[i];
1957
+ for (let j = i + 1; j < m; j++) s -= R[i][j] * c[j];
1958
+ c[i] = Math.abs(R[i][i]) > 1e-12 ? s / R[i][i] : 0;
1959
+ }
1960
+ return c;
1961
+ }
1962
+ __name(backSolve, "backSolve");
1963
+ function leverages(Q) {
1964
+ return Q.map((row) => {
1965
+ let s = 0;
1966
+ for (let j = 0; j < row.length; j++) s += row[j] * row[j];
1967
+ return s;
1968
+ });
1969
+ }
1970
+ __name(leverages, "leverages");
1971
+ function fitCoeffs(t, y, degree) {
1972
+ const X = design(t, degree);
1973
+ const { Q, R } = qr(X);
1974
+ return backSolve(R, qtY(Q, y));
1975
+ }
1976
+ __name(fitCoeffs, "fitCoeffs");
1977
+ function evalPoly(coeffs, t) {
1978
+ let v = 0;
1979
+ let p = 1;
1980
+ for (let k = 0; k < coeffs.length; k++) {
1981
+ v += coeffs[k] * p;
1982
+ p *= t;
1983
+ }
1984
+ return v;
1985
+ }
1986
+ __name(evalPoly, "evalPoly");
1987
+ function loocv2d(t, x, y, degree) {
1988
+ const X = design(t, degree);
1989
+ const { Q, R } = qr(X);
1990
+ const h = leverages(Q);
1991
+ const cx = backSolve(R, qtY(Q, x));
1992
+ const cy = backSolve(R, qtY(Q, y));
1993
+ let press = 0;
1994
+ for (let i = 0; i < t.length; i++) {
1995
+ const denom = 1 - h[i];
1996
+ if (denom <= 0) return Infinity;
1997
+ const ex = x[i] - evalPoly(cx, t[i]);
1998
+ const ey = y[i] - evalPoly(cy, t[i]);
1999
+ press += (ex / denom) ** 2 + (ey / denom) ** 2;
2000
+ }
2001
+ return press;
2002
+ }
2003
+ __name(loocv2d, "loocv2d");
2004
+ function selectDegree(fitPts, maxDegree, mode) {
2005
+ const n = fitPts.length;
2006
+ const cap = Math.min(Math.max(1, Math.floor(maxDegree)), Math.max(1, n - 1));
2007
+ if (mode === "fixed" || n < 3) return cap;
2008
+ const t = normalizedT(n);
2009
+ const x = fitPts.map((p) => p.x);
2010
+ const y = fitPts.map((p) => p.y);
2011
+ let best = 1;
2012
+ let bestVal = Infinity;
2013
+ for (let deg = 1; deg <= cap; deg++) {
2014
+ const v = loocv2d(t, x, y, deg);
2015
+ if (v < bestVal) {
2016
+ bestVal = v;
2017
+ best = deg;
2018
+ }
2019
+ }
2020
+ return best;
2021
+ }
2022
+ __name(selectDegree, "selectDegree");
2023
+ function fitTrajectory(activePts, opts) {
2024
+ const fitPts = nearOriginFilter(activePts);
2025
+ if (fitPts.length < 3) return null;
2026
+ const selBasis = opts.selectionSet ? nearOriginFilter(opts.selectionSet) : fitPts;
2027
+ const basis = selBasis.length >= 3 ? selBasis : fitPts;
2028
+ let degree = selectDegree(basis, opts.maxDegree, opts.degreeSelection);
2029
+ degree = Math.min(degree, Math.max(1, fitPts.length - 1));
2030
+ const t = normalizedT(fitPts.length);
2031
+ const coeffsX = fitCoeffs(t, fitPts.map((p) => p.x), degree);
2032
+ const coeffsY = fitCoeffs(t, fitPts.map((p) => p.y), degree);
2033
+ return { degree, coeffsX, coeffsY };
2034
+ }
2035
+ __name(fitTrajectory, "fitTrajectory");
2036
+ function sampleCurve(fit, steps = 100) {
2037
+ const out = [];
2038
+ for (let s = 0; s <= steps; s++) {
2039
+ const t = s / steps;
2040
+ out.push({ x: evalPoly(fit.coeffsX, t), y: evalPoly(fit.coeffsY, t) });
2041
+ }
2042
+ return out;
2043
+ }
2044
+ __name(sampleCurve, "sampleCurve");
2045
+ const TRAJECTORY_CURVE_STEPS = 100;
2046
+ const _Trajectory = class _Trajectory extends PlottedObject {
2047
+ constructor(options = {}) {
2048
+ const defaultColor = options.color instanceof Color ? options.color : new Color(typeof options.color === "string" ? options.color : "#000000");
2049
+ super([], {
2050
+ ID: options.ID || `traj-${Math.random().toString(36).substring(2, 9)}`,
2051
+ size: options.size || 1,
2052
+ color: defaultColor,
2053
+ highlight: null,
2054
+ active: options.active || false,
2055
+ meta: options.meta || {},
2056
+ ...options
2057
+ });
2058
+ this._typeName = "Trajectory";
2059
+ this.trajPoints = [];
2060
+ this.smooth = "none";
2061
+ this.maxDegree = 3;
2062
+ this.degreeSelection = "loocv_2d";
2063
+ this.selectedDegree = -1;
2064
+ this.polyCoeffsX = [];
2065
+ this.polyCoeffsY = [];
2066
+ this.currentTime = Infinity;
2067
+ this.showPoints = true;
2068
+ this.showTips = true;
2069
+ this.lineStyle = "uniform";
2070
+ this.strokeWidth = 2;
2071
+ this.strokeWidthRange = [1, 6];
2072
+ this.opacityRange = [0.3, 1];
2073
+ this.smooth = options.smooth || "none";
2074
+ this.maxDegree = options.maxDegree !== void 0 ? options.maxDegree : 3;
2075
+ this.degreeSelection = options.degreeSelection || "loocv_2d";
2076
+ this.polyCoeffsX = options.polyCoeffsX || [];
2077
+ this.polyCoeffsY = options.polyCoeffsY || [];
2078
+ this.currentTime = options.currentTime !== void 0 ? options.currentTime : Infinity;
2079
+ this.showPoints = options.showPoints !== void 0 ? options.showPoints : true;
2080
+ this.showTips = options.showTips !== void 0 ? options.showTips : true;
2081
+ this.lineStyle = options.lineStyle || "uniform";
2082
+ this.strokeWidth = options.strokeWidth || 2;
2083
+ this.strokeWidthRange = options.strokeWidthRange || [1, 6];
2084
+ this.opacityRange = options.opacityRange || [0.3, 1];
2085
+ if (options.points && Array.isArray(options.points)) {
2086
+ this.trajPoints = options.points.map((p) => {
2087
+ if (p instanceof Point) return p;
2088
+ const pt = new Point({ dimensions: [p.x, p.y], meta: { time: p.time, label: p.label } });
2089
+ pt.x = p.x;
2090
+ pt.y = p.y;
2091
+ return pt;
2092
+ });
2093
+ }
2094
+ }
2095
+ get activePoints() {
2096
+ if (this.currentTime === Infinity) {
2097
+ return this.trajPoints;
2098
+ }
2099
+ return this.trajPoints.filter((p) => {
2100
+ const t = p.meta && typeof p.meta.time === "number" ? p.meta.time : 0;
2101
+ return t <= this.currentTime;
2102
+ });
2103
+ }
2104
+ get path() {
2105
+ return this.pathPoints.reduce((acc, pt, idx) => {
2106
+ return idx === 0 ? `M${pt.x},${pt.y}` : `${acc} L${pt.x},${pt.y}`;
2107
+ }, "");
2108
+ }
2109
+ get pathPoints() {
2110
+ const pts = this.activePoints;
2111
+ if (pts.length < 2) return [];
2112
+ if (this.smooth === "poly") {
2113
+ if (this.polyCoeffsX.length > 0 && this.polyCoeffsY.length > 0) {
2114
+ return sampleCurve(
2115
+ { degree: this.polyCoeffsX.length - 1, coeffsX: this.polyCoeffsX, coeffsY: this.polyCoeffsY },
2116
+ TRAJECTORY_CURVE_STEPS
2117
+ );
2118
+ }
2119
+ const fit = fitTrajectory(
2120
+ pts.map((p) => ({ x: p.x, y: p.y })),
2121
+ {
2122
+ maxDegree: this.maxDegree,
2123
+ degreeSelection: this.degreeSelection,
2124
+ selectionSet: this.trajPoints.map((p) => ({ x: p.x, y: p.y }))
2125
+ }
2126
+ );
2127
+ if (fit) {
2128
+ this.selectedDegree = fit.degree;
2129
+ return sampleCurve(fit, TRAJECTORY_CURVE_STEPS);
2130
+ }
2131
+ }
2132
+ return pts.map((pt) => ({ x: pt.x, y: pt.y }));
2133
+ }
2134
+ };
2135
+ __name(_Trajectory, "Trajectory");
2136
+ let Trajectory = _Trajectory;
1871
2137
  const zoom_level = 1;
1872
2138
  const _Edge = class _Edge extends Line {
1873
2139
  constructor(data, from, to, weight = 0) {
@@ -1915,7 +2181,7 @@ const _EdgeArrow = class _EdgeArrow extends PlottedObject {
1915
2181
  }
1916
2182
  get path() {
1917
2183
  let _this = __privateGet(this, _edge);
1918
- let _mid = _this.from_point, head = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.7), nock = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.85), mid = Tools.Geometry.Line.midpoint(head, nock), rot_mid = Tools.Geometry.Line.rotate(_this.line, mid), rot_nock = Tools.Geometry.Line.rotate(_this.line, nock), mid_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_mid), mid_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_mid), nock_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_nock), nock_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_nock);
2184
+ let _mid = _this.from_point, head = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.2), nock = Tools.Geometry.Line.point_along([[_this.to.x, _this.to.y], _mid], 0.35), mid = Tools.Geometry.Line.midpoint(head, nock), rot_mid = Tools.Geometry.Line.rotate(_this.line, mid), rot_nock = Tools.Geometry.Line.rotate(_this.line, nock), mid_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_mid), mid_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_mid), nock_int0 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[0]], rot_nock), nock_int1 = Tools.Geometry.Line.intersection([_mid, _this.base_pts[1]], rot_nock);
1919
2185
  let p = `M${head.join(" ")} ${mid_int0.join(" ")} ${nock_int0.join(" ")} ${mid.join(" ")} ${nock_int1.join(" ")} ${mid_int1.join(" ")}z`;
1920
2186
  return p;
1921
2187
  }
@@ -2200,11 +2466,22 @@ const _QEPoint = class _QEPoint extends Drawable {
2200
2466
  const extent = this.graph.__data__.extremes.x_max;
2201
2467
  return (extent > 0 ? extent : 1) * 6e-3 * this.__data__.size;
2202
2468
  }
2469
+ /** Optional halo stroke (e.g. white outline on unit points to separate
2470
+ * overlapping dots in dense clusters). Width scales with the dot so it holds
2471
+ * up at any zoom. No-op unless the object requests it via meta.stroke. */
2472
+ applyStroke(el) {
2473
+ var _a;
2474
+ const stroke = (_a = this.__data__.meta) == null ? void 0 : _a.stroke;
2475
+ if (!stroke) return;
2476
+ el.setAttribute("stroke", stroke);
2477
+ el.setAttribute("stroke-width", (this.computeRadius() * 0.22).toString());
2478
+ }
2203
2479
  create() {
2204
2480
  const tag = this.isSquare ? "rect" : "circle";
2205
2481
  const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
2206
2482
  el.setAttribute("data-point-id", this.__data__.ID);
2207
2483
  el.setAttribute("fill", this.__data__.color.toString());
2484
+ this.applyStroke(el);
2208
2485
  const r = this.computeRadius();
2209
2486
  if (this.isSquare) {
2210
2487
  el.setAttribute("x", (this.x - r).toString());
@@ -2242,6 +2519,7 @@ const _QEPoint = class _QEPoint extends Drawable {
2242
2519
  }
2243
2520
  const r = this.computeRadius();
2244
2521
  this.svg_element.setAttribute("fill", this.__data__.color.toString());
2522
+ this.applyStroke(this.svg_element);
2245
2523
  if (this.isSquare) {
2246
2524
  this.svg_element.setAttribute("x", (this.x - r).toString());
2247
2525
  this.svg_element.setAttribute("y", (this.y - r).toString());
@@ -2260,6 +2538,141 @@ const _QEPoint = class _QEPoint extends Drawable {
2260
2538
  };
2261
2539
  __name(_QEPoint, "QEPoint");
2262
2540
  let QEPoint = _QEPoint;
2541
+ const _QETrajectory = class _QETrajectory extends Drawable {
2542
+ constructor(__data__, parent, graph, __children__ = new QEArray()) {
2543
+ super(__data__, parent, graph, __children__);
2544
+ this.__data__ = __data__;
2545
+ this.parent = parent;
2546
+ this.graph = graph;
2547
+ this.__children__ = __children__;
2548
+ this.__name__ = "trajectory";
2549
+ this._path_el = null;
2550
+ this._segments_group = null;
2551
+ this._tip_el = null;
2552
+ this._points_group = null;
2553
+ this.svg_element = this.create();
2554
+ }
2555
+ // Trajectory styling is specified in display pixels, while SVG paths use
2556
+ // graph coordinates. Convert at render time so marks stay legible at any scale.
2557
+ graphUnitsForPixels(pixels) {
2558
+ var _a;
2559
+ const extent = this.graph.__data__.extremes.x_max || 1;
2560
+ const width = ((_a = this.graph.svg_element) == null ? void 0 : _a.getBoundingClientRect().width) || 0;
2561
+ return width > 0 ? pixels * (2 * extent / width) : pixels * extent * 6e-3;
2562
+ }
2563
+ create() {
2564
+ const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
2565
+ group.setAttribute("class", "qe-trajectory-group");
2566
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2567
+ path.setAttribute("fill", "none");
2568
+ path.setAttribute("stroke", this.__data__.color.toString());
2569
+ path.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2570
+ group.appendChild(path);
2571
+ this._path_el = path;
2572
+ const segmentsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2573
+ segmentsGroup.setAttribute("class", "qe-trajectory-segments");
2574
+ group.appendChild(segmentsGroup);
2575
+ this._segments_group = segmentsGroup;
2576
+ const pointsGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
2577
+ pointsGroup.setAttribute("class", "qe-trajectory-points");
2578
+ group.appendChild(pointsGroup);
2579
+ this._points_group = pointsGroup;
2580
+ if (this.__data__.showTips) {
2581
+ const tip = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2582
+ tip.setAttribute("fill", this.__data__.color.toString());
2583
+ tip.setAttribute("r", this.graphUnitsForPixels(4).toString());
2584
+ tip.setAttribute("stroke", "#ffffff");
2585
+ tip.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2586
+ group.appendChild(tip);
2587
+ this._tip_el = tip;
2588
+ }
2589
+ this.svg_element = group;
2590
+ return group;
2591
+ }
2592
+ update() {
2593
+ if (!this.svg_element) {
2594
+ this.create();
2595
+ }
2596
+ if (!this.svg_element.parentElement) {
2597
+ this.parent.appendChild(this.svg_element);
2598
+ }
2599
+ const dStr = this.__data__.path;
2600
+ if (this._path_el) {
2601
+ this._path_el.setAttribute("d", dStr);
2602
+ this._path_el.setAttribute("stroke", this.__data__.color.toString());
2603
+ this._path_el.setAttribute("stroke-width", this.graphUnitsForPixels(this.__data__.strokeWidth).toString());
2604
+ this._path_el.style.display = this.__data__.lineStyle === "tapered" ? "none" : "block";
2605
+ }
2606
+ if (this._segments_group) {
2607
+ this._segments_group.innerHTML = "";
2608
+ if (this.__data__.lineStyle === "tapered") {
2609
+ const pathPts = this.__data__.pathPoints;
2610
+ const nSegments = pathPts.length - 1;
2611
+ const [minWidth, maxWidth] = this.__data__.strokeWidthRange;
2612
+ const [minOpacity, maxOpacity] = this.__data__.opacityRange;
2613
+ for (let i = 0; i < nSegments; i++) {
2614
+ const start = pathPts[i];
2615
+ const end = pathPts[i + 1];
2616
+ const progress = nSegments <= 1 ? 1 : i / (nSegments - 1);
2617
+ const width = minWidth + (maxWidth - minWidth) * progress;
2618
+ const opacity = minOpacity + (maxOpacity - minOpacity) * progress;
2619
+ const segment = document.createElementNS("http://www.w3.org/2000/svg", "path");
2620
+ segment.setAttribute("d", `M${start.x},${start.y} L${end.x},${end.y}`);
2621
+ segment.setAttribute("fill", "none");
2622
+ segment.setAttribute("stroke", this.__data__.color.toString());
2623
+ segment.setAttribute("stroke-linecap", "round");
2624
+ segment.setAttribute("stroke-linejoin", "round");
2625
+ segment.setAttribute("stroke-width", this.graphUnitsForPixels(width).toString());
2626
+ segment.setAttribute("opacity", opacity.toString());
2627
+ this._segments_group.appendChild(segment);
2628
+ }
2629
+ this._segments_group.style.display = "block";
2630
+ } else {
2631
+ this._segments_group.style.display = "none";
2632
+ }
2633
+ }
2634
+ const pts = this.__data__.activePoints;
2635
+ if (this._points_group) {
2636
+ if (this.__data__.showPoints && pts.length > 0) {
2637
+ this._points_group.innerHTML = "";
2638
+ pts.forEach((pt) => {
2639
+ var _a;
2640
+ const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2641
+ dot.setAttribute("cx", pt.x.toString());
2642
+ dot.setAttribute("cy", pt.y.toString());
2643
+ dot.setAttribute("r", this.graphUnitsForPixels(2.5).toString());
2644
+ dot.setAttribute("fill", this.__data__.color.toString());
2645
+ dot.setAttribute("opacity", "0.7");
2646
+ (_a = this._points_group) == null ? void 0 : _a.appendChild(dot);
2647
+ });
2648
+ this._points_group.style.display = "block";
2649
+ } else {
2650
+ this._points_group.style.display = "none";
2651
+ }
2652
+ }
2653
+ if (this._tip_el && this.__data__.showTips) {
2654
+ if (pts.length > 0) {
2655
+ const lastPt = pts[pts.length - 1];
2656
+ this._tip_el.setAttribute("cx", lastPt.x.toString());
2657
+ this._tip_el.setAttribute("cy", lastPt.y.toString());
2658
+ this._tip_el.setAttribute("fill", this.__data__.color.toString());
2659
+ this._tip_el.setAttribute("r", this.graphUnitsForPixels(4).toString());
2660
+ this._tip_el.setAttribute("stroke-width", this.graphUnitsForPixels(1.5).toString());
2661
+ this._tip_el.style.display = "block";
2662
+ } else {
2663
+ this._tip_el.style.display = "none";
2664
+ }
2665
+ }
2666
+ }
2667
+ draw() {
2668
+ if (this.svg_element && !this.svg_element.parentElement) {
2669
+ this.parent.appendChild(this.svg_element);
2670
+ }
2671
+ this.update();
2672
+ }
2673
+ };
2674
+ __name(_QETrajectory, "QETrajectory");
2675
+ let QETrajectory = _QETrajectory;
2263
2676
  const { ArrayTools } = Tools;
2264
2677
  const _QEArray = class _QEArray extends Array {
2265
2678
  constructor(...args) {
@@ -2319,7 +2732,8 @@ const QEElementConstructors = {
2319
2732
  "Edge": QELine,
2320
2733
  "TaperedEdge": QELine,
2321
2734
  "TaperedWeightedMidpointEdge": QELine,
2322
- "NodeEdge": QELine
2735
+ "NodeEdge": QELine,
2736
+ "Trajectory": QETrajectory
2323
2737
  };
2324
2738
  const _QELayer = class _QELayer {
2325
2739
  constructor(__data__, parent, graph, __children__ = new QEArray()) {
@@ -2343,8 +2757,14 @@ const _QELayer = class _QELayer {
2343
2757
  (obj) => !this.__children__.some((child) => childId(child) === obj.ID)
2344
2758
  );
2345
2759
  adding.forEach((obj) => {
2346
- const obj_name = obj.constructor.name;
2347
- const ena_element = new QEElementConstructors[obj_name](obj, this.svg_element, this.graph);
2760
+ const obj_name = obj._typeName || obj.constructor.name;
2761
+ let constructor = QEElementConstructors[obj_name];
2762
+ if (!constructor) {
2763
+ if (obj instanceof Trajectory) constructor = QETrajectory;
2764
+ else if (obj instanceof Point) constructor = QEPoint;
2765
+ else constructor = QELine;
2766
+ }
2767
+ const ena_element = new constructor(obj, this.svg_element, this.graph);
2348
2768
  this.__children__.push(ena_element);
2349
2769
  });
2350
2770
  updating.forEach((child) => {
@@ -2536,10 +2956,22 @@ const _QEEdgesElement = class _QEEdgesElement extends HTMLElement {
2536
2956
  __name(_QEEdgesElement, "QEEdgesElement");
2537
2957
  _QEEdgesElement.tagName = "qe-edges";
2538
2958
  let QEEdgesElement = _QEEdgesElement;
2959
+ const _QETrajectoriesElement = class _QETrajectoriesElement extends HTMLElement {
2960
+ };
2961
+ __name(_QETrajectoriesElement, "QETrajectoriesElement");
2962
+ _QETrajectoriesElement.tagName = "qe-trajectories";
2963
+ let QETrajectoriesElement = _QETrajectoriesElement;
2964
+ const _QEAxesElement = class _QEAxesElement extends HTMLElement {
2965
+ };
2966
+ __name(_QEAxesElement, "QEAxesElement");
2967
+ _QEAxesElement.tagName = "qe-axes";
2968
+ let QEAxesElement = _QEAxesElement;
2539
2969
  customElements.define(QENodesElement.tagName, QENodesElement);
2540
2970
  customElements.define(QEMeansElement.tagName, QEMeansElement);
2541
2971
  customElements.define(QEPointsElement.tagName, QEPointsElement);
2542
2972
  customElements.define(QEEdgesElement.tagName, QEEdgesElement);
2973
+ customElements.define(QETrajectoriesElement.tagName, QETrajectoriesElement);
2974
+ customElements.define(QEAxesElement.tagName, QEAxesElement);
2543
2975
  const GROUP_PALETTE = [
2544
2976
  "#4477AA",
2545
2977
  "#EE6677",
@@ -2613,6 +3045,10 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2613
3045
  this._nodeLabels = /* @__PURE__ */ new Map();
2614
3046
  this._meanLabels = /* @__PURE__ */ new Map();
2615
3047
  this._pointLabels = /* @__PURE__ */ new Map();
3048
+ this._showAxes = false;
3049
+ this._axisXText = null;
3050
+ this._axisYText = null;
3051
+ this._axisLabelEls = /* @__PURE__ */ new Map();
2616
3052
  this.graph = this;
2617
3053
  this.parent = null;
2618
3054
  this.__data__ = new Graph(DefaultGraphOptions);
@@ -2620,7 +3056,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2620
3056
  this.svg_element = this.create();
2621
3057
  console.log("Created ena-graph svg element: ", this.svg_element);
2622
3058
  document.addEventListener("model-updated", (e) => {
2623
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
3059
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
2624
3060
  if (this._vizena && e.target !== this._vizena) return;
2625
3061
  const model = e.detail;
2626
3062
  if (!model) return;
@@ -2630,6 +3066,9 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2630
3066
  const nodeIdCol = model.node_id_col;
2631
3067
  const groupColName = model.group_col;
2632
3068
  const config = this._readLayerConfig();
3069
+ this._showAxes = config.showAxes;
3070
+ this._axisXText = config.axisXLabel === null ? null : config.axisXLabel || String((dims == null ? void 0 : dims[0]) ?? "x");
3071
+ this._axisYText = config.axisYLabel === null ? null : config.axisYLabel || String((dims == null ? void 0 : dims[1]) ?? "y");
2633
3072
  const groupColorMap = {};
2634
3073
  if (groupColName && ((_a = model.points) == null ? void 0 : _a.names)) {
2635
3074
  const groupCol = model.points.col(groupColName);
@@ -2663,7 +3102,9 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2663
3102
  }, "getExtent");
2664
3103
  const maxNodeExtent = ((_c = model.nodes) == null ? void 0 : _c.rows) ? getExtent(model.nodes.rows()) : 0;
2665
3104
  const maxPointExtent = ((_d = model.points) == null ? void 0 : _d.rows) ? getExtent(model.points.rows()) : 0;
2666
- const pointScale = scalePointsEnabled && maxPointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / maxPointExtent : 1;
3105
+ const maxTrajExtent = ((_e = model.trajectories) == null ? void 0 : _e.rows) ? getExtent(model.trajectories.rows()) : 0;
3106
+ const effectivePointExtent = maxPointExtent > 0 ? maxPointExtent : maxTrajExtent;
3107
+ const pointScale = scalePointsEnabled && effectivePointExtent > 0 && maxNodeExtent > 0 ? maxNodeExtent / effectivePointExtent : 1;
2667
3108
  const labelNodes = config.labelNodes;
2668
3109
  const labelMeans = config.labelMeans;
2669
3110
  const labelPoints = config.labelPoints;
@@ -2686,7 +3127,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2686
3127
  }, "rowToPoint");
2687
3128
  const groupSums = {};
2688
3129
  const showUnitPoints = config.showPoints;
2689
- if ((_e = model.points) == null ? void 0 : _e.rows) {
3130
+ if ((_f = model.points) == null ? void 0 : _f.rows) {
2690
3131
  const pointsLayer = this.__children__.search("__children__", "points");
2691
3132
  if (pointsLayer) {
2692
3133
  model.points.rows().forEach((row) => {
@@ -2695,6 +3136,8 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2695
3136
  const colorStr = group ? groupColorMap[group] ?? "#888888" : "#888888";
2696
3137
  if (showUnitPoints) {
2697
3138
  const pt = rowToPoint(row, unitIdCol, colorStr, pointScale);
3139
+ pt.size = 2.5;
3140
+ pt.meta = { ...pt.meta ?? {}, stroke: "#ffffff" };
2698
3141
  this.__data__.points.set(pt.ID, pt);
2699
3142
  pointsLayer.__data__.objects.set(pt.ID, pt);
2700
3143
  }
@@ -2712,7 +3155,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2712
3155
  groupSums[group].n += 1;
2713
3156
  }
2714
3157
  });
2715
- const hasGroupsFrame = !!(((_f = model.groups) == null ? void 0 : _f.rows) && model.groups.rows().length > 0);
3158
+ const hasGroupsFrame = !!(((_g = model.groups) == null ? void 0 : _g.rows) && model.groups.rows().length > 0);
2716
3159
  const activeGroupRows = config.showMeans && hasGroupsFrame ? model.groups.rows().filter((r) => {
2717
3160
  const g = r.rowName ?? String(r[0]);
2718
3161
  return activeMeanGroups.size === 0 || activeMeanGroups.has(g);
@@ -2798,7 +3241,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2798
3241
  this._ciRects.delete(g);
2799
3242
  }
2800
3243
  });
2801
- if ((_g = model.confidence) == null ? void 0 : _g.rows) {
3244
+ if ((_h = model.confidence) == null ? void 0 : _h.rows) {
2802
3245
  model.confidence.rows().forEach((row) => {
2803
3246
  const groupIdx = row.names.indexOf(groupColName ?? "group");
2804
3247
  const group = groupIdx >= 0 ? String(row[groupIdx]) : row.rowName ?? null;
@@ -2858,7 +3301,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2858
3301
  });
2859
3302
  }
2860
3303
  }
2861
- if ((_h = model.outlier) == null ? void 0 : _h.rows) {
3304
+ if ((_i = model.outlier) == null ? void 0 : _i.rows) {
2862
3305
  this._outlierRects.forEach((el, g) => {
2863
3306
  if (!groupColorMap[g]) {
2864
3307
  el.remove();
@@ -2947,20 +3390,73 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
2947
3390
  pointsLayer.__data__.__state__.setVariable("layer_updated", pointsLayer.__data__);
2948
3391
  }
2949
3392
  }
3393
+ const trajectoriesLayer = this.__children__.search("__children__", "trajectories");
3394
+ const trajectoriesEnabled = config.showTrajectories;
3395
+ if (trajectoriesEnabled && trajectoriesLayer) {
3396
+ trajectoriesLayer.__data__.objects.clear();
3397
+ this.__data__.trajectories.clear();
3398
+ const trajSource = ((_j = model.trajectories) == null ? void 0 : _j.rows) ? model.trajectories : model.points;
3399
+ if (trajSource == null ? void 0 : trajSource.rows) {
3400
+ const unitRows = {};
3401
+ const selectedUnits = config.trajectoryUnits ? config.trajectoryUnits.split(",").map((s) => s.trim()) : null;
3402
+ trajSource.rows().forEach((row) => {
3403
+ 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;
3404
+ const unit = uIdx >= 0 ? String(row[uIdx]) : row.rowName || "unit-1";
3405
+ if (selectedUnits && !selectedUnits.includes(unit)) return;
3406
+ const xIdx = row.names.indexOf(dims[0]);
3407
+ const yIdx = dims.length > 1 ? row.names.indexOf(dims[1]) : -1;
3408
+ 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;
3409
+ const time = tIdx >= 0 ? Number(row[tIdx]) : unitRows[unit] ? unitRows[unit].length + 1 : 1;
3410
+ if (xIdx >= 0 && yIdx >= 0) {
3411
+ if (!unitRows[unit]) unitRows[unit] = [];
3412
+ unitRows[unit].push({
3413
+ x: Number(row[xIdx]) * pointScale,
3414
+ y: -Number(row[yIdx]) * pointScale,
3415
+ time
3416
+ });
3417
+ }
3418
+ });
3419
+ const unitKeys = Object.keys(unitRows);
3420
+ unitKeys.forEach((unitKey, uIdx) => {
3421
+ const rawPts = unitRows[unitKey];
3422
+ if (rawPts.length === 0) return;
3423
+ const colorStr = GROUP_PALETTE[uIdx % GROUP_PALETTE.length];
3424
+ const trajObj = new Trajectory({
3425
+ ID: `traj-${unitKey}`,
3426
+ unitId: unitKey,
3427
+ color: new Color(colorStr),
3428
+ lineStyle: config.trajectoryLineStyle,
3429
+ strokeWidth: config.trajectoryStrokeWidth,
3430
+ strokeWidthRange: config.trajectoryStrokeWidthRange,
3431
+ opacityRange: config.trajectoryOpacityRange,
3432
+ smooth: config.trajectorySmooth,
3433
+ maxDegree: config.trajectoryMaxDegree,
3434
+ degreeSelection: config.trajectoryDegreeSelection,
3435
+ currentTime: config.trajectoryPlayhead ?? Infinity,
3436
+ showPoints: config.trajectoryShowPoints,
3437
+ showTips: config.trajectoryShowTips,
3438
+ points: rawPts.map((p) => ({ x: p.x, y: p.y, time: p.time }))
3439
+ });
3440
+ this.__data__.trajectories.set(unitKey, trajObj);
3441
+ trajectoriesLayer.__data__.objects.set(trajObj.ID, trajObj);
3442
+ });
3443
+ }
3444
+ trajectoriesLayer.__data__.__state__.setVariable("layer_updated", trajectoriesLayer.__data__);
3445
+ }
2950
3446
  const networkLayer = this.__children__.search("__children__", "network");
2951
3447
  const edgeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edges");
2952
3448
  const nodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "nodes");
2953
3449
  const edgenodeLayer = networkLayer == null ? void 0 : networkLayer.__children__.search("__children__", "edgenodes");
2954
3450
  const edgesEnabled = config.showEdges;
2955
- if (((_i = model.nodes) == null ? void 0 : _i.rows) && nodeLayer) {
3451
+ if (((_k = model.nodes) == null ? void 0 : _k.rows) && nodeLayer) {
2956
3452
  model.nodes.rows().forEach((row) => {
2957
3453
  const pt = rowToPoint(row, nodeIdCol, "#444444");
2958
3454
  pt.size = 2;
2959
3455
  nodeLayer.__data__.objects.set(pt.ID, pt);
2960
3456
  });
2961
- if (edgesEnabled && edgeLayer && ((_j = model.edges) == null ? void 0 : _j.rows)) {
3457
+ if (edgesEnabled && edgeLayer && ((_l = model.edges) == null ? void 0 : _l.rows)) {
2962
3458
  const unitGroupMap = {};
2963
- if (groupColName && ((_k = model.points) == null ? void 0 : _k.rows)) {
3459
+ if (groupColName && ((_m = model.points) == null ? void 0 : _m.rows)) {
2964
3460
  model.points.rows().forEach((row) => {
2965
3461
  const qIdx = row.names.indexOf(unitIdCol);
2966
3462
  const gIdx = row.names.indexOf(groupColName);
@@ -3086,7 +3582,6 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3086
3582
  totalWeightByPair[key] = (totalWeightByPair[key] ?? 0) + weight;
3087
3583
  });
3088
3584
  }
3089
- ((_l = this.__data__.extremes) == null ? void 0 : _l.x_max) ?? 1;
3090
3585
  drawEdges.forEach(({ col, pair, weight, color }) => {
3091
3586
  const fromNode = nodeLayer.__data__.objects.get(pair[0]);
3092
3587
  const toNode = nodeLayer.__data__.objects.get(pair[1]);
@@ -3208,6 +3703,8 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3208
3703
  const meansEl = this.querySelector("qe-means");
3209
3704
  const pointsEl = this.querySelector("qe-points");
3210
3705
  const edgesEl = this.querySelector("qe-edges");
3706
+ const trajectoriesEl = this.querySelector("qe-trajectories");
3707
+ const axesEl = this.querySelector("qe-axes");
3211
3708
  const labelsShorthand = this.getAttribute("labels");
3212
3709
  return {
3213
3710
  // Nodes — always rendered (coordinate anchors); <qe-nodes> controls labels.
@@ -3235,9 +3732,35 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3235
3732
  edgeGroup: (edgesEl == null ? void 0 : edgesEl.getAttribute("group")) ?? null,
3236
3733
  edgeUnit: (edgesEl == null ? void 0 : edgesEl.getAttribute("unit")) ?? null,
3237
3734
  edgeCompare: (edgesEl == null ? void 0 : edgesEl.getAttribute("compare")) ?? null,
3238
- edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null
3735
+ edgeAlso: (edgesEl == null ? void 0 : edgesEl.getAttribute("also")) ?? null,
3736
+ // Trajectories — presence of <qe-trajectories> = draw cognitive trajectories
3737
+ showTrajectories: !!trajectoriesEl,
3738
+ trajectoryUnits: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("units")) ?? null,
3739
+ trajectorySmooth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("smooth")) ?? "none",
3740
+ trajectoryMaxDegree: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("max-degree")) ? parseInt(trajectoriesEl.getAttribute("max-degree"), 10) : 3,
3741
+ trajectoryDegreeSelection: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("degree-selection")) ?? "loocv_2d",
3742
+ trajectoryShowPoints: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("points")) !== "false",
3743
+ trajectoryShowTips: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("tips")) !== "false",
3744
+ trajectoryPlayhead: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("playhead")) ? parseFloat(trajectoriesEl.getAttribute("playhead")) : null,
3745
+ trajectoryLineStyle: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("line-style")) ?? "uniform",
3746
+ trajectoryStrokeWidth: (trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width")) ? parseFloat(trajectoriesEl.getAttribute("stroke-width")) : 2,
3747
+ trajectoryStrokeWidthRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("stroke-width-range"), [1, 6]),
3748
+ trajectoryOpacityRange: this._parseNumberPair(trajectoriesEl == null ? void 0 : trajectoriesEl.getAttribute("opacity-range"), [0.3, 1]),
3749
+ // Axes — presence of <qe-axes> = draw axis lines. The x / y attributes add
3750
+ // a per-axis label: a value is used verbatim, an empty attribute falls back
3751
+ // to the dimension name (resolved later, where dims are known). null = no
3752
+ // label for that axis.
3753
+ showAxes: !!axesEl,
3754
+ axisXLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("x")) ? axesEl.getAttribute("x") ?? "" : null,
3755
+ axisYLabel: (axesEl == null ? void 0 : axesEl.hasAttribute("y")) ? axesEl.getAttribute("y") ?? "" : null
3239
3756
  };
3240
3757
  }
3758
+ _parseNumberPair(value, fallback) {
3759
+ if (!value) return fallback;
3760
+ const parts = value.split(",").map((part) => Number(part.trim()));
3761
+ if (parts.length < 2 || !parts.every(Number.isFinite)) return fallback;
3762
+ return [parts[0], parts[1]];
3763
+ }
3241
3764
  attributeChangedCallback(_name2, oldValue, newValue) {
3242
3765
  if (oldValue === newValue) return;
3243
3766
  this._scheduleRerender();
@@ -3246,6 +3769,7 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3246
3769
  var _a;
3247
3770
  const w = this.getAttribute("width");
3248
3771
  const h = this.getAttribute("height");
3772
+ this.style.display = "block";
3249
3773
  if (w) this.style.width = isNaN(Number(w)) ? w : `${w}px`;
3250
3774
  if (h) this.style.height = isNaN(Number(h)) ? h : `${h}px`;
3251
3775
  this.appendChild(this.svg_element);
@@ -3266,7 +3790,19 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3266
3790
  "labels",
3267
3791
  "label-nodes",
3268
3792
  "label-means",
3269
- "label-points"
3793
+ "label-points",
3794
+ "units",
3795
+ "smooth",
3796
+ "degree",
3797
+ "points",
3798
+ "tips",
3799
+ "playhead",
3800
+ "line-style",
3801
+ "stroke-width",
3802
+ "stroke-width-range",
3803
+ "opacity-range",
3804
+ "x",
3805
+ "y"
3270
3806
  ]
3271
3807
  });
3272
3808
  const forId = this.getAttribute("for");
@@ -3293,6 +3829,10 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3293
3829
  create() {
3294
3830
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3295
3831
  svg.classList.add("graph");
3832
+ svg.style.display = "block";
3833
+ svg.style.width = "100%";
3834
+ svg.style.height = "100%";
3835
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
3296
3836
  svg.setAttribute("viewBox", this.__data__.extremes.as_viewBox());
3297
3837
  svg.setAttribute("transform", this.__data__.transform.toString());
3298
3838
  svg.setAttribute("role", "figure");
@@ -3419,6 +3959,56 @@ const _QEGraphElement = class _QEGraphElement extends DrawableElement {
3419
3959
  this.update_layers();
3420
3960
  this.update_labels();
3421
3961
  this.update_extremes();
3962
+ this.update_axes();
3963
+ }
3964
+ /**
3965
+ * Axes (<qe-axes>): toggle the axis-line cross by presence, and draw/position
3966
+ * per-axis labels at the positive ends using the current extent. Labels are
3967
+ * cached SVG <text> nodes in the labels group; null text removes them.
3968
+ */
3969
+ update_axes() {
3970
+ const axesLayer = this.__children__.search("__children__", "axes");
3971
+ if (axesLayer == null ? void 0 : axesLayer.svg_element) {
3972
+ axesLayer.svg_element.style.display = this._showAxes ? "" : "none";
3973
+ }
3974
+ const parent = this._labelsGroup;
3975
+ const extent = this.__data__.extremes.x_max;
3976
+ if (!parent || extent <= 0) return;
3977
+ const fontSize = extent * 0.06;
3978
+ const gap = fontSize * 0.6;
3979
+ const place = /* @__PURE__ */ __name((axis, text) => {
3980
+ let el = this._axisLabelEls.get(axis);
3981
+ if (!this._showAxes || text == null) {
3982
+ if (el) {
3983
+ el.remove();
3984
+ this._axisLabelEls.delete(axis);
3985
+ }
3986
+ return;
3987
+ }
3988
+ if (!el) {
3989
+ el = document.createElementNS("http://www.w3.org/2000/svg", "text");
3990
+ el.setAttribute("data-axis-label", axis);
3991
+ el.setAttribute("pointer-events", "none");
3992
+ parent.appendChild(el);
3993
+ this._axisLabelEls.set(axis, el);
3994
+ }
3995
+ el.textContent = text;
3996
+ el.setAttribute("font-size", fontSize.toString());
3997
+ el.setAttribute("font-family", "system-ui, sans-serif");
3998
+ el.setAttribute("font-weight", "600");
3999
+ el.setAttribute("fill", "#556");
4000
+ if (axis === "x") {
4001
+ el.setAttribute("x", (extent + gap).toString());
4002
+ el.setAttribute("y", (fontSize * 0.35).toString());
4003
+ el.setAttribute("text-anchor", "start");
4004
+ } else {
4005
+ el.setAttribute("x", "0");
4006
+ el.setAttribute("y", (-extent - gap).toString());
4007
+ el.setAttribute("text-anchor", "middle");
4008
+ }
4009
+ }, "place");
4010
+ place("x", this._axisXText);
4011
+ place("y", this._axisYText);
3422
4012
  }
3423
4013
  update_labels() {
3424
4014
  this._nodeLabels.forEach((lbl) => lbl.update());
@@ -3492,6 +4082,7 @@ export {
3492
4082
  DataFrameRow,
3493
4083
  Edge,
3494
4084
  Graph,
4085
+ QEAxesElement,
3495
4086
  QEEdgesElement,
3496
4087
  QEGraphElement,
3497
4088
  QELayer,
@@ -3500,8 +4091,16 @@ export {
3500
4091
  QENodesElement,
3501
4092
  QEPoint,
3502
4093
  QEPointsElement,
4094
+ QETrajectoriesElement,
4095
+ QETrajectory,
3503
4096
  QEVisualElement,
4097
+ TRAJECTORY_ZERO_EPS,
4098
+ Trajectory,
3504
4099
  WeightedColor,
4100
+ fitTrajectory,
4101
+ nearOriginFilter,
4102
+ sampleCurve,
4103
+ selectDegree,
3505
4104
  vizENA
3506
4105
  };
3507
4106
  //# sourceMappingURL=qeviz.es.js.map