archaeopteryx 3.22.0 → 3.23.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/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.22.0
23
+ // v 3.23.0
24
24
  // 2026-09-17
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -7245,6 +7245,192 @@
7245
7245
  return bothPicturesHaveDepth(root);
7246
7246
  };
7247
7247
 
7248
+ // ---- the CLOCK PLOT ---------------------------------------------------
7249
+ // Every node as a point, its DATE against its DIVERGENCE from the root,
7250
+ // and a straight line through the tips: the slope is the rate the tips
7251
+ // accumulated divergence at, and where the line comes down to the root's
7252
+ // divergence is the date it puts the root on. TempEst's and TreeTime's
7253
+ // root-to-tip regression, Auspice's clock view. Christian, 2026-10-02:
7254
+ // a panel linked to the tree, named "Clock plot"; JS only until the
7255
+ // design is final.
7256
+ //
7257
+ // THE RULE, designed for two kinds of tree, the first of them built:
7258
+ //
7259
+ // 1. 'divergence' -- a tree with Time and Div (hasTimeAndDivergence): each
7260
+ // node states a date and a divergence from the root, recorded on it or
7261
+ // summed from each branch's time x its clock rate. Every node is a
7262
+ // point; which of the two layouts is on screen does not matter.
7263
+ // 2. 'root-to-tip' (NOT BUILT) -- a tree whose branch lengths are
7264
+ // divergence and whose tips are dated, with no dates on its ancestors
7265
+ // (it is not a time tree): a tip's divergence is the sum of the stated
7266
+ // lengths from the root. Tips only. This is the tree a clock plot is
7267
+ // wanted for most -- is there a signal worth dating? -- and the one
7268
+ // whose answer depends on its root.
7269
+ //
7270
+ // In both, the plot is offered when its LINE can be drawn: three tips or
7271
+ // more, and not all of them on one date. A tree sampled at one moment has
7272
+ // no slope to estimate. Asked of the tree, never of the view: in the view
7273
+ // of a clade too small or too uniform for a line the points are drawn and
7274
+ // the line is not.
7275
+ //
7276
+ // The FIT is ordinary least squares over the TIPS alone. A tip's date is
7277
+ // an observation; an ancestor's was inferred, usually with a clock, and
7278
+ // counting it would have the estimate confirm itself. The line is not
7279
+ // forced through the root, so the date it reaches the root's divergence
7280
+ // at can be held against the date the tree states for the root.
7281
+ const CLOCK_PLOT_MIN_TIPS = 3;
7282
+
7283
+ /**
7284
+ * Which clock plot the tree has, or null for none (see THE RULE above).
7285
+ *
7286
+ * @param phy the tree
7287
+ * @returns {string|null} 'divergence', or null
7288
+ */
7289
+ forester.clockPlotKind = function (phy) {
7290
+ if (!phy || !forester.hasTimeAndDivergence(phy)) {
7291
+ return null;
7292
+ }
7293
+ let tips = 0;
7294
+ let first = null;
7295
+ let differ = false;
7296
+ forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
7297
+ if (n.children && n.children.length > 0) {
7298
+ return;
7299
+ }
7300
+ ++tips;
7301
+ let d = auspiceNodeDate(n);
7302
+ if (first === null) {
7303
+ first = d;
7304
+ } else if (d !== first) {
7305
+ differ = true;
7306
+ }
7307
+ });
7308
+ return (tips >= CLOCK_PLOT_MIN_TIPS && differ) ? 'divergence' : null;
7309
+ };
7310
+
7311
+ /**
7312
+ * Ordinary least squares of y on x, computed about the means: a date is
7313
+ * near 2020 and its spread a few months, and sums of raw squares would
7314
+ * spend every digit on the 2020.
7315
+ *
7316
+ * @param xs the dates
7317
+ * @param ys the divergences, as many
7318
+ * @returns {{n, slope, intercept, r2, meanX, meanY}|null} null with fewer
7319
+ * than three points or every x the same; r2 is null where every
7320
+ * y is the same (no variance to explain)
7321
+ */
7322
+ forester.clockRegression = function (xs, ys) {
7323
+ let n = xs.length;
7324
+ if (n < CLOCK_PLOT_MIN_TIPS || ys.length !== n) {
7325
+ return null;
7326
+ }
7327
+ let mx = 0;
7328
+ let my = 0;
7329
+ for (let i = 0; i < n; ++i) {
7330
+ mx += xs[i];
7331
+ my += ys[i];
7332
+ }
7333
+ mx /= n;
7334
+ my /= n;
7335
+ let sxx = 0;
7336
+ let sxy = 0;
7337
+ let syy = 0;
7338
+ for (let i = 0; i < n; ++i) {
7339
+ let dx = xs[i] - mx;
7340
+ let dy = ys[i] - my;
7341
+ sxx += dx * dx;
7342
+ sxy += dx * dy;
7343
+ syy += dy * dy;
7344
+ }
7345
+ if (!(sxx > 0) || !isFinite(sxx) || !isFinite(sxy) || !isFinite(syy)) {
7346
+ return null;
7347
+ }
7348
+ let slope = sxy / sxx;
7349
+ return {
7350
+ n: n,
7351
+ slope: slope,
7352
+ intercept: my - (slope * mx),
7353
+ r2: syy > 0 ? ((sxy * sxy) / (sxx * syy)) : null,
7354
+ meanX: mx,
7355
+ meanY: my
7356
+ };
7357
+ };
7358
+
7359
+ /**
7360
+ * The clock plot of the tree, or of the clade on view.
7361
+ *
7362
+ * @param phy the tree: the RULE is asked of it
7363
+ * @param view the tree or clade to plot (the tree, a node, or the
7364
+ * viewer's subtree root); the whole tree where none is given
7365
+ * @returns {object|null} null where the tree has no clock plot; else
7366
+ * kind 'divergence'
7367
+ * forward whether the dates increase toward the tips (calendar
7368
+ * dates); false for ages
7369
+ * unit the dates' unit as the tree states it, or null
7370
+ * fromRates whether the divergence is time x clock rate
7371
+ * points [{node, date, div, tip}] in preorder; the tips'
7372
+ * `residual` is their divergence less the line's
7373
+ * root {node, date, div}: the top of the view
7374
+ * fit forester.clockRegression over the tips, with
7375
+ * rate the slope in the direction time runs: divergence
7376
+ * per unit of time (negative where the tips'
7377
+ * divergence falls with time)
7378
+ * rootDate the date the line reaches the root's divergence
7379
+ * at; null unless the rate is positive
7380
+ * or null where the view has no line
7381
+ */
7382
+ forester.clockPlotData = function (phy, view) {
7383
+ let kind = forester.clockPlotKind(phy);
7384
+ if (kind === null) {
7385
+ return null;
7386
+ }
7387
+ let treeRoot = forester.getTreeRoot(phy);
7388
+ // a node is its own top; the tree, or the viewer's holder of a clade,
7389
+ // is a parentless wrapper around it
7390
+ let top = treeRoot;
7391
+ if (view) {
7392
+ top = (!view.parent && view.children && view.children.length === 1) ? view.children[0] : view;
7393
+ }
7394
+ let points = [];
7395
+ let xs = [];
7396
+ let ys = [];
7397
+ forester.preOrderTraversalAll(top, function (n) {
7398
+ let date = auspiceNodeDate(n);
7399
+ let div = auspiceNodeDiv(n);
7400
+ if (date === null || div === null) {
7401
+ return; // not on a tree with Time and Div; a guard, not a rule
7402
+ }
7403
+ let tip = !(n.children && n.children.length > 0);
7404
+ points.push({node: n, date: date, div: div, tip: tip});
7405
+ if (tip) {
7406
+ xs.push(date);
7407
+ ys.push(div);
7408
+ }
7409
+ });
7410
+ let forward = timeIncreasesTowardTips(treeRoot);
7411
+ let root = {node: top, date: auspiceNodeDate(top), div: auspiceNodeDiv(top)};
7412
+ let fit = forester.clockRegression(xs, ys);
7413
+ if (fit) {
7414
+ fit.rate = forward ? fit.slope : -fit.slope;
7415
+ fit.rootDate = (fit.rate > 0 && root.div !== null)
7416
+ ? ((root.div - fit.intercept) / fit.slope) : null;
7417
+ for (let i = 0; i < points.length; ++i) {
7418
+ if (points[i].tip) {
7419
+ points[i].residual = points[i].div - (fit.intercept + (fit.slope * points[i].date));
7420
+ }
7421
+ }
7422
+ }
7423
+ return {
7424
+ kind: kind,
7425
+ forward: forward,
7426
+ unit: dateUnitOf(treeRoot),
7427
+ fromRates: treeRoot._divergenceFromRates === true && !recordsDivergence(treeRoot),
7428
+ points: points,
7429
+ root: root,
7430
+ fit: fit
7431
+ };
7432
+ };
7433
+
7248
7434
  // A number that is not NaN. It used to test only for null, undefined and
