archaeopteryx 3.22.0 → 3.24.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.24.0
24
24
  // 2026-09-17
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -7245,6 +7245,301 @@
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 (both built; the second on
7258
+ // Christian's word, 2026-10-03):
7259
+ //
7260
+ // 1. 'divergence' -- a tree with Time and Div (hasTimeAndDivergence): each
7261
+ // node states a date and a divergence from the root, recorded on it or
7262
+ // summed from each branch's time x its clock rate. Every node is a
7263
+ // point; which of the two layouts is on screen does not matter.
7264
+ // 2. 'root-to-tip' -- a tree whose branch lengths are its divergence and
7265
+ // whose tips are dated, with no date on any ancestor (it is not a time
7266
+ // tree): a tip's divergence is the sum of the stated lengths from the
7267
+ // root. Tips only. This is the tree a clock plot is wanted for most --
7268
+ // is there a signal worth dating? -- and the one whose answer depends
7269
+ // on its root. TempEst's plot. A tip is dated by what the file STATES
7270
+ // for it (a phyloXML <date>, a num_date annotation), or else by the
7271
+ // date its NAME carries, read by the label-date parser both programs
7272
+ // anchor BEAST heights with (Christian, 2026-10-03: names count, as
7273
+ // TempEst reads them; a year alone is mid-year). An ancestor is never
7274
+ // dated by its name. Every tip must be dated and every branch must
7275
+ // state a length: a tip with either missing has no point, and a plot
7276
+ // that drops tips silently answers a different question than it was
7277
+ // asked. A clock-model tree (a rate on every branch) is never this
7278
+ // kind: its lengths are time.
7279
+ //
7280
+ // In both, the plot is offered when its LINE can be drawn: three tips or
7281
+ // more, and not all of them on one date. A tree sampled at one moment has
7282
+ // no slope to estimate. Asked of the tree, never of the view: in the view
7283
+ // of a clade too small or too uniform for a line the points are drawn and
7284
+ // the line is not.
7285
+ //
7286
+ // The FIT is ordinary least squares over the TIPS alone. A tip's date is
7287
+ // an observation; an ancestor's was inferred, usually with a clock, and
7288
+ // counting it would have the estimate confirm itself. The line is not
7289
+ // forced through the root, so the date it reaches the root's divergence
7290
+ // at can be held against the date the tree states for the root -- where
7291
+ // the tree states one; a root-to-tip tree does not, and there the line's
7292
+ // date is the only estimate.
7293
+ const CLOCK_PLOT_MIN_TIPS = 3;
7294
+
7295
+ // A tip's date on a root-to-tip tree: the one the file states, else the
7296
+ // one its name carries (forester.parseTipLabelDate: the rightmost date in
7297
+ // the label; a year alone is mid-year; an ambiguous numeric date is read
7298
+ // day first). {value, fromName, ambiguous}, or null for no date at all.
7299
+ function rootToTipDate(n) {
7300
+ let d = auspiceNodeDate(n);
7301
+ if (d !== null) {
7302
+ return {value: d, fromName: false, ambiguous: false};
7303
+ }
7304
+ let m = (typeof n.name === 'string' && n.name.length > 0) ? forester.parseTipLabelDate(n.name) : null;
7305
+ return m ? {value: m.decimalYear, fromName: true, ambiguous: m.ambiguous === true} : null;
7306
+ }
7307
+
7308
+ // Kind 2 of THE RULE, before the tips are counted: dated tips (by the
7309
+ // file or by their names), undated ancestors (by the file; a name dates
7310
+ // no ancestor), a length on every branch, no clock rates.
7311
+ function isRootToTipTree(root) {
7312
+ if (!root || root._divergenceFromRates === true || !everyLengthWasStated(root)) {
7313
+ return false;
7314
+ }
7315
+ let ok = true;
7316
+ forester.preOrderTraversalAll(root, function (n) {
7317
+ if (!ok) {
7318
+ return;
7319
+ }
7320
+ ok = (n.children && n.children.length > 0) ? auspiceNodeDate(n) === null : rootToTipDate(n) !== null;
7321
+ });
7322
+ return ok;
7323
+ }
7324
+
7325
+ // Each node's distance from the root along the stated branch lengths:
7326
+ // a root-to-tip tree's divergence.
7327
+ function rootToTipDistances(root) {
7328
+ let dist = new Map();
7329
+ (function walk(n, d) {
7330
+ dist.set(n, d);
7331
+ let children = n.children;
7332
+ if (children) {
7333
+ for (let i = 0; i < children.length; ++i) {
7334
+ let len = children[i].branch_length;
7335
+ walk(children[i], d + ((typeof len === 'number' && isFinite(len)) ? len : 0));
7336
+ }
7337
+ }
7338
+ }(root, 0));
7339
+ return dist;
7340
+ }
7341
+
7342
+ /**
7343
+ * Which clock plot the tree has, or null for none (see THE RULE above).
7344
+ *
7345
+ * @param phy the tree
7346
+ * @returns {string|null} 'divergence', 'root-to-tip', or null
7347
+ */
7348
+ forester.clockPlotKind = function (phy) {
7349
+ if (!phy) {
7350
+ return null;
7351
+ }
7352
+ let kind = forester.hasTimeAndDivergence(phy) ? 'divergence'
7353
+ : (isRootToTipTree(forester.getTreeRoot(phy)) ? 'root-to-tip' : null);
7354
+ if (kind === null) {
7355
+ return null;
7356
+ }
7357
+ let dateOf = kind === 'root-to-tip' ? function (n) {
7358
+ return rootToTipDate(n).value; // every tip has one: the kind says so
7359
+ } : auspiceNodeDate;
7360
+ let tips = 0;
7361
+ let first = null;
7362
+ let differ = false;
7363
+ forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
7364
+ if (n.children && n.children.length > 0) {
7365
+ return;
7366
+ }
7367
+ ++tips;
7368
+ let d = dateOf(n);
7369
+ if (first === null) {
7370
+ first = d;
7371
+ } else if (d !== first) {
7372
+ differ = true;
7373
+ }
7374
+ });
7375
+ return (tips >= CLOCK_PLOT_MIN_TIPS && differ) ? kind : null;
7376
+ };
7377
+
7378
+ /**
7379
+ * Ordinary least squares of y on x, computed about the means: a date is
7380
+ * near 2020 and its spread a few months, and sums of raw squares would
7381
+ * spend every digit on the 2020.
7382
+ *
7383
+ * @param xs the dates
7384
+ * @param ys the divergences, as many
7385
+ * @returns {{n, slope, intercept, r2, meanX, meanY}|null} null with fewer
7386
+ * than three points or every x the same; r2 is null where every
7387
+ * y is the same (no variance to explain)
7388
+ */
7389
+ forester.clockRegression = function (xs, ys) {
7390
+ let n = xs.length;
7391
+ if (n < CLOCK_PLOT_MIN_TIPS || ys.length !== n) {
7392
+ return null;
7393
+ }
7394
+ let mx = 0;
7395
+ let my = 0;
7396
+ for (let i = 0; i < n; ++i) {
7397
+ mx += xs[i];
7398
+ my += ys[i];
7399
+ }
7400
+ mx /= n;
7401
+ my /= n;
7402
+ let sxx = 0;
7403
+ let sxy = 0;
7404
+ let syy = 0;
7405
+ for (let i = 0; i < n; ++i) {
7406
+ let dx = xs[i] - mx;
7407
+ let dy = ys[i] - my;
7408
+ sxx += dx * dx;
7409
+ sxy += dx * dy;
7410
+ syy += dy * dy;
7411
+ }
7412
+ if (!(sxx > 0) || !isFinite(sxx) || !isFinite(sxy) || !isFinite(syy)) {
7413
+ return null;
7414
+ }
7415
+ let slope = sxy / sxx;
7416
+ return {
7417
+ n: n,
7418
+ slope: slope,
7419
+ intercept: my - (slope * mx),
7420
+ r2: syy > 0 ? ((sxy * sxy) / (sxx * syy)) : null,
7421
+ meanX: mx,
7422
+ meanY: my
7423
+ };
7424
+ };
7425
+
7426
+ /**
7427
+ * The clock plot of the tree, or of the clade on view.
7428
+ *
7429
+ * @param phy the tree: the RULE is asked of it
7430
+ * @param view the tree or clade to plot (the tree, a node, or the
7431
+ * viewer's subtree root); the whole tree where none is given
7432
+ * @returns {object|null} null where the tree has no clock plot; else
7433
+ * kind 'divergence' or 'root-to-tip'
7434
+ * forward whether the dates increase toward the tips (calendar
7435
+ * dates); false for ages
7436
+ * unit the dates' unit as the tree states it, or null
7437
+ * divUnit the divergence's unit: 'subs/site' for a tree with Time
7438
+ * and Div, a root-to-tip tree's stated branch length unit,
7439
+ * or null
7440
+ * fromRates whether the divergence is time x clock rate
7441
+ * calendar whether the dates are calendar years: their unit says so,
7442
+ * or they were read off the tips' names
7443
+ * fromNames how many tips are dated by their names (a root-to-tip
7444
+ * tree; 0 otherwise)
7445
+ * ambiguousNames how many of those names carry a day/month-ambiguous
7446
+ * numeric date, read day first
7447
+ * points [{node, date, div, tip}] in preorder; the tips'
7448
+ * `residual` is their divergence less the line's. On a
7449
+ * root-to-tip tree the tips only: no ancestor has a date
7450
+ * root {node, date, div}: the top of the view; `date` null on a
7451
+ * root-to-tip tree
7452
+ * fit forester.clockRegression over the tips, with
7453
+ * rate the slope in the direction time runs: divergence
7454
+ * per unit of time (negative where the tips'
7455
+ * divergence falls with time)
7456
+ * rootDate the date the line reaches the root's divergence
7457
+ * at; null unless the rate is positive
7458
+ * or null where the view has no line
7459
+ */
7460
+ forester.clockPlotData = function (phy, view) {
7461
+ let kind = forester.clockPlotKind(phy);
7462
+ if (kind === null) {
7463
+ return null;
7464
+ }
7465
+ let treeRoot = forester.getTreeRoot(phy);
7466
+ // a node is its own top; the tree, or the viewer's holder of a clade,
7467
+ // is a parentless wrapper around it
7468
+ let top = treeRoot;
7469
+ if (view) {
7470
+ top = (!view.parent && view.children && view.children.length === 1) ? view.children[0] : view;
7471
+ }
7472
+ // a root-to-tip tree's divergence is the path from the TREE's root,
7473
+ // in the view of a clade too: the clade's top then has one as well
7474
+ let rootToTip = kind === 'root-to-tip';
7475
+ let dist = rootToTip ? rootToTipDistances(treeRoot) : null;
7476
+ let divOf = rootToTip ? function (n) {
7477
+ return dist.get(n);
7478
+ } : auspiceNodeDiv;
7479
+ let points = [];
7480
+ let xs = [];
7481
+ let ys = [];
7482
+ let fromNames = 0;
7483
+ let ambiguousNames = 0;
7484
+ forester.preOrderTraversalAll(top, function (n) {
7485
+ let tip = !(n.children && n.children.length > 0);
7486
+ let date = auspiceNodeDate(n);
7487
+ if (rootToTip && tip && date === null) {
7488
+ let named = rootToTipDate(n); // every tip has one: the kind says so
7489
+ date = named ? named.value : null;
7490
+ if (named && named.fromName) {
7491
+ ++fromNames;
7492
+ if (named.ambiguous) {
7493
+ ++ambiguousNames;
7494
+ }
7495
+ }
7496
+ }
7497
+ let div = divOf(n);
7498
+ if (date === null || div === null) {
7499
+ return; // an undated ancestor (root-to-tip), or a guard, not a rule
7500
+ }
7501
+ points.push({node: n, date: date, div: div, tip: tip});
7502
+ if (tip) {
7503
+ xs.push(date);
7504
+ ys.push(div);
7505
+ }
7506
+ });
7507
+ let info = forester.timeAxisInfo(treeRoot);
7508
+ // a root-to-tip tree has no dated pair to read the direction off:
7509
+ // its dates are calendar dates unless their unit says ages
7510
+ let forward = rootToTip ? info.type !== 'geologic' : timeIncreasesTowardTips(treeRoot);
7511
+ // calendar years: the dates' unit says so, or they were read off the
7512
+ // tips' names, which carry no other kind of date
7513
+ let calendar = forward && (info.type === 'calendar' || (rootToTip && info.type === null && fromNames > 0));
7514
+ let root = {node: top, date: auspiceNodeDate(top), div: divOf(top)};
7515
+ let fit = forester.clockRegression(xs, ys);
7516
+ if (fit) {
7517
+ fit.rate = forward ? fit.slope : -fit.slope;
7518
+ fit.rootDate = (fit.rate > 0 && root.div !== null)
7519
+ ? ((root.div - fit.intercept) / fit.slope) : null;
7520
+ for (let i = 0; i < points.length; ++i) {
7521
+ if (points[i].tip) {
7522
+ points[i].residual = points[i].div - (fit.intercept + (fit.slope * points[i].date));
7523
+ }
7524
+ }
7525
+ }
7526
+ let lengthUnit = (phy !== treeRoot && typeof phy.branch_length_unit === 'string'
7527
+ && phy.branch_length_unit.trim().length > 0) ? phy.branch_length_unit.trim() : null;
7528
+ return {
7529
+ kind: kind,
7530
+ forward: forward,
7531
+ unit: dateUnitOf(treeRoot),
7532
+ divUnit: rootToTip ? lengthUnit : DIVERGENCE_LENGTH_UNIT,
7533
+ fromRates: !rootToTip && treeRoot._divergenceFromRates === true && !recordsDivergence(treeRoot),
7534
+ calendar: calendar,
7535
+ fromNames: fromNames,
7536
+ ambiguousNames: ambiguousNames,
7537
+ points: points,
7538
+ root: root,
7539
+ fit: fit
7540
+ };
7541
+ };
7542
+
7248
7543
  // A number that is not NaN. It used to test only for null, undefined and
