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/README.md +107 -6
- package/archaeopteryx.js +912 -15
- package/forester.js +222 -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.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.
|
|
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 .",
|