archaeopteryx 3.13.0 → 3.15.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 +85 -8
- package/archaeopteryx.js +746 -25
- package/forester.js +415 -3
- package/package.json +1 -1
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.15.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3814,6 +3814,411 @@
|
|
|
3814
3814
|
};
|
|
3815
3815
|
|
|
3816
3816
|
|
|
3817
|
+
// The nodes of `list` in PREORDER -- root first, then each subtree -- which
|
|
3818
|
+
// is the claim order the crowded-data rule needs and is NOT the order the
|
|
3819
|
+
// layout hands out. d3's descendants() is BREADTH-first (r,a,b,a1,a2,b1),
|
|
3820
|
+
// and the viewer reverses it for drawing, which puts the root LAST. Taking
|
|
3821
|
+
// that order for preorder inverts the whole rule: the leaf-most mark wins
|
|
3822
|
+
// and the one nearer the root is dropped, which is the opposite of what
|
|
3823
|
+
// the desktop does. Measured, after a review caught it: "descendants()
|
|
3824
|
+
// r,a,b,a1,a2,b1" and "reversed b1,a2,a1,b,a,r".
|
|
3825
|
+
//
|
|
3826
|
+
// Derived from the node objects rather than from any layout, so it cannot
|
|
3827
|
+
// drift with one: the root is the member whose parent is not itself in the
|
|
3828
|
+
// list, and children are visited in their own order. A node whose parent
|
|
3829
|
+
// is absent (a subtree view) roots its own walk, and anything unreachable
|
|
3830
|
+
// is appended so no mark is silently skipped.
|
|
3831
|
+
forester.preorderOf = function (list) {
|
|
3832
|
+
let present = new Set(list);
|
|
3833
|
+
let out = [];
|
|
3834
|
+
let seen = new Set();
|
|
3835
|
+
function walk(n) {
|
|
3836
|
+
if (seen.has(n)) {
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
seen.add(n);
|
|
3840
|
+
out.push(n);
|
|
3841
|
+
let kids = n.children;
|
|
3842
|
+
if (kids) {
|
|
3843
|
+
for (let i = 0; i < kids.length; ++i) {
|
|
3844
|
+
if (present.has(kids[i])) {
|
|
3845
|
+
walk(kids[i]);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
}
|
|
3850
|
+
for (let i = 0; i < list.length; ++i) {
|
|
3851
|
+
let n = list[i];
|
|
3852
|
+
if (!n.parent || !present.has(n.parent)) {
|
|
3853
|
+
walk(n);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
for (let i = 0; i < list.length; ++i) {
|
|
3857
|
+
if (!seen.has(list[i])) {
|
|
3858
|
+
out.push(list[i]); // a cycle or a detached node: never dropped
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return out;
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3864
|
+
// The box to claim for a mark that RIDES ITS BRANCH -- the circular and
|
|
3865
|
+
// unrooted layouts, where a label is turned to the branch's direction
|
|
3866
|
+
// instead of standing along a row. The axis-aligned bounds of the turned
|
|
3867
|
+
// rectangle, centred on (cx, cy):
|
|
3868
|
+
//
|
|
3869
|
+
// rot_w = |w*cos(a)| + |h*sin(a)|
|
|
3870
|
+
// rot_h = |w*sin(a)| + |h*cos(a)|
|
|
3871
|
+
//
|
|
3872
|
+
// The desktop's formula, verbatim (TreePanel.paintBranchDataRadial), and
|
|
3873
|
+
// the second half of the joint rule below. It carries between the two
|
|
3874
|
+
// programs because both measure it in real SCREEN space -- the layout's
|
|
3875
|
+
// own units do not survive the trip, which is why this was parked as a
|
|
3876
|
+
// divergence until forester-71 sent the arithmetic.
|
|
3877
|
+
//
|
|
3878
|
+
// Deliberately conservative: a diagonal label's bounds are wider than its
|
|
3879
|
+
// ink, so a crowded number is dropped rather than overprinted. Exact at
|
|
3880
|
+
// the right angles, worst at 45 degrees. Insensitive to the sign of the
|
|
3881
|
+
// angle and to a half turn, which is what lets the 180-degree flip that
|
|
3882
|
+
// keeps the far half of a fan upright leave the box alone.
|
|
3883
|
+
forester.rotatedLabelBox = function (cx, cy, w, h, angle) {
|
|
3884
|
+
let cos = Math.abs(Math.cos(angle));
|
|
3885
|
+
let sin = Math.abs(Math.sin(angle));
|
|
3886
|
+
let rw = (w * cos) + (h * sin);
|
|
3887
|
+
let rh = (w * sin) + (h * cos);
|
|
3888
|
+
return [cx - (rw / 2), cy - (rh / 2), rw, rh];
|
|
3889
|
+
};
|
|
3890
|
+
|
|
3891
|
+
// The occupancy map behind "auto-hide crowded branch data": a mark is
|
|
3892
|
+
// drawn only when its box overlaps nothing already granted in the same
|
|
3893
|
+
// pass. Ported from the desktop's LabelOccupancy (0.11.161) as a JOINT
|
|
3894
|
+
// rule on Christian's word, 2026-09-25, so neither side retunes it alone.
|
|
3895
|
+
//
|
|
3896
|
+
// Here rather than in the viewer because it is a pure algorithm and the
|
|
3897
|
+
// suite can reach it: the parts that MUST match the desktop -- first claim
|
|
3898
|
+
// wins, a refused claim records nothing, a zero-size box is granted and
|
|
3899
|
+
// not recorded, and boxes that merely touch do not overlap -- are exactly
|
|
3900
|
+
// the parts no screenshot would show going wrong.
|
|
3901
|
+
// A uniform grid rather than all-pairs: a 13,246-tip tree with branch
|
|
3902
|
+
// lengths on offers 18,511 boxes, and 171 million comparisons per redraw
|
|
3903
|
+
// is not a redraw.
|
|
3904
|
+
forester.labelOccupancy = function (cell) {
|
|
3905
|
+
let size = Math.max(4, cell);
|
|
3906
|
+
let cells = new Map();
|
|
3907
|
+
return {
|
|
3908
|
+
// true when nothing already granted overlaps, and then recorded
|
|
3909
|
+
claim: function (x, y, w, h) {
|
|
3910
|
+
if (!(w > 0) || !(h > 0)) {
|
|
3911
|
+
return true; // reserves no space; recording it would only cost comparisons
|
|
3912
|
+
}
|
|
3913
|
+
let c0 = Math.floor(x / size), c1 = Math.floor((x + w) / size);
|
|
3914
|
+
let r0 = Math.floor(y / size), r1 = Math.floor((y + h) / size);
|
|
3915
|
+
for (let c = c0; c <= c1; ++c) {
|
|
3916
|
+
for (let r = r0; r <= r1; ++r) {
|
|
3917
|
+
let bucket = cells.get(c + ':' + r);
|
|
3918
|
+
if (!bucket) {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
for (let i = 0; i < bucket.length; ++i) {
|
|
3922
|
+
let b = bucket[i];
|
|
3923
|
+
// strict: boxes that merely touch do not overlap
|
|
3924
|
+
if (x < (b[0] + b[2]) && (x + w) > b[0]
|
|
3925
|
+
&& y < (b[1] + b[3]) && (y + h) > b[1]) {
|
|
3926
|
+
return false;
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
let box = [x, y, w, h];
|
|
3932
|
+
for (let c = c0; c <= c1; ++c) {
|
|
3933
|
+
for (let r = r0; r <= r1; ++r) {
|
|
3934
|
+
let key = c + ':' + r;
|
|
3935
|
+
let bucket = cells.get(key);
|
|
3936
|
+
if (!bucket) {
|
|
3937
|
+
bucket = [];
|
|
3938
|
+
cells.set(key, bucket);
|
|
3939
|
+
}
|
|
3940
|
+
bucket.push(box);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
};
|
|
3946
|
+
};
|
|
3947
|
+
|
|
3948
|
+
// Where a protein domain can be read about, as {url, exact} or null.
|
|
3949
|
+
//
|
|
3950
|
+
// An InterPro ENTRY is addressed by ACCESSION and by nothing else --
|
|
3951
|
+
// measured, not assumed: /api/entry/pfam/PF00931/ answers 200 and names
|
|
3952
|
+
// NB-ARC, while /api/entry/pfam/NB-ARC/, /CARD/ and /Death/ are all 404.
|
|
3953
|
+
// phyloXML's <domain id="..."> carries the accession when the annotation
|
|
3954
|
+
// had one, and most files have none (our own apaf.xml names all 166 of its
|
|
3955
|
+
// boxes and gives not one an id), so the fallback is an InterPro text
|
|
3956
|
+
// search.
|
|
3957
|
+
//
|
|
3958
|
+
// The search goes by NAME, never by the id. An id that is not a bare Pfam
|
|
3959
|
+
// accession -- a versioned one from an hmmscan (PF00931.24), or a SMART or
|
|
3960
|
+
// CDD accession -- searches to nothing, while the name is exactly what a
|
|
3961
|
+
// text search is for. A version suffix is stripped for the entry URL
|
|
3962
|
+
// rather than disqualifying it: PF00931.24 IS PF00931.
|
|
3963
|
+
//
|
|
3964
|
+
// Lives here rather than in the viewer so it can be tested: a wrong URL
|
|
3965
|
+
// fails silently in someone else's browser, which is the worst place to
|
|
3966
|
+
// find out.
|
|
3967
|
+
const INTERPRO_ENTRY = 'https://www.ebi.ac.uk/interpro/entry/pfam/';
|
|
3968
|
+
const INTERPRO_SEARCH = 'https://www.ebi.ac.uk/interpro/search/text/';
|
|
3969
|
+
|
|
3970
|
+
forester.domainReference = function (domain) {
|
|
3971
|
+
if (!domain) {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
let id = (domain.id || '').trim();
|
|
3975
|
+
let acc = /^(PF\d{5,})(?:\.\d+)?$/i.exec(id);
|
|
3976
|
+
if (acc) {
|
|
3977
|
+
return {url: INTERPRO_ENTRY + acc[1].toUpperCase() + '/', exact: true};
|
|
3978
|
+
}
|
|
3979
|
+
let q = (domain.name || '').trim() || id;
|
|
3980
|
+
return q ? {url: INTERPRO_SEARCH + encodeURIComponent(q) + '/', exact: false} : null;
|
|
3981
|
+
};
|
|
3982
|
+
|
|
3983
|
+
// A domain's E-value for display. It spans many orders of magnitude, so a
|
|
3984
|
+
// small one keeps its exponent; the count of significant digits is the
|
|
3985
|
+
// only thing normalized, because a scan writes 7.2E-117 and 1E-6 and a
|
|
3986
|
+
// readout that mixed those forms would read as two different measures.
|
|
3987
|
+
forester.domainEvalueText = function (e) {
|
|
3988
|
+
if (typeof e !== 'number' || !isFinite(e)) {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
if (e === 0) {
|
|
3992
|
+
return '0';
|
|
3993
|
+
}
|
|
3994
|
+
return (Math.abs(e) >= 0.001 && Math.abs(e) < 1e7)
|
|
3995
|
+
? String(forester.roundNumber(e, 6)) : e.toExponential(2);
|
|
3996
|
+
};
|
|
3997
|
+
|
|
3998
|
+
// Everything the Tree Properties dialog reports, gathered in ONE pass:
|
|
3999
|
+
// the desktop's TreeFacts.scan, minus its histogram (Christian, 2026-09-23:
|
|
4000
|
+
// "we don't need a histogram ... but a more detailed stats would be
|
|
4001
|
+
// useful, min, median, max, n, also type ... also need to include the
|
|
4002
|
+
// properties").
|
|
4003
|
+
//
|
|
4004
|
+
// Side-effect free and DOM-free, so it can be tested in Node -- the whole
|
|
4005
|
+
// reason it lives here rather than in the dialog that renders it, which is
|
|
4006
|
+
// the same split the desktop makes between TreeFacts and
|
|
4007
|
+
// TreePropertiesForm.
|
|
4008
|
+
//
|
|
4009
|
+
// Support values are grouped BY TYPE, because a tree can carry a bootstrap
|
|
4010
|
+
// and a posterior on the same branch and their ranges are not comparable
|
|
4011
|
+
// (0-100 against 0-1); averaging them together would produce a number that
|
|
4012
|
+
// describes neither. MAD values are excluded for the same reason they are
|
|
4013
|
+
// excluded from the Newick support slot: they rate a root position, not a
|
|
4014
|
+
// clade.
|
|
4015
|
+
forester.treeStatistics = function (phyOrNode) {
|
|
4016
|
+
// Callers hand this either a phylogeny wrapper or the root node, and
|
|
4017
|
+
// the difference is one extra internal node, one extra branch and one
|
|
4018
|
+
// more of depth -- small enough to read as plausible. getTreeRoot
|
|
4019
|
+
// normalizes both, as collectBasicTreeProperties does.
|
|
4020
|
+
let root = phyOrNode ? forester.getTreeRoot(phyOrNode) : null;
|
|
4021
|
+
let stats = {
|
|
4022
|
+
tips: 0, internal: 0, nodes: 0, branches: 0,
|
|
4023
|
+
polytomies: 0, maxChildren: 0,
|
|
4024
|
+
depth: 0, height: 0, heightKnown: false,
|
|
4025
|
+
collapsed: 0,
|
|
4026
|
+
branchLengths: null,
|
|
4027
|
+
support: [],
|
|
4028
|
+
ultrametric: false,
|
|
4029
|
+
tipsWithTaxonomy: 0, distinctTaxonomies: 0, tipsWithTaxonomyId: 0,
|
|
4030
|
+
tipsWithSequence: 0, tipsWithMolSeq: 0, tipsWithDomains: 0,
|
|
4031
|
+
nodesWithDate: 0, tipsWithDistribution: 0, namedInternal: 0,
|
|
4032
|
+
duplications: 0, speciations: 0, nodesWithEvents: 0,
|
|
4033
|
+
propertyRefs: []
|
|
4034
|
+
};
|
|
4035
|
+
if (!root) {
|
|
4036
|
+
return stats;
|
|
4037
|
+
}
|
|
4038
|
+
let lengths = [];
|
|
4039
|
+
let zeroLengths = 0;
|
|
4040
|
+
let negativeLengths = 0;
|
|
4041
|
+
let byType = Object.create(null);
|
|
4042
|
+
let taxa = Object.create(null);
|
|
4043
|
+
let refs = Object.create(null);
|
|
4044
|
+
let refOrder = [];
|
|
4045
|
+
let tipDepths = [];
|
|
4046
|
+
let maxDepth = 0;
|
|
4047
|
+
|
|
4048
|
+
(function walk(n, depth, distance) {
|
|
4049
|
+
++stats.nodes;
|
|
4050
|
+
let kids = n.children || [];
|
|
4051
|
+
if (depth > maxDepth) {
|
|
4052
|
+
maxDepth = depth;
|
|
4053
|
+
}
|
|
4054
|
+
if (n !== root) {
|
|
4055
|
+
++stats.branches;
|
|
4056
|
+
let b = n.branch_length;
|
|
4057
|
+
if (typeof b === 'number' && isFinite(b)) {
|
|
4058
|
+
lengths.push(b);
|
|
4059
|
+
if (b === 0) {
|
|
4060
|
+
++zeroLengths;
|
|
4061
|
+
} else if (b < 0) {
|
|
4062
|
+
++negativeLengths;
|
|
4063
|
+
}
|
|
4064
|
+
distance += b;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
// support, by type, MAD left out
|
|
4068
|
+
if (n.confidences) {
|
|
4069
|
+
for (let i = 0; i < n.confidences.length; ++i) {
|
|
4070
|
+
let c = n.confidences[i];
|
|
4071
|
+
if (c.type === forester.MAD_CONFIDENCE_TYPE) {
|
|
4072
|
+
continue;
|
|
4073
|
+
}
|
|
4074
|
+
if (typeof c.value !== 'number' || !isFinite(c.value)) {
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
let key = c.type || '';
|
|
4078
|
+
if (!byType[key]) {
|
|
4079
|
+
byType[key] = [];
|
|
4080
|
+
stats.support.push({type: key, values: byType[key]});
|
|
4081
|
+
}
|
|
4082
|
+
byType[key].push(c.value);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
|
|
4086
|
+
++stats.nodesWithDate;
|
|
4087
|
+
}
|
|
4088
|
+
if (n.properties) {
|
|
4089
|
+
let seen = Object.create(null);
|
|
4090
|
+
for (let i = 0; i < n.properties.length; ++i) {
|
|
4091
|
+
let ref = n.properties[i] && n.properties[i].ref;
|
|
4092
|
+
if (!ref || seen[ref]) {
|
|
4093
|
+
continue; // a node carrying a ref twice counts once
|
|
4094
|
+
}
|
|
4095
|
+
seen[ref] = true;
|
|
4096
|
+
if (refs[ref] === undefined) {
|
|
4097
|
+
refs[ref] = 0;
|
|
4098
|
+
refOrder.push(ref);
|
|
4099
|
+
}
|
|
4100
|
+
++refs[ref];
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
if (n.events) {
|
|
4104
|
+
++stats.nodesWithEvents;
|
|
4105
|
+
if (n.events.duplications > 0) {
|
|
4106
|
+
stats.duplications += n.events.duplications;
|
|
4107
|
+
}
|
|
4108
|
+
if (n.events.speciations > 0) {
|
|
4109
|
+
stats.speciations += n.events.speciations;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (kids.length > 0) {
|
|
4113
|
+
++stats.internal;
|
|
4114
|
+
if (kids.length > stats.maxChildren) {
|
|
4115
|
+
stats.maxChildren = kids.length;
|
|
4116
|
+
}
|
|
4117
|
+
if (kids.length > 2) {
|
|
4118
|
+
++stats.polytomies;
|
|
4119
|
+
}
|
|
4120
|
+
if (n !== root && n.name) {
|
|
4121
|
+
++stats.namedInternal;
|
|
4122
|
+
}
|
|
4123
|
+
for (let i = 0; i < kids.length; ++i) {
|
|
4124
|
+
walk(kids[i], depth + 1, distance);
|
|
4125
|
+
}
|
|
4126
|
+
return;
|
|
4127
|
+
}
|
|
4128
|
+
++stats.tips;
|
|
4129
|
+
tipDepths.push(distance);
|
|
4130
|
+
if (n.taxonomies && n.taxonomies.length > 0) {
|
|
4131
|
+
++stats.tipsWithTaxonomy;
|
|
4132
|
+
let t = n.taxonomies[0];
|
|
4133
|
+
let label = t.scientific_name || t.code || t.common_name || '';
|
|
4134
|
+
if (label) {
|
|
4135
|
+
taxa[label] = true;
|
|
4136
|
+
}
|
|
4137
|
+
if (t.id && t.id.value) {
|
|
4138
|
+
++stats.tipsWithTaxonomyId;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
if (n.sequences && n.sequences.length > 0) {
|
|
4142
|
+
++stats.tipsWithSequence;
|
|
4143
|
+
let q = n.sequences[0];
|
|
4144
|
+
if (q.mol_seq && q.mol_seq.value) {
|
|
4145
|
+
++stats.tipsWithMolSeq;
|
|
4146
|
+
}
|
|
4147
|
+
if (forester.domainArchitectureOf(n)) {
|
|
4148
|
+
++stats.tipsWithDomains;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
if (n.distributions && n.distributions.length > 0) {
|
|
4152
|
+
++stats.tipsWithDistribution;
|
|
4153
|
+
}
|
|
4154
|
+
})(root, 0, 0);
|
|
4155
|
+
|
|
4156
|
+
stats.depth = maxDepth;
|
|
4157
|
+
stats.distinctTaxonomies = Object.keys(taxa).length;
|
|
4158
|
+
stats.propertyRefs = refOrder.map(function (r) {
|
|
4159
|
+
return {ref: r, nodes: refs[r]};
|
|
4160
|
+
});
|
|
4161
|
+
stats.branchLengths = forester.describeValues(lengths);
|
|
4162
|
+
if (stats.branchLengths) {
|
|
4163
|
+
stats.branchLengths.zero = zeroLengths;
|
|
4164
|
+
stats.branchLengths.negative = negativeLengths;
|
|
4165
|
+
}
|
|
4166
|
+
stats.support = stats.support.map(function (g) {
|
|
4167
|
+
let d = forester.describeValues(g.values);
|
|
4168
|
+
d.type = g.type;
|
|
4169
|
+
return d;
|
|
4170
|
+
}).filter(function (d) {
|
|
4171
|
+
return d.n > 0;
|
|
4172
|
+
});
|
|
4173
|
+
if (tipDepths.length > 0) {
|
|
4174
|
+
// running, not Math.min.apply: the spread of 150k tip depths
|
|
4175
|
+
// overflows the call stack, as timeAxisInfo found before this
|
|
4176
|
+
let lo = Infinity;
|
|
4177
|
+
let hi = -Infinity;
|
|
4178
|
+
for (let i = 0; i < tipDepths.length; ++i) {
|
|
4179
|
+
if (tipDepths[i] < lo) {
|
|
4180
|
+
lo = tipDepths[i];
|
|
4181
|
+
}
|
|
4182
|
+
if (tipDepths[i] > hi) {
|
|
4183
|
+
hi = tipDepths[i];
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
stats.height = hi;
|
|
4187
|
+
stats.heightKnown = lengths.length > 0;
|
|
4188
|
+
// ultrametric: every tip the same distance from the root, to a
|
|
4189
|
+
// tolerance scaled to the tree, since branch lengths are floats
|
|
4190
|
+
stats.ultrametric = stats.heightKnown && (hi - lo) <= (Math.max(hi, 1e-12) * 1e-6);
|
|
4191
|
+
}
|
|
4192
|
+
return stats;
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
// n, minimum, median, maximum, mean and sum of a list of numbers, or null
|
|
4196
|
+
// when there are none. The median is the middle value of the sorted list,
|
|
4197
|
+
// or the mean of the two middle ones -- taken on a COPY, because sorting
|
|
4198
|
+
// the caller's array would reorder data it still owns.
|
|
4199
|
+
forester.describeValues = function (values) {
|
|
4200
|
+
if (!values || values.length === 0) {
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4203
|
+
let v = values.slice().sort(function (a, b) {
|
|
4204
|
+
return a - b;
|
|
4205
|
+
});
|
|
4206
|
+
let n = v.length;
|
|
4207
|
+
let sum = 0;
|
|
4208
|
+
for (let i = 0; i < n; ++i) {
|
|
4209
|
+
sum += v[i];
|
|
4210
|
+
}
|
|
4211
|
+
let mid = Math.floor(n / 2);
|
|
4212
|
+
return {
|
|
4213
|
+
n: n,
|
|
4214
|
+
min: v[0],
|
|
4215
|
+
max: v[n - 1],
|
|
4216
|
+
median: (n % 2 === 1) ? v[mid] : ((v[mid - 1] + v[mid]) / 2),
|
|
4217
|
+
mean: sum / n,
|
|
4218
|
+
sum: sum
|
|
4219
|
+
};
|
|
4220
|
+
};
|
|
4221
|
+
|
|
3817
4222
|
forester.calcMaxBranchLength = function (node) {
|
|
3818
4223
|
let max = 0;
|
|
3819
4224
|
forester.preOrderTraversalAll(node, function (n) {
|
|
@@ -6709,7 +7114,13 @@
|
|
|
6709
7114
|
ignored++;
|
|
6710
7115
|
return;
|
|
6711
7116
|
}
|
|
6712
|
-
|
|
7117
|
+
// `id` is phyloXML's optional domain identifier, which for a Pfam
|
|
7118
|
+
// scan is the accession (PF00931). Carried through because the
|
|
7119
|
+
// hover readout links to the entry with it when it is there, and
|
|
7120
|
+
// has to search by name when it is not -- apaf.xml, our own domain
|
|
7121
|
+
// demo, has names and no ids at all.
|
|
7122
|
+
domains.push({name: d.name ? String(d.name) : '', from: from, to: to,
|
|
7123
|
+
length: to - from + 1, evalue: e, id: d.id ? String(d.id) : ''});
|
|
6713
7124
|
});
|
|
6714
7125
|
domains.sort(function (a, b) {
|
|
6715
7126
|
return a.from - b.from;
|
|
@@ -6811,7 +7222,8 @@
|
|
|
6811
7222
|
if (!(w > 0) || !isFinite(w)) {
|
|
6812
7223
|
return;
|
|
6813
7224
|
}
|
|
6814
|
-
boxes.push({name: d.name, x: start + ((d.from - 1) * f), w: w, from: d.from, to: d.to,
|
|
7225
|
+
boxes.push({name: d.name, x: start + ((d.from - 1) * f), w: w, from: d.from, to: d.to,
|
|
7226
|
+
evalue: d.evalue, id: d.id});
|
|
6815
7227
|
});
|
|
6816
7228
|
return {backbone: {x: start, w: Number(da.length) * f}, boxes: boxes};
|
|
6817
7229
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.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",
|