archaeopteryx 3.21.0 → 3.23.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.21.0
23
+ // v 3.23.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;
@@ -7227,6 +7245,192 @@
7227
7245
  return bothPicturesHaveDepth(root);
7228
7246
  };
7229
7247
 
7248
+ // ---- the CLOCK PLOT ---------------------------------------------------
7249
+ // Every node as a point, its DATE against its DIVERGENCE from the root,
7250
+ // and a straight line through the tips: the slope is the rate the tips
7251
+ // accumulated divergence at, and where the line comes down to the root's
7252
+ // divergence is the date it puts the root on. TempEst's and TreeTime's
7253
+ // root-to-tip regression, Auspice's clock view. Christian, 2026-10-02:
7254
+ // a panel linked to the tree, named "Clock plot"; JS only until the
7255
+ // design is final.
7256
+ //
7257
+ // THE RULE, designed for two kinds of tree, the first of them built:
7258
+ //
7259
+ // 1. 'divergence' -- a tree with Time and Div (hasTimeAndDivergence): each
7260
+ // node states a date and a divergence from the root, recorded on it or
7261
+ // summed from each branch's time x its clock rate. Every node is a
7262
+ // point; which of the two layouts is on screen does not matter.
7263
+ // 2. 'root-to-tip' (NOT BUILT) -- a tree whose branch lengths are
7264
+ // divergence and whose tips are dated, with no dates on its ancestors
7265
+ // (it is not a time tree): a tip's divergence is the sum of the stated
7266
+ // lengths from the root. Tips only. This is the tree a clock plot is
7267
+ // wanted for most -- is there a signal worth dating? -- and the one
7268
+ // whose answer depends on its root.
7269
+ //
7270
+ // In both, the plot is offered when its LINE can be drawn: three tips or
7271
+ // more, and not all of them on one date. A tree sampled at one moment has
7272
+ // no slope to estimate. Asked of the tree, never of the view: in the view
7273
+ // of a clade too small or too uniform for a line the points are drawn and
7274
+ // the line is not.
7275
+ //
7276
+ // The FIT is ordinary least squares over the TIPS alone. A tip's date is
7277
+ // an observation; an ancestor's was inferred, usually with a clock, and
7278
+ // counting it would have the estimate confirm itself. The line is not
7279
+ // forced through the root, so the date it reaches the root's divergence
7280
+ // at can be held against the date the tree states for the root.
7281
+ const CLOCK_PLOT_MIN_TIPS = 3;
7282
+
7283
+ /**
7284
+ * Which clock plot the tree has, or null for none (see THE RULE above).
7285
+ *
7286
+ * @param phy the tree
7287
+ * @returns {string|null} 'divergence', or null
7288
+ */
7289
+ forester.clockPlotKind = function (phy) {
7290
+ if (!phy || !forester.hasTimeAndDivergence(phy)) {
7291
+ return null;
7292
+ }
7293
+ let tips = 0;
7294
+ let first = null;
7295
+ let differ = false;
7296
+ forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
7297
+ if (n.children && n.children.length > 0) {
7298
+ return;
7299
+ }
7300
+ ++tips;
7301
+ let d = auspiceNodeDate(n);
7302
+ if (first === null) {
7303
+ first = d;
7304
+ } else if (d !== first) {
7305
+ differ = true;
7306
+ }
7307
+ });
7308
+ return (tips >= CLOCK_PLOT_MIN_TIPS && differ) ? 'divergence' : null;
7309
+ };
7310
+
7311
+ /**
7312
+ * Ordinary least squares of y on x, computed about the means: a date is
7313
+ * near 2020 and its spread a few months, and sums of raw squares would
7314
+ * spend every digit on the 2020.
7315
+ *
7316
+ * @param xs the dates
7317
+ * @param ys the divergences, as many
7318
+ * @returns {{n, slope, intercept, r2, meanX, meanY}|null} null with fewer
7319
+ * than three points or every x the same; r2 is null where every
7320
+ * y is the same (no variance to explain)
7321
+ */
7322
+ forester.clockRegression = function (xs, ys) {
7323
+ let n = xs.length;
7324
+ if (n < CLOCK_PLOT_MIN_TIPS || ys.length !== n) {
7325
+ return null;
7326
+ }
7327
+ let mx = 0;
7328
+ let my = 0;
7329
+ for (let i = 0; i < n; ++i) {
7330
+ mx += xs[i];
7331
+ my += ys[i];
7332
+ }
7333
+ mx /= n;
7334
+ my /= n;
7335
+ let sxx = 0;
7336
+ let sxy = 0;
7337
+ let syy = 0;
7338
+ for (let i = 0; i < n; ++i) {
7339
+ let dx = xs[i] - mx;
7340
+ let dy = ys[i] - my;
7341
+ sxx += dx * dx;
7342
+ sxy += dx * dy;
7343
+ syy += dy * dy;
7344
+ }
7345
+ if (!(sxx > 0) || !isFinite(sxx) || !isFinite(sxy) || !isFinite(syy)) {
7346
+ return null;
7347
+ }
7348
+ let slope = sxy / sxx;
7349
+ return {
7350
+ n: n,
7351
+ slope: slope,
7352
+ intercept: my - (slope * mx),
7353
+ r2: syy > 0 ? ((sxy * sxy) / (sxx * syy)) : null,
7354
+ meanX: mx,
7355
+ meanY: my
7356
+ };
7357
+ };
7358
+
7359
+ /**
7360
+ * The clock plot of the tree, or of the clade on view.
7361
+ *
7362
+ * @param phy the tree: the RULE is asked of it
7363
+ * @param view the tree or clade to plot (the tree, a node, or the
7364
+ * viewer's subtree root); the whole tree where none is given
7365
+ * @returns {object|null} null where the tree has no clock plot; else
7366
+ * kind 'divergence'
7367
+ * forward whether the dates increase toward the tips (calendar
7368
+ * dates); false for ages
7369
+ * unit the dates' unit as the tree states it, or null
7370
+ * fromRates whether the divergence is time x clock rate
7371
+ * points [{node, date, div, tip}] in preorder; the tips'
7372
+ * `residual` is their divergence less the line's
7373
+ * root {node, date, div}: the top of the view
7374
+ * fit forester.clockRegression over the tips, with
7375
+ * rate the slope in the direction time runs: divergence
7376
+ * per unit of time (negative where the tips'
7377
+ * divergence falls with time)
7378
+ * rootDate the date the line reaches the root's divergence
7379
+ * at; null unless the rate is positive
7380
+ * or null where the view has no line
7381
+ */
7382
+ forester.clockPlotData = function (phy, view) {
7383
+ let kind = forester.clockPlotKind(phy);
7384
+ if (kind === null) {
7385
+ return null;
7386
+ }
7387
+ let treeRoot = forester.getTreeRoot(phy);
7388
+ // a node is its own top; the tree, or the viewer's holder of a clade,
7389
+ // is a parentless wrapper around it
7390
+ let top = treeRoot;
7391
+ if (view) {
7392
+ top = (!view.parent && view.children && view.children.length === 1) ? view.children[0] : view;
7393
+ }
7394
+ let points = [];
7395
+ let xs = [];
7396
+ let ys = [];
7397
+ forester.preOrderTraversalAll(top, function (n) {
7398
+ let date = auspiceNodeDate(n);
7399
+ let div = auspiceNodeDiv(n);
7400
+ if (date === null || div === null) {
7401
+ return; // not on a tree with Time and Div; a guard, not a rule
7402
+ }
7403
+ let tip = !(n.children && n.children.length > 0);
7404
+ points.push({node: n, date: date, div: div, tip: tip});
7405
+ if (tip) {
7406
+ xs.push(date);
7407
+ ys.push(div);
7408
+ }
7409
+ });
7410
+ let forward = timeIncreasesTowardTips(treeRoot);
7411
+ let root = {node: top, date: auspiceNodeDate(top), div: auspiceNodeDiv(top)};
7412
+ let fit = forester.clockRegression(xs, ys);
7413
+ if (fit) {
7414
+ fit.rate = forward ? fit.slope : -fit.slope;
7415
+ fit.rootDate = (fit.rate > 0 && root.div !== null)
7416
+ ? ((root.div - fit.intercept) / fit.slope) : null;
7417
+ for (let i = 0; i < points.length; ++i) {
7418
+ if (points[i].tip) {
7419
+ points[i].residual = points[i].div - (fit.intercept + (fit.slope * points[i].date));
7420
+ }
7421
+ }
7422
+ }
7423
+ return {
7424
+ kind: kind,
7425
+ forward: forward,
7426
+ unit: dateUnitOf(treeRoot),
7427
+ fromRates: treeRoot._divergenceFromRates === true && !recordsDivergence(treeRoot),
7428
+ points: points,
7429
+ root: root,
7430
+ fit: fit
7431
+ };
7432
+ };
7433
+
7230
7434
  // A number that is not NaN. It used to test only for null, undefined and
