archaeopteryx 3.3.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 +166 -14
- package/archaeopteryx.d.ts +100 -10
- package/archaeopteryx.js +2220 -397
- package/forester.js +387 -3
- 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.4.1
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -1602,11 +1602,23 @@
|
|
|
1602
1602
|
// everything found here is in the candidate's domain and keeps its
|
|
1603
1603
|
// colour. A numeric candidate also gets the view's colour-mode band
|
|
1604
1604
|
// (see visNumericModes); a category keeps its mode.
|
|
1605
|
+
// `root` is a node, walked in preorder, or an array of the tips to
|
|
1606
|
+
// describe (the viewer passes what is on screen, which a collapsed clade
|
|
1607
|
+
// shortens).
|
|
1605
1608
|
forester.visualizationSummary = function (candidate, root) {
|
|
1606
1609
|
let counts = Object.create(null);
|
|
1607
1610
|
let total = 0;
|
|
1608
1611
|
let coverage = 0;
|
|
1609
|
-
|
|
1612
|
+
let tips = root;
|
|
1613
|
+
if (!Array.isArray(root)) {
|
|
1614
|
+
tips = [];
|
|
1615
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
1616
|
+
if (!n.children) {
|
|
1617
|
+
tips.push(n);
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
tips.forEach(function (n) {
|
|
1610
1622
|
if (n.children) {
|
|
1611
1623
|
return;
|
|
1612
1624
|
}
|
|
@@ -2318,6 +2330,17 @@
|
|
|
2318
2330
|
// the shared contract -- and throwing on a combination a user
|
|
2319
2331
|
// plausibly wants is what forced callers into workarounds.
|
|
2320
2332
|
|
|
2333
|
+
// A text holding several trees (one per ';') reads as its FIRST;
|
|
2334
|
+
// parseNewHampshireTrees reads them all. Before this the statements
|
|
2335
|
+
// ran together and the LAST tree came back, the others silently
|
|
2336
|
+
// dropped.
|
|
2337
|
+
{
|
|
2338
|
+
let statements = forester.splitNewHampshire(nhStr);
|
|
2339
|
+
if (statements.length > 1) {
|
|
2340
|
+
nhStr = statements[0];
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2321
2344
|
let ancs = [];
|
|
2322
2345
|
let x = {};
|
|
2323
2346
|
|
|
@@ -2569,6 +2592,66 @@
|
|
|
2569
2592
|
}
|
|
2570
2593
|
};
|
|
2571
2594
|
|
|
2595
|
+
// Splits a New Hampshire text into its tree statements, one per ';'
|
|
2596
|
+
// outside quotes and [...] comments (a ';' inside a quoted label or a
|
|
2597
|
+
// [&...] annotation is data), each trimmed; blank statements are
|
|
2598
|
+
// dropped. A text without a terminating ';' is one statement.
|
|
2599
|
+
forester.splitNewHampshire = function (nhStr) {
|
|
2600
|
+
let s = String(nhStr);
|
|
2601
|
+
let parts = [];
|
|
2602
|
+
let start = 0;
|
|
2603
|
+
let inSingle = false;
|
|
2604
|
+
let inDouble = false;
|
|
2605
|
+
let depth = 0;
|
|
2606
|
+
for (let i = 0, n = s.length; i < n; ++i) {
|
|
2607
|
+
let c = s.charAt(i);
|
|
2608
|
+
if (inSingle) {
|
|
2609
|
+
if (c === "'") {
|
|
2610
|
+
inSingle = false;
|
|
2611
|
+
}
|
|
2612
|
+
} else if (inDouble) {
|
|
2613
|
+
if (c === '"') {
|
|
2614
|
+
inDouble = false;
|
|
2615
|
+
}
|
|
2616
|
+
} else if (depth > 0) {
|
|
2617
|
+
if (c === ']') {
|
|
2618
|
+
--depth;
|
|
2619
|
+
} else if (c === '[') {
|
|
2620
|
+
++depth;
|
|
2621
|
+
}
|
|
2622
|
+
} else if (c === "'") {
|
|
2623
|
+
inSingle = true;
|
|
2624
|
+
} else if (c === '"') {
|
|
2625
|
+
inDouble = true;
|
|
2626
|
+
} else if (c === '[') {
|
|
2627
|
+
depth = 1;
|
|
2628
|
+
} else if (c === ';') {
|
|
2629
|
+
parts.push(s.substring(start, i + 1));
|
|
2630
|
+
start = i + 1;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
parts.push(s.substring(start));
|
|
2634
|
+
return parts.map(function (p) {
|
|
2635
|
+
return p.trim();
|
|
2636
|
+
}).filter(function (p) {
|
|
2637
|
+
return p.length > 0;
|
|
2638
|
+
});
|
|
2639
|
+
};
|
|
2640
|
+
|
|
2641
|
+
// Every tree in a New Hampshire text (a file can hold many, one per
|
|
2642
|
+
// ';'), each parsed as parseNewHampshire does, in file order. A text
|
|
2643
|
+
// with no statement at all is handed to parseNewHampshire whole, so it
|
|
2644
|
+
// fails the way an empty tree always did.
|
|
2645
|
+
forester.parseNewHampshireTrees = function (nhStr, confidenceValuesInBrackets, confidenceValuesAsInternalNames) {
|
|
2646
|
+
let statements = forester.splitNewHampshire(nhStr);
|
|
2647
|
+
if (statements.length === 0) {
|
|
2648
|
+
return [forester.parseNewHampshire(nhStr, confidenceValuesInBrackets, confidenceValuesAsInternalNames)];
|
|
2649
|
+
}
|
|
2650
|
+
return statements.map(function (statement) {
|
|
2651
|
+
return forester.parseNewHampshire(statement, confidenceValuesInBrackets, confidenceValuesAsInternalNames);
|
|
2652
|
+
});
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2572
2655
|
// Parses a Nexus-formatted string and returns an ARRAY of tree objects,
|
|
2573
2656
|
// each in the same shape parseNewHampshire produces (a Nexus file can
|
|
2574
2657
|
// hold any number of trees). Ported from the desktop's
|
|
@@ -3793,6 +3876,301 @@
|
|
|
3793
3876
|
return {backbone: {x: start, w: Number(da.length) * f}, boxes: boxes};
|
|
3794
3877
|
};
|
|
3795
3878
|
|
|
3879
|
+
// --------------------------------------------------------------
|
|
3880
|
+
// Scale bar
|
|
3881
|
+
// --------------------------------------------------------------
|
|
3882
|
+
// A phylogram's scale bar spans a round number of branch-length units
|
|
3883
|
+
// -- 1, 2 or 5 times a power of ten -- chosen so the bar comes out about
|
|
3884
|
+
// targetPx long at pxPerUnit pixels per unit. Returns {length, label,
|
|
3885
|
+
// px}, or null when the scale is unusable (zero, negative or infinite).
|
|
3886
|
+
forester.scaleBarLength = function (pxPerUnit, targetPx) {
|
|
3887
|
+
if (!(pxPerUnit > 0) || !isFinite(pxPerUnit)) {
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
let raw = (targetPx || 100) / pxPerUnit;
|
|
3891
|
+
let k = Math.floor(Math.log10(raw));
|
|
3892
|
+
let base = raw / Math.pow(10, k);
|
|
3893
|
+
let nice = base < 1.5 ? 1 : (base < 3.5 ? 2 : (base < 7.5 ? 5 : 10));
|
|
3894
|
+
let length = Number((nice * Math.pow(10, k)).toPrecision(2));
|
|
3895
|
+
return {length: length, label: String(length), px: length * pxPerUnit};
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
// --------------------------------------------------------------
|
|
3899
|
+
// Metadata tables
|
|
3900
|
+
// --------------------------------------------------------------
|
|
3901
|
+
// A table beside the tree -- TSV or CSV with a header row, the first
|
|
3902
|
+
// column naming the tip -- joined onto the tips as node properties, so
|
|
3903
|
+
// that everything downstream of a property sees the columns as if the
|
|
3904
|
+
// file had carried them: the automatic Color-by and Shape candidates,
|
|
3905
|
+
// the legends, the search fields, the node-data dialog, the phyloXML
|
|
3906
|
+
// writer. Every other browser viewer takes such a table; it was the one
|
|
3907
|
+
// input this library lacked (field review, 2026-09-13).
|
|
3908
|
+
|
|
3909
|
+
forester.METADATA_NAMESPACE = 'meta';
|
|
3910
|
+
|
|
3911
|
+
function splitDelimitedLine(line, delimiter) {
|
|
3912
|
+
let out = [];
|
|
3913
|
+
let cur = '';
|
|
3914
|
+
let quoted = false;
|
|
3915
|
+
for (let i = 0; i < line.length; ++i) {
|
|
3916
|
+
let c = line.charAt(i);
|
|
3917
|
+
if (quoted) {
|
|
3918
|
+
if (c === '"') {
|
|
3919
|
+
if (line.charAt(i + 1) === '"') { // a doubled quote is a literal one
|
|
3920
|
+
cur += '"';
|
|
3921
|
+
i++;
|
|
3922
|
+
} else {
|
|
3923
|
+
quoted = false;
|
|
3924
|
+
}
|
|
3925
|
+
} else {
|
|
3926
|
+
cur += c;
|
|
3927
|
+
}
|
|
3928
|
+
} else if (c === '"') {
|
|
3929
|
+
quoted = true;
|
|
3930
|
+
} else if (c === delimiter) {
|
|
3931
|
+
out.push(cur);
|
|
3932
|
+
cur = '';
|
|
3933
|
+
} else {
|
|
3934
|
+
cur += c;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
out.push(cur);
|
|
3938
|
+
return out.map(function (s) {
|
|
3939
|
+
return s.trim();
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// Splits delimited text into its header and rows. The delimiter is
|
|
3944
|
+
// whichever of tab, comma and semicolon occurs most in the header line
|
|
3945
|
+
// (tab when none does); fields may be double-quoted; blank lines and
|
|
3946
|
+
// lines starting with '#' are skipped; Windows line ends are fine.
|
|
3947
|
+
// Header names and cells come back trimmed. Throws when there is no
|
|
3948
|
+
// header or fewer than two columns.
|
|
3949
|
+
forester.parseDelimitedTable = function (text) {
|
|
3950
|
+
let lines = String(text || '').split(/\r?\n/).filter(function (l) {
|
|
3951
|
+
return l.trim().length > 0 && l.charAt(0) !== '#';
|
|
3952
|
+
});
|
|
3953
|
+
if (lines.length === 0) {
|
|
3954
|
+
throw new Error('the table is empty');
|
|
3955
|
+
}
|
|
3956
|
+
let delimiter = '\t';
|
|
3957
|
+
let best = -1;
|
|
3958
|
+
['\t', ',', ';'].forEach(function (d) {
|
|
3959
|
+
let n = lines[0].split(d).length - 1;
|
|
3960
|
+
if (n > best) {
|
|
3961
|
+
best = n;
|
|
3962
|
+
delimiter = d;
|
|
3963
|
+
}
|
|
3964
|
+
});
|
|
3965
|
+
let columns = splitDelimitedLine(lines[0], delimiter);
|
|
3966
|
+
if (columns.length < 2) {
|
|
3967
|
+
throw new Error('the table needs a header row with at least two columns: the tip name, then the data');
|
|
3968
|
+
}
|
|
3969
|
+
let rows = [];
|
|
3970
|
+
for (let i = 1; i < lines.length; ++i) {
|
|
3971
|
+
rows.push(splitDelimitedLine(lines[i], delimiter));
|
|
3972
|
+
}
|
|
3973
|
+
return {columns: columns, rows: rows, delimiter: delimiter};
|
|
3974
|
+
};
|
|
3975
|
+
|
|
3976
|
+
// The property ref for a column. A header that already reads as a
|
|
3977
|
+
// phyloXML ref (ns:local, no whitespace) is kept as it is; any other
|
|
3978
|
+
// becomes "meta:" plus the header with its whitespace as '_' -- the
|
|
3979
|
+
// display name prettifies that back to spaces, so "Collection Date"
|
|
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
|
+
|
|
4073
|
+
forester.metadataColumnRef = function (header, index) {
|
|
4074
|
+
let h = String(header || '').trim();
|
|
4075
|
+
if (h.length === 0) {
|
|
4076
|
+
h = 'column_' + (index + 1);
|
|
4077
|
+
}
|
|
4078
|
+
if (/^[A-Za-z0-9_]+:\S+$/.test(h)) {
|
|
4079
|
+
return h;
|
|
4080
|
+
}
|
|
4081
|
+
return forester.METADATA_NAMESPACE + ':' + h.replace(/\s+/g, '_');
|
|
4082
|
+
};
|
|
4083
|
+
|
|
4084
|
+
// Joins a table onto the tree's tips. The first column is the key,
|
|
4085
|
+
// matched to the tip's name exactly, then case-insensitively. Every
|
|
4086
|
+
// other column becomes one property per matched tip with a non-empty
|
|
4087
|
+
// cell (applies_to node; the datatype is xsd:integer or xsd:double when
|
|
4088
|
+
// every filled cell of the column is such a number, xsd:string
|
|
4089
|
+
// otherwise). A property the tip already carries under the same ref is
|
|
4090
|
+
// replaced -- the table wins. Returns what happened:
|
|
4091
|
+
// {columns: [{header, ref, datatype, filled}], tips, matchedTips,
|
|
4092
|
+
// unmatchedTips: [names], unmatchedRows: [keys], properties}
|
|
4093
|
+
forester.joinMetadataTable = function (tree, text) {
|
|
4094
|
+
let table = forester.parseDelimitedTable(text);
|
|
4095
|
+
let tips = forester.getAllExternalNodes(tree);
|
|
4096
|
+
let byName = Object.create(null);
|
|
4097
|
+
let byLower = Object.create(null);
|
|
4098
|
+
tips.forEach(function (n) {
|
|
4099
|
+
if (n.name) {
|
|
4100
|
+
byName[n.name] = n;
|
|
4101
|
+
let lower = n.name.toLowerCase();
|
|
4102
|
+
if (!byLower[lower]) {
|
|
4103
|
+
byLower[lower] = n;
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
let columns = [];
|
|
4108
|
+
for (let j = 1; j < table.columns.length; ++j) {
|
|
4109
|
+
let filled = table.rows.map(function (r) {
|
|
4110
|
+
return r[j] === undefined ? '' : r[j];
|
|
4111
|
+
}).filter(function (v) {
|
|
4112
|
+
return v.length > 0;
|
|
4113
|
+
});
|
|
4114
|
+
let datatype = 'xsd:string';
|
|
4115
|
+
if (filled.length > 0) {
|
|
4116
|
+
if (filled.every(function (v) { return /^[+-]?\d+$/.test(v); })) {
|
|
4117
|
+
datatype = 'xsd:integer';
|
|
4118
|
+
} else if (filled.every(function (v) { return VIS_NUMERIC_RE.test(v); })) {
|
|
4119
|
+
datatype = 'xsd:double';
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
columns.push({header: table.columns[j], ref: forester.metadataColumnRef(table.columns[j], j),
|
|
4123
|
+
datatype: datatype, filled: 0});
|
|
4124
|
+
}
|
|
4125
|
+
let matched = new Set();
|
|
4126
|
+
let unmatchedRows = [];
|
|
4127
|
+
let properties = 0;
|
|
4128
|
+
table.rows.forEach(function (r) {
|
|
4129
|
+
let key = r[0] === undefined ? '' : r[0];
|
|
4130
|
+
if (key.length === 0) {
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
let tip = byName[key] || byLower[key.toLowerCase()];
|
|
4134
|
+
if (!tip) {
|
|
4135
|
+
unmatchedRows.push(key);
|
|
4136
|
+
return;
|
|
4137
|
+
}
|
|
4138
|
+
matched.add(tip);
|
|
4139
|
+
columns.forEach(function (col, k) {
|
|
4140
|
+
let v = r[k + 1] === undefined ? '' : r[k + 1];
|
|
4141
|
+
if (v.length === 0) {
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
if (!tip.properties) {
|
|
4145
|
+
tip.properties = [];
|
|
4146
|
+
}
|
|
4147
|
+
let existing = null;
|
|
4148
|
+
for (let i = 0; i < tip.properties.length; ++i) {
|
|
4149
|
+
if (tip.properties[i].ref === col.ref) {
|
|
4150
|
+
existing = tip.properties[i];
|
|
4151
|
+
break;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
if (existing) {
|
|
4155
|
+
existing.value = v;
|
|
4156
|
+
existing.datatype = col.datatype;
|
|
4157
|
+
existing.applies_to = 'node';
|
|
4158
|
+
} else {
|
|
4159
|
+
tip.properties.push({ref: col.ref, value: v, datatype: col.datatype, applies_to: 'node'});
|
|
4160
|
+
}
|
|
4161
|
+
col.filled++;
|
|
4162
|
+
properties++;
|
|
4163
|
+
});
|
|
4164
|
+
});
|
|
4165
|
+
let unmatchedTips = tips.filter(function (n) {
|
|
4166
|
+
return !matched.has(n);
|
|
4167
|
+
}).map(function (n) {
|
|
4168
|
+
return n.name || '';
|
|
4169
|
+
});
|
|
4170
|
+
return {columns: columns, tips: tips.length, matchedTips: matched.size,
|
|
4171
|
+
unmatchedTips: unmatchedTips, unmatchedRows: unmatchedRows, properties: properties};
|
|
4172
|
+
};
|
|
4173
|
+
|
|
3796
4174
|
// --------------------------------------------------------------
|
|
3797
4175
|
// Search engine
|
|
3798
4176
|
// --------------------------------------------------------------
|
|
@@ -3849,7 +4227,13 @@
|
|
|
3849
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));
|
|
3850
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});
|
|
3851
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});
|
|
3852
|
-
|
|
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});
|
|
3853
4237
|
// in menu order
|
|
3854
4238
|
const SEARCH_TEXT_FIELDS = [
|
|
3855
4239
|
SEARCH_NODE_NAME, SEARCH_TAXONOMY_SCIENTIFIC_NAME, SEARCH_TAXONOMY_COMMON_NAME, SEARCH_TAXONOMY_CODE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
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",
|
|
23
|
+
"test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/metadata_test.js",
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"format": "prettier --write .",
|