archaeopteryx 3.4.0 → 3.4.1
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 +29 -9
- package/archaeopteryx.js +244 -203
- package/forester.js +100 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,15 +217,24 @@ any node does — an event, a search hit, a selection, a visualization.
|
|
|
217
217
|
internal node's whole clade into a wedge and opens it again; **Uncollapse
|
|
218
218
|
Subtree** opens everything below a node; the tool row's uncollapse-all button
|
|
219
219
|
(the desktop's glyph, lit only while something is collapsed) opens the whole
|
|
220
|
-
tree, and so does **Esc**. The wedge
|
|
221
|
-
the
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
tree, and so does **Esc**. The wedge has its apex at the node, one edge
|
|
221
|
+
reaching the clade's nearest tip and the other its farthest, so the shape
|
|
222
|
+
shows how uneven the clade's branch lengths are, as iTOL draws it (one depth
|
|
223
|
+
step in a cladogram). Its label stands where a tip's would: on the label
|
|
224
|
+
column in the aligned phylogram and on the outer ring in circular, with the
|
|
225
|
+
same guide line; it is filled in the colour most of its tips wear under the current
|
|
226
|
+
Color-by (the colour
|
|
227
|
+
they wore, even when no tip on screen shares their value), grows gently taller with its tip count, and is named — the node's
|
|
224
228
|
own name if it has one; else the one Color-by value nearly all its tips share,
|
|
225
229
|
so a clade reads "Bovine · 12 tips" while you look at hosts; else the tips'
|
|
226
230
|
common name prefix; always with the tip count, and with `[found/total]` while
|
|
227
231
|
a search hits inside it. Legends, alignment rows and domain tracks describe
|
|
228
|
-
the tips on screen, so a collapsed clade's tips leave them
|
|
232
|
+
the tips on screen, so a collapsed clade's tips leave them,
|
|
233
|
+
and the counts follow every clade you fold or open. A clade holding search
|
|
234
|
+
hits is one dot in the overview and one stop for the hit navigator; one
|
|
235
|
+
holding selected tips is outlined in the selection colour, and filled when
|
|
236
|
+
all its tips are selected. Re-rooting opens any collapsed clade whose tips it
|
|
237
|
+
would change, such as one the new midpoint falls inside. Collapsing is
|
|
229
238
|
display state only: nothing is removed, exports and downloads carry every
|
|
230
239
|
tip, and the unrooted layout, which has no rows to fold, shows every clade
|
|
231
240
|
open. The controls are the desktop's; the drawing and naming are this
|
|
@@ -270,6 +279,13 @@ nothing; a column the tree already carries under the same ref is replaced by
|
|
|
270
279
|
the table's values. Quoted cells, `#` comment lines and Windows line ends are
|
|
271
280
|
fine.
|
|
272
281
|
|
|
282
|
+
The node menu's **Download Ext. Node Data** writes the other direction: the
|
|
283
|
+
tips under a node as a tab-separated table, header first, with the desktop
|
|
284
|
+
Archaeopteryx's column names (`name`, `taxonomy_scientific_name`, …,
|
|
285
|
+
`branch_length`, then one column per property ref). A column no tip fills
|
|
286
|
+
is left out, and a `node_id` column comes first when tip names are blank or
|
|
287
|
+
repeated. Such a file opens again as a metadata table.
|
|
288
|
+
|
|
273
289
|
Embedders do the same in two lines, before `launch()`:
|
|
274
290
|
|
|
275
291
|
```js
|
|
@@ -291,10 +307,12 @@ and **Inverse** apply to both.
|
|
|
291
307
|
Hits are hard to miss: their labels take the search colour **in bold**, a
|
|
292
308
|
translucent **pulsing halo** breathes behind each hit, and everything that is
|
|
293
309
|
*not* a hit fades — the desktop's "dim non-matches", engaged only while at
|
|
294
|
-
least one hit is
|
|
295
|
-
|
|
310
|
+
least one hit is on screen, so a fruitless search never washes the tree out.
|
|
311
|
+
A collapsed clade holding a hit counts as on screen: it stays bright, its
|
|
312
|
+
wedge outlined in the search colour and its label counting the hits, and
|
|
313
|
+
the rest fades even when every hit is inside collapsed clades. The **overview** miniature marks every hit as a dot in the same
|
|
296
314
|
colour, and a **◀ k / N ▶** navigator appears under the search boxes: each
|
|
297
|
-
press centres the previous / next hit in the viewport, wrapping around.
|
|
315
|
+
press centres the previous / next hit in the viewport, wrapping around. A collapsed clade holding hits is one dot and one stop.
|
|
298
316
|
|
|
299
317
|
## Keyboard
|
|
300
318
|
|
|
@@ -403,6 +421,8 @@ displayed), and a 1-based **column ruler**. **Hover any residue** for its
|
|
|
403
421
|
alignment column, its position within that sequence's own ungapped residues,
|
|
404
422
|
its full name, class, and Kyte-Doolittle hydropathy. The **Alignment**
|
|
405
423
|
checkbox under Display Data toggles the whole track.
|
|
424
|
+
To find a motif, pick **Molecular Sequence** in a search box: it matches the
|
|
425
|
+
residues as written, gap characters included, as the desktop does.
|
|
406
426
|
|
|
407
427
|
Alignments arrive with the tree: as phyloXML `<mol_seq is_aligned="true">`
|
|
408
428
|
elements, or in a **Nexus** file whose characters matrix accompanies its tree.
|
|
@@ -615,7 +635,7 @@ a name ending in `xml` as phyloXML, anything else as New Hampshire (Newick).
|
|
|
615
635
|
A file holding **several trees** — a Nexus TREES block, a Newick file with one
|
|
616
636
|
tree per `;`, a phyloXML with several phylogenies — opens on the first, and a
|
|
617
637
|
picker with previous / next buttons at the top of the control panel moves
|
|
618
|
-
between them; each tree opens fresh under the same config, the way a new tab
|
|
638
|
+
between them; each tree opens fresh under the same config, nothing collapsed, the way a new tab
|
|
619
639
|
does on the desktop. A protein/DNA/RNA characters matrix in a Nexus file
|
|
620
640
|
(sequential or interleaved) lands on the tips as an aligned `mol_seq`, so the
|
|
621
641
|
alignment track appears just as it does for phyloXML.
|
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.4.
|
|
23
|
+
// v 3.4.1
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.4.
|
|
106
|
+
const VERSION = '3.4.1';
|
|
107
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
108
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
109
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -1147,12 +1147,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
1147
1147
|
// on the desktop: dots in the search colours, so hits outside the
|
|
1148
1148
|
// current viewport can be spotted and steered to. Search results
|
|
1149
1149
|
// re-run update(), which ends here, so the dots track every search.
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1150
|
+
// Displayed nodes only: a hit inside a collapsed clade has no drawn
|
|
1151
|
+
// position, so the clade gets the dot, in its wedge's mark colour.
|
|
1152
1152
|
let hits = [];
|
|
1153
1153
|
if (_root) {
|
|
1154
1154
|
forEachDisplayed(function (n) {
|
|
1155
|
-
let c = getFoundColor(n);
|
|
1155
|
+
let c = getFoundColor(n) || (isCollapsed(n) ? collapsedMarkColor(n) : null);
|
|
1156
1156
|
if (c) {
|
|
1157
1157
|
hits.push({node: n, color: c});
|
|
1158
1158
|
}
|
|
@@ -1997,6 +1997,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1997
1997
|
}
|
|
1998
1998
|
});
|
|
1999
1999
|
_vis.colorNext[vis.id] = next;
|
|
2000
|
+
vis.categoryOnScreen = new Set(vis.values); // what the scale may be asked about (visualizationColorFor)
|
|
2000
2001
|
vis.categoryScale = d3.scaleOrdinal()
|
|
2001
2002
|
.domain(vis.values)
|
|
2002
2003
|
.range(vis.values.map(function (v) {
|
|
@@ -2758,13 +2759,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
2758
2759
|
}
|
|
2759
2760
|
}
|
|
2760
2761
|
|
|
2761
|
-
// The dim gate: only while a hit is actually
|
|
2762
|
+
// The dim gate: only while a hit is actually SHOWN, so the tree never
|
|
2762
2763
|
// washes out with nothing emphasised (a 0-hit search, or every hit
|
|
2763
|
-
//
|
|
2764
|
+
// outside the displayed subtree). A collapsed clade holding a hit
|
|
2765
|
+
// counts as shown: it stays bright, outlined and counting its hits,
|
|
2766
|
+
// so the rest fading points at it (Christian, 2026-09-14: "less
|
|
2767
|
+
// confusing to users" than no dimming at all).
|
|
2764
2768
|
_dimNonMatches = false;
|
|
2765
2769
|
if ((_foundNodes0 && _foundNodes0.size > 0) || (_foundNodes1 && _foundNodes1.size > 0)) {
|
|
2766
2770
|
for (let i = 0, len = nodes.length; i !== len; ++i) {
|
|
2767
|
-
if (isNodeFound(nodes[i])) {
|
|
2771
|
+
if (isNodeFound(nodes[i]) || (isCollapsed(nodes[i]) && collapsedFoundCounts(nodes[i]).found > 0)) {
|
|
2768
2772
|
_dimNonMatches = true;
|
|
2769
2773
|
break;
|
|
2770
2774
|
}
|
|
@@ -2782,10 +2786,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
2782
2786
|
placeRootStub();
|
|
2783
2787
|
|
|
2784
2788
|
if (_state.circularDisplay) {
|
|
2789
|
+
// the outermost drawn point sets the scale: a node, or the far
|
|
2790
|
+
// edge of a collapsed clade's wedge -- which reaches the clade's
|
|
2791
|
+
// farthest tip, hidden from nodes but not from the drawing, and
|
|
2792
|
+
// would otherwise poke through the ring the labels stand on
|
|
2785
2793
|
let maxY = 0;
|
|
2786
2794
|
for (let i = 0; i < nodes.length; ++i) {
|
|
2787
|
-
|
|
2788
|
-
|
|
2795
|
+
let y = isCollapsed(nodes[i]) ? Math.max(nodes[i].y, collapsedReach(nodes[i])[1]) : nodes[i].y;
|
|
2796
|
+
if (y > maxY) {
|
|
2797
|
+
maxY = y;
|
|
2789
2798
|
}
|
|
2790
2799
|
}
|
|
2791
2800
|
_radial = {
|
|
@@ -3153,9 +3162,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
3153
3162
|
// Dim Non-Matches: one opacity on the node GROUP dims its labels, dot,
|
|
3154
3163
|
// shape and branch-data numbers together; the branch lines are separate
|
|
3155
3164
|
// path.link elements and keep their full colour, as on the desktop.
|
|
3156
|
-
// Hits and selected nodes (getFoundColor) are never dimmed
|
|
3165
|
+
// Hits and selected nodes (getFoundColor) are never dimmed -- and
|
|
3166
|
+
// neither is a collapsed clade holding one: its wedge and label live in
|
|
3167
|
+
// the clade node's group, and the clade node itself is never a hit.
|
|
3157
3168
|
node.style('opacity', function (d) {
|
|
3158
|
-
|
|
3169
|
+
if (!_dimNonMatches || getFoundColor(d) || (isCollapsed(d) && collapsedHoldsHighlight(d))) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
return DIM_NON_MATCH_OPACITY;
|
|
3159
3173
|
});
|
|
3160
3174
|
|
|
3161
3175
|
let nodeUpdate = animateOrSet(node, transitionDuration)
|
|
@@ -3303,43 +3317,75 @@ function (root, d3, forester, phyloXml) {
|
|
|
3303
3317
|
// _svgGroup, so that name put them in the way of selectAll('path.link')
|
|
3304
3318
|
// -- the main link data-join, and the overview's miniature.
|
|
3305
3319
|
_svgGroup.selectAll('g.aptx-align-ext').remove();
|
|
3306
|
-
|
|
3307
|
-
|
|
3320
|
+
let alignedRect = !radialDisplay() && _state.phylogram && _state.alignPhylogram;
|
|
3321
|
+
let tipGuides = alignedRect && _state.showExternalLabels
|
|
3322
|
+
&& (_state.showNodeName || _state.showTaxonomy || _state.showSequence);
|
|
3323
|
+
// a collapsed clade's label always shows, so its guide does too: from
|
|
3324
|
+
// past the wedge's farthest tip to the label column
|
|
3325
|
+
let collapsedGuides = alignedRect ? nodes.filter(function (d) {
|
|
3326
|
+
return isCollapsed(d) && !(_state.dynahide && d.hide);
|
|
3327
|
+
}) : [];
|
|
3328
|
+
if (tipGuides || collapsedGuides.length > 0) {
|
|
3308
3329
|
let ext = _svgGroup.insert('g', 'g').attr('class', 'aptx-align-ext');
|
|
3309
|
-
|
|
3310
|
-
.
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
.
|
|
3317
|
-
|
|
3330
|
+
let guideStyle = function (sel) {
|
|
3331
|
+
return sel.attr('fill', 'none')
|
|
3332
|
+
.attr('stroke-width', 1)
|
|
3333
|
+
.attr('stroke', _state.branchColorDefault)
|
|
3334
|
+
.style('stroke-opacity', 0.25);
|
|
3335
|
+
};
|
|
3336
|
+
if (tipGuides) {
|
|
3337
|
+
guideStyle(ext.selectAll('path.aptx-tip-guide')
|
|
3338
|
+
.data(links.filter(function (d) {
|
|
3339
|
+
return (!d.target.children && !(_state.dynahide && d.target.hide));
|
|
3340
|
+
}))
|
|
3341
|
+
.enter().append('path').attr('class', 'aptx-tip-guide'))
|
|
3342
|
+
.attr('d', function (d) {
|
|
3343
|
+
return connection(d.target);
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
guideStyle(ext.selectAll('path.aptx-collapsed-guide')
|
|
3347
|
+
.data(collapsedGuides)
|
|
3348
|
+
.enter().append('path').attr('class', 'aptx-collapsed-guide'))
|
|
3318
3349
|
.attr('d', function (d) {
|
|
3319
|
-
|
|
3350
|
+
let x1 = collapsedReach(d)[1] + 5;
|
|
3351
|
+
return (_w - x1) > 5 ? 'M' + x1 + ',' + d.x + 'L' + _w + ',' + d.x : null;
|
|
3320
3352
|
});
|
|
3321
3353
|
}
|
|
3322
3354
|
|
|
3323
3355
|
// circular: a thin dashed connector from each external node out to the
|
|
3324
3356
|
// common label ring (so labels line up like iTOL's aligned display).
|
|
3325
3357
|
_svgGroup.selectAll('g.aptx-radial-conn').remove();
|
|
3326
|
-
if (_state.circularDisplay
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
.
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
.
|
|
3358
|
+
if (_state.circularDisplay) {
|
|
3359
|
+
// tips while their labels show; a collapsed clade always (its label
|
|
3360
|
+
// always shows), from its wedge's farthest reach out to the ring
|
|
3361
|
+
let connected = nodes.filter(function (d) {
|
|
3362
|
+
if (_state.dynahide && d.hide) {
|
|
3363
|
+
return false;
|
|
3364
|
+
}
|
|
3365
|
+
if (isCollapsed(d)) {
|
|
3366
|
+
return _radial.maxRad - radialRadius(collapsedReach(d)[1]) > 2;
|
|
3367
|
+
}
|
|
3368
|
+
return !d.children && _state.showExternalLabels;
|
|
3369
|
+
});
|
|
3370
|
+
let innerY = function (d) {
|
|
3371
|
+
return isCollapsed(d) ? collapsedReach(d)[1] : d.y;
|
|
3372
|
+
};
|
|
3373
|
+
if (connected.length > 0) {
|
|
3374
|
+
let conn = _svgGroup.insert('g', 'g').attr('class', 'aptx-radial-conn');
|
|
3375
|
+
conn.selectAll('line')
|
|
3376
|
+
.data(connected)
|
|
3377
|
+
.enter().append('line')
|
|
3378
|
+
.attr('stroke', _state.branchColorDefault)
|
|
3379
|
+
// width/opacity match the rectangular aligned extensions; the
|
|
3380
|
+
// dash alone marks these as connectors (0.5px at 0.3 vanished)
|
|
3381
|
+
.attr('stroke-width', 1)
|
|
3382
|
+
.style('stroke-opacity', 0.4)
|
|
3383
|
+
.style('stroke-dasharray', '2,3')
|
|
3384
|
+
.attr('x1', function (d) { return radialXY(d.x, innerY(d))[0]; })
|
|
3385
|
+
.attr('y1', function (d) { return radialXY(d.x, innerY(d))[1]; })
|
|
3386
|
+
.attr('x2', function (d) { return polarXY(radialAngle(d.x), _radial.maxRad)[0]; })
|
|
3387
|
+
.attr('y2', function (d) { return polarXY(radialAngle(d.x), _radial.maxRad)[1]; });
|
|
3388
|
+
}
|
|
3343
3389
|
}
|
|
3344
3390
|
|
|
3345
3391
|
for (let i = 0, len = nodes.length; i !== len; ++i) {
|
|
@@ -3553,6 +3599,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
3553
3599
|
if (colorModeOf(vis) === 'range') {
|
|
3554
3600
|
return vis.rangeScale ? vis.rangeScale(Number(value)) : null;
|
|
3555
3601
|
}
|
|
3602
|
+
// A value only tips hidden in a collapsed clade carry is not on
|
|
3603
|
+
// screen, so not in the scale's domain -- and a d3 ordinal scale asked
|
|
3604
|
+
// about an unknown value extends its domain and hands out an on-screen
|
|
3605
|
+
// value's colour. Such a value keeps its remembered colour instead
|
|
3606
|
+
// (assigned at launch over the whole tree, or the user's override):
|
|
3607
|
+
// the clade's wedge keeps the colour its tips wore. Christian,
|
|
3608
|
+
// 2026-09-14; reported by the desktop session, which follows.
|
|
3609
|
+
if (vis.categoryOnScreen && !vis.categoryOnScreen.has(value)) {
|
|
3610
|
+
let mem = _vis.colorMemory[vis.id];
|
|
3611
|
+
return (mem && mem[visMemoryKey(vis, value)]) || null;
|
|
3612
|
+
}
|
|
3556
3613
|
return vis.categoryScale(value);
|
|
3557
3614
|
}
|
|
3558
3615
|
|
|
@@ -3593,11 +3650,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
3593
3650
|
if (_root && ((_foundNodes0 && _foundNodes0.size > 0) || (_foundNodes1 && _foundNodes1.size > 0))) {
|
|
3594
3651
|
// natural (top-to-bottom) drawing order -- forester's preorder
|
|
3595
3652
|
// walks children in reverse, which made the NEXT arrow step upward
|
|
3653
|
+
// a collapsed clade holding hits is ONE stop, on its node: the hits
|
|
3654
|
+
// inside have no position of their own to step to
|
|
3596
3655
|
let walk = function (n) {
|
|
3597
|
-
if (isNodeFound(n)) {
|
|
3656
|
+
if (isNodeFound(n) || (isCollapsed(n) && collapsedFoundCounts(n).found > 0)) {
|
|
3598
3657
|
hits.push(n);
|
|
3599
3658
|
}
|
|
3600
|
-
if (n.children && !isCollapsed(n)) {
|
|
3659
|
+
if (n.children && !isCollapsed(n)) {
|
|
3601
3660
|
for (let i = 0; i < n.children.length; ++i) {
|
|
3602
3661
|
walk(n.children[i]);
|
|
3603
3662
|
}
|
|
@@ -5412,6 +5471,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5412
5471
|
if (index === _treeIndex) {
|
|
5413
5472
|
return makeViewerHandle();
|
|
5414
5473
|
}
|
|
5474
|
+
// collapsing is part of the view, and a switched-to tree opens fresh:
|
|
5475
|
+
// nothing collapsed, now or when you come back (Christian, 2026-09-14)
|
|
5476
|
+
clearCollapsedFlags(_trees[_treeIndex]);
|
|
5477
|
+
clearCollapsedFlags(_trees[index]);
|
|
5415
5478
|
return launchInto(_container, _trees, index, _launchConfig);
|
|
5416
5479
|
}
|
|
5417
5480
|
|
|
@@ -5571,134 +5634,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
5571
5634
|
update();
|
|
5572
5635
|
}
|
|
5573
5636
|
|
|
5637
|
+
// The tips under the node as a tab-separated table with a header,
|
|
5638
|
+
// the desktop's columns (forester.externalNodeDataTable), top to
|
|
5639
|
+
// bottom as drawn; it reads back through a metadata-table join.
|
|
5574
5640
|
function downloadExternalNodeDataAll(node) {
|
|
5575
|
-
|
|
5576
|
-
let addSep = function (t) {
|
|
5577
|
-
if (t.length > 0) {
|
|
5578
|
-
t += '\t';
|
|
5579
|
-
}
|
|
5580
|
-
return t;
|
|
5581
|
-
};
|
|
5582
|
-
|
|
5583
|
-
let addSepSame = function (t) {
|
|
5584
|
-
if (t.length > 0) {
|
|
5585
|
-
t += ', ';
|
|
5586
|
-
}
|
|
5587
|
-
return t;
|
|
5588
|
-
};
|
|
5589
|
-
let text_all = '';
|
|
5590
|
-
|
|
5591
5641
|
const ext_nodes = forester.getAllExternalNodes(node).reverse();
|
|
5592
|
-
|
|
5593
5642
|
let filename;
|
|
5594
5643
|
if (ext_nodes.length === 1 && ext_nodes[0].name) {
|
|
5595
|
-
filename = 'External_Node_Data_for_Node_' + ext_nodes[0].name.replace(/\W/g, '_') + '.
|
|
5644
|
+
filename = 'External_Node_Data_for_Node_' + ext_nodes[0].name.replace(/\W/g, '_') + '.tsv';
|
|
5596
5645
|
} else {
|
|
5597
|
-
filename = 'External_Node_Data_for_' + ext_nodes.length + '_Nodes.
|
|
5646
|
+
filename = 'External_Node_Data_for_' + ext_nodes.length + '_Nodes.tsv';
|
|
5598
5647
|
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
if (n.name) {
|
|
5604
|
-
text += n.name
|
|
5605
|
-
}
|
|
5606
|
-
|
|
5607
|
-
if (n.properties && (n.properties.length > 0)) {
|
|
5608
|
-
const sorted_properties = n.properties.concat().sort();
|
|
5609
|
-
const l = sorted_properties.length;
|
|
5610
|
-
let properties_text = '';
|
|
5611
|
-
let prev_property_ref = null;
|
|
5612
|
-
for (let pl = 0; pl < l; ++pl) {
|
|
5613
|
-
if (forester.isNodeScopedProperty(sorted_properties[pl])) {
|
|
5614
|
-
if (sorted_properties[pl].ref === prev_property_ref) {
|
|
5615
|
-
properties_text = addSepSame(properties_text);
|
|
5616
|
-
} else {
|
|
5617
|
-
prev_property_ref = sorted_properties[pl].ref;
|
|
5618
|
-
properties_text = addSep(properties_text);
|
|
5619
|
-
}
|
|
5620
|
-
properties_text += sorted_properties[pl].value;
|
|
5621
|
-
}
|
|
5622
|
-
}
|
|
5623
|
-
if (properties_text.length > 0) {
|
|
5624
|
-
text = addSep(text);
|
|
5625
|
-
text += properties_text;
|
|
5626
|
-
}
|
|
5627
|
-
}
|
|
5628
|
-
|
|
5629
|
-
if (n.taxonomies) {
|
|
5630
|
-
let tax_text = '';
|
|
5631
|
-
for (let i = 0; i < n.taxonomies.length; ++i) {
|
|
5632
|
-
let t = n.taxonomies[i];
|
|
5633
|
-
if (t.id) {
|
|
5634
|
-
if (t.id.provider) {
|
|
5635
|
-
tax_text = addSep(tax_text);
|
|
5636
|
-
tax_text += '[' + t.id.provider + ']:' + t.id.value;
|
|
5637
|
-
} else {
|
|
5638
|
-
tax_text = addSep(tax_text);
|
|
5639
|
-
tax_text += t.id.value;
|
|
5640
|
-
}
|
|
5641
|
-
}
|
|
5642
|
-
if (t.code) {
|
|
5643
|
-
tax_text = addSep(tax_text);
|
|
5644
|
-
tax_text += t.code;
|
|
5645
|
-
}
|
|
5646
|
-
if (t.scientific_name) {
|
|
5647
|
-
tax_text = addSep(tax_text);
|
|
5648
|
-
tax_text += t.scientific_name;
|
|
5649
|
-
}
|
|
5650
|
-
if (t.common_name) {
|
|
5651
|
-
tax_text = addSep(tax_text);
|
|
5652
|
-
tax_text += t.common_name;
|
|
5653
|
-
}
|
|
5654
|
-
if (t.rank) {
|
|
5655
|
-
tax_text = addSep(tax_text);
|
|
5656
|
-
tax_text += t.rank;
|
|
5657
|
-
}
|
|
5658
|
-
}
|
|
5659
|
-
text = addSep(text);
|
|
5660
|
-
text += tax_text;
|
|
5661
|
-
}
|
|
5662
|
-
if (n.sequences) {
|
|
5663
|
-
let seq_text = '';
|
|
5664
|
-
for (let i = 0; i < n.sequences.length; ++i) {
|
|
5665
|
-
let s = n.sequences[i];
|
|
5666
|
-
if (s.accession) {
|
|
5667
|
-
if (s.accession.source) {
|
|
5668
|
-
seq_text = addSep(seq_text);
|
|
5669
|
-
seq_text += '[' + s.accession.source + ']:' + s.accession.value;
|
|
5670
|
-
} else {
|
|
5671
|
-
seq_text = addSep(seq_text);
|
|
5672
|
-
seq_text += s.accession.value;
|
|
5673
|
-
}
|
|
5674
|
-
}
|
|
5675
|
-
if (s.symbol) {
|
|
5676
|
-
seq_text = addSep(seq_text);
|
|
5677
|
-
seq_text += s.symbol;
|
|
5678
|
-
}
|
|
5679
|
-
if (s.name) {
|
|
5680
|
-
seq_text = addSep(seq_text);
|
|
5681
|
-
seq_text += s.name;
|
|
5682
|
-
}
|
|
5683
|
-
if (s.gene_name) {
|
|
5684
|
-
seq_text = addSep(seq_text);
|
|
5685
|
-
seq_text += s.gene_name;
|
|
5686
|
-
}
|
|
5687
|
-
if (s.location) {
|
|
5688
|
-
seq_text = addSep(seq_text);
|
|
5689
|
-
seq_text += s.location;
|
|
5690
|
-
}
|
|
5691
|
-
}
|
|
5692
|
-
text = addSep(text);
|
|
5693
|
-
text += seq_text;
|
|
5694
|
-
}
|
|
5695
|
-
if (text.length > 0) {
|
|
5696
|
-
text_all += text + '\n';
|
|
5697
|
-
}
|
|
5698
|
-
}
|
|
5699
|
-
|
|
5700
|
-
saveAs(new Blob([text_all], {type: "application/txt"}), filename);
|
|
5701
|
-
|
|
5648
|
+
const tsv = forester.externalNodeDataTsv(ext_nodes, function (n, i) {
|
|
5649
|
+
return (n.viewId !== undefined) ? n.viewId : i + 1;
|
|
5650
|
+
});
|
|
5651
|
+
saveAs(new Blob([tsv], {type: 'text/tab-separated-values'}), filename);
|
|
5702
5652
|
update();
|
|
5703
5653
|
}
|
|
5704
5654
|
|
|
@@ -5929,7 +5879,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5929
5879
|
}
|
|
5930
5880
|
if (!_in_subtree && d.parent && d.parent.parent
|
|
5931
5881
|
&& ((_treeData.rerootable === undefined) || (_treeData.rerootable === true))) {
|
|
5932
|
-
items.push({label: 'Reroot', action: function () {
|
|
5882
|
+
items.push({label: 'Reroot', action: function () {
|
|
5883
|
+
rerootKeepingCollapse(function () {
|
|
5884
|
+
forester.reRoot(tree, d, -1);
|
|
5885
|
+
});
|
|
5886
|
+
zoomToFit();
|
|
5887
|
+
}});
|
|
5933
5888
|
}
|
|
5934
5889
|
if (_settings.enableManualNodeSelection) {
|
|
5935
5890
|
items.push({label: 'Select/Deselect Node', action: function () { selectDeselectNode(d); }});
|
|
@@ -6848,6 +6803,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6848
6803
|
}
|
|
6849
6804
|
d.collapsed = !d.collapsed;
|
|
6850
6805
|
calcMaxExtLabel();
|
|
6806
|
+
refreshVisualizations(false); // the legends describe the tips on screen
|
|
6851
6807
|
scheduleUpdate(null, 0);
|
|
6852
6808
|
}
|
|
6853
6809
|
|
|
@@ -6856,6 +6812,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6856
6812
|
n.collapsed = false;
|
|
6857
6813
|
});
|
|
6858
6814
|
calcMaxExtLabel();
|
|
6815
|
+
refreshVisualizations(false);
|
|
6859
6816
|
scheduleUpdate(null, 0);
|
|
6860
6817
|
}
|
|
6861
6818
|
|
|
@@ -6871,6 +6828,47 @@ function (root, d3, forester, phyloXml) {
|
|
|
6871
6828
|
});
|
|
6872
6829
|
}
|
|
6873
6830
|
|
|
6831
|
+
// Runs a re-rooting without turning a collapsed clade inside out. A clade
|
|
6832
|
+
// on the path to the new root has its parent turned into a child, so its
|
|
6833
|
+
// flag would go on hiding a different set of tips -- with a midpoint
|
|
6834
|
+
// inside a collapsed clade, the whole rest of the tree. Such a clade
|
|
6835
|
+
// opens; every other clade keeps its tips and stays collapsed.
|
|
6836
|
+
function rerootKeepingCollapse(reroot) {
|
|
6837
|
+
let flagged = [];
|
|
6838
|
+
forester.preOrderTraversalAll(_root_const || _root, function (n) {
|
|
6839
|
+
if (n.collapsed === true && n.children) {
|
|
6840
|
+
flagged.push({node: n, tips: forester.getAllExternalNodes(n)});
|
|
6841
|
+
}
|
|
6842
|
+
});
|
|
6843
|
+
reroot();
|
|
6844
|
+
let opened = false;
|
|
6845
|
+
flagged.forEach(function (f) {
|
|
6846
|
+
let now = f.node.children ? forester.getAllExternalNodes(f.node) : [];
|
|
6847
|
+
let kept = new Set(f.tips);
|
|
6848
|
+
if (now.length !== f.tips.length || !now.every(function (t) { return kept.has(t); })) {
|
|
6849
|
+
f.node.collapsed = false;
|
|
6850
|
+
opened = true;
|
|
6851
|
+
}
|
|
6852
|
+
});
|
|
6853
|
+
if (opened) {
|
|
6854
|
+
calcMaxExtLabel();
|
|
6855
|
+
refreshVisualizations(false);
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
|
|
6859
|
+
// What a collapsed clade is marked with for the tips it hides: the colour
|
|
6860
|
+
// of its first search hit, else the selection colour when a tip is
|
|
6861
|
+
// selected, else null. The wedge, the overview dot and the dimming all
|
|
6862
|
+
// read it, so they always agree.
|
|
6863
|
+
function collapsedMarkColor(d) {
|
|
6864
|
+
let tips = forester.getAllExternalNodes(d);
|
|
6865
|
+
let hit = tips.filter(isNodeFound)[0];
|
|
6866
|
+
if (hit) {
|
|
6867
|
+
return getFoundColor(hit);
|
|
6868
|
+
}
|
|
6869
|
+
return tips.some(function (t) { return _selectedNodes.has(t); }) ? _state.selectedColorDefault : null;
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6874
6872
|
// How tall a collapsed clade draws, in rows: 1 for a pair, growing with
|
|
6875
6873
|
// the logarithm of the tip count, capped so a huge clade does not eat
|
|
6876
6874
|
// the screen. The cluster layout's separation turns it into space.
|
|
@@ -6912,6 +6910,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
6912
6910
|
return prefix.length >= 2 ? prefix : '';
|
|
6913
6911
|
}
|
|
6914
6912
|
|
|
6913
|
+
// Whether any tip hidden in a collapsed clade is a hit or selected -- the
|
|
6914
|
+
// clade then stays undimmed, as that tip would be if it were drawn.
|
|
6915
|
+
function collapsedHoldsHighlight(d) {
|
|
6916
|
+
return collapsedMarkColor(d) !== null;
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
// Whether every tip a collapsed clade hides is marked: all of them search
|
|
6920
|
+
// hits when the clade holds any hit, else all of them selected. Such a
|
|
6921
|
+
// wedge is filled in the mark colour and its label set in it, bold.
|
|
6922
|
+
function collapsedFullyMarked(d) {
|
|
6923
|
+
let hits = collapsedFoundCounts(d);
|
|
6924
|
+
if (hits.found > 0) {
|
|
6925
|
+
return hits.found === hits.total;
|
|
6926
|
+
}
|
|
6927
|
+
let tips = forester.getAllExternalNodes(d);
|
|
6928
|
+
return tips.length > 0 && tips.every(function (t) { return _selectedNodes.has(t); });
|
|
6929
|
+
}
|
|
6930
|
+
|
|
6915
6931
|
function collapsedFoundCounts(d) {
|
|
6916
6932
|
let found = 0;
|
|
6917
6933
|
let total = 0;
|
|
@@ -6959,26 +6975,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
6959
6975
|
return best || _state.branchColorDefault;
|
|
6960
6976
|
}
|
|
6961
6977
|
|
|
6962
|
-
// The wedge's
|
|
6963
|
-
//
|
|
6964
|
-
//
|
|
6965
|
-
//
|
|
6966
|
-
//
|
|
6967
|
-
|
|
6978
|
+
// The wedge's reach: the clade's nearest and farthest tips, in the
|
|
6979
|
+
// layout's x. One edge runs to the nearest tip and the other to the
|
|
6980
|
+
// farthest, so the shape itself shows how uneven the clade's branch
|
|
6981
|
+
// lengths are (as iTOL draws it). Christian chose this over the
|
|
6982
|
+
// desktop's symmetric triangle on an average base (2026-09-14). A
|
|
6983
|
+
// phylogram measures the tips; a cladogram, where every leaf sits on
|
|
6984
|
+
// the tip column, gives the wedge one depth step.
|
|
6985
|
+
function collapsedReach(d) {
|
|
6968
6986
|
if (_state.phylogram && _yScale) {
|
|
6969
|
-
let
|
|
6970
|
-
let
|
|
6987
|
+
let min = Infinity;
|
|
6988
|
+
let max = -Infinity;
|
|
6971
6989
|
forester.preOrderTraversal(d, function (t) {
|
|
6972
6990
|
if (!t.children && typeof t.distToRoot === 'number') {
|
|
6973
|
-
|
|
6974
|
-
|
|
6991
|
+
if (t.distToRoot < min) { min = t.distToRoot; }
|
|
6992
|
+
if (t.distToRoot > max) { max = t.distToRoot; }
|
|
6975
6993
|
}
|
|
6976
6994
|
});
|
|
6977
|
-
if (
|
|
6978
|
-
return Math.max(d.y + 2, _yScale(
|
|
6995
|
+
if (isFinite(min)) {
|
|
6996
|
+
return [Math.max(d.y + 2, _yScale(min)), Math.max(d.y + 2, _yScale(max))];
|
|
6979
6997
|
}
|
|
6980
6998
|
}
|
|
6981
|
-
return d.y + _cladogramUnit;
|
|
6999
|
+
return [d.y + _cladogramUnit, d.y + _cladogramUnit];
|
|
6982
7000
|
}
|
|
6983
7001
|
|
|
6984
7002
|
let _cladogramUnit = 0; // one depth step of the last cladogram layout, px
|
|
@@ -7001,52 +7019,71 @@ function (root, d3, forester, phyloXml) {
|
|
|
7001
7019
|
let fs = _state.externalNodeFontSize;
|
|
7002
7020
|
wedge.each(function (d) {
|
|
7003
7021
|
let color = collapsedColor(d);
|
|
7004
|
-
let
|
|
7005
|
-
let
|
|
7022
|
+
let mark = collapsedMarkColor(d);
|
|
7023
|
+
let full = collapsedFullyMarked(d);
|
|
7006
7024
|
let h = Math.max(6, collapsedRows(d) * _rowUnit * 0.82);
|
|
7007
|
-
let
|
|
7025
|
+
let reach = collapsedReach(d);
|
|
7008
7026
|
let path;
|
|
7009
7027
|
if (radialDisplay()) {
|
|
7010
7028
|
// in the rotated frame: +x along the spoke from the node (the
|
|
7011
|
-
// group's origin, already at the node's radius)
|
|
7012
|
-
//
|
|
7013
|
-
//
|
|
7014
|
-
let
|
|
7015
|
-
|
|
7029
|
+
// group's origin, already at the node's radius) -- distances,
|
|
7030
|
+
// never absolute radii, or the wedge lands a whole radius
|
|
7031
|
+
// further out than its label
|
|
7032
|
+
let r0 = radialRadius(d.y);
|
|
7033
|
+
let dr1 = radialRadius(reach[0]) - r0;
|
|
7034
|
+
let dr2 = radialRadius(reach[1]) - r0;
|
|
7035
|
+
path = 'M0,0 L' + dr1 + ',' + (-h / 2) + ' L' + dr2 + ',' + (h / 2) + ' Z';
|
|
7016
7036
|
} else {
|
|
7017
|
-
let
|
|
7018
|
-
|
|
7037
|
+
let dx1 = reach[0] - d.y;
|
|
7038
|
+
let dx2 = reach[1] - d.y;
|
|
7039
|
+
path = 'M0,0 L' + dx1 + ',' + (-h / 2) + ' L' + dx2 + ',' + (h / 2) + ' Z';
|
|
7019
7040
|
}
|
|
7020
7041
|
d3.select(this)
|
|
7021
7042
|
.attr('d', path)
|
|
7022
7043
|
.attr('transform', radialDisplay() ? 'rotate(' + labelAngleDeg(d) + ')' : null)
|
|
7023
|
-
.style('fill',
|
|
7024
|
-
.style('fill-opacity',
|
|
7025
|
-
.style('stroke',
|
|
7026
|
-
.style('stroke-width',
|
|
7044
|
+
.style('fill', full ? mark : color)
|
|
7045
|
+
.style('fill-opacity', full ? 0.45 : 0.22)
|
|
7046
|
+
.style('stroke', mark || color)
|
|
7047
|
+
.style('stroke-width', mark ? 1.5 : 1)
|
|
7027
7048
|
.style('stroke-opacity', 0.9)
|
|
7028
7049
|
.style('stroke-linejoin', 'round');
|
|
7029
7050
|
});
|
|
7030
7051
|
label.each(function (d) {
|
|
7031
|
-
let
|
|
7032
|
-
let
|
|
7033
|
-
let
|
|
7034
|
-
let ink = allFound ? getFoundColor(forester.getAllExternalNodes(d).filter(isNodeFound)[0]) : _state.labelColorDefault;
|
|
7052
|
+
let far = collapsedReach(d)[1]; // the label clears the farthest tip
|
|
7053
|
+
let allFound = collapsedFullyMarked(d);
|
|
7054
|
+
let ink = allFound ? collapsedMarkColor(d) : _state.labelColorDefault;
|
|
7035
7055
|
let t = d3.select(this)
|
|
7036
7056
|
.text(collapsedLabel(d))
|
|
7037
7057
|
.style('font', (allFound ? '600 ' : '') + fs + 'px ' + FONT_DEFAULTS)
|
|
7038
7058
|
.style('fill', ink)
|
|
7039
7059
|
.style('pointer-events', 'none');
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7060
|
+
// placed exactly as a tip's label is (update's text.extlabel): on
|
|
7061
|
+
// the common ring in circular, rotated along the spoke or upright;
|
|
7062
|
+
// on the label column in the aligned phylogram; else just past the
|
|
7063
|
+
// wedge, where a tip's label sits past its tip
|
|
7064
|
+
let gap = _state.nodeLabelGap;
|
|
7065
|
+
let flip = radialDisplay() && labelFlip(d);
|
|
7066
|
+
if (_state.circularDisplay && _radial) {
|
|
7067
|
+
t.attr('text-anchor', flip ? 'end' : 'start')
|
|
7068
|
+
.attr('x', flip ? -gap : gap).attr('dy', '0.32em');
|
|
7069
|
+
if (_radialLabelsHorizontal) {
|
|
7070
|
+
let p = radialXY(d.x, d.y);
|
|
7071
|
+
let q = polarXY(radialAngle(d.x), _radial.maxRad);
|
|
7072
|
+
t.attr('transform', 'translate(' + (q[0] - p[0]) + ',' + (q[1] - p[1]) + ')');
|
|
7073
|
+
} else {
|
|
7074
|
+
let off = _radial.maxRad - radialRadius(d.y);
|
|
7075
|
+
t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + off + ',0)' + (flip ? ' rotate(180)' : ''));
|
|
7076
|
+
}
|
|
7077
|
+
} else if (radialDisplay()) {
|
|
7078
|
+
let r = radialRadius(far) - radialRadius(d.y) + gap;
|
|
7043
7079
|
t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + r + ',0)' + (flip ? ' rotate(180)' : ''))
|
|
7044
7080
|
.attr('text-anchor', flip ? 'end' : 'start')
|
|
7045
7081
|
.attr('x', 0).attr('dy', '0.32em');
|
|
7046
7082
|
} else {
|
|
7083
|
+
let column = (_state.phylogram && _state.alignPhylogram) ? _w : far;
|
|
7047
7084
|
t.attr('transform', null)
|
|
7048
7085
|
.attr('text-anchor', 'start')
|
|
7049
|
-
.attr('x',
|
|
7086
|
+
.attr('x', column - d.y + gap)
|
|
7050
7087
|
.attr('dy', (0.3 * fs) + 'px');
|
|
7051
7088
|
}
|
|
7052
7089
|
});
|
|
@@ -7236,7 +7273,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7236
7273
|
}
|
|
7237
7274
|
if (s.root === 'midpoint' && _viewOps.root !== 'midpoint'
|
|
7238
7275
|
&& (_treeData.rerootable === undefined || _treeData.rerootable === true)) {
|
|
7239
|
-
|
|
7276
|
+
rerootKeepingCollapse(function () {
|
|
7277
|
+
forester.midpointRoot(_root_const);
|
|
7278
|
+
});
|
|
7240
7279
|
_viewOps.root = 'midpoint';
|
|
7241
7280
|
}
|
|
7242
7281
|
if (s.order === 'asc' || s.order === 'desc') {
|
|
@@ -8779,7 +8818,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8779
8818
|
showNodeMenu([
|
|
8780
8819
|
{
|
|
8781
8820
|
label: 'Midpoint re-root', action: function () {
|
|
8782
|
-
|
|
8821
|
+
rerootKeepingCollapse(function () {
|
|
8822
|
+
forester.midpointRoot(_root);
|
|
8823
|
+
});
|
|
8783
8824
|
_viewOps.root = 'midpoint'; // what a shared view replays
|
|
8784
8825
|
zoomToFit();
|
|
8785
8826
|
}
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.4.
|
|
23
|
+
// v 3.4.1
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3978,6 +3978,98 @@
|
|
|
3978
3978
|
// becomes "meta:" plus the header with its whitespace as '_' -- the
|
|
3979
3979
|
// display name prettifies that back to spaces, so "Collection Date"
|
|
3980
3980
|
// stays "Collection Date" in every menu.
|
|
3981
|
+
// The tips' data as a table, one row per tip in the order given, header
|
|
3982
|
+
// first: what the node menu's "Download Ext. Node Data" writes. The
|
|
3983
|
+
// columns and their names are the desktop's (NodeDataExporter.toNodeDataTsv),
|
|
3984
|
+
// so the two programs write the same table: name (always), the first
|
|
3985
|
+
// taxonomy's scientific name, common name, code, id and rank, the first
|
|
3986
|
+
// sequence's name, gene name, symbol, accession and type, the branch
|
|
3987
|
+
// length, then one column per property ref, sorted, holding its first
|
|
3988
|
+
// value. A column no tip has a value for is left out. When the tip names
|
|
3989
|
+
// cannot key the rows (one blank or repeated), a node_id column comes
|
|
3990
|
+
// first, from idOf(tip, index) or the row number. Tabs and line breaks
|
|
3991
|
+
// inside a value become spaces. A property keeps its ref as the header,
|
|
3992
|
+
// so the table joins back onto a tree with joinMetadataTable.
|
|
3993
|
+
forester.externalNodeDataTable = function (tips, idOf) {
|
|
3994
|
+
tips = tips || [];
|
|
3995
|
+
if (tips.length === 0) {
|
|
3996
|
+
return {columns: [], rows: []};
|
|
3997
|
+
}
|
|
3998
|
+
let clean = function (v) {
|
|
3999
|
+
return (v === undefined || v === null) ? '' : String(v).replace(/[\t\n\r]/g, ' ');
|
|
4000
|
+
};
|
|
4001
|
+
let tax = function (n) {
|
|
4002
|
+
return (n.taxonomies && n.taxonomies[0]) || {};
|
|
4003
|
+
};
|
|
4004
|
+
let seq = function (n) {
|
|
4005
|
+
return (n.sequences && n.sequences[0]) || {};
|
|
4006
|
+
};
|
|
4007
|
+
let cols = [];
|
|
4008
|
+
let add = function (name, extract, force) {
|
|
4009
|
+
let vals = tips.map(function (n, i) {
|
|
4010
|
+
return clean(extract(n, i));
|
|
4011
|
+
});
|
|
4012
|
+
if (force || vals.some(function (v) { return v.length > 0; })) {
|
|
4013
|
+
cols.push({name: name, vals: vals});
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
let seen = new Set();
|
|
4017
|
+
let unique = tips.every(function (n) {
|
|
4018
|
+
if (!n.name || seen.has(n.name)) {
|
|
4019
|
+
return false;
|
|
4020
|
+
}
|
|
4021
|
+
seen.add(n.name);
|
|
4022
|
+
return true;
|
|
4023
|
+
});
|
|
4024
|
+
if (!unique) {
|
|
4025
|
+
add('node_id', function (n, i) { return idOf ? idOf(n, i) : i + 1; }, true);
|
|
4026
|
+
}
|
|
4027
|
+
add('name', function (n) { return n.name; }, true);
|
|
4028
|
+
add('taxonomy_scientific_name', function (n) { return tax(n).scientific_name; });
|
|
4029
|
+
add('taxonomy_common_name', function (n) { return tax(n).common_name; });
|
|
4030
|
+
add('taxonomy_code', function (n) { return tax(n).code; });
|
|
4031
|
+
add('taxonomy_id', function (n) { return tax(n).id && tax(n).id.value; });
|
|
4032
|
+
add('taxonomy_rank', function (n) { return tax(n).rank; });
|
|
4033
|
+
add('sequence_name', function (n) { return seq(n).name; });
|
|
4034
|
+
add('gene_name', function (n) { return seq(n).gene_name; });
|
|
4035
|
+
add('sequence_symbol', function (n) { return seq(n).symbol; });
|
|
4036
|
+
add('sequence_accession', function (n) { return seq(n).accession && seq(n).accession.value; });
|
|
4037
|
+
add('sequence_type', function (n) { return seq(n).type; });
|
|
4038
|
+
add('branch_length', function (n) { return (typeof n.branch_length === 'number') ? n.branch_length : ''; });
|
|
4039
|
+
let refs = new Set();
|
|
4040
|
+
tips.forEach(function (n) {
|
|
4041
|
+
(n.properties || []).forEach(function (p) {
|
|
4042
|
+
if (p.ref) {
|
|
4043
|
+
refs.add(p.ref);
|
|
4044
|
+
}
|
|
4045
|
+
});
|
|
4046
|
+
});
|
|
4047
|
+
Array.from(refs).sort().forEach(function (ref) {
|
|
4048
|
+
add(ref, function (n) {
|
|
4049
|
+
let p = (n.properties || []).filter(function (q) { return q.ref === ref; })[0];
|
|
4050
|
+
return p ? p.value : '';
|
|
4051
|
+
});
|
|
4052
|
+
});
|
|
4053
|
+
return {
|
|
4054
|
+
columns: cols.map(function (c) { return c.name; }),
|
|
4055
|
+
rows: tips.map(function (n, i) {
|
|
4056
|
+
return cols.map(function (c) { return c.vals[i]; });
|
|
4057
|
+
})
|
|
4058
|
+
};
|
|
4059
|
+
};
|
|
4060
|
+
|
|
4061
|
+
// externalNodeDataTable as tab-separated text, header line first; empty
|
|
4062
|
+
// for no tips.
|
|
4063
|
+
forester.externalNodeDataTsv = function (tips, idOf) {
|
|
4064
|
+
let t = forester.externalNodeDataTable(tips, idOf);
|
|
4065
|
+
if (t.columns.length === 0) {
|
|
4066
|
+
return '';
|
|
4067
|
+
}
|
|
4068
|
+
return [t.columns].concat(t.rows).map(function (r) {
|
|
4069
|
+
return r.join('\t');
|
|
4070
|
+
}).join('\n') + '\n';
|
|
4071
|
+
};
|
|
4072
|
+
|
|
3981
4073
|
forester.metadataColumnRef = function (header, index) {
|
|
3982
4074
|
let h = String(header || '').trim();
|
|
3983
4075
|
if (h.length === 0) {
|
|
@@ -4135,7 +4227,13 @@
|
|
|
4135
4227
|
const SEARCH_DOMAIN = textField('Domain', n => searchSeqs(n).reduce((a, s) => a.concat((s.domain_architecture && s.domain_architecture.domains) ? s.domain_architecture.domains.map(d => d.name) : []), []).filter(Boolean));
|
|
4136
4228
|
const SEARCH_ANNOTATION = textField('Annotation', n => searchSeqs(n).reduce((a, s) => a.concat((s.annotations || []).reduce((b, an) => b.concat([an.desc, an.ref]), [])), []).filter(Boolean), {anyText: true});
|
|
4137
4229
|
const SEARCH_CROSS_REFERENCE = textField('Cross-Reference', n => searchSeqs(n).reduce((a, s) => a.concat((s.cross_references || []).reduce((b, x) => b.concat([x.value, x.source, x.comment]), [])), []).filter(Boolean), {anyText: true});
|
|
4138
|
-
|
|
4230
|
+
// The residues: the phyloXML and Nexus readers give mol_seq as {value,
|
|
4231
|
+
// is_aligned}, and a hand-built tree may carry the plain string. Reading
|
|
4232
|
+
// the object itself compared every query against "[object Object]", so
|
|
4233
|
+
// this field never matched a real file (its test built the string form).
|
|
4234
|
+
const SEARCH_MOLECULAR_SEQUENCE = textField('Molecular Sequence', n => searchSeqs(n).map(function (s) {
|
|
4235
|
+
return (s.mol_seq && typeof s.mol_seq === 'object') ? s.mol_seq.value : s.mol_seq;
|
|
4236
|
+
}).filter(Boolean), {suggest: false});
|
|
4139
4237
|
// in menu order
|
|
4140
4238
|
const SEARCH_TEXT_FIELDS = [
|
|
4141
4239
|
SEARCH_NODE_NAME, SEARCH_TAXONOMY_SCIENTIFIC_NAME, SEARCH_TAXONOMY_COMMON_NAME, SEARCH_TAXONOMY_CODE,
|
package/package.json
CHANGED