7231
7435
  // NaN, and so answered TRUE for "hello", "", {}, [] and true -- which no
7232
7436
  // caller was hurt by, since all of them pass the result of parseFloat, but
@@ -7943,6 +8147,527 @@
7943
8147
  return {backbone: {x: start, w: Number(da.length) * f}, boxes: boxes};
7944
8148
  };
7945
8149
 
8150
+ // --------------------------------------------------------------
8151
+ // Genome regions (the gene track)
8152
+ // --------------------------------------------------------------
8153
+ // A tip that is a genome can carry (part of) that genome as phyloXML
8154
+ // <sequence> elements: one per gene, each with a <location> on a contig
8155
+ // and its family in an <annotation ref="...">, plus one record per
8156
+ // contig. The viewer draws them as arrows on a backbone beside the tip,
8157
+ // the rows anchored on one family so gene order compares across genomes
8158
+ // (Christian, 2026-10-01: the encoding, the location grammar, anchoring
8159
+ // by default, the words "Genes" and "Genome regions"). Nothing in the
8160
+ // schema changes: <location> is phyloXML's "location of a sequence on a
8161
+ // genome/chromosome", an annotation's ref its "colon-separated reference
8162
+ // to an external database".
8163
+ //
8164
+ // Everything that can be tested without a DOM lives here: the grammar,
8165
+ // which sequences are genes and which the contig record, the anchor, the
8166
+ // mapping of every row onto one shared coordinate (flips, lanes, contig
8167
+ // ends), the arrow outline, the families, the legend rows and the links.
8168
+ // test/gene_test.js pins the numbers. The SVG and the controls are
8169
+ // archaeopteryx.js's.
8170
+
8171
+ // contig:start-end(+) -- 1-based, inclusive, the strand optional. Split
8172
+ // on the LAST colon so a contig id with a colon of its own survives.
8173
+ // A located sequence WITH a strand is a gene; one WITHOUT is the record
8174
+ // of the contig itself (its accession, name and length), which is how
8175
+ // the drawing knows where a contig ends.
8176
+ const SEQUENCE_LOCATION_RE = /^(.+):(\d+)-(\d+)(?:\(([+-])\))?$/;
8177
+
8178
+ forester.parseSequenceLocation = function (text) {
8179
+ if (typeof text !== 'string') {
8180
+ return null;
8181
+ }
8182
+ let m = SEQUENCE_LOCATION_RE.exec(text.trim());
8183
+ if (!m) {
8184
+ return null;
8185
+ }
8186
+ let start = Number(m[2]);
8187
+ let end = Number(m[3]);
8188
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 1 || end < start) {
8189
+ return null;
8190
+ }
8191
+ let contig = m[1].trim();
8192
+ if (contig.length === 0) {
8193
+ return null;
8194
+ }
8195
+ return {contig: contig, start: start, end: end, strand: m[4] || null};
8196
+ };
8197
+
8198
+ // The namespace of an annotation ref: "pgfam" of "pgfam:PGF_00019355".
8199
+ // A ref without a colon has none and names no family.
8200
+ forester.annotationNamespace = function (ref) {
8201
+ if (typeof ref !== 'string') {
8202
+ return null;
8203
+ }
8204
+ let c = ref.indexOf(':');
8205
+ return c > 0 ? ref.substring(0, c) : null;
8206
+ };
8207
+
8208
+ function textOf(v) {
8209
+ return (v === null || v === undefined) ? '' : String(v);
8210
+ }
8211
+
8212
+ // Every located sequence of a node, sorted into genes, contig records
8213
+ // and the ones whose location is not in the grammar (counted, never
8214
+ // fatal: <location> is free text in phyloXML, and an older file may say
8215
+ // "chromosome 3" there).
8216
+ function locatedSequences(node) {
8217
+ let out = {genes: [], records: [], ignored: 0};
8218
+ if (!node.sequences) {
8219
+ return out;
8220
+ }
8221
+ node.sequences.forEach(function (s, i) {
8222
+ if (!s || s.location === undefined || s.location === null || s.location === '') {
8223
+ return;
8224
+ }
8225
+ let loc = forester.parseSequenceLocation(s.location);
8226
+ if (!loc) {
8227
+ out.ignored++;
8228
+ return;
8229
+ }
8230
+ let acc = (s.accession && s.accession.value !== undefined && s.accession.value !== null
8231
+ && s.accession.value !== '')
8232
+ ? {source: textOf(s.accession.source), value: textOf(s.accession.value)} : null;
8233
+ if (loc.strand === null) {
8234
+ out.records.push({contig: loc.contig, start: loc.start, end: loc.end, accession: acc,
8235
+ name: textOf(s.name)});
8236
+ return;
8237
+ }
8238
+ let refs = [];
8239
+ (s.annotations || []).forEach(function (a) {
8240
+ if (a && forester.annotationNamespace(a.ref) !== null) {
8241
+ refs.push(a.ref);
8242
+ }
8243
+ });
8244
+ out.genes.push({index: i, contig: loc.contig, start: loc.start, end: loc.end, strand: loc.strand,
8245
+ length: loc.end - loc.start + 1, symbol: textOf(s.symbol), geneName: textOf(s.gene_name),
8246
+ product: textOf(s.name), accession: acc, refs: refs});
8247
+ });
8248
+ return out;
8249
+ }
8250
+
8251
+ // The region a tip carries: its genes on ONE contig -- the contig with
8252
+ // the most genes (ties: the first listed) -- in position order, with
8253
+ // that contig's record when there is one, the span of the genes, and
8254
+ // how many genes on other contigs were left out. Null for a tip with no
8255
+ // genes (a record alone is not a region).
8256
+ forester.geneRegionOf = function (node) {
8257
+ let found = locatedSequences(node);
8258
+ if (found.genes.length === 0) {
8259
+ return null;
8260
+ }
8261
+ let counts = new Map();
8262
+ found.genes.forEach(function (g) {
8263
+ counts.set(g.contig, (counts.get(g.contig) || 0) + 1);
8264
+ });
8265
+ let contig = null;
8266
+ let best = 0;
8267
+ counts.forEach(function (n, c) {
8268
+ if (n > best) {
8269
+ best = n;
8270
+ contig = c;
8271
+ }
8272
+ });
8273
+ let genes = found.genes.filter(function (g) {
8274
+ return g.contig === contig;
8275
+ }).sort(function (a, b) {
8276
+ return (a.start - b.start) || (a.end - b.end) || (a.index - b.index);
8277
+ });
8278
+ let record = null;
8279
+ for (let i = 0; i < found.records.length; ++i) {
8280
+ if (found.records[i].contig === contig) {
8281
+ record = found.records[i];
8282
+ break;
8283
+ }
8284
+ }
8285
+ let span = {start: genes[0].start, end: genes[0].end};
8286
+ genes.forEach(function (g) {
8287
+ if (g.end > span.end) {
8288
+ span.end = g.end;
8289
+ }
8290
+ });
8291
+ return {contig: contig, genes: genes, record: record, span: span,
8292
+ otherContigs: found.genes.length - genes.length, contigs: counts.size};
8293
+ };
8294
+
8295
+ // The tree's gene facts: tips with a region, genes drawn, genes on other
8296
+ // contigs left out, the longest span, and locations not in the grammar.
8297
+ forester.geneRegionStats = function (tree) {
8298
+ let stats = {tips: 0, genes: 0, otherContigs: 0, maxSpan: 0, ignored: 0};
8299
+ forester.preOrderTraversalAll(tree, function (n) {
8300
+ if (n.children) {
8301
+ return;
8302
+ }
8303
+ stats.ignored += locatedSequences(n).ignored;
8304
+ let r = forester.geneRegionOf(n);
8305
+ if (!r) {
8306
+ return;
8307
+ }
8308
+ stats.tips++;
8309
+ stats.genes += r.genes.length;
8310
+ stats.otherContigs += r.otherContigs;
8311
+ let L = r.span.end - r.span.start + 1;
8312
+ if (L > stats.maxSpan) {
8313
+ stats.maxSpan = L;
8314
+ }
8315
+ });
8316
+ return stats;
8317
+ };
8318
+
8319
+ // The family namespaces the genes carry, most-used first (ties by code
8320
+ // unit): a BV-BRC export has both pgfam and plfam on every gene, and the
8321
+ // track colours by ONE of them.
8322
+ forester.geneFamilyNamespaces = function (regions) {
8323
+ let counts = new Map();
8324
+ regions.forEach(function (r) {
8325
+ if (!r) {
8326
+ return;
8327
+ }
8328
+ r.genes.forEach(function (g) {
8329
+ let seen = new Set();
8330
+ g.refs.forEach(function (ref) {
8331
+ let ns = forester.annotationNamespace(ref);
8332
+ if (ns !== null && !seen.has(ns)) {
8333
+ seen.add(ns);
8334
+ counts.set(ns, (counts.get(ns) || 0) + 1);
8335
+ }
8336
+ });
8337
+ });
8338
+ });
8339
+ let out = [];
8340
+ counts.forEach(function (genes, ns) {
8341
+ out.push({ns: ns, genes: genes});
8342
+ });
8343
+ out.sort(function (a, b) {
8344
+ return (b.genes - a.genes) || (a.ns < b.ns ? -1 : (a.ns > b.ns ? 1 : 0));
8345
+ });
8346
+ return out;
8347
+ };
8348
+
8349
+ // A gene's family in a namespace: its first annotation ref there, in
8350
+ // full ("pgfam:PGF_00019355"), or null.
8351
+ forester.geneFamilyOf = function (gene, ns) {
8352
+ if (!ns) {
8353
+ return null;
8354
+ }
8355
+ for (let i = 0; i < gene.refs.length; ++i) {
8356
+ if (forester.annotationNamespace(gene.refs[i]) === ns) {
8357
+ return gene.refs[i];
8358
+ }
8359
+ }
8360
+ return null;
8361
+ };
8362
+
8363
+ // Which families could anchor the rows: for each, the regions carrying
8364
+ // it exactly once (where it CAN anchor) and the regions carrying it at
8365
+ // all; most single-copy regions first, ties by code unit. A product and
8366
+ // symbol ride along for the control's labels.
8367
+ forester.geneAnchorCandidates = function (regions, ns) {
8368
+ let fams = new Map();
8369
+ regions.forEach(function (r) {
8370
+ if (!r) {
8371
+ return;
8372
+ }
8373
+ let here = new Map();
8374
+ r.genes.forEach(function (g) {
8375
+ let fam = forester.geneFamilyOf(g, ns);
8376
+ if (fam === null) {
8377
+ return;
8378
+ }
8379
+ here.set(fam, (here.get(fam) || 0) + 1);
8380
+ if (!fams.has(fam)) {
8381
+ fams.set(fam, {family: fam, single: 0, tips: 0, product: g.product, symbol: g.symbol});
8382
+ }
8383
+ });
8384
+ here.forEach(function (n, fam) {
8385
+ let f = fams.get(fam);
8386
+ f.tips++;
8387
+ if (n === 1) {
8388
+ f.single++;
8389
+ }
8390
+ });
8391
+ });
8392
+ let out = [];
8393
+ fams.forEach(function (f) {
8394
+ out.push(f);
8395
+ });
8396
+ out.sort(function (a, b) {
8397
+ return (b.single - a.single) || (a.family < b.family ? -1 : (a.family > b.family ? 1 : 0));
8398
+ });
8399
+ return out;
8400
+ };
8401
+
8402
+ // The anchor a tree opens with: the family single-copy in the most
8403
+ // regions, provided that is at least half of them. Otherwise none, and
8404
+ // the rows are left-aligned.
8405
+ forester.geneDefaultAnchor = function (regions, ns) {
8406
+ let n = 0;
8407
+ regions.forEach(function (r) {
8408
+ if (r) {
8409
+ n++;
8410
+ }
8411
+ });
8412
+ let c = forester.geneAnchorCandidates(regions, ns);
8413
+ return (c.length > 0 && c[0].single > 0 && c[0].single * 2 >= n) ? c[0].family : null;
8414
+ };
8415
+
8416
+ // The backbone reaches this far past the first and last gene, as a share
8417
+ // of the span -- unless the contig record says the contig ends first.
8418
+ forester.GENE_REGION_PAD = 0.02;
8419
+
8420
+ // Every row onto one shared coordinate, in base pairs. A row whose
8421
+ // anchor family occurs exactly once puts that gene's midpoint at 0 and,
8422
+ // when the gene is on the minus strand, is MIRRORED so the anchor points
8423
+ // right and the neighbours flip with it (the Compare Region convention).
8424
+ // Any other row -- no anchor, no such gene, two of them -- is left-
8425
+ // aligned at the leftmost edge of the anchored rows (at 0 when there are
8426
+ // none). Overlapping genes take a second lane. `regions` is per
8427
+ // displayed tip, in display order, null where a tip has none; the result
8428
+ // has a row (or null) per entry and the extent over all rows.
8429
+ forester.geneTrackRows = function (regions, anchor, ns) {
8430
+ let rows = regions.map(function (region) {
8431
+ if (!region) {
8432
+ return null;
8433
+ }
8434
+ let span = region.span;
8435
+ let pad = forester.GENE_REGION_PAD * (span.end - span.start + 1);
8436
+ let b0 = span.start - pad;
8437
+ let b1 = span.end + pad;
8438
+ let capStart = false;
8439
+ let capEnd = false;
8440
+ if (region.record) {
8441
+ if (b0 <= region.record.start) {
8442
+ b0 = region.record.start;
8443
+ capStart = true;
8444
+ }
8445
+ if (b1 >= region.record.end) {
8446
+ b1 = region.record.end;
8447
+ capEnd = true;
8448
+ }
8449
+ }
8450
+ let anchors = anchor ? region.genes.filter(function (g) {
8451
+ return forester.geneFamilyOf(g, ns) === anchor;
8452
+ }) : [];
8453
+ let anchored = anchors.length === 1;
8454
+ let flip = anchored && anchors[0].strand === '-';
8455
+ return {region: region, b0: b0, b1: b1, capStart: capStart, capEnd: capEnd, anchored: anchored, flip: flip,
8456
+ origin: anchored ? (anchors[0].start + anchors[0].end) / 2 : null};
8457
+ });
8458
+ let min = Infinity;
8459
+ let max = -Infinity;
8460
+ let place = function (row) {
8461
+ let o = row.origin;
8462
+ let map = row.flip
8463
+ ? function (a, b) {
8464
+ return [o - b, o - a];
8465
+ }
8466
+ : function (a, b) {
8467
+ return [a - o, b - o];
8468
+ };
8469
+ let bb = map(row.b0, row.b1);
8470
+ let genes = row.region.genes.map(function (g) {
8471
+ let x = map(g.start, g.end);
8472
+ let dir = g.strand === '+' ? 1 : -1;
8473
+ return {gene: g, x0: x[0], x1: x[1], dir: row.flip ? -dir : dir, lane: 0};
8474
+ });
8475
+ genes.sort(function (p, q) {
8476
+ return (p.x0 - q.x0) || (p.x1 - q.x1);
8477
+ });
8478
+ // greedy lanes: the first lane that is free, else the one that
8479
+ // frees first; two lanes at most
8480
+ let ends = [-Infinity, -Infinity];
8481
+ genes.forEach(function (g) {
8482
+ let lane = g.x0 >= ends[0] ? 0 : (g.x0 >= ends[1] ? 1 : (ends[0] <= ends[1] ? 0 : 1));
8483
+ g.lane = lane;
8484
+ ends[lane] = Math.max(ends[lane], g.x1);
8485
+ });
8486
+ if (bb[0] < min) {
8487
+ min = bb[0];
8488
+ }
8489
+ if (bb[1] > max) {
8490
+ max = bb[1];
8491
+ }
8492
+ return {backbone: {x0: bb[0], x1: bb[1]}, capLeft: row.flip ? row.capEnd : row.capStart,
8493
+ capRight: row.flip ? row.capStart : row.capEnd, anchored: row.anchored, flip: row.flip,
8494
+ lanes: genes.some(function (g) {
8495
+ return g.lane === 1;
8496
+ }) ? 2 : 1, genes: genes, region: row.region};
8497
+ };
8498
+ let out = new Array(rows.length);
8499
+ rows.forEach(function (row, i) {
8500
+ if (row && row.anchored) {
8501
+ out[i] = place(row);
8502
+ }
8503
+ });
8504
+ let left = isFinite(min) ? min : 0;
8505
+ rows.forEach(function (row, i) {
8506
+ if (row && !row.anchored) {
8507
+ row.origin = row.b0 - left; // its backbone starts at the left edge
8508
+ out[i] = place(row);
8509
+ } else if (!row) {
8510
+ out[i] = null;
8511
+ }
8512
+ });
8513
+ return {rows: out, extent: isFinite(min) ? {min: min, max: max} : null};
8514
+ };
8515
+
8516
+ // One row in pixels: the backbone and each gene's box, given the track's
8517
+ // start x, the px per base pair f and the extent's left edge.
8518
+ forester.geneRowGeometry = function (row, start, f, min) {
8519
+ let px = function (x) {
8520
+ return start + ((x - min) * f);
8521
+ };
8522
+ return {
8523
+ backbone: {x: px(row.backbone.x0), w: (row.backbone.x1 - row.backbone.x0) * f},
8524
+ capLeft: row.capLeft,
8525
+ capRight: row.capRight,
8526
+ genes: row.genes.map(function (g) {
8527
+ return {x: px(g.x0), w: (g.x1 - g.x0) * f, dir: g.dir, lane: g.lane, gene: g.gene};
8528
+ })
8529
+ };
8530
+ };
8531
+
8532
+ // The arrow: a box whose leading end tapers to a point in the strand's
8533
+ // direction, the head half the height long; a gene shorter than the head
8534
+ // is a triangle and still shows its direction. Path data to a hundredth
8535
+ // of a pixel; '' for a degenerate box.
8536
+ forester.geneArrowPath = function (x, y, w, h, dir) {
8537
+ if (!isFinite(x) || !isFinite(y) || !(w > 0) || !isFinite(w) || !(h > 0) || !isFinite(h)) {
8538
+ return '';
8539
+ }
8540
+ let num = function (n) {
8541
+ return Math.round(n * 100) / 100;
8542
+ };
8543
+ let head = Math.min(h / 2, w);
8544
+ let body = w - head;
8545
+ let hh = h / 2;
8546
+ if (dir < 0) {
8547
+ // the point on the left
8548
+ return 'M' + num(x + w) + ',' + num(y)
8549
+ + (body > 0 ? 'h' + num(-body) : '')
8550
+ + 'l' + num(-head) + ',' + num(hh)
8551
+ + 'l' + num(head) + ',' + num(hh)
8552
+ + (body > 0 ? 'h' + num(body) : '') + 'Z';
8553
+ }
8554
+ return 'M' + num(x) + ',' + num(y)
8555
+ + (body > 0 ? 'h' + num(body) : '')
8556
+ + 'l' + num(head) + ',' + num(hh)
8557
+ + 'l' + num(-head) + ',' + num(hh)
8558
+ + (body > 0 ? 'h' + num(-body) : '') + 'Z';
8559
+ };
8560
+
8561
+ forester.GENE_SINGLETON_COLOR = '#9e9e9e'; // a family met in one region only
8562
+ forester.GENE_NO_FAMILY_COLOR = '#cfcfcf'; // a gene with no family at all
8563
+
8564
+ // The families over the regions given (display order): the SHARED ones
8565
+ // -- in two regions or more, or, when `shared` (a Set of family refs) is
8566
+ // given, the ones in it -- sorted by code unit (the palette's order),
8567
+ // the legend rows in first-appearance order with each family's region
8568
+ // and gene counts and its commonest product and symbol, and how many
8569
+ // drawn genes are singletons or carry no family. The viewer passes the
8570
+ // TREE's shared families as `shared` when it summarizes a view, so a
8571
+ // family shown once in a subtree keeps the colour it has on the tree.
8572
+ forester.geneFamilySummary = function (regions, ns, shared) {
8573
+ let fams = new Map();
8574
+ let singles = 0;
8575
+ let unfamilied = 0;
8576
+ regions.forEach(function (r) {
8577
+ if (!r) {
8578
+ return;
8579
+ }
8580
+ let here = new Set();
8581
+ r.genes.forEach(function (g) {
8582
+ let fam = forester.geneFamilyOf(g, ns);
8583
+ if (fam === null) {
8584
+ unfamilied++;
8585
+ return;
8586
+ }
8587
+ let f = fams.get(fam);
8588
+ if (!f) {
8589
+ f = {family: fam, tips: 0, genes: 0, products: new Map(), symbols: new Map()};
8590
+ fams.set(fam, f);
8591
+ }
8592
+ f.genes++;
8593
+ if (g.product) {
8594
+ f.products.set(g.product, (f.products.get(g.product) || 0) + 1);
8595
+ }
8596
+ if (g.symbol) {
8597
+ f.symbols.set(g.symbol, (f.symbols.get(g.symbol) || 0) + 1);
8598
+ }
8599
+ here.add(fam);
8600
+ });
8601
+ here.forEach(function (fam) {
8602
+ fams.get(fam).tips++;
8603
+ });
8604
+ });
8605
+ let commonest = function (m) {
8606
+ let best = '';
8607
+ let n = 0;
8608
+ m.forEach(function (count, text) {
8609
+ if (count > n) {
8610
+ n = count;
8611
+ best = text;
8612
+ }
8613
+ });
8614
+ return best;
8615
+ };
8616
+ let legend = [];
8617
+ let names = [];
8618
+ fams.forEach(function (f) {
8619
+ if (shared ? !shared.has(f.family) : f.tips < 2) {
8620
+ singles += f.genes;
8621
+ return;
8622
+ }
8623
+ names.push(f.family);
8624
+ legend.push({family: f.family, tips: f.tips, genes: f.genes,
8625
+ product: commonest(f.products), symbol: commonest(f.symbols)});
8626
+ });
8627
+ names.sort();
8628
+ return {names: names, legend: legend, singletons: singles, unfamilied: unfamilied};
8629
+ };
8630
+
8631
+ // Where a click on a gene goes: its BV-BRC feature page for a BV-BRC /
8632
+ // PATRIC accession (fig|...), NCBI's protein page for a GenBank / RefSeq
8633
+ // one; nowhere otherwise.
8634
+ const BVBRC_FEATURE = 'https://www.bv-brc.org/view/Feature/';
8635
+ const NCBI_PROTEIN = 'https://www.ncbi.nlm.nih.gov/protein/';
8636
+
8637
+ forester.geneReference = function (gene) {
8638
+ if (!gene || !gene.accession) {
8639
+ return null;
8640
+ }
8641
+ let v = gene.accession.value.trim();
8642
+ let src = gene.accession.source.toLowerCase();
8643
+ if (v.length === 0) {
8644
+ return null;
8645
+ }
8646
+ if (/^fig\|/.test(v) || /bv-?brc|patric/.test(src)) {
8647
+ return {url: BVBRC_FEATURE + encodeURIComponent(v).replace(/%7C/gi, '|'), site: 'BV-BRC'};
8648
+ }
8649
+ if (/ncbi|genbank|refseq|uniprot/.test(src)) {
8650
+ return {url: NCBI_PROTEIN + encodeURIComponent(v), site: 'NCBI'};
8651
+ }
8652
+ return null;
8653
+ };
8654
+
8655
+ // The track's own scale bar: a round number of base pairs about
8656
+ // targetPx long, labelled in kb from 1000 up.
8657
+ forester.geneScaleBar = function (pxPerBp, targetPx) {
8658
+ let bar = forester.scaleBarLength(pxPerBp, targetPx);
8659
+ if (!bar) {
8660
+ return null;
8661
+ }
8662
+ let bp = bar.length;
8663
+ return {bp: bp, px: bar.px, label: bp >= 1000 ? (bp / 1000) + ' kb' : bp + ' bp'};
8664
+ };
8665
+
8666
+ // A location as the readout and the node-data dialog show it.
8667
+ forester.geneLocationText = function (gene) {
8668
+ return gene.contig + ':' + gene.start + '-' + gene.end + '(' + gene.strand + ')';
8669
+ };
8670
+
7946
8671
  // --------------------------------------------------------------
