archaeopteryx 3.20.0 → 3.22.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 +165 -18
- package/archaeopteryx.d.ts +24 -0
- package/archaeopteryx.js +971 -91
- package/forester.js +541 -2
- 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.22.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3828,6 +3828,10 @@
|
|
|
3828
3828
|
// one, and the longest (Lmax, the scale every track shares)
|
|
3829
3829
|
properties.domainArchitectures = false;
|
|
3830
3830
|
properties.maxDomainArchitectureLength = 0;
|
|
3831
|
+
// genome regions on the tips (genes as located sequences): whether
|
|
3832
|
+
// any tip carries one, and the longest span in base pairs
|
|
3833
|
+
properties.geneRegions = false;
|
|
3834
|
+
properties.maxGeneRegionLength = 0;
|
|
3831
3835
|
properties.externalNodesCount = 0;
|
|
3832
3836
|
properties.nodeCount = 0;
|
|
3833
3837
|
// Branches that carry a length AT ALL -- an explicit zero is a real
|
|
@@ -3918,6 +3922,17 @@
|
|
|
3918
3922
|
properties.maxDomainArchitectureLength = Number(da.length);
|
|
3919
3923
|
}
|
|
3920
3924
|
}
|
|
3925
|
+
// a tree carries genome regions when some tip carries
|
|
3926
|
+
// TWO genes or more: a gene-family tree whose tips each
|
|
3927
|
+
// locate their one gene is not a tree of genomes
|
|
3928
|
+
let gr = forester.geneRegionOf(n);
|
|
3929
|
+
if (gr && gr.genes.length >= 2) {
|
|
3930
|
+
properties.geneRegions = true;
|
|
3931
|
+
let L = gr.span.end - gr.span.start + 1;
|
|
3932
|
+
if (L > properties.maxGeneRegionLength) {
|
|
3933
|
+
properties.maxGeneRegionLength = L;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3921
3936
|
}
|
|
3922
3937
|
}
|
|
3923
3938
|
if (n.taxonomies && n.taxonomies.length > 0) {
|
|
@@ -4446,7 +4461,7 @@
|
|
|
4446
4461
|
support: [],
|
|
4447
4462
|
ultrametric: false,
|
|
4448
4463
|
tipsWithTaxonomy: 0, distinctTaxonomies: 0, tipsWithTaxonomyId: 0,
|
|
4449
|
-
tipsWithSequence: 0, tipsWithMolSeq: 0, tipsWithDomains: 0,
|
|
4464
|
+
tipsWithSequence: 0, tipsWithMolSeq: 0, tipsWithDomains: 0, tipsWithGenes: 0,
|
|
4450
4465
|
nodesWithDate: 0, tipsWithDistribution: 0, namedInternal: 0,
|
|
4451
4466
|
duplications: 0, speciations: 0, nodesWithEvents: 0,
|
|
4452
4467
|
propertyRefs: []
|
|
@@ -4566,6 +4581,9 @@
|
|
|
4566
4581
|
if (forester.domainArchitectureOf(n)) {
|
|
4567
4582
|
++stats.tipsWithDomains;
|
|
4568
4583
|
}
|
|
4584
|
+
if (forester.geneRegionOf(n)) {
|
|
4585
|
+
++stats.tipsWithGenes;
|
|
4586
|
+
}
|
|
4569
4587
|
}
|
|
4570
4588
|
if (n.distributions && n.distributions.length > 0) {
|
|
4571
4589
|
++stats.tipsWithDistribution;
|
|
@@ -7943,6 +7961,527 @@
|
|
|
7943
7961
|
return {backbone: {x: start, w: Number(da.length) * f}, boxes: boxes};
|
|
7944
7962
|
};
|
|
7945
7963
|
|
|
7964
|
+
// --------------------------------------------------------------
|
|
7965
|
+
// Genome regions (the gene track)
|
|
7966
|
+
// --------------------------------------------------------------
|
|
7967
|
+
// A tip that is a genome can carry (part of) that genome as phyloXML
|
|
7968
|
+
// <sequence> elements: one per gene, each with a <location> on a contig
|
|
7969
|
+
// and its family in an <annotation ref="...">, plus one record per
|
|
7970
|
+
// contig. The viewer draws them as arrows on a backbone beside the tip,
|
|
7971
|
+
// the rows anchored on one family so gene order compares across genomes
|
|
7972
|
+
// (Christian, 2026-10-01: the encoding, the location grammar, anchoring
|
|
7973
|
+
// by default, the words "Genes" and "Genome regions"). Nothing in the
|
|
7974
|
+
// schema changes: <location> is phyloXML's "location of a sequence on a
|
|
7975
|
+
// genome/chromosome", an annotation's ref its "colon-separated reference
|
|
7976
|
+
// to an external database".
|
|
7977
|
+
//
|
|
7978
|
+
// Everything that can be tested without a DOM lives here: the grammar,
|
|
7979
|
+
// which sequences are genes and which the contig record, the anchor, the
|
|
7980
|
+
// mapping of every row onto one shared coordinate (flips, lanes, contig
|
|
7981
|
+
// ends), the arrow outline, the families, the legend rows and the links.
|
|
7982
|
+
// test/gene_test.js pins the numbers. The SVG and the controls are
|
|
7983
|
+
// archaeopteryx.js's.
|
|
7984
|
+
|
|
7985
|
+
// contig:start-end(+) -- 1-based, inclusive, the strand optional. Split
|
|
7986
|
+
// on the LAST colon so a contig id with a colon of its own survives.
|
|
7987
|
+
// A located sequence WITH a strand is a gene; one WITHOUT is the record
|
|
7988
|
+
// of the contig itself (its accession, name and length), which is how
|
|
7989
|
+
// the drawing knows where a contig ends.
|
|
7990
|
+
const SEQUENCE_LOCATION_RE = /^(.+):(\d+)-(\d+)(?:\(([+-])\))?$/;
|
|
7991
|
+
|
|
7992
|
+
forester.parseSequenceLocation = function (text) {
|
|
7993
|
+
if (typeof text !== 'string') {
|
|
7994
|
+
return null;
|
|
7995
|
+
}
|
|
7996
|
+
let m = SEQUENCE_LOCATION_RE.exec(text.trim());
|
|
7997
|
+
if (!m) {
|
|
7998
|
+
return null;
|
|
7999
|
+
}
|
|
8000
|
+
let start = Number(m[2]);
|
|
8001
|
+
let end = Number(m[3]);
|
|
8002
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
|
|
8003
|
+
return null;
|
|
8004
|
+
}
|
|
8005
|
+
let contig = m[1].trim();
|
|
8006
|
+
if (contig.length === 0) {
|
|
8007
|
+
return null;
|
|
8008
|
+
}
|
|
8009
|
+
return {contig: contig, start: start, end: end, strand: m[4] || null};
|
|
8010
|
+
};
|
|
8011
|
+
|
|
8012
|
+
// The namespace of an annotation ref: "pgfam" of "pgfam:PGF_00019355".
|
|
8013
|
+
// A ref without a colon has none and names no family.
|
|
8014
|
+
forester.annotationNamespace = function (ref) {
|
|
8015
|
+
if (typeof ref !== 'string') {
|
|
8016
|
+
return null;
|
|
8017
|
+
}
|
|
8018
|
+
let c = ref.indexOf(':');
|
|
8019
|
+
return c > 0 ? ref.substring(0, c) : null;
|
|
8020
|
+
};
|
|
8021
|
+
|
|
8022
|
+
function textOf(v) {
|
|
8023
|
+
return (v === null || v === undefined) ? '' : String(v);
|
|
8024
|
+
}
|
|
8025
|
+
|
|
8026
|
+
// Every located sequence of a node, sorted into genes, contig records
|
|
8027
|
+
// and the ones whose location is not in the grammar (counted, never
|
|
8028
|
+
// fatal: <location> is free text in phyloXML, and an older file may say
|
|
8029
|
+
// "chromosome 3" there).
|
|
8030
|
+
function locatedSequences(node) {
|
|
8031
|
+
let out = {genes: [], records: [], ignored: 0};
|
|
8032
|
+
if (!node.sequences) {
|
|
8033
|
+
return out;
|
|
8034
|
+
}
|
|
8035
|
+
node.sequences.forEach(function (s, i) {
|
|
8036
|
+
if (!s || s.location === undefined || s.location === null || s.location === '') {
|
|
8037
|
+
return;
|
|
8038
|
+
}
|
|
8039
|
+
let loc = forester.parseSequenceLocation(s.location);
|
|
8040
|
+
if (!loc) {
|
|
8041
|
+
out.ignored++;
|
|
8042
|
+
return;
|
|
8043
|
+
}
|
|
8044
|
+
let acc = (s.accession && s.accession.value !== undefined && s.accession.value !== null
|
|
8045
|
+
&& s.accession.value !== '')
|
|
8046
|
+
? {source: textOf(s.accession.source), value: textOf(s.accession.value)} : null;
|
|
8047
|
+
if (loc.strand === null) {
|
|
8048
|
+
out.records.push({contig: loc.contig, start: loc.start, end: loc.end, accession: acc,
|
|
8049
|
+
name: textOf(s.name)});
|
|
8050
|
+
return;
|
|
8051
|
+
}
|
|
8052
|
+
let refs = [];
|
|
8053
|
+
(s.annotations || []).forEach(function (a) {
|
|
8054
|
+
if (a && forester.annotationNamespace(a.ref) !== null) {
|
|
8055
|
+
refs.push(a.ref);
|
|
8056
|
+
}
|
|
8057
|
+
});
|
|
8058
|
+
out.genes.push({index: i, contig: loc.contig, start: loc.start, end: loc.end, strand: loc.strand,
|
|
8059
|
+
length: loc.end - loc.start + 1, symbol: textOf(s.symbol), geneName: textOf(s.gene_name),
|
|
8060
|
+
product: textOf(s.name), accession: acc, refs: refs});
|
|
8061
|
+
});
|
|
8062
|
+
return out;
|
|
8063
|
+
}
|
|
8064
|
+
|
|
8065
|
+
// The region a tip carries: its genes on ONE contig -- the contig with
|
|
8066
|
+
// the most genes (ties: the first listed) -- in position order, with
|
|
8067
|
+
// that contig's record when there is one, the span of the genes, and
|
|
8068
|
+
// how many genes on other contigs were left out. Null for a tip with no
|
|
8069
|
+
// genes (a record alone is not a region).
|
|
8070
|
+
forester.geneRegionOf = function (node) {
|
|
8071
|
+
let found = locatedSequences(node);
|
|
8072
|
+
if (found.genes.length === 0) {
|
|
8073
|
+
return null;
|
|
8074
|
+
}
|
|
8075
|
+
let counts = new Map();
|
|
8076
|
+
found.genes.forEach(function (g) {
|
|
8077
|
+
counts.set(g.contig, (counts.get(g.contig) || 0) + 1);
|
|
8078
|
+
});
|
|
8079
|
+
let contig = null;
|
|
8080
|
+
let best = 0;
|
|
8081
|
+
counts.forEach(function (n, c) {
|
|
8082
|
+
if (n > best) {
|
|
8083
|
+
best = n;
|
|
8084
|
+
contig = c;
|
|
8085
|
+
}
|
|
8086
|
+
});
|
|
8087
|
+
let genes = found.genes.filter(function (g) {
|
|
8088
|
+
return g.contig === contig;
|
|
8089
|
+
}).sort(function (a, b) {
|
|
8090
|
+
return (a.start - b.start) || (a.end - b.end) || (a.index - b.index);
|
|
8091
|
+
});
|
|
8092
|
+
let record = null;
|
|
8093
|
+
for (let i = 0; i < found.records.length; ++i) {
|
|
8094
|
+
if (found.records[i].contig === contig) {
|
|
8095
|
+
record = found.records[i];
|
|
8096
|
+
break;
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
let span = {start: genes[0].start, end: genes[0].end};
|
|
8100
|
+
genes.forEach(function (g) {
|
|
8101
|
+
if (g.end > span.end) {
|
|
8102
|
+
span.end = g.end;
|
|
8103
|
+
}
|
|
8104
|
+
});
|
|
8105
|
+
return {contig: contig, genes: genes, record: record, span: span,
|
|
8106
|
+
otherContigs: found.genes.length - genes.length, contigs: counts.size};
|
|
8107
|
+
};
|
|
8108
|
+
|
|
8109
|
+
// The tree's gene facts: tips with a region, genes drawn, genes on other
|
|
8110
|
+
// contigs left out, the longest span, and locations not in the grammar.
|
|
8111
|
+
forester.geneRegionStats = function (tree) {
|
|
8112
|
+
let stats = {tips: 0, genes: 0, otherContigs: 0, maxSpan: 0, ignored: 0};
|
|
8113
|
+
forester.preOrderTraversalAll(tree, function (n) {
|
|
8114
|
+
if (n.children) {
|
|
8115
|
+
return;
|
|
8116
|
+
}
|
|
8117
|
+
stats.ignored += locatedSequences(n).ignored;
|
|
8118
|
+
let r = forester.geneRegionOf(n);
|
|
8119
|
+
if (!r) {
|
|
8120
|
+
return;
|
|
8121
|
+
}
|
|
8122
|
+
stats.tips++;
|
|
8123
|
+
stats.genes += r.genes.length;
|
|
8124
|
+
stats.otherContigs += r.otherContigs;
|
|
8125
|
+
let L = r.span.end - r.span.start + 1;
|
|
8126
|
+
if (L > stats.maxSpan) {
|
|
8127
|
+
stats.maxSpan = L;
|
|
8128
|
+
}
|
|
8129
|
+
});
|
|
8130
|
+
return stats;
|
|
8131
|
+
};
|
|
8132
|
+
|
|
8133
|
+
// The family namespaces the genes carry, most-used first (ties by code
|
|
8134
|
+
// unit): a BV-BRC export has both pgfam and plfam on every gene, and the
|
|
8135
|
+
// track colours by ONE of them.
|
|
8136
|
+
forester.geneFamilyNamespaces = function (regions) {
|
|
8137
|
+
let counts = new Map();
|
|
8138
|
+
regions.forEach(function (r) {
|
|
8139
|
+
if (!r) {
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8142
|
+
r.genes.forEach(function (g) {
|
|
8143
|
+
let seen = new Set();
|
|
8144
|
+
g.refs.forEach(function (ref) {
|
|
8145
|
+
let ns = forester.annotationNamespace(ref);
|
|
8146
|
+
if (ns !== null && !seen.has(ns)) {
|
|
8147
|
+
seen.add(ns);
|
|
8148
|
+
counts.set(ns, (counts.get(ns) || 0) + 1);
|
|
8149
|
+
}
|
|
8150
|
+
});
|
|
8151
|
+
});
|
|
8152
|
+
});
|
|
8153
|
+
let out = [];
|
|
8154
|
+
counts.forEach(function (genes, ns) {
|
|
8155
|
+
out.push({ns: ns, genes: genes});
|
|
8156
|
+
});
|
|
8157
|
+
out.sort(function (a, b) {
|
|
8158
|
+
return (b.genes - a.genes) || (a.ns < b.ns ? -1 : (a.ns > b.ns ? 1 : 0));
|
|
8159
|
+
});
|
|
8160
|
+
return out;
|
|
8161
|
+
};
|
|
8162
|
+
|
|
8163
|
+
// A gene's family in a namespace: its first annotation ref there, in
|
|
8164
|
+
// full ("pgfam:PGF_00019355"), or null.
|
|
8165
|
+
forester.geneFamilyOf = function (gene, ns) {
|
|
8166
|
+
if (!ns) {
|
|
8167
|
+
return null;
|
|
8168
|
+
}
|
|
8169
|
+
for (let i = 0; i < gene.refs.length; ++i) {
|
|
8170
|
+
if (forester.annotationNamespace(gene.refs[i]) === ns) {
|
|
8171
|
+
return gene.refs[i];
|
|
8172
|
+
}
|
|
8173
|
+
}
|
|
8174
|
+
return null;
|
|
8175
|
+
};
|
|
8176
|
+
|
|
8177
|
+
// Which families could anchor the rows: for each, the regions carrying
|
|
8178
|
+
// it exactly once (where it CAN anchor) and the regions carrying it at
|
|
8179
|
+
// all; most single-copy regions first, ties by code unit. A product and
|
|
8180
|
+
// symbol ride along for the control's labels.
|
|
8181
|
+
forester.geneAnchorCandidates = function (regions, ns) {
|
|
8182
|
+
let fams = new Map();
|
|
8183
|
+
regions.forEach(function (r) {
|
|
8184
|
+
if (!r) {
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
8187
|
+
let here = new Map();
|
|
8188
|
+
r.genes.forEach(function (g) {
|
|
8189
|
+
let fam = forester.geneFamilyOf(g, ns);
|
|
8190
|
+
if (fam === null) {
|
|
8191
|
+
return;
|
|
8192
|
+
}
|
|
8193
|
+
here.set(fam, (here.get(fam) || 0) + 1);
|
|
8194
|
+
if (!fams.has(fam)) {
|
|
8195
|
+
fams.set(fam, {family: fam, single: 0, tips: 0, product: g.product, symbol: g.symbol});
|
|
8196
|
+
}
|
|
8197
|
+
});
|
|
8198
|
+
here.forEach(function (n, fam) {
|
|
8199
|
+
let f = fams.get(fam);
|
|
8200
|
+
f.tips++;
|
|
8201
|
+
if (n === 1) {
|
|
8202
|
+
f.single++;
|
|
8203
|
+
}
|
|
8204
|
+
});
|
|
8205
|
+
});
|
|
8206
|
+
let out = [];
|
|
8207
|
+
fams.forEach(function (f) {
|
|
8208
|
+
out.push(f);
|
|
8209
|
+
});
|
|
8210
|
+
out.sort(function (a, b) {
|
|
8211
|
+
return (b.single - a.single) || (a.family < b.family ? -1 : (a.family > b.family ? 1 : 0));
|
|
8212
|
+
});
|
|
8213
|
+
return out;
|
|
8214
|
+
};
|
|
8215
|
+
|
|
8216
|
+
// The anchor a tree opens with: the family single-copy in the most
|
|
8217
|
+
// regions, provided that is at least half of them. Otherwise none, and
|
|
8218
|
+
// the rows are left-aligned.
|
|
8219
|
+
forester.geneDefaultAnchor = function (regions, ns) {
|
|
8220
|
+
let n = 0;
|
|
8221
|
+
regions.forEach(function (r) {
|
|
8222
|
+
if (r) {
|
|
8223
|
+
n++;
|
|
8224
|
+
}
|
|
8225
|
+
});
|
|
8226
|
+
let c = forester.geneAnchorCandidates(regions, ns);
|
|
8227
|
+
return (c.length > 0 && c[0].single > 0 && c[0].single * 2 >= n) ? c[0].family : null;
|
|
8228
|
+
};
|
|
8229
|
+
|
|
8230
|
+
// The backbone reaches this far past the first and last gene, as a share
|
|
8231
|
+
// of the span -- unless the contig record says the contig ends first.
|
|
8232
|
+
forester.GENE_REGION_PAD = 0.02;
|
|
8233
|
+
|
|
8234
|
+
// Every row onto one shared coordinate, in base pairs. A row whose
|
|
8235
|
+
// anchor family occurs exactly once puts that gene's midpoint at 0 and,
|
|
8236
|
+
// when the gene is on the minus strand, is MIRRORED so the anchor points
|
|
8237
|
+
// right and the neighbours flip with it (the Compare Region convention).
|
|
8238
|
+
// Any other row -- no anchor, no such gene, two of them -- is left-
|
|
8239
|
+
// aligned at the leftmost edge of the anchored rows (at 0 when there are
|
|
8240
|
+
// none). Overlapping genes take a second lane. `regions` is per
|
|
8241
|
+
// displayed tip, in display order, null where a tip has none; the result
|
|
8242
|
+
// has a row (or null) per entry and the extent over all rows.
|
|
8243
|
+
forester.geneTrackRows = function (regions, anchor, ns) {
|
|
8244
|
+
let rows = regions.map(function (region) {
|
|
8245
|
+
if (!region) {
|
|
8246
|
+
return null;
|
|
8247
|
+
}
|
|
8248
|
+
let span = region.span;
|
|
8249
|
+
let pad = forester.GENE_REGION_PAD * (span.end - span.start + 1);
|
|
8250
|
+
let b0 = span.start - pad;
|
|
8251
|
+
let b1 = span.end + pad;
|
|
8252
|
+
let capStart = false;
|
|
8253
|
+
let capEnd = false;
|
|
8254
|
+
if (region.record) {
|
|
8255
|
+
if (b0 <= region.record.start) {
|
|
8256
|
+
b0 = region.record.start;
|
|
8257
|
+
capStart = true;
|
|
8258
|
+
}
|
|
8259
|
+
if (b1 >= region.record.end) {
|
|
8260
|
+
b1 = region.record.end;
|
|
8261
|
+
capEnd = true;
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
let anchors = anchor ? region.genes.filter(function (g) {
|
|
8265
|
+
return forester.geneFamilyOf(g, ns) === anchor;
|
|
8266
|
+
}) : [];
|
|
8267
|
+
let anchored = anchors.length === 1;
|
|
8268
|
+
let flip = anchored && anchors[0].strand === '-';
|
|
8269
|
+
return {region: region, b0: b0, b1: b1, capStart: capStart, capEnd: capEnd, anchored: anchored, flip: flip,
|
|
8270
|
+
origin: anchored ? (anchors[0].start + anchors[0].end) / 2 : null};
|
|
8271
|
+
});
|
|
8272
|
+
let min = Infinity;
|
|
8273
|
+
let max = -Infinity;
|
|
8274
|
+
let place = function (row) {
|
|
8275
|
+
let o = row.origin;
|
|
8276
|
+
let map = row.flip
|
|
8277
|
+
? function (a, b) {
|
|
8278
|
+
return [o - b, o - a];
|
|
8279
|
+
}
|
|
8280
|
+
: function (a, b) {
|
|
8281
|
+
return [a - o, b - o];
|
|
8282
|
+
};
|
|
8283
|
+
let bb = map(row.b0, row.b1);
|
|
8284
|
+
let genes = row.region.genes.map(function (g) {
|
|
8285
|
+
let x = map(g.start, g.end);
|
|
8286
|
+
let dir = g.strand === '+' ? 1 : -1;
|
|
8287
|
+
return {gene: g, x0: x[0], x1: x[1], dir: row.flip ? -dir : dir, lane: 0};
|
|
8288
|
+
});
|
|
8289
|
+
genes.sort(function (p, q) {
|
|
8290
|
+
return (p.x0 - q.x0) || (p.x1 - q.x1);
|
|
8291
|
+
});
|
|
8292
|
+
// greedy lanes: the first lane that is free, else the one that
|
|
8293
|
+
// frees first; two lanes at most
|
|
8294
|
+
let ends = [-Infinity, -Infinity];
|
|
8295
|
+
genes.forEach(function (g) {
|
|
8296
|
+
let lane = g.x0 >= ends[0] ? 0 : (g.x0 >= ends[1] ? 1 : (ends[0] <= ends[1] ? 0 : 1));
|
|
8297
|
+
g.lane = lane;
|
|
8298
|
+
ends[lane] = Math.max(ends[lane], g.x1);
|
|
8299
|
+
});
|
|
8300
|
+
if (bb[0] < min) {
|
|
8301
|
+
min = bb[0];
|
|
8302
|
+
}
|
|
8303
|
+
if (bb[1] > max) {
|
|
8304
|
+
max = bb[1];
|
|
8305
|
+
}
|
|
8306
|
+
return {backbone: {x0: bb[0], x1: bb[1]}, capLeft: row.flip ? row.capEnd : row.capStart,
|
|
8307
|
+
capRight: row.flip ? row.capStart : row.capEnd, anchored: row.anchored, flip: row.flip,
|
|
8308
|
+
lanes: genes.some(function (g) {
|
|
8309
|
+
return g.lane === 1;
|
|
8310
|
+
}) ? 2 : 1, genes: genes, region: row.region};
|
|
8311
|
+
};
|
|
8312
|
+
let out = new Array(rows.length);
|
|
8313
|
+
rows.forEach(function (row, i) {
|
|
8314
|
+
if (row && row.anchored) {
|
|
8315
|
+
out[i] = place(row);
|
|
8316
|
+
}
|
|
8317
|
+
});
|
|
8318
|
+
let left = isFinite(min) ? min : 0;
|
|
8319
|
+
rows.forEach(function (row, i) {
|
|
8320
|
+
if (row && !row.anchored) {
|
|
8321
|
+
row.origin = row.b0 - left; // its backbone starts at the left edge
|
|
8322
|
+
out[i] = place(row);
|
|
8323
|
+
} else if (!row) {
|
|
8324
|
+
out[i] = null;
|
|
8325
|
+
}
|
|
8326
|
+
});
|
|
8327
|
+
return {rows: out, extent: isFinite(min) ? {min: min, max: max} : null};
|
|
8328
|
+
};
|
|
8329
|
+
|
|
8330
|
+
// One row in pixels: the backbone and each gene's box, given the track's
|
|
8331
|
+
// start x, the px per base pair f and the extent's left edge.
|
|
8332
|
+
forester.geneRowGeometry = function (row, start, f, min) {
|
|
8333
|
+
let px = function (x) {
|
|
8334
|
+
return start + ((x - min) * f);
|
|
8335
|
+
};
|
|
8336
|
+
return {
|
|
8337
|
+
backbone: {x: px(row.backbone.x0), w: (row.backbone.x1 - row.backbone.x0) * f},
|
|
8338
|
+
capLeft: row.capLeft,
|
|
8339
|
+
capRight: row.capRight,
|
|
8340
|
+
genes: row.genes.map(function (g) {
|
|
8341
|
+
return {x: px(g.x0), w: (g.x1 - g.x0) * f, dir: g.dir, lane: g.lane, gene: g.gene};
|
|
8342
|
+
})
|
|
8343
|
+
};
|
|
8344
|
+
};
|
|
8345
|
+
|
|
8346
|
+
// The arrow: a box whose leading end tapers to a point in the strand's
|
|
8347
|
+
// direction, the head half the height long; a gene shorter than the head
|
|
8348
|
+
// is a triangle and still shows its direction. Path data to a hundredth
|
|
8349
|
+
// of a pixel; '' for a degenerate box.
|
|
8350
|
+
forester.geneArrowPath = function (x, y, w, h, dir) {
|
|
8351
|
+
if (!isFinite(x) || !isFinite(y) || !(w > 0) || !isFinite(w) || !(h > 0) || !isFinite(h)) {
|
|
8352
|
+
return '';
|
|
8353
|
+
}
|
|
8354
|
+
let num = function (n) {
|
|
8355
|
+
return Math.round(n * 100) / 100;
|
|
8356
|
+
};
|
|
8357
|
+
let head = Math.min(h / 2, w);
|
|
8358
|
+
let body = w - head;
|
|
8359
|
+
let hh = h / 2;
|
|
8360
|
+
if (dir < 0) {
|
|
8361
|
+
// the point on the left
|
|
8362
|
+
return 'M' + num(x + w) + ',' + num(y)
|
|
8363
|
+
+ (body > 0 ? 'h' + num(-body) : '')
|
|
8364
|
+
+ 'l' + num(-head) + ',' + num(hh)
|
|
8365
|
+
+ 'l' + num(head) + ',' + num(hh)
|
|
8366
|
+
+ (body > 0 ? 'h' + num(body) : '') + 'Z';
|
|
8367
|
+
}
|
|
8368
|
+
return 'M' + num(x) + ',' + num(y)
|
|
8369
|
+
+ (body > 0 ? 'h' + num(body) : '')
|
|
8370
|
+
+ 'l' + num(head) + ',' + num(hh)
|
|
8371
|
+
+ 'l' + num(-head) + ',' + num(hh)
|
|
8372
|
+
+ (body > 0 ? 'h' + num(-body) : '') + 'Z';
|
|
8373
|
+
};
|
|
8374
|
+
|
|
8375
|
+
forester.GENE_SINGLETON_COLOR = '#9e9e9e'; // a family met in one region only
|
|
8376
|
+
forester.GENE_NO_FAMILY_COLOR = '#cfcfcf'; // a gene with no family at all
|
|
8377
|
+
|
|
8378
|
+
// The families over the regions given (display order): the SHARED ones
|
|
8379
|
+
// -- in two regions or more, or, when `shared` (a Set of family refs) is
|
|
8380
|
+
// given, the ones in it -- sorted by code unit (the palette's order),
|
|
8381
|
+
// the legend rows in first-appearance order with each family's region
|
|
8382
|
+
// and gene counts and its commonest product and symbol, and how many
|
|
8383
|
+
// drawn genes are singletons or carry no family. The viewer passes the
|
|
8384
|
+
// TREE's shared families as `shared` when it summarizes a view, so a
|
|
8385
|
+
// family shown once in a subtree keeps the colour it has on the tree.
|
|
8386
|
+
forester.geneFamilySummary = function (regions, ns, shared) {
|
|
8387
|
+
let fams = new Map();
|
|
8388
|
+
let singles = 0;
|
|
8389
|
+
let unfamilied = 0;
|
|
8390
|
+
regions.forEach(function (r) {
|
|
8391
|
+
if (!r) {
|
|
8392
|
+
return;
|
|
8393
|
+
}
|
|
8394
|
+
let here = new Set();
|
|
8395
|
+
r.genes.forEach(function (g) {
|
|
8396
|
+
let fam = forester.geneFamilyOf(g, ns);
|
|
8397
|
+
if (fam === null) {
|
|
8398
|
+
unfamilied++;
|
|
8399
|
+
return;
|
|
8400
|
+
}
|
|
8401
|
+
let f = fams.get(fam);
|
|
8402
|
+
if (!f) {
|
|
8403
|
+
f = {family: fam, tips: 0, genes: 0, products: new Map(), symbols: new Map()};
|
|
8404
|
+
fams.set(fam, f);
|
|
8405
|
+
}
|
|
8406
|
+
f.genes++;
|
|
8407
|
+
if (g.product) {
|
|
8408
|
+
f.products.set(g.product, (f.products.get(g.product) || 0) + 1);
|
|
8409
|
+
}
|
|
8410
|
+
if (g.symbol) {
|
|
8411
|
+
f.symbols.set(g.symbol, (f.symbols.get(g.symbol) || 0) + 1);
|
|
8412
|
+
}
|
|
8413
|
+
here.add(fam);
|
|
8414
|
+
});
|
|
8415
|
+
here.forEach(function (fam) {
|
|
8416
|
+
fams.get(fam).tips++;
|
|
8417
|
+
});
|
|
8418
|
+
});
|
|
8419
|
+
let commonest = function (m) {
|
|
8420
|
+
let best = '';
|
|
8421
|
+
let n = 0;
|
|
8422
|
+
m.forEach(function (count, text) {
|
|
8423
|
+
if (count > n) {
|
|
8424
|
+
n = count;
|
|
8425
|
+
best = text;
|
|
8426
|
+
}
|
|
8427
|
+
});
|
|
8428
|
+
return best;
|
|
8429
|
+
};
|
|
8430
|
+
let legend = [];
|
|
8431
|
+
let names = [];
|
|
8432
|
+
fams.forEach(function (f) {
|
|
8433
|
+
if (shared ? !shared.has(f.family) : f.tips < 2) {
|
|
8434
|
+
singles += f.genes;
|
|
8435
|
+
return;
|
|
8436
|
+
}
|
|
8437
|
+
names.push(f.family);
|
|
8438
|
+
legend.push({family: f.family, tips: f.tips, genes: f.genes,
|
|
8439
|
+
product: commonest(f.products), symbol: commonest(f.symbols)});
|
|
8440
|
+
});
|
|
8441
|
+
names.sort();
|
|
8442
|
+
return {names: names, legend: legend, singletons: singles, unfamilied: unfamilied};
|
|
8443
|
+
};
|
|
8444
|
+
|
|
8445
|
+
// Where a click on a gene goes: its BV-BRC feature page for a BV-BRC /
|
|
8446
|
+
// PATRIC accession (fig|...), NCBI's protein page for a GenBank / RefSeq
|
|
8447
|
+
// one; nowhere otherwise.
|
|
8448
|
+
const BVBRC_FEATURE = 'https://www.bv-brc.org/view/Feature/';
|
|
8449
|
+
const NCBI_PROTEIN = 'https://www.ncbi.nlm.nih.gov/protein/';
|
|
8450
|
+
|
|
8451
|
+
forester.geneReference = function (gene) {
|
|
8452
|
+
if (!gene || !gene.accession) {
|
|
8453
|
+
return null;
|
|
8454
|
+
}
|
|
8455
|
+
let v = gene.accession.value.trim();
|
|
8456
|
+
let src = gene.accession.source.toLowerCase();
|
|
8457
|
+
if (v.length === 0) {
|
|
8458
|
+
return null;
|
|
8459
|
+
}
|
|
8460
|
+
if (/^fig\|/.test(v) || /bv-?brc|patric/.test(src)) {
|
|
8461
|
+
return {url: BVBRC_FEATURE + encodeURIComponent(v).replace(/%7C/gi, '|'), site: 'BV-BRC'};
|
|
8462
|
+
}
|
|
8463
|
+
if (/ncbi|genbank|refseq|uniprot/.test(src)) {
|
|
8464
|
+
return {url: NCBI_PROTEIN + encodeURIComponent(v), site: 'NCBI'};
|
|
8465
|
+
}
|
|
8466
|
+
return null;
|
|
8467
|
+
};
|
|
8468
|
+
|
|
8469
|
+
// The track's own scale bar: a round number of base pairs about
|
|
8470
|
+
// targetPx long, labelled in kb from 1000 up.
|
|
8471
|
+
forester.geneScaleBar = function (pxPerBp, targetPx) {
|
|
8472
|
+
let bar = forester.scaleBarLength(pxPerBp, targetPx);
|
|
8473
|
+
if (!bar) {
|
|
8474
|
+
return null;
|
|
8475
|
+
}
|
|
8476
|
+
let bp = bar.length;
|
|
8477
|
+
return {bp: bp, px: bar.px, label: bp >= 1000 ? (bp / 1000) + ' kb' : bp + ' bp'};
|
|
8478
|
+
};
|
|
8479
|
+
|
|
8480
|
+
// A location as the readout and the node-data dialog show it.
|
|
8481
|
+
forester.geneLocationText = function (gene) {
|
|
8482
|
+
return gene.contig + ':' + gene.start + '-' + gene.end + '(' + gene.strand + ')';
|
|
8483
|
+
};
|
|
8484
|
+
|
|
7946
8485
|
// --------------------------------------------------------------
|
|
7947
8486
|
// Scale bar
|
|
7948
8487
|
// --------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.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/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/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 .",
|