7249
7544
  // NaN, and so answered TRUE for "hello", "", {}, [] and true -- which no
7250
7545
  // caller was hurt by, since all of them pass the result of parseFloat, but
@@ -10220,6 +10515,41 @@
10220
10515
  return vals;
10221
10516
  };
10222
10517
 
10518
+ // Month ticks over [from, to] in decimal years, for a span too short for
10519
+ // whole years (an outbreak sampled over some months): the first of every
10520
+ // 1st, 2nd, 3rd, 6th or 12th month, the smallest step leaving six ticks
10521
+ // or fewer; none where even a tick a year is too many. {value, year,
10522
+ // month}; month is 1-12, value the decimal year at which that month
10523
+ // begins.
10524
+ forester.calendarTickMonths = function (from, to) {
10525
+ let span = to - from;
10526
+ if (!(span > 0) || !isFinite(span) || !isFinite(from)) {
10527
+ return [];
10528
+ }
10529
+ let steps = [1, 2, 3, 6, 12];
10530
+ for (let s = 0; s < steps.length; ++s) {
10531
+ let step = steps[s];
10532
+ let ticks = [];
10533
+ let year = Math.floor(from);
10534
+ // month index = year * 12 + (month - 1); ticks where it divides
10535
+ for (let k = year * 12; ticks.length <= 6; ++k) {
10536
+ let y = Math.floor(k / 12);
10537
+ let m = (k - (y * 12)) + 1;
10538
+ let v = y + ((tipDateDayOfYear(y, m, 1) - 1) / tipDateYearLength(y));
10539
+ if (v > to + 1e-9) {
10540
+ break;
10541
+ }
10542
+ if (v >= from - 1e-9 && (k % step) === 0) {
10543
+ ticks.push({value: v, year: y, month: m});
10544
+ }
10545
+ }
10546
+ if (ticks.length <= 6) {
10547
+ return ticks;
10548
+ }
10549
+ }
10550
+ return [];
10551
+ };
10552
+
10223
10553
 
10224
10554
  // --------------------------------------------------------------
10225
10555
  // 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.24.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 .",