7947
8672
  // Scale bar
7948
8673
  // --------------------------------------------------------------
@@ -9681,6 +10406,41 @@
9681
10406
  return vals;
9682
10407
  };
9683
10408
 
10409
+ // Month ticks over [from, to] in decimal years, for a span too short for
10410
+ // whole years (an outbreak sampled over some months): the first of every
10411
+ // 1st, 2nd, 3rd, 6th or 12th month, the smallest step leaving six ticks
10412
+ // or fewer; none where even a tick a year is too many. {value, year,
10413
+ // month}; month is 1-12, value the decimal year at which that month
10414
+ // begins.
10415
+ forester.calendarTickMonths = function (from, to) {
10416
+ let span = to - from;
10417
+ if (!(span > 0) || !isFinite(span) || !isFinite(from)) {
10418
+ return [];
10419
+ }
10420
+ let steps = [1, 2, 3, 6, 12];
10421
+ for (let s = 0; s < steps.length; ++s) {
10422
+ let step = steps[s];
10423
+ let ticks = [];
10424
+ let year = Math.floor(from);
10425
+ // month index = year * 12 + (month - 1); ticks where it divides
10426
+ for (let k = year * 12; ticks.length <= 6; ++k) {
10427
+ let y = Math.floor(k / 12);
10428
+ let m = (k - (y * 12)) + 1;
10429
+ let v = y + ((tipDateDayOfYear(y, m, 1) - 1) / tipDateYearLength(y));
10430
+ if (v > to + 1e-9) {
10431
+ break;
10432
+ }
10433
+ if (v >= from - 1e-9 && (k % step) === 0) {
10434
+ ticks.push({value: v, year: y, month: m});
10435
+ }
10436
+ }
10437
+ if (ticks.length <= 6) {
10438
+ return ticks;
10439
+ }
10440
+ }
10441
+ return [];
10442
+ };
10443
+
9684
10444
 
9685
10445
  // --------------------------------------------------------------
9686
10446
  // Multiple sequence alignment (the desktop's alignment track)