@figtreejs/core 0.0.1-alpha.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/CHANGELOG.md +9 -0
- package/eslint.config.js +9 -0
- package/package.json +76 -0
- package/src/@custom-types/normalize-svg-path.d.ts +13 -0
- package/src/@custom-types/parse-svg-path.d.ts +8 -0
- package/src/@custom-types/svg-path-types.d.ts +37 -0
- package/src/bauble-makers/makers.ts +112 -0
- package/src/bauble-makers/set-up-baubles.ts +197 -0
- package/src/bauble-makers/utils.ts +61 -0
- package/src/components/baubles/bauble.tsx +61 -0
- package/src/components/baubles/branches.tsx +13 -0
- package/src/components/baubles/clades/cartoon.tsx +68 -0
- package/src/components/baubles/clades/highlight.tsx +96 -0
- package/src/components/baubles/clades/index.ts +1 -0
- package/src/components/baubles/clades.tsx +45 -0
- package/src/components/baubles/helpers.tsx +62 -0
- package/src/components/baubles/index.ts +16 -0
- package/src/components/baubles/labels.tsx +38 -0
- package/src/components/baubles/nodes.tsx +51 -0
- package/src/components/baubles/shapes/branch.tsx +53 -0
- package/src/components/baubles/shapes/circle.tsx +64 -0
- package/src/components/baubles/shapes/index.ts +9 -0
- package/src/components/baubles/shapes/label.tsx +104 -0
- package/src/components/baubles/shapes/rectangle.tsx +83 -0
- package/src/components/baubles/types.ts +99 -0
- package/src/components/decorations/axis/axis-types.ts +123 -0
- package/src/components/decorations/axis/axis.tsx +21 -0
- package/src/components/decorations/axis/index.ts +2 -0
- package/src/components/decorations/axis/polar-axis-bars.tsx +102 -0
- package/src/components/decorations/axis/polar-axis.tsx +175 -0
- package/src/components/decorations/axis/rectangular-axis-bars.tsx +53 -0
- package/src/components/decorations/axis/rectangular-axis.tsx +151 -0
- package/src/components/decorations/index.ts +2 -0
- package/src/components/decorations/legend/discrete-legend.tsx +93 -0
- package/src/components/decorations/legend/index.ts +1 -0
- package/src/components/decorations/legend/legend.tsx +1 -0
- package/src/components/figtree/figtree-types.ts +69 -0
- package/src/components/figtree/figtree.tsx +136 -0
- package/src/components/figtree/index.ts +3 -0
- package/src/components/hoc/index.ts +7 -0
- package/src/components/hoc/with-branch.tsx +148 -0
- package/src/components/hoc/with-branches.tsx +54 -0
- package/src/components/hoc/with-clades.tsx +47 -0
- package/src/components/hoc/with-node.tsx +183 -0
- package/src/components/hoc/with-nodes.tsx +45 -0
- package/src/components/index.ts +4 -0
- package/src/context/aminated-context.ts +3 -0
- package/src/context/dimension-context.ts +22 -0
- package/src/context/layout-context.ts +20 -0
- package/src/context/scale-context.ts +12 -0
- package/src/evo/index.ts +1 -0
- package/src/evo/tree/index.ts +5 -0
- package/src/evo/tree/mcc-tree.ts +0 -0
- package/src/evo/tree/normalized-tree/immutable-tree-helpers.ts +136 -0
- package/src/evo/tree/normalized-tree/immutable-tree.test.ts +158 -0
- package/src/evo/tree/normalized-tree/immutable-tree.ts +1365 -0
- package/src/evo/tree/normalized-tree/index.ts +3 -0
- package/src/evo/tree/parsers/annotation-parser.ts +276 -0
- package/src/evo/tree/parsers/index.ts +3 -0
- package/src/evo/tree/parsers/newick-character-parser.ts +246 -0
- package/src/evo/tree/parsers/newick-parsing.ts +22 -0
- package/src/evo/tree/parsers/nexus-parser.ts +12 -0
- package/src/evo/tree/parsers/nexus-parsing.ts +68 -0
- package/src/evo/tree/parsers/parsing.test.ts +289 -0
- package/src/evo/tree/parsers/stream-reader/index.ts +1 -0
- package/src/evo/tree/parsers/stream-reader/newick-importer.txt +395 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.test.ts +99 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.ts +293 -0
- package/src/evo/tree/parsers/stream-reader/nexus-tokenizer.ts +77 -0
- package/src/evo/tree/parsers/stream-reader/nexus-transform-stream.txt +109 -0
- package/src/evo/tree/taxa/helper-functions.ts +46 -0
- package/src/evo/tree/taxa/index.ts +1 -0
- package/src/evo/tree/taxa/taxon.ts +116 -0
- package/src/evo/tree/traversals/index.ts +1 -0
- package/src/evo/tree/traversals/preorder-traversal.ts +89 -0
- package/src/evo/tree/traversals/traversal-types.ts +6 -0
- package/src/evo/tree/tree-types.ts +197 -0
- package/src/evo/tree/utilities.ts +44 -0
- package/src/index.ts +6 -0
- package/src/layouts/functional/index.ts +2 -0
- package/src/layouts/functional/radial-layout.ts +150 -0
- package/src/layouts/functional/rectangular-layout.ts +71 -0
- package/src/layouts/index.ts +3 -0
- package/src/layouts/layout-interface.ts +90 -0
- package/src/layouts/types.ts +32 -0
- package/src/path.helpers.ts +81 -0
- package/src/store/polar-scale.ts +145 -0
- package/src/store/store.ts +144 -0
- package/src/tests/baubles/__snapshots__/branch-labels.test.tsx.snap +901 -0
- package/src/tests/baubles/__snapshots__/node-labels.test.tsx.snap +1516 -0
- package/src/tests/baubles/branch-labels.test.tsx +103 -0
- package/src/tests/baubles/label.svg +131 -0
- package/src/tests/baubles/node-labels.test.tsx +126 -0
- package/src/tests/clades/__snapshots__/cartoon.test.tsx.snap +327 -0
- package/src/tests/clades/__snapshots__/highlight.test.tsx.snap +337 -0
- package/src/tests/clades/cartoon.test.tsx +65 -0
- package/src/tests/clades/highlight.test.tsx +66 -0
- package/src/tests/figtree/__snapshots__/figtree.test.tsx.snap +761 -0
- package/src/tests/figtree/figtree.test.tsx +123 -0
- package/src/tests/figtree/simple.svg +47 -0
- package/src/tests/layouts/radiallayout.test.ts +23 -0
- package/src/tests/layouts/rectangularlayout.test.ts +65 -0
- package/src/tests/shapes/branch.test.tsx +40 -0
- package/src/tests/shapes/circle.test.tsx +47 -0
- package/src/tests/shapes/label.test.tsx +101 -0
- package/src/tests/shapes/rectangle.test.tsx +67 -0
- package/src/tests/shapes/types.ts +1 -0
- package/src/utils.ts +57 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +34 -0
- package/vitetest.config.ts +11 -0
|
@@ -0,0 +1,1365 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Annotation,
|
|
3
|
+
AnnotationDomain,
|
|
4
|
+
AnnotationSummary,
|
|
5
|
+
AnnotationValue,
|
|
6
|
+
DomainOf,
|
|
7
|
+
MarkovJumpValue,
|
|
8
|
+
NodeRef,
|
|
9
|
+
RawAnnotationValue,
|
|
10
|
+
Tree,
|
|
11
|
+
newickParsingOptions,
|
|
12
|
+
} from "../tree-types";
|
|
13
|
+
import { BaseAnnotationType } from "../tree-types";
|
|
14
|
+
import {
|
|
15
|
+
parseNewick,
|
|
16
|
+
parseNexus,
|
|
17
|
+
processAnnotationValue,
|
|
18
|
+
writeAnnotationValue,
|
|
19
|
+
} from "../parsers";
|
|
20
|
+
import { immerable, produce } from "immer";
|
|
21
|
+
import type { Taxon, TaxonSetInterface } from "../taxa/taxon";
|
|
22
|
+
import { TaxonSet } from "../taxa/taxon";
|
|
23
|
+
import { format } from "d3-format";
|
|
24
|
+
import { extent } from "d3-array";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
MaybeType,
|
|
28
|
+
type Undefinable,
|
|
29
|
+
UnwrapErr,
|
|
30
|
+
type Maybe,
|
|
31
|
+
} from "@figtreejs/maybe/maybe";
|
|
32
|
+
import {
|
|
33
|
+
maybeGetAnnotation,
|
|
34
|
+
maybeGetNode,
|
|
35
|
+
maybeGetNodeByTaxon,
|
|
36
|
+
maybeGetParent,
|
|
37
|
+
maybeGetTaxon,
|
|
38
|
+
maybeGetTaxonFromNode,
|
|
39
|
+
} from "./immutable-tree-helpers";
|
|
40
|
+
import { notNull, unNullify } from "../../../utils";
|
|
41
|
+
import { v4 as uuidv4 } from "uuid";
|
|
42
|
+
|
|
43
|
+
export type nodeIndex = string | number | Taxon;
|
|
44
|
+
export type maybeNodeIndex = Maybe<nodeIndex>;
|
|
45
|
+
|
|
46
|
+
//TODO will need to think about taxonsets and immutability.
|
|
47
|
+
export interface Node extends NodeRef {
|
|
48
|
+
number: number;
|
|
49
|
+
taxon: number | undefined;
|
|
50
|
+
label: string | undefined;
|
|
51
|
+
children: number[];
|
|
52
|
+
parent: number | undefined;
|
|
53
|
+
length: number | undefined;
|
|
54
|
+
annotations: { [annotation: string]: Annotation };
|
|
55
|
+
_id: string; // an id so we can mark updates to root when topology changes
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ImmutableTreeData {
|
|
59
|
+
nodes: {
|
|
60
|
+
allNodes: Node[];
|
|
61
|
+
byTaxon: number[];
|
|
62
|
+
byLabel: {
|
|
63
|
+
[label: string]: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
heights: [];
|
|
67
|
+
nodeToTaxon: number[];
|
|
68
|
+
rootNode: number;
|
|
69
|
+
is_rooted: boolean;
|
|
70
|
+
hasLengths: boolean;
|
|
71
|
+
annotations: { [annotation: string]: AnnotationSummary };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ImmutableTree implements Tree, TaxonSetInterface {
|
|
75
|
+
//TODO remove the TaxonSetInterface implementation.
|
|
76
|
+
[immerable] = true;
|
|
77
|
+
|
|
78
|
+
_data: ImmutableTreeData;
|
|
79
|
+
taxonSet: TaxonSet;
|
|
80
|
+
constructor(input: { data?: ImmutableTreeData; taxonSet?: TaxonSet } = {}) {
|
|
81
|
+
const { data: _data, taxonSet } = input;
|
|
82
|
+
let data = _data;
|
|
83
|
+
if (taxonSet) {
|
|
84
|
+
this.taxonSet = taxonSet;
|
|
85
|
+
} else {
|
|
86
|
+
this.taxonSet = new TaxonSet();
|
|
87
|
+
}
|
|
88
|
+
if (data === undefined) {
|
|
89
|
+
data = {
|
|
90
|
+
nodes: {
|
|
91
|
+
allNodes: [
|
|
92
|
+
{
|
|
93
|
+
number: 0,
|
|
94
|
+
children: [],
|
|
95
|
+
parent: undefined,
|
|
96
|
+
label: undefined,
|
|
97
|
+
length: undefined,
|
|
98
|
+
taxon: undefined,
|
|
99
|
+
annotations: {},
|
|
100
|
+
_id: uuidv4(),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
byTaxon: [],
|
|
104
|
+
byLabel: {},
|
|
105
|
+
},
|
|
106
|
+
nodeToTaxon: [],
|
|
107
|
+
rootNode: 0,
|
|
108
|
+
is_rooted: true,
|
|
109
|
+
annotations: {},
|
|
110
|
+
heights: [],
|
|
111
|
+
hasLengths: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
this._data = data;
|
|
115
|
+
}
|
|
116
|
+
lockTaxa(): TaxonSetInterface {
|
|
117
|
+
this.taxonSet.lockTaxa();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
addTaxon(taxonOrName: string | Taxon): this {
|
|
122
|
+
this.taxonSet.addTaxon(taxonOrName);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getTaxonCount(): number {
|
|
127
|
+
return this.taxonSet.getTaxonCount();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getTaxonSet(): TaxonSetInterface {
|
|
131
|
+
return this.taxonSet;
|
|
132
|
+
}
|
|
133
|
+
// Parsers and constructors
|
|
134
|
+
|
|
135
|
+
static fromNewick(
|
|
136
|
+
newick: string,
|
|
137
|
+
options: newickParsingOptions = {},
|
|
138
|
+
): ImmutableTree {
|
|
139
|
+
// const tree = new this()
|
|
140
|
+
return parseNewick(newick, options);
|
|
141
|
+
}
|
|
142
|
+
static fromNexus(
|
|
143
|
+
nexus: string,
|
|
144
|
+
options?: newickParsingOptions,
|
|
145
|
+
): ImmutableTree {
|
|
146
|
+
const tree = new this();
|
|
147
|
+
return parseNexus(tree, nexus, options);
|
|
148
|
+
// throw new Error("Nexus parsing not implemented")
|
|
149
|
+
}
|
|
150
|
+
static fromString(
|
|
151
|
+
string: string,
|
|
152
|
+
options?: newickParsingOptions,
|
|
153
|
+
): ImmutableTree {
|
|
154
|
+
if (string.toLowerCase().includes("#nexus")) {
|
|
155
|
+
return this.fromNexus(string, options);
|
|
156
|
+
} else {
|
|
157
|
+
return this.fromNewick(string, options);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static fromTree(tree: ImmutableTree, rootNode: NodeRef): ImmutableTree {
|
|
162
|
+
// make a new tree.
|
|
163
|
+
let newTree = new this();
|
|
164
|
+
|
|
165
|
+
const traverseAndCopy = (tree: ImmutableTree, node: NodeRef): NodeRef => {
|
|
166
|
+
const children = [];
|
|
167
|
+
let newNode: NodeRef;
|
|
168
|
+
for (const child of tree.getChildren(node)) {
|
|
169
|
+
children.push(traverseAndCopy(tree, child));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (node !== rootNode) {
|
|
173
|
+
newTree = this._addNodeWithMetadata(tree, node, newTree);
|
|
174
|
+
newNode = newTree.getNode(newTree.getNodeCount() - 1);
|
|
175
|
+
} else {
|
|
176
|
+
// already have the node
|
|
177
|
+
newNode = newTree.getRoot();
|
|
178
|
+
// add metadata
|
|
179
|
+
this._copyNodeMetadata(tree, node, newTree, newNode);
|
|
180
|
+
}
|
|
181
|
+
for (const child of children) {
|
|
182
|
+
newTree = newTree.addChild(newNode, child);
|
|
183
|
+
}
|
|
184
|
+
return newNode;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
traverseAndCopy(tree, rootNode);
|
|
188
|
+
newTree = newTree.deleteLength(newTree.getRoot()); //
|
|
189
|
+
|
|
190
|
+
return newTree;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static _addNodeWithMetadata(
|
|
194
|
+
tree: ImmutableTree,
|
|
195
|
+
node: NodeRef,
|
|
196
|
+
newTree: ImmutableTree,
|
|
197
|
+
): ImmutableTree {
|
|
198
|
+
const added = newTree.addNodes(1);
|
|
199
|
+
const newNode = added.nodes[0];
|
|
200
|
+
newTree = added.tree;
|
|
201
|
+
newTree = this._copyNodeMetadata(tree, node, newTree, newNode);
|
|
202
|
+
return newTree;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static _copyNodeMetadata(
|
|
206
|
+
tree: ImmutableTree,
|
|
207
|
+
node: NodeRef,
|
|
208
|
+
newTree: ImmutableTree,
|
|
209
|
+
newNode: NodeRef,
|
|
210
|
+
): ImmutableTree {
|
|
211
|
+
if (tree.hasTaxon(node)) {
|
|
212
|
+
const taxon = tree.getTaxonFromNode(node);
|
|
213
|
+
newTree = newTree.addTaxon(taxon);
|
|
214
|
+
console.log("Current taxa:", newTree.taxonSet);
|
|
215
|
+
newTree = newTree.setTaxon(newNode, taxon);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (tree.hasLabel(node)) {
|
|
219
|
+
const label = tree.getLabel(node);
|
|
220
|
+
newTree = newTree.setLabel(newNode, label);
|
|
221
|
+
}
|
|
222
|
+
for (const key of tree.getAnnotationKeys()) {
|
|
223
|
+
if (tree.hasAnnotation(node, key)) {
|
|
224
|
+
// access directly to get the full type
|
|
225
|
+
const annotation = tree.getFullNodeAnnotation(node, key);
|
|
226
|
+
|
|
227
|
+
if (annotation.type === BaseAnnotationType.MARKOV_JUMPS) {
|
|
228
|
+
const value = annotation.value.map((j) => [
|
|
229
|
+
Number(j.time),
|
|
230
|
+
j.from,
|
|
231
|
+
j.to,
|
|
232
|
+
]) as [[number, string, string]]; // todo infer type from above
|
|
233
|
+
newTree = newTree.annotateNode(newNode, key, value) as ImmutableTree; // TODO make tree interface genaric and return the same type of tree
|
|
234
|
+
} else {
|
|
235
|
+
newTree = newTree.annotateNode(
|
|
236
|
+
newNode,
|
|
237
|
+
key,
|
|
238
|
+
annotation.value,
|
|
239
|
+
) as ImmutableTree;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (tree.hasBranchLength(node)) {
|
|
245
|
+
const length = tree.getLength(node);
|
|
246
|
+
newTree = newTree.setLength(newNode, length);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return newTree;
|
|
250
|
+
}
|
|
251
|
+
// ------------------ Getters ----------------------
|
|
252
|
+
|
|
253
|
+
isRooted(): boolean {
|
|
254
|
+
return this._data.is_rooted;
|
|
255
|
+
}
|
|
256
|
+
getAnnotationType(name: string): BaseAnnotationType {
|
|
257
|
+
if (
|
|
258
|
+
(this._data.annotations[name] as Undefinable<AnnotationSummary>) ===
|
|
259
|
+
undefined
|
|
260
|
+
) {
|
|
261
|
+
throw new Error(`No annotation found with name: ${name}`);
|
|
262
|
+
}
|
|
263
|
+
return this._data.annotations[name].type;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getAnnotationKeys(): string[] {
|
|
267
|
+
return Object.keys(this._data.annotations);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getRoot(): NodeRef {
|
|
271
|
+
return this._data.nodes.allNodes[this._data.rootNode];
|
|
272
|
+
}
|
|
273
|
+
getNodeCount(): number {
|
|
274
|
+
return this._data.nodes.allNodes.length;
|
|
275
|
+
}
|
|
276
|
+
getInternalNodeCount(): number {
|
|
277
|
+
return this._data.nodes.allNodes.filter((n) => n.children.length > 0)
|
|
278
|
+
.length;
|
|
279
|
+
}
|
|
280
|
+
getExternalNodeCount(): number {
|
|
281
|
+
return this._data.nodes.allNodes.filter((n) => n.children.length == 0)
|
|
282
|
+
.length;
|
|
283
|
+
}
|
|
284
|
+
getInternalNodes(): NodeRef[] {
|
|
285
|
+
return this._data.nodes.allNodes.filter((n) => n.children.length > 0);
|
|
286
|
+
}
|
|
287
|
+
getExternalNodes(): NodeRef[] {
|
|
288
|
+
return this._data.nodes.allNodes.filter((n) => n.children.length == 0);
|
|
289
|
+
}
|
|
290
|
+
getNodes(): NodeRef[] {
|
|
291
|
+
return this._data.nodes.allNodes;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getNode(i: nodeIndex): NodeRef {
|
|
295
|
+
const node = maybeGetNode(this, i);
|
|
296
|
+
switch (node.type) {
|
|
297
|
+
case MaybeType.Some:
|
|
298
|
+
return node.value;
|
|
299
|
+
case MaybeType.Nothing:
|
|
300
|
+
throw new Error(`No node found`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getNodeByTaxon(taxon: Taxon): NodeRef {
|
|
305
|
+
const n = maybeGetNodeByTaxon(this, taxon);
|
|
306
|
+
switch (n.type) {
|
|
307
|
+
case MaybeType.Nothing:
|
|
308
|
+
throw new Error(`No node found for Taxon ${taxon.name}`);
|
|
309
|
+
case MaybeType.Some:
|
|
310
|
+
return n.value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getTaxonByName(name: string): Taxon {
|
|
315
|
+
return this.taxonSet.getTaxonByName(name);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
getNodeByLabel(label: string): NodeRef {
|
|
319
|
+
return this.getNode(this._data.nodes.byLabel[label]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
hasTaxon(node: NodeRef): boolean {
|
|
323
|
+
const t = maybeGetTaxonFromNode(this, node);
|
|
324
|
+
|
|
325
|
+
switch (t.type) {
|
|
326
|
+
case MaybeType.Some:
|
|
327
|
+
return true;
|
|
328
|
+
case MaybeType.Nothing:
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getTaxonFromNode(node: NodeRef): Taxon {
|
|
334
|
+
const t = maybeGetTaxonFromNode(this, node);
|
|
335
|
+
switch (t.type) {
|
|
336
|
+
case MaybeType.Some:
|
|
337
|
+
return t.value;
|
|
338
|
+
case MaybeType.Nothing:
|
|
339
|
+
throw new Error(`Node taxon found for the provided node`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
//TODO overload as above.
|
|
343
|
+
|
|
344
|
+
getTaxon(id: number | NodeRef | string): Taxon {
|
|
345
|
+
const t = maybeGetTaxon(this, id);
|
|
346
|
+
switch (t.type) {
|
|
347
|
+
case MaybeType.Some:
|
|
348
|
+
return t.value;
|
|
349
|
+
case MaybeType.Nothing:
|
|
350
|
+
throw new Error(`Node taxon found that matched the provided id`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
hasNodeHeights(): boolean {
|
|
355
|
+
throw new Error("hasNodeHeights not implemented.");
|
|
356
|
+
}
|
|
357
|
+
//todo cache
|
|
358
|
+
getHeight(node: NodeRef): number {
|
|
359
|
+
let maxDiv = -1;
|
|
360
|
+
for (const t of tipIterator(this)) {
|
|
361
|
+
const d = this.getDivergence(t);
|
|
362
|
+
if (d > maxDiv) {
|
|
363
|
+
maxDiv = d;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return maxDiv - this.getDivergence(node);
|
|
367
|
+
}
|
|
368
|
+
hasBranchLength(node: NodeRef): boolean {
|
|
369
|
+
const n = this.getNode(node.number) as Node;
|
|
370
|
+
return n.length !== undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
getLength(node: NodeRef): number {
|
|
374
|
+
const thisNode = this.getNode(node.number);
|
|
375
|
+
const length = (thisNode as Node).length;
|
|
376
|
+
if (length === undefined) {
|
|
377
|
+
if (this.hasLengths()) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`The tree has lengths but, no length was found for node ${node.number}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
console.warn(
|
|
383
|
+
`The tree does not have branchlengths so a length of 1 is used as default`,
|
|
384
|
+
);
|
|
385
|
+
return 1.0;
|
|
386
|
+
}
|
|
387
|
+
return length;
|
|
388
|
+
}
|
|
389
|
+
hasLengths(): boolean {
|
|
390
|
+
return this._data.hasLengths;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_toString(
|
|
394
|
+
node: NodeRef,
|
|
395
|
+
options?: {
|
|
396
|
+
blFormat: (value: number) => string;
|
|
397
|
+
includeAnnotations: boolean;
|
|
398
|
+
},
|
|
399
|
+
): string {
|
|
400
|
+
if (options === undefined) {
|
|
401
|
+
options = { blFormat: format("0.2"), includeAnnotations: false };
|
|
402
|
+
}
|
|
403
|
+
return (
|
|
404
|
+
(this.getChildCount(node) > 0
|
|
405
|
+
? `(${this.getChildren(node)
|
|
406
|
+
.map((child) => this._toString(child, options))
|
|
407
|
+
.join(",")})${this.hasLabel(node) ? "#" + this.getLabel(node) : ""}`
|
|
408
|
+
: this.hasTaxon(node)
|
|
409
|
+
? this.getTaxonFromNode(node).name
|
|
410
|
+
: "") +
|
|
411
|
+
(options.includeAnnotations ? this._writeAnnotations(node) : "") +
|
|
412
|
+
(this.hasBranchLength(node)
|
|
413
|
+
? `:${options.blFormat(this.getLength(node))}`
|
|
414
|
+
: "")
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_writeAnnotations(node: NodeRef): string {
|
|
419
|
+
const annotations = this._data.nodes.allNodes[node.number].annotations;
|
|
420
|
+
if (Object.keys(annotations).length === 0) {
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
let annotationString = "[&";
|
|
424
|
+
let i = 0;
|
|
425
|
+
for (const [key, annotation] of Object.entries(annotations)) {
|
|
426
|
+
if (i > 0) {
|
|
427
|
+
annotationString += ", "; // add a comma before the next annotation
|
|
428
|
+
}
|
|
429
|
+
annotationString += `${key}=${writeAnnotationValue(annotation)}`;
|
|
430
|
+
i += 1;
|
|
431
|
+
}
|
|
432
|
+
annotationString += "]";
|
|
433
|
+
return annotationString;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
toNewick(
|
|
437
|
+
node?: NodeRef,
|
|
438
|
+
options?: {
|
|
439
|
+
blFormat?: (value: number) => string;
|
|
440
|
+
includeAnnotations?: boolean;
|
|
441
|
+
},
|
|
442
|
+
): string {
|
|
443
|
+
const ops = {
|
|
444
|
+
blFormat: format("0.2"),
|
|
445
|
+
includeAnnotations: false,
|
|
446
|
+
...options,
|
|
447
|
+
};
|
|
448
|
+
if (node === undefined) {
|
|
449
|
+
node = this.getRoot();
|
|
450
|
+
}
|
|
451
|
+
return this._toString(node, ops) + ";";
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
getMRCA(node1: NodeRef | NodeRef[], node2?: NodeRef): NodeRef {
|
|
455
|
+
if (Array.isArray(node1)) {
|
|
456
|
+
const nodes = node1;
|
|
457
|
+
if (nodes.length === 0) {
|
|
458
|
+
throw new Error("No nodes provided to get MRCA");
|
|
459
|
+
}
|
|
460
|
+
let mrca = nodes[0];
|
|
461
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
462
|
+
mrca = this.getMRCA(mrca, nodes[i]);
|
|
463
|
+
if (this.isRoot(mrca)) {
|
|
464
|
+
return mrca;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return mrca;
|
|
468
|
+
} else {
|
|
469
|
+
if (node2 === undefined) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`No second node provided. A node must be provided if the first value is not an array`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const path1 = [...this.getPathToRoot(node1)];
|
|
475
|
+
let mrca = null;
|
|
476
|
+
|
|
477
|
+
for (const ancestor of this.getPathToRoot(node2)) {
|
|
478
|
+
if (path1.includes(ancestor)) {
|
|
479
|
+
mrca = ancestor;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (mrca === null) {
|
|
484
|
+
throw new Error("No MRCA found");
|
|
485
|
+
}
|
|
486
|
+
return mrca;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
getPath(node1: NodeRef, node2: NodeRef): NodeRef[] {
|
|
491
|
+
const path = [];
|
|
492
|
+
const mrca = this.getMRCA(node1, node2);
|
|
493
|
+
for (let node of [node1, node2]) {
|
|
494
|
+
while (node != mrca) {
|
|
495
|
+
path.push(node);
|
|
496
|
+
const parent = this.getParent(node);
|
|
497
|
+
node = parent;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return path;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getPathLength(node1: NodeRef, node2: NodeRef): number {
|
|
504
|
+
let sum = 0;
|
|
505
|
+
const mrca = this.getMRCA(node1, node2);
|
|
506
|
+
for (let node of [node1, node2]) {
|
|
507
|
+
while (node != mrca) {
|
|
508
|
+
const length = this.getLength(node);
|
|
509
|
+
sum += length;
|
|
510
|
+
const parent = this.getParent(node);
|
|
511
|
+
node = parent;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return sum;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
*getPathToRoot(node: NodeRef): Generator<NodeRef> {
|
|
519
|
+
let n: NodeRef = node;
|
|
520
|
+
while (!this.isRoot(n)) {
|
|
521
|
+
yield n;
|
|
522
|
+
n = this.getParent(n);
|
|
523
|
+
}
|
|
524
|
+
yield n; // yield root
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
getNextSibling(node: NodeRef): NodeRef {
|
|
528
|
+
if (!this.hasLeftSibling(node) && !this.hasRightSibling(node)) {
|
|
529
|
+
throw new Error(`Node ${node.number} has no sibling`);
|
|
530
|
+
}
|
|
531
|
+
const parent = this.getParent(node);
|
|
532
|
+
const index = (parent as Node).children.map((c) => c).indexOf(node.number);
|
|
533
|
+
return this.getChild(parent, (index + 1) % this.getChildCount(parent));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
hasRightSibling(node: NodeRef): boolean {
|
|
537
|
+
const parent = maybeGetParent(this, node);
|
|
538
|
+
switch (parent.type) {
|
|
539
|
+
case MaybeType.Nothing:
|
|
540
|
+
return false;
|
|
541
|
+
case MaybeType.Some:
|
|
542
|
+
return (
|
|
543
|
+
(parent.value as Node).children.map((c) => c).indexOf(node.number) <
|
|
544
|
+
this.getChildCount(parent.value) - 1
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
getRightSibling(node: NodeRef): NodeRef {
|
|
549
|
+
if (!this.hasRightSibling(node)) {
|
|
550
|
+
throw new Error(`node ${node.number} does not have a right sibling`);
|
|
551
|
+
}
|
|
552
|
+
const parent = this.getParent(node);
|
|
553
|
+
const index = (parent as Node).children.map((c) => c).indexOf(node.number);
|
|
554
|
+
return this.getChild(parent, index + 1);
|
|
555
|
+
}
|
|
556
|
+
hasLeftSibling(node: NodeRef): boolean {
|
|
557
|
+
const parent = maybeGetParent(this, node);
|
|
558
|
+
switch (parent.type) {
|
|
559
|
+
case MaybeType.Nothing:
|
|
560
|
+
return false;
|
|
561
|
+
case MaybeType.Some:
|
|
562
|
+
return (
|
|
563
|
+
this.getChildCount(parent.value) > 1 &&
|
|
564
|
+
(parent.value as Node).children.map((c) => c).indexOf(node.number) > 0
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getLeftSibling(node: NodeRef): NodeRef {
|
|
570
|
+
if (!this.hasLeftSibling(node)) {
|
|
571
|
+
throw new Error(`node ${node.number} does not have a left sibling`);
|
|
572
|
+
}
|
|
573
|
+
const parent = this.getParent(node);
|
|
574
|
+
const index = (parent as Node).children.map((c) => c).indexOf(node.number);
|
|
575
|
+
return this.getChild(parent, index - 1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
getDivergence(node: NodeRef): number {
|
|
579
|
+
let divergence = 0;
|
|
580
|
+
for (const n of this.getPathToRoot(node)) {
|
|
581
|
+
if (this.isRoot(n)) {
|
|
582
|
+
// the root does not need a length but may have one
|
|
583
|
+
if (this.hasBranchLength(n)) {
|
|
584
|
+
divergence += this.getLength(n);
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
divergence += this.getLength(n);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return divergence;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
getChildCount(node: NodeRef): number {
|
|
594
|
+
if (!this._data.nodes.allNodes[node.number]) {
|
|
595
|
+
throw new Error(`Node ${node.number} not found`);
|
|
596
|
+
}
|
|
597
|
+
return this._data.nodes.allNodes[node.number].children.length;
|
|
598
|
+
}
|
|
599
|
+
getChild(node: NodeRef, index: number): NodeRef {
|
|
600
|
+
return this._data.nodes.allNodes[
|
|
601
|
+
this._data.nodes.allNodes[node.number].children[index]
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
getParent(node: NodeRef): NodeRef {
|
|
606
|
+
const parent = maybeGetParent(this, node);
|
|
607
|
+
switch (parent.type) {
|
|
608
|
+
case MaybeType.Some:
|
|
609
|
+
return parent.value;
|
|
610
|
+
case MaybeType.Nothing:
|
|
611
|
+
throw new Error(`No parent for node ${node.number}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
getChildren(node: NodeRef): NodeRef[] {
|
|
615
|
+
return this._data.nodes.allNodes[node.number].children.map((n) =>
|
|
616
|
+
this.getNode(n),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
hasLabel(node: NodeRef): boolean {
|
|
621
|
+
return this._data.nodes.allNodes[node.number].label !== undefined;
|
|
622
|
+
}
|
|
623
|
+
getLabel(node: NodeRef): string {
|
|
624
|
+
const l = this._data.nodes.allNodes[node.number].label;
|
|
625
|
+
if (l === undefined) {
|
|
626
|
+
throw new Error(`no label for node ${node.number}`);
|
|
627
|
+
}
|
|
628
|
+
return l;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
isExternal(node: NodeRef): boolean {
|
|
632
|
+
return (this.getNode(node.number) as Node).children.length === 0;
|
|
633
|
+
}
|
|
634
|
+
isInternal(node: NodeRef): boolean {
|
|
635
|
+
return (this.getNode(node.number) as Node).children.length > 0;
|
|
636
|
+
}
|
|
637
|
+
isRoot(node: NodeRef) {
|
|
638
|
+
return this._data.rootNode === node.number;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
addNodes(n: number = 1): { tree: ImmutableTree; nodes: NodeRef[] } {
|
|
642
|
+
const newNodes: NodeRef[] = [];
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
tree: produce(this, (draft) => {
|
|
646
|
+
const number = draft._data.nodes.allNodes.length;
|
|
647
|
+
for (let i = 0; i < n; i++) {
|
|
648
|
+
const newNode: Node = {
|
|
649
|
+
number: number + i,
|
|
650
|
+
children: [],
|
|
651
|
+
parent: undefined,
|
|
652
|
+
label: undefined,
|
|
653
|
+
length: undefined,
|
|
654
|
+
taxon: undefined,
|
|
655
|
+
annotations: {},
|
|
656
|
+
_id: uuidv4(),
|
|
657
|
+
};
|
|
658
|
+
newNodes.push(newNode);
|
|
659
|
+
draft._data.nodes.allNodes.push(newNode);
|
|
660
|
+
}
|
|
661
|
+
}),
|
|
662
|
+
nodes: newNodes,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
setTaxon(node: NodeRef, taxon: Taxon): this {
|
|
667
|
+
// check we know about this taxon;
|
|
668
|
+
if (taxon !== this.taxonSet.getTaxonByName(taxon.name)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`Taxon ${taxon.name} is either not in the taxon set. Has it been copied?`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return produce(this, (draft) => {
|
|
674
|
+
const n = draft.getNode(node.number) as Node;
|
|
675
|
+
n.taxon = taxon.number;
|
|
676
|
+
draft._data.nodes.byTaxon[taxon.number] = node.number;
|
|
677
|
+
draft._data.nodeToTaxon[node.number] = taxon.number;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
getAnnotationSummary(name: string): AnnotationSummary {
|
|
681
|
+
if (
|
|
682
|
+
(this._data.annotations[name] as Undefinable<AnnotationSummary>) ===
|
|
683
|
+
undefined
|
|
684
|
+
) {
|
|
685
|
+
throw new Error(`No annotation with name ${name} found in tree`);
|
|
686
|
+
}
|
|
687
|
+
return this._data.annotations[name];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
getAnnotations(): AnnotationSummary[] {
|
|
691
|
+
return Object.values(this._data.annotations);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
getAnnotation(
|
|
695
|
+
node: NodeRef,
|
|
696
|
+
name: string,
|
|
697
|
+
d?: AnnotationValue,
|
|
698
|
+
): AnnotationValue {
|
|
699
|
+
const a = maybeGetAnnotation(this, this.getNode(node.number), name);
|
|
700
|
+
if (d === undefined) {
|
|
701
|
+
const { value } = UnwrapErr(
|
|
702
|
+
a,
|
|
703
|
+
`Node ${node.number} is not annotated with ${name}`,
|
|
704
|
+
);
|
|
705
|
+
return value;
|
|
706
|
+
} else {
|
|
707
|
+
switch (a.type) {
|
|
708
|
+
case MaybeType.Some:
|
|
709
|
+
return a.value.value; // value of the maybe = annotation . value of the annotation
|
|
710
|
+
case MaybeType.Nothing:
|
|
711
|
+
return d;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
getFullNodeAnnotation(node: NodeRef, name: string): Annotation {
|
|
717
|
+
const a = maybeGetAnnotation(this, this.getNode(node.number), name);
|
|
718
|
+
return UnwrapErr(a, `Node ${node.number} is not annotated with ${name}`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
hasAnnotation(node: NodeRef, name: string): boolean {
|
|
722
|
+
const a = maybeGetAnnotation(this, this.getNode(node.number), name);
|
|
723
|
+
switch (a.type) {
|
|
724
|
+
case MaybeType.Some:
|
|
725
|
+
return true;
|
|
726
|
+
case MaybeType.Nothing:
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ---------------- Setters ---------------------
|
|
732
|
+
|
|
733
|
+
annotateNode(node: NodeRef, name: string, value: RawAnnotationValue): Tree;
|
|
734
|
+
annotateNode(
|
|
735
|
+
node: NodeRef,
|
|
736
|
+
annotation: Record<string, RawAnnotationValue>,
|
|
737
|
+
): Tree;
|
|
738
|
+
annotateNode(
|
|
739
|
+
node: NodeRef,
|
|
740
|
+
a: string | Record<string, RawAnnotationValue>,
|
|
741
|
+
b?: RawAnnotationValue,
|
|
742
|
+
): Tree {
|
|
743
|
+
if (typeof a === "string") {
|
|
744
|
+
const name = a;
|
|
745
|
+
const value = b as RawAnnotationValue;
|
|
746
|
+
const classifiedAnnotation = processAnnotationValue(value);
|
|
747
|
+
const currentSummary = this._data.annotations[
|
|
748
|
+
name
|
|
749
|
+
] as Undefinable<AnnotationSummary>;
|
|
750
|
+
if (currentSummary !== undefined) {
|
|
751
|
+
if (currentSummary.type !== classifiedAnnotation.type) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
`Tried annotation ${name} was parsed as ${classifiedAnnotation.type} - but is ${currentSummary.type} in tree.`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return produce(this, (draft) => {
|
|
758
|
+
const currentDomain = currentSummary
|
|
759
|
+
? currentSummary.domain
|
|
760
|
+
: undefined;
|
|
761
|
+
const domain = updateDomain(classifiedAnnotation, currentDomain);
|
|
762
|
+
draft._data.nodes.allNodes[node.number].annotations[name] = {
|
|
763
|
+
id: name,
|
|
764
|
+
type: classifiedAnnotation.type,
|
|
765
|
+
value: classifiedAnnotation.value,
|
|
766
|
+
} as Annotation;
|
|
767
|
+
draft._data.annotations[name] = {
|
|
768
|
+
id: name,
|
|
769
|
+
type: classifiedAnnotation.type,
|
|
770
|
+
domain: domain,
|
|
771
|
+
} as AnnotationSummary;
|
|
772
|
+
});
|
|
773
|
+
} else {
|
|
774
|
+
// loop over entries
|
|
775
|
+
let t: Tree = this as Tree;
|
|
776
|
+
for (const [k, v] of Object.entries(a)) {
|
|
777
|
+
t = t.annotateNode(node, k, v);
|
|
778
|
+
}
|
|
779
|
+
return t;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
//TODO handle height and divergence changes still not very happy with how these are handled.
|
|
784
|
+
|
|
785
|
+
setHeight(node: NodeRef, height: number): this {
|
|
786
|
+
if (!this.hasLengths()) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
"Can not set the heights of nodes in a tree without branch lengths",
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return produce(this, (draft) => {
|
|
792
|
+
const n = draft.getNode(node.number) as Node;
|
|
793
|
+
if (height < 0) {
|
|
794
|
+
throw new Error("Height must be non-negative");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const currentHeight = draft.getHeight(node);
|
|
798
|
+
const change = currentHeight - height; // positive change increases length
|
|
799
|
+
// height goes 2 to 3 length should decease new length = length + (-1) childe lengths must increase
|
|
800
|
+
|
|
801
|
+
if (n.length === undefined) {
|
|
802
|
+
if (!draft.isRoot(node)) {
|
|
803
|
+
throw new Error("Cannot set height on a node without length");
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
n.length = n.length + change;
|
|
807
|
+
}
|
|
808
|
+
// update length of children
|
|
809
|
+
for (const child of draft.getChildren(node)) {
|
|
810
|
+
const childNode = draft.getNode(child.number) as Node;
|
|
811
|
+
const newLength = draft.getLength(childNode) - change;
|
|
812
|
+
|
|
813
|
+
childNode.length = newLength;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
setLength(node: NodeRef, length: number): this {
|
|
819
|
+
return produce(this, (draft) => {
|
|
820
|
+
const n = draft.getNode(node.number) as Node;
|
|
821
|
+
n.length = length;
|
|
822
|
+
draft._data.hasLengths = true;
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
deleteLength(node: NodeRef): this {
|
|
826
|
+
return produce(this, (draft) => {
|
|
827
|
+
const n = draft.getNode(node.number) as Node;
|
|
828
|
+
n.length = undefined;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
// can only be called once heights are known.
|
|
832
|
+
setDivergence(node: NodeRef, divergence: number): this {
|
|
833
|
+
if (!this.hasLengths()) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
"Can not set the divergences of nodes in a tree without branch lengths",
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return produce(this, (draft) => {
|
|
839
|
+
const n = draft.getNode(node.number) as Node;
|
|
840
|
+
const height = draft.getHeight(node);
|
|
841
|
+
const rootHeight = draft.getHeight(draft.getRoot());
|
|
842
|
+
const currentDivergence = rootHeight - height;
|
|
843
|
+
const change = currentDivergence - divergence; // a negative change increases the length
|
|
844
|
+
const l = draft.getLength(node);
|
|
845
|
+
n.length = l - change;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
setLabel(node: NodeRef, label: string): this {
|
|
850
|
+
if (
|
|
851
|
+
(this._data.nodes.byLabel[label] as Undefinable<number>) !== undefined
|
|
852
|
+
) {
|
|
853
|
+
throw new Error(`Duplicate node label ${label}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return produce(this, (draft) => {
|
|
857
|
+
const n = draft.getNode(node.number) as Node;
|
|
858
|
+
n.label = label;
|
|
859
|
+
draft._data.nodes.byLabel[label] = node.number;
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
insertNode(n: NodeRef, proportion: number = 0.5): this {
|
|
864
|
+
return produce(this, (draft) => {
|
|
865
|
+
const newNode: Node = {
|
|
866
|
+
number: draft._data.nodes.allNodes.length,
|
|
867
|
+
children: [],
|
|
868
|
+
parent: undefined,
|
|
869
|
+
label: "",
|
|
870
|
+
length: undefined,
|
|
871
|
+
taxon: undefined,
|
|
872
|
+
annotations: {}, // todo copy annotations from root
|
|
873
|
+
_id: uuidv4(),
|
|
874
|
+
};
|
|
875
|
+
draft._data.nodes.allNodes.push(newNode);
|
|
876
|
+
draft._data.nodes.byTaxon.length += 1;
|
|
877
|
+
// insert halfway on first child's branch.
|
|
878
|
+
|
|
879
|
+
const node = draft.getNode(n.number) as Node;
|
|
880
|
+
const parentNode = draft.getParent(node) as Node;
|
|
881
|
+
|
|
882
|
+
const index = parentNode.children.indexOf(node.number);
|
|
883
|
+
|
|
884
|
+
parentNode.children.splice(index, 1, newNode.number);
|
|
885
|
+
newNode.parent = parentNode.number;
|
|
886
|
+
const l = draft.getLength(node);
|
|
887
|
+
const oldLength = l;
|
|
888
|
+
node.length = oldLength * (1 - proportion);
|
|
889
|
+
newNode.length = oldLength * proportion;
|
|
890
|
+
newNode.children = [node.number];
|
|
891
|
+
|
|
892
|
+
node.parent = newNode.number;
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
896
|
+
unroot(_n: NodeRef): ImmutableTree {
|
|
897
|
+
throw new Error("unroot not implemented in immutable tree");
|
|
898
|
+
}
|
|
899
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
900
|
+
deleteNode(_n: NodeRef): ImmutableTree {
|
|
901
|
+
throw new Error("deleteNode not implemented in immutable tree");
|
|
902
|
+
}
|
|
903
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
904
|
+
deleteClade(_n: NodeRef): ImmutableTree {
|
|
905
|
+
throw new Error("deleteClade not implemented in immutable tree");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
orderNodesByDensity(down: boolean, node?: NodeRef): this {
|
|
909
|
+
return produce(this, (draft) => {
|
|
910
|
+
if (node === undefined) {
|
|
911
|
+
node = draft._data.nodes.allNodes[draft._data.rootNode];
|
|
912
|
+
}
|
|
913
|
+
const factor = down ? 1 : -1;
|
|
914
|
+
orderNodes(draft._data, node, (_nodeA, countA, _nodeB, countB) => {
|
|
915
|
+
return (countA - countB) * factor;
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
rotate(nodeRef: NodeRef, recursive: boolean = false): this {
|
|
921
|
+
return produce(this, (draft) => {
|
|
922
|
+
const node = draft.getNode(nodeRef.number) as Node;
|
|
923
|
+
node.children = node.children.reverse();
|
|
924
|
+
if (recursive) {
|
|
925
|
+
for (const child of node.children.map((n) => draft.getNode(n))) {
|
|
926
|
+
draft.rotate(child, recursive);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
//TODO handle lengthless tree
|
|
932
|
+
reroot(node: NodeRef, proportion: number = 0.5): this {
|
|
933
|
+
return produce(this, (draft) => {
|
|
934
|
+
if (node.number === draft._data.rootNode) {
|
|
935
|
+
// the node is the root - nothing to do
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const rootNode = draft.getRoot() as Node;
|
|
939
|
+
if (rootNode.children.length !== 2) {
|
|
940
|
+
console.warn(
|
|
941
|
+
"Root node has more than two children and we are rerooting! There be dragons!",
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
let rootLength = 0;
|
|
945
|
+
|
|
946
|
+
if (rootNode.children.length == 2) {
|
|
947
|
+
// just sum them.
|
|
948
|
+
rootLength = rootNode.children
|
|
949
|
+
.map((n) => draft.getNode(n))
|
|
950
|
+
.map((n) => draft.getLength(n))
|
|
951
|
+
.reduce((acc, l) => l + acc, 0);
|
|
952
|
+
} else {
|
|
953
|
+
// we need the length of the incoming root branch
|
|
954
|
+
const pathToRoot = [...draft.getPathToRoot(node)];
|
|
955
|
+
|
|
956
|
+
const rootChild = pathToRoot[pathToRoot.length - 2]; // last one is the root
|
|
957
|
+
notNull(rootChild, `Index error when looking for the root child`);
|
|
958
|
+
if (!rootNode.children.includes(rootChild.number)) {
|
|
959
|
+
throw new Error(
|
|
960
|
+
"Root child not in path to root - likely an internal error",
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
rootLength = draft.getLength(rootChild);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const treeNode = draft.getNode(node.number) as Node;
|
|
967
|
+
if (draft.getParent(node) !== rootNode) {
|
|
968
|
+
// the node is not a child of the existing root so the root is actually changing
|
|
969
|
+
|
|
970
|
+
let node0 = treeNode;
|
|
971
|
+
let parent = draft.getParent(treeNode) as Node;
|
|
972
|
+
|
|
973
|
+
// was the node the first child in the parent's children?
|
|
974
|
+
const nodeAtTop = draft.getChild(parent, 0).number === node.number;
|
|
975
|
+
|
|
976
|
+
const rootChild1 = treeNode;
|
|
977
|
+
const rootChild2 = parent;
|
|
978
|
+
|
|
979
|
+
let oldLength = draft.getLength(parent);
|
|
980
|
+
|
|
981
|
+
while (!draft.isRoot(parent)) {
|
|
982
|
+
// remove the node that will becoming the parent from the children
|
|
983
|
+
|
|
984
|
+
parent.children = parent.children.filter((n) => n !== node0.number);
|
|
985
|
+
|
|
986
|
+
if (draft.getParent(parent).number === rootNode.number) {
|
|
987
|
+
// at the root
|
|
988
|
+
if (rootNode.children.length == 2) {
|
|
989
|
+
if (
|
|
990
|
+
!draft.hasLeftSibling(parent) &&
|
|
991
|
+
!draft.hasRightSibling(parent)
|
|
992
|
+
) {
|
|
993
|
+
throw new Error("no sibling in rerooting");
|
|
994
|
+
}
|
|
995
|
+
const ls = draft.hasLeftSibling(parent)
|
|
996
|
+
? draft.getLeftSibling(parent)
|
|
997
|
+
: draft.getRightSibling(parent);
|
|
998
|
+
// const ls = draft.getLeftSibling(parent)
|
|
999
|
+
const sibling = ls as Node;
|
|
1000
|
+
parent.children.push(sibling.number);
|
|
1001
|
+
sibling.parent = parent.number;
|
|
1002
|
+
sibling.length = rootLength;
|
|
1003
|
+
} else {
|
|
1004
|
+
// need to add a new node here.
|
|
1005
|
+
const newNode: Node = {
|
|
1006
|
+
number: draft._data.nodes.allNodes.length,
|
|
1007
|
+
children: [],
|
|
1008
|
+
parent: undefined,
|
|
1009
|
+
label: "",
|
|
1010
|
+
length: undefined,
|
|
1011
|
+
taxon: undefined,
|
|
1012
|
+
annotations: {},
|
|
1013
|
+
_id: uuidv4(),
|
|
1014
|
+
};
|
|
1015
|
+
draft._data.nodes.allNodes.push(newNode);
|
|
1016
|
+
newNode.length = rootLength;
|
|
1017
|
+
parent.children.push(newNode.number);
|
|
1018
|
+
newNode.parent = parent.number;
|
|
1019
|
+
for (const childNumber of rootNode.children) {
|
|
1020
|
+
const child = draft.getNode(childNumber) as Node;
|
|
1021
|
+
if (child.number !== parent.number) {
|
|
1022
|
+
child.parent = newNode.number;
|
|
1023
|
+
newNode.children.push(child.number);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} else {
|
|
1028
|
+
// swap the parent and parent's parent's length around
|
|
1029
|
+
const nan = draft.getParent(parent) as Node; // your mammy's mammy
|
|
1030
|
+
const nanLength = draft.getLength(nan);
|
|
1031
|
+
nan.length = oldLength;
|
|
1032
|
+
oldLength = nanLength;
|
|
1033
|
+
|
|
1034
|
+
//
|
|
1035
|
+
|
|
1036
|
+
// add the new child don't update the parent yet - need for loop.
|
|
1037
|
+
// nan.parent = parent.number;
|
|
1038
|
+
parent.children.push(nan.number);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
node0 = parent;
|
|
1042
|
+
|
|
1043
|
+
parent = draft.getParent(parent) as Node;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Reuse the root node as root...
|
|
1047
|
+
|
|
1048
|
+
// Set the order of the children to be the same as for the original parent of the node.
|
|
1049
|
+
// This makes for a more visually consistent rerooting graphically.
|
|
1050
|
+
rootChild1.parent = rootNode.number;
|
|
1051
|
+
rootChild2.parent = rootNode.number;
|
|
1052
|
+
rootNode.children = [rootChild1.number, rootChild2.number];
|
|
1053
|
+
|
|
1054
|
+
if (!nodeAtTop) {
|
|
1055
|
+
rootNode.children = rootNode.children.reverse();
|
|
1056
|
+
}
|
|
1057
|
+
// connect all the children to their parents which we put off above
|
|
1058
|
+
this.getInternalNodes().forEach((node) => {
|
|
1059
|
+
for (const child of draft.getChildren(node) as Node[]) {
|
|
1060
|
+
child.parent = node.number;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const l = draft.getLength(rootChild1) * proportion;
|
|
1065
|
+
rootChild2.length = l;
|
|
1066
|
+
notNull(
|
|
1067
|
+
rootChild1.length,
|
|
1068
|
+
`Expected the root's new child to have a length`,
|
|
1069
|
+
);
|
|
1070
|
+
rootChild1.length -= l;
|
|
1071
|
+
} else {
|
|
1072
|
+
// the root is staying the same, just the position of the root changing
|
|
1073
|
+
const l = draft.getLength(node) * (1.0 - proportion);
|
|
1074
|
+
treeNode.length = l;
|
|
1075
|
+
const sibling = draft.getNextSibling(node) as Node;
|
|
1076
|
+
sibling.length = rootLength - l;
|
|
1077
|
+
}
|
|
1078
|
+
// todo reset heights.
|
|
1079
|
+
// traverse and get max height;
|
|
1080
|
+
// set all heights to max height - divergence
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
removeChild(parent: NodeRef, child: NodeRef): this {
|
|
1084
|
+
return produce(this, (draft) => {
|
|
1085
|
+
draft._data.nodes.allNodes[parent.number].children =
|
|
1086
|
+
draft._data.nodes.allNodes[parent.number].children.filter(
|
|
1087
|
+
(n) => n !== child.number,
|
|
1088
|
+
);
|
|
1089
|
+
draft._data.nodes.allNodes[child.number].parent = -1;
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
sortChildren(
|
|
1093
|
+
node: NodeRef,
|
|
1094
|
+
compare: (a: NodeRef, b: NodeRef) => number,
|
|
1095
|
+
): this {
|
|
1096
|
+
return produce(this, (draft) => {
|
|
1097
|
+
draft._data.nodes.allNodes[node.number].children =
|
|
1098
|
+
this._data.nodes.allNodes[node.number].children
|
|
1099
|
+
.map((n) => draft.getNode(n))
|
|
1100
|
+
.sort(compare)
|
|
1101
|
+
.map((n) => n.number);
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
addChild(parent: NodeRef, child: NodeRef): this {
|
|
1106
|
+
return produce(this, (draft) => {
|
|
1107
|
+
const c = draft.getNode(child.number) as Node;
|
|
1108
|
+
const p = draft.getNode(parent.number) as Node;
|
|
1109
|
+
p.children.push(c.number);
|
|
1110
|
+
c.parent = parent.number;
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
setRoot(node: NodeRef): this {
|
|
1115
|
+
return produce(this, (draft) => {
|
|
1116
|
+
draft._data.rootNode = node.number;
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* A private recursive function that rotates nodes to give an ordering provided
|
|
1123
|
+
* by a function.
|
|
1124
|
+
* @param node
|
|
1125
|
+
* @param ordering function that takes (a,number of tips form a, b, number of tips from b) and sorts a and be by the output.
|
|
1126
|
+
* @param callback an optional callback that is called each rotate
|
|
1127
|
+
* @returns {number}
|
|
1128
|
+
*/
|
|
1129
|
+
//I don't think this needs to update to the root because it is a preorder traversal or is it postorder anyway it's a traversal that probably works
|
|
1130
|
+
function orderNodes(
|
|
1131
|
+
treeData: ImmutableTreeData,
|
|
1132
|
+
node: NodeRef,
|
|
1133
|
+
ordering: (
|
|
1134
|
+
a: NodeRef,
|
|
1135
|
+
numberOfATips: number,
|
|
1136
|
+
b: NodeRef,
|
|
1137
|
+
numberOfBTips: number,
|
|
1138
|
+
) => number,
|
|
1139
|
+
): number {
|
|
1140
|
+
let count = 0;
|
|
1141
|
+
if (treeData.nodes.allNodes[node.number].children.length > 0) {
|
|
1142
|
+
// count the number of descendents for each child
|
|
1143
|
+
const counts = new Map<number, number>();
|
|
1144
|
+
for (const child of treeData.nodes.allNodes[node.number].children.map(
|
|
1145
|
+
(n) => treeData.nodes.allNodes[n],
|
|
1146
|
+
)) {
|
|
1147
|
+
const value = orderNodes(treeData, child, ordering);
|
|
1148
|
+
counts.set(child.number, value);
|
|
1149
|
+
count += value;
|
|
1150
|
+
}
|
|
1151
|
+
// sort the children using the provided function
|
|
1152
|
+
const reorderedChildren = treeData.nodes.allNodes[node.number].children
|
|
1153
|
+
.slice()
|
|
1154
|
+
.sort((a, b) => {
|
|
1155
|
+
// sort updates the array in place so changed is always false
|
|
1156
|
+
return ordering(
|
|
1157
|
+
treeData.nodes.allNodes[a],
|
|
1158
|
+
unNullify(
|
|
1159
|
+
counts.get(a),
|
|
1160
|
+
`Internal error when ordering. Counts not defined.`,
|
|
1161
|
+
),
|
|
1162
|
+
treeData.nodes.allNodes[b],
|
|
1163
|
+
unNullify(
|
|
1164
|
+
counts.get(b),
|
|
1165
|
+
`Internal error when ordering. Counts not defined.`,
|
|
1166
|
+
),
|
|
1167
|
+
);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const changed = reorderedChildren.reduce(
|
|
1171
|
+
(acc: boolean, n: number, i: number) =>
|
|
1172
|
+
acc || n !== treeData.nodes.allNodes[node.number].children[i],
|
|
1173
|
+
true,
|
|
1174
|
+
);
|
|
1175
|
+
if (changed) {
|
|
1176
|
+
treeData.nodes.allNodes[node.number].children = reorderedChildren;
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
count = 1;
|
|
1180
|
+
}
|
|
1181
|
+
return count;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export function* preOrderIterator(
|
|
1185
|
+
tree: Tree,
|
|
1186
|
+
node: NodeRef | undefined = undefined,
|
|
1187
|
+
): Generator<NodeRef> {
|
|
1188
|
+
const traverse = function* (node: NodeRef): Generator<NodeRef> {
|
|
1189
|
+
yield tree.getNode(node.number); // get from tree so we keep proxy when used in draft
|
|
1190
|
+
const childCount = tree.getChildCount(node);
|
|
1191
|
+
if (childCount > 0) {
|
|
1192
|
+
for (let i = 0; i < childCount; i++) {
|
|
1193
|
+
const child = tree.getChild(node, i);
|
|
1194
|
+
yield* traverse(child);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
if (node === undefined) {
|
|
1199
|
+
node = tree.getRoot();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
yield* traverse(node);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
//TODO return the node and edge length of direction
|
|
1206
|
+
export function* psuedoRootPreOrderIterator(
|
|
1207
|
+
tree: Tree,
|
|
1208
|
+
node: NodeRef | undefined = undefined,
|
|
1209
|
+
sort: (a: NodeRef, b: NodeRef) => number = (a, b) => a.number - b.number,
|
|
1210
|
+
): Generator<NodeRef> {
|
|
1211
|
+
const traverse = function* (
|
|
1212
|
+
node: NodeRef,
|
|
1213
|
+
visited: number | undefined = undefined,
|
|
1214
|
+
): Generator<NodeRef> {
|
|
1215
|
+
yield tree.getNode(node.number); // get from tree so we keep proxy when used in draft
|
|
1216
|
+
const branches = [...tree.getChildren(node), tree.getParent(node)].filter(
|
|
1217
|
+
(n) => n.number !== visited,
|
|
1218
|
+
); //
|
|
1219
|
+
branches.sort(sort);
|
|
1220
|
+
for (const branch of branches) {
|
|
1221
|
+
yield* traverse(branch, node.number);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
if (node === undefined) {
|
|
1226
|
+
node = tree.getRoot();
|
|
1227
|
+
}
|
|
1228
|
+
yield* traverse(node);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function* psuedoRootPostOrderIterator(
|
|
1232
|
+
tree: Tree,
|
|
1233
|
+
node: NodeRef | undefined = undefined,
|
|
1234
|
+
sort: (a: NodeRef, b: NodeRef) => number = (a, b) => a.number - b.number,
|
|
1235
|
+
): Generator<NodeRef> {
|
|
1236
|
+
const traverse = function* (
|
|
1237
|
+
node: NodeRef,
|
|
1238
|
+
visited: number | undefined = undefined,
|
|
1239
|
+
): Generator<NodeRef> {
|
|
1240
|
+
// get from tree so we keep proxy when used in draft
|
|
1241
|
+
const branches = [...tree.getChildren(node), tree.getParent(node)].filter(
|
|
1242
|
+
(n) => n.number !== visited,
|
|
1243
|
+
);
|
|
1244
|
+
branches.sort(sort);
|
|
1245
|
+
for (const branch of branches) {
|
|
1246
|
+
yield* traverse(branch, node.number);
|
|
1247
|
+
}
|
|
1248
|
+
yield tree.getNode(node.number);
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
if (node === undefined) {
|
|
1252
|
+
node = tree.getRoot();
|
|
1253
|
+
}
|
|
1254
|
+
yield* traverse(node);
|
|
1255
|
+
}
|
|
1256
|
+
export function* postOrderIterator(
|
|
1257
|
+
tree: Tree,
|
|
1258
|
+
node: NodeRef | undefined = undefined,
|
|
1259
|
+
): Generator<NodeRef> {
|
|
1260
|
+
const traverse = function* (node: NodeRef): Generator<NodeRef> {
|
|
1261
|
+
const childCount = tree.getChildCount(node);
|
|
1262
|
+
if (childCount > 0) {
|
|
1263
|
+
for (let i = 0; i < childCount; i++) {
|
|
1264
|
+
const child = tree.getChild(node, i);
|
|
1265
|
+
yield* traverse(child);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
yield node;
|
|
1269
|
+
};
|
|
1270
|
+
if (node === undefined) {
|
|
1271
|
+
node = tree.getRoot();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
yield* traverse(node);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
export function* tipIterator(tree: Tree, node?: NodeRef): Generator<NodeRef> {
|
|
1278
|
+
if (node === undefined) {
|
|
1279
|
+
node = tree.getRoot();
|
|
1280
|
+
}
|
|
1281
|
+
const traverse = function* (node: NodeRef): Generator<NodeRef> {
|
|
1282
|
+
const childCount = tree.getChildCount(node);
|
|
1283
|
+
if (childCount > 0) {
|
|
1284
|
+
for (let i = 0; i < childCount; i++) {
|
|
1285
|
+
const child = tree.getChild(node, i);
|
|
1286
|
+
yield* traverse(child);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
yield node;
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
yield* traverse(node);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
export function* pathToRootIterator(
|
|
1296
|
+
tree: Tree,
|
|
1297
|
+
node: NodeRef,
|
|
1298
|
+
): Generator<NodeRef> {
|
|
1299
|
+
let n: NodeRef = node;
|
|
1300
|
+
while (!tree.isRoot(n)) {
|
|
1301
|
+
yield n;
|
|
1302
|
+
n = tree.getParent(n);
|
|
1303
|
+
}
|
|
1304
|
+
yield n; // gibe the root at the end
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function updateDomain(
|
|
1308
|
+
annotation: { type: BaseAnnotationType; value: AnnotationValue },
|
|
1309
|
+
currentDomain: AnnotationDomain | undefined,
|
|
1310
|
+
): AnnotationDomain {
|
|
1311
|
+
switch (annotation.type) {
|
|
1312
|
+
case BaseAnnotationType.BOOLEAN:
|
|
1313
|
+
return [true, false];
|
|
1314
|
+
|
|
1315
|
+
case BaseAnnotationType.DISCRETE: {
|
|
1316
|
+
const value = annotation.value as string;
|
|
1317
|
+
if (currentDomain !== undefined) {
|
|
1318
|
+
const curr = currentDomain as DomainOf<BaseAnnotationType.DISCRETE>;
|
|
1319
|
+
return [...new Set([...curr, value])].sort();
|
|
1320
|
+
} else {
|
|
1321
|
+
return [value];
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
case BaseAnnotationType.NUMERICAL: {
|
|
1325
|
+
const value = annotation.value as number;
|
|
1326
|
+
const curr = currentDomain ? (currentDomain as [number, number]) : [];
|
|
1327
|
+
return extent([...curr, value]) as [number, number];
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
case BaseAnnotationType.DISCRETE_SET: {
|
|
1331
|
+
const value = annotation.value as string[];
|
|
1332
|
+
const curr = currentDomain ? (currentDomain as string[]) : [];
|
|
1333
|
+
return [...new Set([...curr, ...value])].sort();
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
case BaseAnnotationType.NUMERICAL_SET: {
|
|
1337
|
+
const value = annotation.value as number[];
|
|
1338
|
+
const curr = currentDomain ? (currentDomain as [number, number]) : [];
|
|
1339
|
+
return extent([...curr, ...value]) as [number, number];
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
case BaseAnnotationType.DENSITIES: {
|
|
1343
|
+
if (currentDomain !== undefined) {
|
|
1344
|
+
const curr = currentDomain as DomainOf<BaseAnnotationType.DENSITIES>;
|
|
1345
|
+
return [...new Set([...curr, ...Object.keys(annotation.value)])].sort();
|
|
1346
|
+
} else {
|
|
1347
|
+
return [...new Set(Object.keys(annotation.value))]
|
|
1348
|
+
.sort()
|
|
1349
|
+
.filter((d) => d);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
case BaseAnnotationType.MARKOV_JUMPS: {
|
|
1354
|
+
const value = (annotation.value as MarkovJumpValue[]).reduce<string[]>(
|
|
1355
|
+
(acc: string[], d: MarkovJumpValue) => acc.concat([d.to, d.from]),
|
|
1356
|
+
[],
|
|
1357
|
+
);
|
|
1358
|
+
const curr = currentDomain ? (currentDomain as string[]) : [];
|
|
1359
|
+
return [...new Set([...curr, ...value])].sort();
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
default:
|
|
1363
|
+
throw new Error(`Unrecognized type when updating domain`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|