@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,289 @@
|
|
|
1
|
+
import { u } from "../../../utils";
|
|
2
|
+
import { ImmutableTree } from "../normalized-tree/immutable-tree";
|
|
3
|
+
import { processAnnotationValue } from "./annotation-parser";
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
describe("Test annotation parsing", () => {
|
|
6
|
+
it("integer", function () {
|
|
7
|
+
const annotation = processAnnotationValue("1");
|
|
8
|
+
expect(annotation).toEqual({ type: "NUMERICAL", value: 1 });
|
|
9
|
+
});
|
|
10
|
+
it("integer array", function () {
|
|
11
|
+
const annotation = processAnnotationValue(["1", "2", "3"]);
|
|
12
|
+
expect(annotation).toEqual({ type: "NUMERICAL_SET", value: [1, 2, 3] });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("continuous array", function () {
|
|
16
|
+
const annotation = processAnnotationValue(["1", "2.5", "3"]);
|
|
17
|
+
expect(annotation).toEqual({ type: "NUMERICAL_SET", value: [1, 2.5, 3] });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("continuous range", function () {
|
|
21
|
+
const annotation = processAnnotationValue([0.0, 1]);
|
|
22
|
+
expect(annotation).toEqual({ type: "NUMERICAL_SET", value: [0, 1] });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("continuous", function () {
|
|
26
|
+
const annotation = processAnnotationValue("1.3");
|
|
27
|
+
expect(annotation).toEqual({ type: "NUMERICAL", value: 1.3 });
|
|
28
|
+
});
|
|
29
|
+
it("discrete", function () {
|
|
30
|
+
const annotation = processAnnotationValue("a");
|
|
31
|
+
expect(annotation).toEqual({ type: "DISCRETE", value: "a" });
|
|
32
|
+
});
|
|
33
|
+
it("discrete array", function () {
|
|
34
|
+
const annotation = processAnnotationValue(["a", "b", "c"]);
|
|
35
|
+
expect(annotation).toEqual({
|
|
36
|
+
type: "DISCRETE_SET",
|
|
37
|
+
value: ["a", "b", "c"],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("markov jump", function () {
|
|
41
|
+
const annotation = processAnnotationValue([["0.1", "U", "me"]]);
|
|
42
|
+
expect(annotation).toEqual({
|
|
43
|
+
type: "MARKOV_JUMPS",
|
|
44
|
+
value: [{ time: 0.1, from: "U", to: "me" }],
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it("markov jump array", function () {
|
|
48
|
+
const annotation = processAnnotationValue([
|
|
49
|
+
["0.1", "U", "me"],
|
|
50
|
+
["0.2", "me", "U"],
|
|
51
|
+
]);
|
|
52
|
+
expect(annotation).toEqual({
|
|
53
|
+
type: "MARKOV_JUMPS",
|
|
54
|
+
value: [
|
|
55
|
+
{ time: 0.1, from: "U", to: "me" },
|
|
56
|
+
{ time: 0.2, from: "me", to: "U" },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it("probabilities", function () {
|
|
61
|
+
const annotation = processAnnotationValue({ HERE: 0.3, THERE: 0.7 });
|
|
62
|
+
expect(annotation).toEqual({
|
|
63
|
+
type: "DENSITIES",
|
|
64
|
+
value: { HERE: 0.3, THERE: 0.7 },
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Test tree parsing and normalized Tree", () => {
|
|
70
|
+
it("simpleParse", function () {
|
|
71
|
+
const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
|
|
72
|
+
|
|
73
|
+
const tree = ImmutableTree.fromNewick(newickString, {
|
|
74
|
+
parseAnnotations: false,
|
|
75
|
+
labelName: "probability",
|
|
76
|
+
});
|
|
77
|
+
expect(tree.toNewick()).toEqual(newickString);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("height", function () {
|
|
81
|
+
const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
|
|
82
|
+
const tree = ImmutableTree.fromNewick(newickString, { labelName: "prob" });
|
|
83
|
+
const virus1 = tree.getTaxonByName("virus1");
|
|
84
|
+
const virus1Node = tree.getNodeByTaxon(virus1);
|
|
85
|
+
expect(tree.getHeight(virus1Node)).toBeCloseTo(0.06, 1e-6);
|
|
86
|
+
});
|
|
87
|
+
it("divergence", function () {
|
|
88
|
+
const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
|
|
89
|
+
const tree = ImmutableTree.fromNewick(newickString, { labelName: "prob" });
|
|
90
|
+
const virus6 = tree.getTaxonByName("virus6");
|
|
91
|
+
const virus6Node = tree.getNodeByTaxon(virus6);
|
|
92
|
+
expect(tree.getDivergence(virus6Node)).toBeCloseTo(0.67, 1e-6);
|
|
93
|
+
});
|
|
94
|
+
it("general_parse", function () {
|
|
95
|
+
const tree = ImmutableTree.fromNewick("(a:1,b:4)#l;");
|
|
96
|
+
const root = tree.getRoot();
|
|
97
|
+
const label = tree.getLabel(root);
|
|
98
|
+
expect(label).toEqual("l");
|
|
99
|
+
|
|
100
|
+
const r = tree.getNodeByLabel("l");
|
|
101
|
+
expect(r).toEqual(root);
|
|
102
|
+
|
|
103
|
+
const names = [];
|
|
104
|
+
const bl = [];
|
|
105
|
+
|
|
106
|
+
const count = tree.getChildCount(root);
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < count; i++) {
|
|
109
|
+
const child = tree.getChild(root, i);
|
|
110
|
+
names.push(tree.getTaxonFromNode(child).name);
|
|
111
|
+
bl.push(tree.getLength(child));
|
|
112
|
+
}
|
|
113
|
+
expect(names).toEqual(["a", "b"]);
|
|
114
|
+
|
|
115
|
+
expect(bl).toEqual([1, 4]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("scientific notation", function () {
|
|
119
|
+
const tree = ImmutableTree.fromNewick("(a:1E1,b:2e-5);");
|
|
120
|
+
const root = tree.getRoot();
|
|
121
|
+
const bl = [];
|
|
122
|
+
const count = tree.getChildCount(root);
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < count; i++) {
|
|
125
|
+
const child = tree.getChild(root, i);
|
|
126
|
+
bl.push(tree.getLength(child));
|
|
127
|
+
}
|
|
128
|
+
expect(bl[0]).toBeCloseTo(10.0, 1e-6);
|
|
129
|
+
expect(bl[1]).toBeCloseTo(0.00002, 1e-6);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("quoted taxa", function () {
|
|
133
|
+
const tree = ImmutableTree.fromNewick("('234] ':1,'here a *':1);");
|
|
134
|
+
const names = [...tree.getExternalNodes()].map(
|
|
135
|
+
(node) => tree.getTaxonFromNode(node).name,
|
|
136
|
+
);
|
|
137
|
+
expect(names).toEqual(["234]", "here a *"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("whitespace", function () {
|
|
141
|
+
const tree = ImmutableTree.fromNewick(" (a,b:1);\t");
|
|
142
|
+
expect(tree.toNewick()).toEqual("(a,b:1);");
|
|
143
|
+
});
|
|
144
|
+
it("node id", function () {
|
|
145
|
+
const tree = ImmutableTree.fromNewick("((A,T)#Node_1:1,(a,b:1));");
|
|
146
|
+
const A = tree.getTaxonByName("A");
|
|
147
|
+
const B = tree.getTaxonByName("b");
|
|
148
|
+
const Anode = tree.getNodeByTaxon(A);
|
|
149
|
+
const node1 = tree.getNodeByLabel("Node_1");
|
|
150
|
+
const parent = tree.getParent(Anode);
|
|
151
|
+
|
|
152
|
+
expect(parent).toEqual(node1);
|
|
153
|
+
|
|
154
|
+
expect(tree.getLength(u(tree.getNodeByTaxon(B)))).toEqual(1);
|
|
155
|
+
expect(tree.toNewick()).toEqual("((A,T)#Node_1:1,(a,b:1));");
|
|
156
|
+
});
|
|
157
|
+
it("root length and label", function () {
|
|
158
|
+
const tree = ImmutableTree.fromNewick("((A,T)#Node_1:1,(a,b:1))#root:0.1;");
|
|
159
|
+
const root = tree.getRoot();
|
|
160
|
+
const rootLength = tree.getLength(root);
|
|
161
|
+
expect(rootLength).toEqual(0.1);
|
|
162
|
+
const label = tree.getLabel(root);
|
|
163
|
+
expect(label).toEqual("root");
|
|
164
|
+
expect(tree.toNewick()).toEqual("((A,T)#Node_1:1,(a,b:1))#root:0.1;");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("fail no ;", function () {
|
|
168
|
+
expect(() => ImmutableTree.fromNewick("('234] ','here a *')")).toThrow(
|
|
169
|
+
"expecting a semi-colon at the end of the newick string",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("fail unbalanced )", function () {
|
|
174
|
+
expect(() => ImmutableTree.fromNewick("(a,b));")).toThrow(
|
|
175
|
+
"the brackets in the newick file are not balanced: too many closed",
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("fail unbalanced (", function () {
|
|
180
|
+
expect(() => ImmutableTree.fromNewick("((a,b);")).toThrow(
|
|
181
|
+
"unexpected semi-colon in tree did not reach the root yet",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("comment", function () {
|
|
186
|
+
const tree = ImmutableTree.fromNewick("(a[&test=ok],b:1);", {
|
|
187
|
+
parseAnnotations: true,
|
|
188
|
+
});
|
|
189
|
+
const a = tree.getTaxonByName("a");
|
|
190
|
+
const aNode = tree.getNodeByTaxon(a);
|
|
191
|
+
const testAnnotation = tree.getAnnotation(aNode, "test");
|
|
192
|
+
expect(testAnnotation).toEqual("ok");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("markov jump comment", function () {
|
|
196
|
+
const tree = ImmutableTree.fromNewick(
|
|
197
|
+
"(a[&test=ok],b[&jump={{0.1,U,me}}]);",
|
|
198
|
+
{ parseAnnotations: true },
|
|
199
|
+
);
|
|
200
|
+
const a = tree.getNodeByTaxon(tree.getTaxonByName("a"));
|
|
201
|
+
const b = tree.getNodeByTaxon(tree.getTaxonByName("b"));
|
|
202
|
+
const testAnnotation = tree.getAnnotation(a, "test");
|
|
203
|
+
expect(testAnnotation).toEqual("ok");
|
|
204
|
+
const jumpAnnotation = tree.getAnnotation(b, "jump");
|
|
205
|
+
expect(jumpAnnotation).toEqual([{ time: 0.1, from: "U", to: "me" }]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("double comment", function () {
|
|
209
|
+
const tree = ImmutableTree.fromNewick("(a[&test=ok,other test = 1],b:1);", {
|
|
210
|
+
parseAnnotations: true,
|
|
211
|
+
});
|
|
212
|
+
const a = tree.getNodeByTaxon(tree.getTaxonByName("a"));
|
|
213
|
+
const testAnnotation = tree.getFullNodeAnnotation(a, "test");
|
|
214
|
+
|
|
215
|
+
expect(testAnnotation).toEqual({
|
|
216
|
+
id: "test",
|
|
217
|
+
type: "DISCRETE",
|
|
218
|
+
value: "ok",
|
|
219
|
+
});
|
|
220
|
+
const otherTestAnnotation = tree.getFullNodeAnnotation(a, "other test");
|
|
221
|
+
expect(otherTestAnnotation).toEqual({
|
|
222
|
+
id: "other test",
|
|
223
|
+
type: "NUMERICAL",
|
|
224
|
+
value: 1,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("SET comment", function () {
|
|
229
|
+
const tree = ImmutableTree.fromNewick(
|
|
230
|
+
"(a[&test={ok,Not ok},other test = {0,1}],b:1);",
|
|
231
|
+
{
|
|
232
|
+
parseAnnotations: true,
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
const a = tree.getNodeByTaxon(tree.getTaxonByName("a"));
|
|
236
|
+
const testAnnotation = tree.getFullNodeAnnotation(a, "test");
|
|
237
|
+
expect(testAnnotation).toEqual({
|
|
238
|
+
id: "test",
|
|
239
|
+
type: "DISCRETE_SET",
|
|
240
|
+
value: ["ok", "Not ok"],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const otestAnnotation = tree.getFullNodeAnnotation(a, "other test");
|
|
244
|
+
expect(otestAnnotation).toEqual({
|
|
245
|
+
id: "other test",
|
|
246
|
+
type: "NUMERICAL_SET",
|
|
247
|
+
value: [0, 1],
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("SET comment writing", function () {
|
|
252
|
+
const treeString = "(a[&test={ok, Not ok}, other test={0, 1}],b:1);";
|
|
253
|
+
const tree = ImmutableTree.fromNewick(treeString, {
|
|
254
|
+
parseAnnotations: true,
|
|
255
|
+
});
|
|
256
|
+
expect(tree.toNewick(undefined, { includeAnnotations: true })).toEqual(
|
|
257
|
+
treeString,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
it("label annotation", function () {
|
|
261
|
+
const tree = ImmutableTree.fromNewick(
|
|
262
|
+
"((((((virus1:0.1,virus2:0.12)0.95:0.08,(virus3:0.011,virus4:0.0087)1.0:0.15)0.65:0.03,virus5:0.21)1.0:0.2,(virus6:0.45,virus7:0.4)0.51:0.02)1.0:0.1,virus8:0.4)1.0:0.1,(virus9:0.04,virus10:0.03)1.0:0.6);",
|
|
263
|
+
{ parseAnnotations: true, labelName: "probability" },
|
|
264
|
+
);
|
|
265
|
+
const virus1Node = tree.getNodeByTaxon(tree.getTaxonByName("virus1"));
|
|
266
|
+
|
|
267
|
+
const probability = tree.getAnnotation(
|
|
268
|
+
tree.getParent(virus1Node),
|
|
269
|
+
"probability",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
expect(probability).toEqual(0.95);
|
|
273
|
+
});
|
|
274
|
+
it("nexus", function () {
|
|
275
|
+
const nexusString = `#NEXUS
|
|
276
|
+
BEGIN TAXA;
|
|
277
|
+
DIMENSIONS NTAX=10;
|
|
278
|
+
TAXLABELS virus1 virus10 virus2 virus3 virus4 virus5 virus6 virus7 virus8 virus9;
|
|
279
|
+
END;
|
|
280
|
+
BEGIN TREES;
|
|
281
|
+
TREE tree0 = ((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);
|
|
282
|
+
END;
|
|
283
|
+
`;
|
|
284
|
+
const tree = ImmutableTree.fromNexus(nexusString);
|
|
285
|
+
const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
|
|
286
|
+
|
|
287
|
+
expect(tree.toNewick()).toEqual(newickString);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./nexus-importer";
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { ImmutableTree } from "../../NormalizedTree"
|
|
2
|
+
import { TaxonSet } from "../../Taxa/Taxon"
|
|
3
|
+
|
|
4
|
+
class newickImporter {
|
|
5
|
+
reader: ReadableStreamDefaultReader<string>
|
|
6
|
+
utf8Decoder: TextDecoder
|
|
7
|
+
taxonSet: TaxonSet | undefined
|
|
8
|
+
currentBlock: string | undefined
|
|
9
|
+
hasTree: boolean | undefined
|
|
10
|
+
options: { labelName?: string }
|
|
11
|
+
|
|
12
|
+
//TODO update the options here to just read in trees.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
stream: ReadableStream<Uint8Array>,
|
|
17
|
+
options: { labelName?: string } = {},
|
|
18
|
+
) {
|
|
19
|
+
this.utf8Decoder = new TextDecoder("utf-8")
|
|
20
|
+
const newickTransformer = new TransformStream(transformerOptions())
|
|
21
|
+
|
|
22
|
+
this.reader = stream
|
|
23
|
+
.pipeThrough(new TextDecoderStream())
|
|
24
|
+
.pipeThrough(newickTransformer)
|
|
25
|
+
.getReader()
|
|
26
|
+
|
|
27
|
+
this.taxonSet = undefined
|
|
28
|
+
this.currentBlock = undefined
|
|
29
|
+
this.options = options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async *getNextTree(taxa:TaxonSet): AsyncIterableIterator<ImmutableTree> {
|
|
33
|
+
yield* this.parseNextTree()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async parseNextBlock() {
|
|
37
|
+
const blockName = await this.getNextBlockName()
|
|
38
|
+
switch (blockName) {
|
|
39
|
+
case "taxa":
|
|
40
|
+
this.currentBlock = "taxa"
|
|
41
|
+
await this.parseTaxaBlock()
|
|
42
|
+
break
|
|
43
|
+
case "trees":
|
|
44
|
+
this.currentBlock = "trees"
|
|
45
|
+
break
|
|
46
|
+
default:
|
|
47
|
+
console.log(
|
|
48
|
+
`skipping block ${blockName}. Only parsing blocks are taxa and trees for now.`,
|
|
49
|
+
)
|
|
50
|
+
await this.readToEndOfBlock()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async nextToken() {
|
|
55
|
+
const { done, value } = await this.reader.read()
|
|
56
|
+
if (done) {
|
|
57
|
+
throw "unexpectedly hit the end of the stream"
|
|
58
|
+
}
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
//make skip and then a list of regex to skip
|
|
62
|
+
async skipSemiColon() {
|
|
63
|
+
const { done, value } = await this.reader.read()
|
|
64
|
+
if (done) {
|
|
65
|
+
throw "unexpectedly hit the end of the stream"
|
|
66
|
+
}
|
|
67
|
+
if (!value.match(/;$/)) {
|
|
68
|
+
throw `expected ";" got ${value}`
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getNextBlockName() {
|
|
73
|
+
while (true) {
|
|
74
|
+
const value = await this.nextToken()
|
|
75
|
+
|
|
76
|
+
if (value.match(/\bbegin/i)) {
|
|
77
|
+
const token = await this.nextToken()
|
|
78
|
+
this.skipSemiColon()
|
|
79
|
+
return token
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async readToEndOfBlock() {
|
|
85
|
+
while (true) {
|
|
86
|
+
const value = await this.nextToken()
|
|
87
|
+
if (value.match(/\bend;/i)) {
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getNextCommand(command: RegExp) {
|
|
94
|
+
while (true) {
|
|
95
|
+
const value = await this.nextToken()
|
|
96
|
+
if (value === ";") {
|
|
97
|
+
throw `Hit ; looking for ${command}`
|
|
98
|
+
}
|
|
99
|
+
if (command.test(value)) {
|
|
100
|
+
return value
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// skip until match and return match
|
|
106
|
+
async skipUntil(stopper: RegExp) {
|
|
107
|
+
while (true) {
|
|
108
|
+
const value = await this.nextToken()
|
|
109
|
+
if (stopper.test(value)) {
|
|
110
|
+
return value
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// read up to match return everything up to including the match
|
|
115
|
+
async readUntil(stopper: RegExp) {
|
|
116
|
+
let buffer = ""
|
|
117
|
+
while (true) {
|
|
118
|
+
const value = await this.nextToken()
|
|
119
|
+
if (stopper.test(value)) {
|
|
120
|
+
return buffer + value
|
|
121
|
+
}
|
|
122
|
+
buffer += value
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async parseTaxaBlock() {
|
|
126
|
+
let ntax
|
|
127
|
+
let command = await this.skipUntil(/dimensions|taxlabels|end/i)
|
|
128
|
+
while (true) {
|
|
129
|
+
switch (true) {
|
|
130
|
+
case /dimensions/i.test(command):
|
|
131
|
+
const taxaLine = await this.readUntil(/;/)
|
|
132
|
+
const ntaxa = taxaLine.match(/ntax=(\d+);/)
|
|
133
|
+
if (ntaxa) {
|
|
134
|
+
ntax = parseInt(ntaxa[1])
|
|
135
|
+
} else {
|
|
136
|
+
throw `Expected dimension in form of ntax=(\d+);. Got ${taxaLine}`
|
|
137
|
+
}
|
|
138
|
+
break
|
|
139
|
+
case /taxlabels/i.test(command):
|
|
140
|
+
let token = await this.nextToken()
|
|
141
|
+
this.taxonSet = new TaxonSet()
|
|
142
|
+
while (token !== ";") {
|
|
143
|
+
this.taxonSet.addTaxon(token)
|
|
144
|
+
token = await this.nextToken()
|
|
145
|
+
}
|
|
146
|
+
if (ntax) {
|
|
147
|
+
if (ntax != this.taxonSet.getTaxonCount()) {
|
|
148
|
+
throw `found ${this.taxonSet.getTaxonCount()} taxa. Expected: ${ntax}}`
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
break
|
|
152
|
+
case /end/i.test(command):
|
|
153
|
+
if (this.taxonSet!.getTaxonCount() === 0) {
|
|
154
|
+
throw "hit end of taxa section but didn't find any taxa"
|
|
155
|
+
}
|
|
156
|
+
this.skipSemiColon()
|
|
157
|
+
default:
|
|
158
|
+
throw `Reached impossible code looking for dimensions or taxlabels in taxa block "${command}"`
|
|
159
|
+
}
|
|
160
|
+
command = await this.skipUntil(/dimensions|taxlabels|end/i)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async *parseNextTree() {
|
|
165
|
+
let command = await this.skipUntil(/translate|tree|end/i)
|
|
166
|
+
let token
|
|
167
|
+
while (true) {
|
|
168
|
+
switch (true) {
|
|
169
|
+
case /translate/i.test(command):
|
|
170
|
+
// all white space removed by tranformStream so will be
|
|
171
|
+
// ['key','taxon,'] but may be ['key','taxon',','] if space tween taxa and ,
|
|
172
|
+
this.translateTaxonMap = new Map()
|
|
173
|
+
let newTaxonSet = false
|
|
174
|
+
if (!this.taxonSet) {
|
|
175
|
+
this.taxonSet = new TaxonSet()
|
|
176
|
+
newTaxonSet = true
|
|
177
|
+
}
|
|
178
|
+
let i = 0
|
|
179
|
+
let key
|
|
180
|
+
token = await this.nextToken()
|
|
181
|
+
while (token !== ";") {
|
|
182
|
+
if (i % 2 == 0) {
|
|
183
|
+
key = token
|
|
184
|
+
} else {
|
|
185
|
+
if (token[token.length - 1] === ",") {
|
|
186
|
+
token = token.slice(0, -1)
|
|
187
|
+
}
|
|
188
|
+
// todo get taxa to add here.
|
|
189
|
+
if (!newTaxonSet) {
|
|
190
|
+
if (this.taxonSet.getTaxonByName(token) === undefined) {
|
|
191
|
+
throw `Taxon ${token} not found in taxa block - but found in translate block`
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
//new taxon set so add it;
|
|
195
|
+
this.taxonSet.addTaxon(token)
|
|
196
|
+
}
|
|
197
|
+
const taxon = this.taxonSet.getTaxonByName(token)
|
|
198
|
+
this.translateTaxonMap.set(key!, taxon)
|
|
199
|
+
}
|
|
200
|
+
token = await this.nextToken()
|
|
201
|
+
while (token === ",") {
|
|
202
|
+
token = await this.nextToken()
|
|
203
|
+
} //incase some white space in there
|
|
204
|
+
i++
|
|
205
|
+
}
|
|
206
|
+
break
|
|
207
|
+
case /tree/i.test(command):
|
|
208
|
+
//parse tree
|
|
209
|
+
// put this in loop so the next call parses the next tree;
|
|
210
|
+
|
|
211
|
+
// first token will be tree id
|
|
212
|
+
// then possible annotation
|
|
213
|
+
// Then =
|
|
214
|
+
// then possible annotations
|
|
215
|
+
// then tree
|
|
216
|
+
const treeId = await this.nextToken()
|
|
217
|
+
// read to first '(';
|
|
218
|
+
token = await this.skipUntil(/\(/)
|
|
219
|
+
let buffer = token
|
|
220
|
+
.split(newickDeliminators)
|
|
221
|
+
.filter((n) => n.length > 0)
|
|
222
|
+
.reverse()
|
|
223
|
+
|
|
224
|
+
let level = 0
|
|
225
|
+
let currentNode: NodeRef | undefined = undefined
|
|
226
|
+
let nodeStack: NodeRef[] = []
|
|
227
|
+
let labelNext = false
|
|
228
|
+
let lengthNext = false
|
|
229
|
+
|
|
230
|
+
let tree = new ImmutableTree({ taxonSet: this.taxonSet })
|
|
231
|
+
|
|
232
|
+
while (token !== ";") {
|
|
233
|
+
while (buffer.length > 0) {
|
|
234
|
+
const t = buffer.pop()!
|
|
235
|
+
if (t.length > 2 && t.substring(0, 2) === "[&") {
|
|
236
|
+
const annotations = parseAnnotation(token)
|
|
237
|
+
|
|
238
|
+
tree = tree.annotateNode(currentNode!, annotations)
|
|
239
|
+
} else if (token === "(") {
|
|
240
|
+
// an internal node
|
|
241
|
+
|
|
242
|
+
if (labelNext) {
|
|
243
|
+
// if labelNext is set then the last bracket has just closed
|
|
244
|
+
// so there shouldn't be an open bracket.
|
|
245
|
+
throw new Error("expecting a comma")
|
|
246
|
+
}
|
|
247
|
+
let node
|
|
248
|
+
level += 1
|
|
249
|
+
if (currentNode !== undefined) {
|
|
250
|
+
const added = tree.addNodes(1)
|
|
251
|
+
tree = added.tree
|
|
252
|
+
node = added.nodes[0]
|
|
253
|
+
nodeStack.push(currentNode)
|
|
254
|
+
} else {
|
|
255
|
+
node = tree.getRoot()
|
|
256
|
+
}
|
|
257
|
+
currentNode = node
|
|
258
|
+
} else if (token === ",") {
|
|
259
|
+
// another branch in an internal node
|
|
260
|
+
|
|
261
|
+
labelNext = false // labels are optional
|
|
262
|
+
if (lengthNext) {
|
|
263
|
+
throw new Error("branch length missing")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let parent = nodeStack.pop()! as NodeRef
|
|
267
|
+
tree = tree.addChild(parent, currentNode!)
|
|
268
|
+
// tree.setParent(currentNode!,parent)
|
|
269
|
+
|
|
270
|
+
currentNode = parent
|
|
271
|
+
} else if (token === ")") {
|
|
272
|
+
if (level === 0) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"the brackets in the newick file are not balanced: too many closed",
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
// finished an internal node
|
|
278
|
+
|
|
279
|
+
labelNext = false // labels are optional
|
|
280
|
+
if (lengthNext) {
|
|
281
|
+
throw new Error("branch length missing")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// the end of an internal node
|
|
285
|
+
let parent = nodeStack.pop()! as NodeRef
|
|
286
|
+
tree = tree.addChild(parent, currentNode!)
|
|
287
|
+
// tree.setParent(currentNode!,parent)
|
|
288
|
+
|
|
289
|
+
level -= 1
|
|
290
|
+
currentNode = parent
|
|
291
|
+
|
|
292
|
+
labelNext = true
|
|
293
|
+
} else if (token === ":") {
|
|
294
|
+
labelNext = false // labels are optional
|
|
295
|
+
lengthNext = true
|
|
296
|
+
} else {
|
|
297
|
+
// not any specific token so may be a label, a length, or an external node name
|
|
298
|
+
if (lengthNext) {
|
|
299
|
+
tree = tree.setLength(currentNode!, parseFloat(token))
|
|
300
|
+
lengthNext = false
|
|
301
|
+
} else if (labelNext) {
|
|
302
|
+
if (!token.startsWith("#")) {
|
|
303
|
+
let value: number | any = parseFloat(token)
|
|
304
|
+
if (isNaN(value)) {
|
|
305
|
+
value = token
|
|
306
|
+
}
|
|
307
|
+
if (this.options.labelName) {
|
|
308
|
+
let label_annotation = {
|
|
309
|
+
name: this.options.labelName,
|
|
310
|
+
value: value,
|
|
311
|
+
}
|
|
312
|
+
tree = tree.annotateNode(currentNode!, label_annotation)
|
|
313
|
+
} else {
|
|
314
|
+
console.warn(
|
|
315
|
+
`No label name provided to newick parser but found label ${token}. It will be ignored`,
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
tree = tree.setLabel(currentNode!, token.slice(1)) //remove the # todo put it back when writing to newick
|
|
320
|
+
}
|
|
321
|
+
labelNext = false
|
|
322
|
+
} else {
|
|
323
|
+
let name = token // TODO tree needs be a map that's not the ID
|
|
324
|
+
|
|
325
|
+
// remove any quoting and then trim whitespace
|
|
326
|
+
// TODO add to bit that parses taxa block
|
|
327
|
+
if (name.startsWith('"') || name.startsWith("'")) {
|
|
328
|
+
name = name.substr(1)
|
|
329
|
+
}
|
|
330
|
+
if (name.endsWith('"') || name.endsWith("'")) {
|
|
331
|
+
name = name.substr(0, name.length - 1)
|
|
332
|
+
}
|
|
333
|
+
name = name.trim()
|
|
334
|
+
|
|
335
|
+
const added = tree.addNodes(1)
|
|
336
|
+
tree = added.tree
|
|
337
|
+
const externalNode = added.nodes[0]
|
|
338
|
+
let taxon: Taxon
|
|
339
|
+
if (this.translateTaxonMap) {
|
|
340
|
+
if (this.translateTaxonMap.has(name)) {
|
|
341
|
+
taxon = this.translateTaxonMap.get(name)!
|
|
342
|
+
} else {
|
|
343
|
+
throw `No mapping found for ${name} in tipNameMap. It's name will not be updated`
|
|
344
|
+
}
|
|
345
|
+
} else if (this.taxonSet) {
|
|
346
|
+
taxon = this.taxonSet.getTaxonByName(name)
|
|
347
|
+
if (taxon === undefined) {
|
|
348
|
+
throw `Taxon ${name} not found in taxa - but found in tree`
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
tree.addTaxon(name) //does this affect immutability - using the first tree as a taxon set.
|
|
352
|
+
taxon = tree.getTaxonByName(name)!
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
tree = tree.setTaxon(externalNode, taxon)
|
|
356
|
+
|
|
357
|
+
if (currentNode) {
|
|
358
|
+
nodeStack.push(currentNode)
|
|
359
|
+
}
|
|
360
|
+
currentNode = externalNode
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// get next token from reader
|
|
365
|
+
token = await this.nextToken()
|
|
366
|
+
buffer = token
|
|
367
|
+
.split(newickDeliminators)
|
|
368
|
+
.filter((n) => n.length > 0)
|
|
369
|
+
.reverse()
|
|
370
|
+
}
|
|
371
|
+
if (level > 0) {
|
|
372
|
+
throw new Error(`unexpected semi-colon in tree: ${treeId}`)
|
|
373
|
+
}
|
|
374
|
+
if (this.taxonSet === undefined) {
|
|
375
|
+
this.taxonSet === tree
|
|
376
|
+
}
|
|
377
|
+
yield tree
|
|
378
|
+
break
|
|
379
|
+
case /end/i.test(command):
|
|
380
|
+
this.skipSemiColon()
|
|
381
|
+
this.hasTree = false
|
|
382
|
+
// Give up the reader.
|
|
383
|
+
//TODO read to the end of the file.
|
|
384
|
+
this.reader.releaseLock()
|
|
385
|
+
break
|
|
386
|
+
default:
|
|
387
|
+
throw `Reached impossible code in treeblock block "${command}"`
|
|
388
|
+
}
|
|
389
|
+
command = await this.skipUntil(/translate|tree|end/i)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|