7249
7435
  // NaN, and so answered TRUE for "hello", "", {}, [] and true -- which no
7250
7436
  // caller was hurt by, since all of them pass the result of parseFloat, but
@@ -10220,6 +10406,41 @@
10220
10406
  return vals;
10221
10407
  };
10222
10408
 
10409
+ // Month ticks over [from, to] in decimal years, for a span too short for
10410
+ // whole years (an outbreak sampled over some months): the first of every
10411
+ // 1st, 2nd, 3rd, 6th or 12th month, the smallest step leaving six ticks
10412
+ // or fewer; none where even a tick a year is too many. {value, year,
10413
+ // month}; month is 1-12, value the decimal year at which that month
10414
+ // begins.
10415
+ forester.calendarTickMonths = function (from, to) {
10416
+ let span = to - from;
10417
+ if (!(span > 0) || !isFinite(span) || !isFinite(from)) {
10418
+ return [];
10419
+ }
10420
+ let steps = [1, 2, 3, 6, 12];
10421
+ for (let s = 0; s < steps.length; ++s) {
10422
+ let step = steps[s];
10423
+ let ticks = [];
10424
+ let year = Math.floor(from);
10425
+ // month index = year * 12 + (month - 1); ticks where it divides
10426
+ for (let k = year * 12; ticks.length <= 6; ++k) {
10427
+ let y = Math.floor(k / 12);
10428
+ let m = (k - (y * 12)) + 1;
10429
+ let v = y + ((tipDateDayOfYear(y, m, 1) - 1) / tipDateYearLength(y));
10430
+ if (v > to + 1e-9) {
10431
+ break;
10432
+ }
10433
+ if (v >= from - 1e-9 && (k % step) === 0) {
10434
+ ticks.push({value: v, year: y, month: m});
10435
+ }
10436
+ }
10437
+ if (ticks.length <= 6) {
10438
+ return ticks;
10439
+ }
10440
+ }
10441
+ return [];
10442
+ };
10443
+
10223
10444
 
10224
10445
  // --------------------------------------------------------------
10225
10446
  // Multiple sequence alignment (the desktop's alignment track)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archaeopteryx",
3
- "version": "3.22.0",
3
+ "version": "3.23.0",
4
4
  "description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
5
5
  "main": "archaeopteryx.js",
6
6
  "types": "archaeopteryx.d.ts",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js",
23
+ "test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/clock_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js",
24
24
  "lint": "eslint .",
25
25
  "lint:fix": "eslint . --fix",
26
26
  "format": "prettier --write .",