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/README.md +135 -7
- package/archaeopteryx.js +930 -16
- package/forester.js +331 -1
- package/package.json +2 -2
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
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.
|
|
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 .",
|