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/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.20.0
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.20.0",
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 .",