@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,276 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Annotation,
|
|
3
|
+
MarkovJumpValue,
|
|
4
|
+
RawAnnotationValue,
|
|
5
|
+
ValueOf,
|
|
6
|
+
} from "../tree-types";
|
|
7
|
+
import { BaseAnnotationType } from "../tree-types";
|
|
8
|
+
|
|
9
|
+
/** What the *parser* can emit before classification */
|
|
10
|
+
|
|
11
|
+
export type ClassifiedValue =
|
|
12
|
+
| {
|
|
13
|
+
type: BaseAnnotationType.DISCRETE;
|
|
14
|
+
value: ValueOf<BaseAnnotationType.DISCRETE>;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: BaseAnnotationType.NUMERICAL;
|
|
18
|
+
value: ValueOf<BaseAnnotationType.NUMERICAL>;
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: BaseAnnotationType.BOOLEAN;
|
|
22
|
+
value: ValueOf<BaseAnnotationType.BOOLEAN>;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: BaseAnnotationType.NUMERICAL_SET;
|
|
26
|
+
value: ValueOf<BaseAnnotationType.NUMERICAL_SET>;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
type: BaseAnnotationType.DISCRETE_SET;
|
|
30
|
+
value: ValueOf<BaseAnnotationType.DISCRETE_SET>;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: BaseAnnotationType.MARKOV_JUMPS;
|
|
34
|
+
value: ValueOf<BaseAnnotationType.MARKOV_JUMPS>;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: BaseAnnotationType.DENSITIES;
|
|
38
|
+
value: ValueOf<BaseAnnotationType.DENSITIES>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ParsedAnnotationRaw = Record<string, RawAnnotationValue>;
|
|
42
|
+
|
|
43
|
+
// Parse the annotation found in a nexus (or newick string - perish the thought!)
|
|
44
|
+
export function parseAnnotation(annotationString: string): ParsedAnnotationRaw {
|
|
45
|
+
const tokens = annotationString
|
|
46
|
+
.split(/\s*('[^']+'|"[^"]+"|;|\(|\)|,|:|=|\[&|\]|\{|\})\s*/)
|
|
47
|
+
.filter((token) => token.length > 0);
|
|
48
|
+
let annotationKeyNext = true;
|
|
49
|
+
let annotationKey: string = "";
|
|
50
|
+
let isAnnotationARange = false;
|
|
51
|
+
let inSubRange = false;
|
|
52
|
+
let subValue: string[] = [];
|
|
53
|
+
let value: RawAnnotationValue | undefined = undefined;
|
|
54
|
+
const annotations: ParsedAnnotationRaw = {};
|
|
55
|
+
|
|
56
|
+
// expect the first token to be a [& and last ]
|
|
57
|
+
if (tokens[0] !== "[&" || tokens[tokens.length - 1] !== "]") {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"expecting a [& at the start and ] at the end of the annotation",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (token === "[&") {
|
|
64
|
+
// open BEAST annotation
|
|
65
|
+
annotationKeyNext = true;
|
|
66
|
+
} else if (token === "=") {
|
|
67
|
+
annotationKeyNext = false;
|
|
68
|
+
} else if (token === ",") {
|
|
69
|
+
if (!isAnnotationARange) {
|
|
70
|
+
annotationKeyNext = true;
|
|
71
|
+
|
|
72
|
+
if (value === undefined) throw new Error(`Empty annotation value`);
|
|
73
|
+
//finalize annotation
|
|
74
|
+
annotations[annotationKey] = value;
|
|
75
|
+
} else {
|
|
76
|
+
continue; //to next value in range
|
|
77
|
+
}
|
|
78
|
+
} else if (token === "{") {
|
|
79
|
+
if (isAnnotationARange) {
|
|
80
|
+
inSubRange = true;
|
|
81
|
+
subValue = [];
|
|
82
|
+
} else {
|
|
83
|
+
value = [];
|
|
84
|
+
}
|
|
85
|
+
isAnnotationARange = true;
|
|
86
|
+
} else if (token === "}") {
|
|
87
|
+
if (inSubRange) {
|
|
88
|
+
inSubRange = false;
|
|
89
|
+
// eslint-disable-next-line
|
|
90
|
+
(value as any[])!.push(subValue);
|
|
91
|
+
} else {
|
|
92
|
+
isAnnotationARange = false;
|
|
93
|
+
}
|
|
94
|
+
} else if (token === "]") {
|
|
95
|
+
// close BEAST annotation
|
|
96
|
+
|
|
97
|
+
//finalize annotation
|
|
98
|
+
if (value === undefined) throw new Error(`Empty annotation value`);
|
|
99
|
+
annotations[annotationKey] = value;
|
|
100
|
+
} else {
|
|
101
|
+
// must be annotation
|
|
102
|
+
// remove any quoting and then trim whitespace
|
|
103
|
+
let annotationToken = token;
|
|
104
|
+
if (annotationToken.startsWith('"') || annotationToken.startsWith("'")) {
|
|
105
|
+
annotationToken = annotationToken.slice(1);
|
|
106
|
+
}
|
|
107
|
+
if (annotationToken.endsWith('"') || annotationToken.endsWith("'")) {
|
|
108
|
+
annotationToken = annotationToken.slice(0, -1);
|
|
109
|
+
}
|
|
110
|
+
if (annotationKeyNext) {
|
|
111
|
+
annotationKey = annotationToken.replace(".", "_");
|
|
112
|
+
} else {
|
|
113
|
+
if (isAnnotationARange) {
|
|
114
|
+
if (inSubRange) {
|
|
115
|
+
subValue.push(annotationToken);
|
|
116
|
+
} else {
|
|
117
|
+
// eslint-disable-next-line
|
|
118
|
+
(value as any[]).push(annotationToken);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (isNaN(annotationToken as unknown as number)) {
|
|
122
|
+
value = annotationToken;
|
|
123
|
+
} else {
|
|
124
|
+
value = parseFloat(annotationToken);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return annotations;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function processAnnotationValue(
|
|
134
|
+
values: RawAnnotationValue,
|
|
135
|
+
): ClassifiedValue {
|
|
136
|
+
if (Array.isArray(values)) {
|
|
137
|
+
// if is it an array it could be markov jump or array of values
|
|
138
|
+
// is a set of values
|
|
139
|
+
//Array of array?
|
|
140
|
+
if (Array.isArray(values[0])) {
|
|
141
|
+
// an array of arrays is a markov jump
|
|
142
|
+
// This may be a markov jump array
|
|
143
|
+
const tuples = values as string[][];
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
tuples
|
|
147
|
+
.map((v) => v.length === 3)
|
|
148
|
+
.reduce((acc, curr) => acc && curr, true)
|
|
149
|
+
) {
|
|
150
|
+
const jumps: MarkovJumpValue[] = tuples.map(
|
|
151
|
+
([timeStr, source, dest]) => {
|
|
152
|
+
const timeNum = Number(timeStr);
|
|
153
|
+
if (!Number.isFinite(timeNum)) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Expected a markov jump annotation but the first entry ${timeStr} could not be make a number`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return { time: timeNum, from: source, to: dest };
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
return { type: BaseAnnotationType.MARKOV_JUMPS, value: jumps };
|
|
162
|
+
} else {
|
|
163
|
+
throw Error(
|
|
164
|
+
`Markov jump with dimension ${tuples[0].length} detected. Expected 3. ${tuples.map((t) => t.length).join(",")}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Flat array check types
|
|
169
|
+
const flat = values as Array<string | number>;
|
|
170
|
+
|
|
171
|
+
const allStrings = flat.every((v) => typeof v === "string");
|
|
172
|
+
const allNumbersAfterCoerce = flat.every((v) => Number.isFinite(Number(v)));
|
|
173
|
+
|
|
174
|
+
if (allNumbersAfterCoerce) {
|
|
175
|
+
const nums = flat.map((v) => Number(v));
|
|
176
|
+
return { type: BaseAnnotationType.NUMERICAL_SET, value: nums };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (allStrings) {
|
|
180
|
+
return {
|
|
181
|
+
type: BaseAnnotationType.DISCRETE_SET,
|
|
182
|
+
value: flat.slice(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// coerce to strings
|
|
187
|
+
return {
|
|
188
|
+
type: BaseAnnotationType.DISCRETE_SET,
|
|
189
|
+
value: flat.map(String),
|
|
190
|
+
};
|
|
191
|
+
// densities
|
|
192
|
+
} else if (isPlainObject(values)) {
|
|
193
|
+
// is a set of properties with values
|
|
194
|
+
|
|
195
|
+
const obj = values as Record<string, string | number | boolean>;
|
|
196
|
+
const entries = Object.entries(obj);
|
|
197
|
+
|
|
198
|
+
const allNumbers = entries.every(([, v]) => Number.isFinite(Number(v)));
|
|
199
|
+
const allBooleans = entries.every(([, v]) => typeof v === "boolean");
|
|
200
|
+
|
|
201
|
+
if (allNumbers) {
|
|
202
|
+
const probs: Record<string, number> = {};
|
|
203
|
+
for (const [k, v] of entries) {
|
|
204
|
+
const n = Number(v);
|
|
205
|
+
probs[k] = n;
|
|
206
|
+
}
|
|
207
|
+
return { type: BaseAnnotationType.DENSITIES, value: probs };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (allBooleans) {
|
|
211
|
+
const set = entries
|
|
212
|
+
.filter(([, v]) => v === true)
|
|
213
|
+
.map(([k]) => k)
|
|
214
|
+
.sort();
|
|
215
|
+
return { type: BaseAnnotationType.DISCRETE_SET, value: set };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new Error(
|
|
219
|
+
"Unsupported object value: expected numeric (probabilities) or boolean map",
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
if (typeof values === "boolean") {
|
|
223
|
+
return { type: BaseAnnotationType.BOOLEAN, value: values };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof values === "number") {
|
|
227
|
+
return { type: BaseAnnotationType.NUMERICAL, value: values };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof values === "string") {
|
|
231
|
+
// try boolean literal
|
|
232
|
+
const lower = values.toLowerCase();
|
|
233
|
+
if (lower === "true" || lower === "false") {
|
|
234
|
+
return { type: BaseAnnotationType.BOOLEAN, value: lower === "true" };
|
|
235
|
+
}
|
|
236
|
+
// try number
|
|
237
|
+
const n = Number(values);
|
|
238
|
+
if (Number.isFinite(n)) {
|
|
239
|
+
return { type: BaseAnnotationType.NUMERICAL, value: n };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// otherwise discrete label
|
|
243
|
+
return { type: BaseAnnotationType.DISCRETE, value: values };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`Unsupported annotation value: ${String(values)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
250
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function writeAnnotationValue(a: Annotation): string {
|
|
254
|
+
switch (a.type) {
|
|
255
|
+
case BaseAnnotationType.DISCRETE:
|
|
256
|
+
return a.value;
|
|
257
|
+
case BaseAnnotationType.BOOLEAN:
|
|
258
|
+
return String(a.value);
|
|
259
|
+
case BaseAnnotationType.NUMERICAL:
|
|
260
|
+
return String(a.value);
|
|
261
|
+
case BaseAnnotationType.NUMERICAL_SET:
|
|
262
|
+
return "{" + a.value.map((d) => String(d)).join(", ") + "}";
|
|
263
|
+
case BaseAnnotationType.DISCRETE_SET:
|
|
264
|
+
return "{" + a.value.join(", ") + "}";
|
|
265
|
+
case BaseAnnotationType.MARKOV_JUMPS:
|
|
266
|
+
return (
|
|
267
|
+
"{" +
|
|
268
|
+
a.value.map((d) => `{${String(d.time)},${d.from},${d.to}}`).join(", ") +
|
|
269
|
+
"}"
|
|
270
|
+
);
|
|
271
|
+
case BaseAnnotationType.DENSITIES:
|
|
272
|
+
throw new Error(
|
|
273
|
+
`No defined why to write densities (${a.id}) as a string. \n Please convert keys and values to separate array annotations.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { Taxon } from "../taxa/taxon";
|
|
2
|
+
import { TaxonSet } from "../taxa/taxon";
|
|
3
|
+
import type { NodeRef } from "../tree-types";
|
|
4
|
+
import { ImmutableTree } from "../normalized-tree/immutable-tree";
|
|
5
|
+
import { parseAnnotation } from "./annotation-parser";
|
|
6
|
+
import { notNull, unNullify } from "../../../utils";
|
|
7
|
+
|
|
8
|
+
export class NewickCharacterParser {
|
|
9
|
+
done: boolean;
|
|
10
|
+
started: boolean;
|
|
11
|
+
level: number;
|
|
12
|
+
currentNode: NodeRef | undefined;
|
|
13
|
+
nodeStack: NodeRef[];
|
|
14
|
+
labelNext: boolean;
|
|
15
|
+
lengthNext: boolean;
|
|
16
|
+
taxonSet: TaxonSet;
|
|
17
|
+
tree: ImmutableTree;
|
|
18
|
+
options: { labelName?: string; translateTaxonNames?: Map<string, string> };
|
|
19
|
+
constructor(
|
|
20
|
+
taxonSet: TaxonSet = new TaxonSet(),
|
|
21
|
+
options: {
|
|
22
|
+
labelName?: string;
|
|
23
|
+
translateTaxonNames?: Map<string, string>;
|
|
24
|
+
} = {},
|
|
25
|
+
) {
|
|
26
|
+
this.done = false;
|
|
27
|
+
this.started = false;
|
|
28
|
+
this.level = 0;
|
|
29
|
+
this.currentNode = undefined;
|
|
30
|
+
this.nodeStack = [];
|
|
31
|
+
this.labelNext = false;
|
|
32
|
+
this.lengthNext = false;
|
|
33
|
+
this.taxonSet = taxonSet;
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.tree = new ImmutableTree({ taxonSet: this.taxonSet });
|
|
36
|
+
}
|
|
37
|
+
isDone() {
|
|
38
|
+
return this.done;
|
|
39
|
+
}
|
|
40
|
+
isStarted() {
|
|
41
|
+
return this.started;
|
|
42
|
+
}
|
|
43
|
+
getTree(): ImmutableTree {
|
|
44
|
+
if (!this.done) {
|
|
45
|
+
throw new Error("expecting a semi-colon at the end of the newick string");
|
|
46
|
+
}
|
|
47
|
+
if (!this.started) {
|
|
48
|
+
throw new Error("No tree to give - parsing has not started.");
|
|
49
|
+
}
|
|
50
|
+
//set hights
|
|
51
|
+
|
|
52
|
+
return this.tree;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
parseCharacter(t: string): void {
|
|
56
|
+
if (this.done) {
|
|
57
|
+
throw new Error("Parsing is done. We have seen a ';'");
|
|
58
|
+
}
|
|
59
|
+
if (t.length > 2 && t.substring(0, 2) === "[&") {
|
|
60
|
+
const annotations = parseAnnotation(t);
|
|
61
|
+
|
|
62
|
+
// for(const annotation of annotations){
|
|
63
|
+
notNull(
|
|
64
|
+
this.currentNode,
|
|
65
|
+
"Internal Parsing error - Current not is not defined",
|
|
66
|
+
);
|
|
67
|
+
this.tree = this.tree.annotateNode(
|
|
68
|
+
this.currentNode,
|
|
69
|
+
annotations,
|
|
70
|
+
) as ImmutableTree;
|
|
71
|
+
// }
|
|
72
|
+
} else if (t === ";") {
|
|
73
|
+
// check if done.
|
|
74
|
+
//set done.
|
|
75
|
+
if (this.level > 0) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`unexpected semi-colon in tree did not reach the root yet`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (!this.started) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`unexpected semi-colon in tree parsing has not started yet`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
this.done = true;
|
|
86
|
+
} else if (t === "(") {
|
|
87
|
+
// an internal node
|
|
88
|
+
this.started = true;
|
|
89
|
+
if (this.labelNext) {
|
|
90
|
+
// if labelNext is set then the last bracket has just closed
|
|
91
|
+
// so there shouldn't be an open bracket.
|
|
92
|
+
throw new Error("expecting a comma");
|
|
93
|
+
}
|
|
94
|
+
let node;
|
|
95
|
+
this.level += 1;
|
|
96
|
+
if (this.currentNode !== undefined) {
|
|
97
|
+
const added = this.tree.addNodes(1);
|
|
98
|
+
this.tree = added.tree;
|
|
99
|
+
node = added.nodes[0];
|
|
100
|
+
this.nodeStack.push(this.currentNode);
|
|
101
|
+
} else {
|
|
102
|
+
node = this.tree.getRoot();
|
|
103
|
+
}
|
|
104
|
+
this.currentNode = node;
|
|
105
|
+
} else if (t === ",") {
|
|
106
|
+
// another branch in an internal node
|
|
107
|
+
|
|
108
|
+
this.labelNext = false; // labels are optional
|
|
109
|
+
if (this.lengthNext) {
|
|
110
|
+
throw new Error("branch length missing");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parent = this.nodeStack.pop();
|
|
114
|
+
notNull(parent, `Internal Parsing error - node stack unexpectedly empty`);
|
|
115
|
+
notNull(
|
|
116
|
+
this.currentNode,
|
|
117
|
+
"Internal Parsing error - Current not is not defined",
|
|
118
|
+
);
|
|
119
|
+
this.tree = this.tree.addChild(parent, this.currentNode);
|
|
120
|
+
// tree.setParent(currentNode!,parent)
|
|
121
|
+
|
|
122
|
+
this.currentNode = parent;
|
|
123
|
+
} else if (t === ")") {
|
|
124
|
+
if (this.level === 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"the brackets in the newick file are not balanced: too many closed",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
// finished an internal node
|
|
130
|
+
|
|
131
|
+
this.labelNext = false; // labels are optional
|
|
132
|
+
if (this.lengthNext) {
|
|
133
|
+
throw new Error("branch length missing");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// the end of an internal node
|
|
137
|
+
const parent = this.nodeStack.pop();
|
|
138
|
+
|
|
139
|
+
notNull(parent, `Internal Parsing error - node stack unexpectedly empty`);
|
|
140
|
+
notNull(
|
|
141
|
+
this.currentNode,
|
|
142
|
+
"Internal Parsing error - Current not is not defined",
|
|
143
|
+
);
|
|
144
|
+
this.tree = this.tree.addChild(parent, this.currentNode);
|
|
145
|
+
// tree.setParent(currentNode!,parent)
|
|
146
|
+
|
|
147
|
+
this.level -= 1;
|
|
148
|
+
this.currentNode = parent;
|
|
149
|
+
|
|
150
|
+
this.labelNext = true;
|
|
151
|
+
} else if (t === ":") {
|
|
152
|
+
this.labelNext = false; // labels are optional
|
|
153
|
+
this.lengthNext = true;
|
|
154
|
+
} else {
|
|
155
|
+
// not any specific token so may be a label, a length, or an external node name
|
|
156
|
+
if (this.lengthNext) {
|
|
157
|
+
notNull(
|
|
158
|
+
this.currentNode,
|
|
159
|
+
"Internal Parsing error - Current not is not defined",
|
|
160
|
+
);
|
|
161
|
+
this.tree = this.tree.setLength(this.currentNode, parseFloat(t));
|
|
162
|
+
this.lengthNext = false;
|
|
163
|
+
} else if (this.labelNext) {
|
|
164
|
+
if (!t.startsWith("#")) {
|
|
165
|
+
let value: number | string = parseFloat(t);
|
|
166
|
+
if (isNaN(value)) {
|
|
167
|
+
value = t;
|
|
168
|
+
}
|
|
169
|
+
if (this.options.labelName) {
|
|
170
|
+
notNull(
|
|
171
|
+
this.currentNode,
|
|
172
|
+
"Internal Parsing error - Current not is not defined",
|
|
173
|
+
);
|
|
174
|
+
this.tree = this.tree.annotateNode(
|
|
175
|
+
this.currentNode,
|
|
176
|
+
this.options.labelName,
|
|
177
|
+
value,
|
|
178
|
+
) as ImmutableTree;
|
|
179
|
+
} else {
|
|
180
|
+
console.warn(
|
|
181
|
+
`No label name provided to newick parser but found label ${t}. It will be ignored`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
notNull(
|
|
186
|
+
this.currentNode,
|
|
187
|
+
"Internal Parsing error - Current not is not defined",
|
|
188
|
+
);
|
|
189
|
+
this.tree = this.tree.setLabel(this.currentNode, t.slice(1)); //remove the # todo put it back when writing to newick
|
|
190
|
+
}
|
|
191
|
+
this.labelNext = false;
|
|
192
|
+
} else {
|
|
193
|
+
let name = t; // TODO tree needs be a map that's not the ID
|
|
194
|
+
|
|
195
|
+
// remove any quoting and then trim whitespace
|
|
196
|
+
// TODO add to bit that parses taxa block
|
|
197
|
+
if (name.startsWith('"') || name.startsWith("'")) {
|
|
198
|
+
name = name.slice(1);
|
|
199
|
+
}
|
|
200
|
+
if (name.endsWith('"') || name.endsWith("'")) {
|
|
201
|
+
name = name.slice(0, -1);
|
|
202
|
+
}
|
|
203
|
+
name = name.trim();
|
|
204
|
+
|
|
205
|
+
const added = this.tree.addNodes(1);
|
|
206
|
+
this.tree = added.tree;
|
|
207
|
+
const externalNode = added.nodes[0];
|
|
208
|
+
let taxon: Taxon;
|
|
209
|
+
if (this.options.translateTaxonNames) {
|
|
210
|
+
if (this.options.translateTaxonNames.has(name)) {
|
|
211
|
+
name = unNullify(
|
|
212
|
+
this.options.translateTaxonNames.get(name),
|
|
213
|
+
`${name} not found in taxon translation map`,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`No mapping found for ${name} in tipNameMap. It's name will not be updated`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.taxonSet.isFinalized) {
|
|
223
|
+
// if set then it will be finalised by now.
|
|
224
|
+
|
|
225
|
+
if (!this.taxonSet.hasTaxon(name)) {
|
|
226
|
+
// hmm trees won't have
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Taxon ${name} not found in taxa - but found in tree`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
taxon = this.taxonSet.getTaxonByName(name);
|
|
232
|
+
} else {
|
|
233
|
+
this.taxonSet.addTaxon(name);
|
|
234
|
+
taxon = this.taxonSet.getTaxonByName(name);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.tree = this.tree.setTaxon(externalNode, taxon);
|
|
238
|
+
|
|
239
|
+
if (this.currentNode) {
|
|
240
|
+
this.nodeStack.push(this.currentNode);
|
|
241
|
+
}
|
|
242
|
+
this.currentNode = externalNode;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NewickCharacterParser } from "./newick-character-parser";
|
|
2
|
+
import type { ImmutableTree, newickParsingOptions } from "..";
|
|
3
|
+
import { TaxonSet } from "..";
|
|
4
|
+
|
|
5
|
+
export function parseNewick(
|
|
6
|
+
newick: string,
|
|
7
|
+
options: newickParsingOptions = {},
|
|
8
|
+
): ImmutableTree {
|
|
9
|
+
const taxonSet = options.taxonSet ? options.taxonSet : new TaxonSet();
|
|
10
|
+
const tokens = newick
|
|
11
|
+
.split(/\s*('[^']+'|"[^"]+"|\[&[^[]+]|,|:|\)|\(|;)\s*/)
|
|
12
|
+
.filter((token) => token.length > 0);
|
|
13
|
+
|
|
14
|
+
const parser = new NewickCharacterParser(taxonSet, options);
|
|
15
|
+
|
|
16
|
+
for (const token of tokens) {
|
|
17
|
+
parser.parseCharacter(token);
|
|
18
|
+
}
|
|
19
|
+
const tree = parser.getTree();
|
|
20
|
+
|
|
21
|
+
return tree;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default class NexusParser {
|
|
2
|
+
// get lock on file
|
|
3
|
+
// only read when asked for tree.
|
|
4
|
+
tokens: string[];
|
|
5
|
+
constructor(contents: string) {
|
|
6
|
+
this.tokens = contents
|
|
7
|
+
.split(
|
|
8
|
+
/\s*(?:\bBegin\s+|\bbegin\s+|\bBEGIN\s+|\bend\s*;|\bEnd\s*;|\bEND\s*;)\s*/,
|
|
9
|
+
)
|
|
10
|
+
.filter((d) => d !== "");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { ImmutableTree } from "../normalized-tree/immutable-tree";
|
|
3
|
+
import { parseNewick } from "./newick-parsing";
|
|
4
|
+
|
|
5
|
+
//ONLY parses the first tree
|
|
6
|
+
export function parseNexus(
|
|
7
|
+
_tree: ImmutableTree,
|
|
8
|
+
nexus: string,
|
|
9
|
+
options = {},
|
|
10
|
+
): ImmutableTree {
|
|
11
|
+
// odd parts ensure we're not in a taxon label
|
|
12
|
+
//TODO make this parsing more robust
|
|
13
|
+
const nexusTokens = nexus
|
|
14
|
+
.split(
|
|
15
|
+
/\s*(?:\bBegin\s+|\bbegin\s+|\bBEGIN\s+|\bend\s*;|\bEnd\s*;|\bEND\s*;)\s*/,
|
|
16
|
+
)
|
|
17
|
+
.filter((d) => d !== "");
|
|
18
|
+
if (nexusTokens.length === 0 || nexusTokens === undefined) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"No nexus tokens found in string. This may not be a nexus formated tree",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const firstToken = nexusTokens.shift()!.trim();
|
|
24
|
+
if (firstToken.toLowerCase() !== "#nexus") {
|
|
25
|
+
throw Error("File does not begin with #NEXUS is it a nexus file?");
|
|
26
|
+
}
|
|
27
|
+
for (const section of nexusTokens) {
|
|
28
|
+
const workingSection = section.replace(/^\s+|\s+$/g, "").split(/\n/);
|
|
29
|
+
const sectionTitle = workingSection.shift()!;
|
|
30
|
+
if (sectionTitle.toLowerCase().trim() === "trees;") {
|
|
31
|
+
let inTaxaMap = false;
|
|
32
|
+
const tipNameMap = new Map();
|
|
33
|
+
for (const token of workingSection) {
|
|
34
|
+
if (token.trim().toLowerCase() === "translate") {
|
|
35
|
+
inTaxaMap = true;
|
|
36
|
+
} else {
|
|
37
|
+
if (inTaxaMap) {
|
|
38
|
+
if (token.trim() === ";") {
|
|
39
|
+
inTaxaMap = false;
|
|
40
|
+
} else {
|
|
41
|
+
const taxaData = token
|
|
42
|
+
.trim()
|
|
43
|
+
.replace(",", "")
|
|
44
|
+
.split(/\s*\s\s*/);
|
|
45
|
+
tipNameMap.set(taxaData[0], taxaData[1]);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
const treeString = token.substring(token.indexOf("("));
|
|
49
|
+
if (tipNameMap.size > 0) {
|
|
50
|
+
return parseNewick(treeString, {
|
|
51
|
+
parseAnnotations: true,
|
|
52
|
+
...options,
|
|
53
|
+
tipNameMap: tipNameMap,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
return parseNewick(treeString, {
|
|
57
|
+
parseAnnotations: true,
|
|
58
|
+
...options,
|
|
59
|
+
tipNameMap: tipNameMap,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error("No tree section found in nexus file");
|
|
68
|
+
}
|