@euphrasiologist/lwphylo 1.1.7 → 1.1.9
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/dist/index.js +2 -0
- package/package.json +9 -5
- package/src/phisheye/phisheye.js +111 -65
- package/src/radial/describeArc.js +22 -12
- package/src/radial/getArcs.js +47 -56
- package/src/radial/getRadii.js +32 -38
- package/src/radial/polarToCartesian.js +5 -7
- package/src/radial/radialData.js +71 -89
- package/src/radial/radialLayout.js +8 -9
- package/src/radial/reflectAngle.js +7 -17
- package/src/rectangle/getChildVerticals.js +47 -0
- package/src/rectangle/getHorizontal.js +13 -7
- package/src/rectangle/getVertical.js +2 -1
- package/src/rectangle/rectangleLayout.js +23 -10
- package/src/unrooted/equalAngleLayout.js +36 -25
- package/src/unrooted/unrooted.js +1 -1
- package/src/utils/edges.js +13 -42
- package/src/utils/indexing.js +64 -0
- package/src/utils/meanAngle.js +10 -35
- package/src/utils/numTips.js +15 -20
- package/src/utils/parentFisheye.js +9 -11
- package/src/utils/preorder.js +33 -6
- package/src/utils/readTree.js +53 -63
- package/dist/@euphrasiologist/lwphylo.js +0 -822
- package/dist/@euphrasiologist/lwphylo.min.js +0 -2
|
@@ -1,822 +0,0 @@
|
|
|
1
|
-
// https://github.com/Euphrasiologist/lwPhylo#readme v1.1.7 Copyright 2025 Max Brown
|
|
2
|
-
(function (global, factory) {
|
|
3
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
4
|
-
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
5
|
-
(global = global || self, factory(global.lwphylo = global.lwphylo || {}));
|
|
6
|
-
}(this, (function (exports) { 'use strict';
|
|
7
|
-
|
|
8
|
-
// based on https://github.com/d3/d3-plugins/blob/master/fisheye/fisheye.js
|
|
9
|
-
// now archived d3 code?
|
|
10
|
-
|
|
11
|
-
const phisheye = {
|
|
12
|
-
circular: () => {
|
|
13
|
-
var radius = 200,
|
|
14
|
-
distortion = 2,
|
|
15
|
-
k0,
|
|
16
|
-
k1,
|
|
17
|
-
focus = [0, 0],
|
|
18
|
-
scales = {};
|
|
19
|
-
|
|
20
|
-
function fisheye(d) {
|
|
21
|
-
var dx = scales.xscale(d.x) - focus[0],
|
|
22
|
-
dy = scales.yscale(d.y) - focus[1],
|
|
23
|
-
dd = Math.sqrt(dx * dx + dy * dy);
|
|
24
|
-
if (!dd || dd >= radius)
|
|
25
|
-
return {
|
|
26
|
-
x: scales.xscale(d.x),
|
|
27
|
-
y: scales.yscale(d.y),
|
|
28
|
-
z: dd >= radius ? 1 : 10
|
|
29
|
-
};
|
|
30
|
-
var k = ((k0 * (1 - Math.exp(-dd * k1))) / dd) * .75 + .25;
|
|
31
|
-
return {
|
|
32
|
-
x: focus[0] + dx * k,
|
|
33
|
-
y: focus[1] + dy * k,
|
|
34
|
-
z: Math.min(k, 10)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function rescale() {
|
|
39
|
-
k0 = Math.exp(distortion);
|
|
40
|
-
k0 = (k0 / (k0 - 1)) * radius;
|
|
41
|
-
k1 = distortion / radius;
|
|
42
|
-
return fisheye;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
fisheye.radius = function (_) {
|
|
46
|
-
if (!arguments.length) return radius;
|
|
47
|
-
radius = +_;
|
|
48
|
-
return rescale();
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
fisheye.distortion = function (_) {
|
|
52
|
-
if (!arguments.length) return distortion;
|
|
53
|
-
distortion = +_;
|
|
54
|
-
return rescale();
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
fisheye.focus = function (_) {
|
|
58
|
-
if (!arguments.length) return focus;
|
|
59
|
-
focus = _;
|
|
60
|
-
return fisheye;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
fisheye.scales = function (_, __) {
|
|
64
|
-
if (!arguments.length) return scales;
|
|
65
|
-
scales = {
|
|
66
|
-
xscale: _,
|
|
67
|
-
yscale: __
|
|
68
|
-
};
|
|
69
|
-
return fisheye;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
return rescale();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Convert polar co-ordinates to cartesian
|
|
78
|
-
* https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
|
|
79
|
-
*/
|
|
80
|
-
|
|
81
|
-
function polarToCartesian (centerX, centerY, radius, angleInRadians) {
|
|
82
|
-
return {
|
|
83
|
-
x: centerX + (radius * Math.cos(angleInRadians)),
|
|
84
|
-
y: centerY + (radius * Math.sin(angleInRadians))
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Describe the arc to draw
|
|
90
|
-
* https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
|
|
91
|
-
*/
|
|
92
|
-
|
|
93
|
-
function describeArc (x, y, radius, startAngle, endAngle) {
|
|
94
|
-
|
|
95
|
-
var start = polarToCartesian(x, y, radius, startAngle);
|
|
96
|
-
var end = polarToCartesian(x, y, radius, endAngle);
|
|
97
|
-
|
|
98
|
-
// TODO: refers to middle zero below... but no large arcs needed..?
|
|
99
|
-
// var largeArcFlag = endAngle - startAngle <= Math.PI ? "0" : "1";
|
|
100
|
-
|
|
101
|
-
var d = [
|
|
102
|
-
"M", start.x, start.y,
|
|
103
|
-
"A", radius, radius, 0, 0, 0, end.x, end.y
|
|
104
|
-
].join(" ");
|
|
105
|
-
|
|
106
|
-
return d;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Recursive function for pre-order traversal of tree
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
function preorder(node, list = []) {
|
|
114
|
-
list.push(node);
|
|
115
|
-
for (var i = 0; i < node.children.length; i++) {
|
|
116
|
-
list = preorder(node.children[i], list);
|
|
117
|
-
}
|
|
118
|
-
return (list);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Convert parsed Newick tree from readTree() into data
|
|
123
|
-
* frame.
|
|
124
|
-
* this is akin to a "phylo" object in R.
|
|
125
|
-
*/
|
|
126
|
-
|
|
127
|
-
function fortify (tree, sort = true) {
|
|
128
|
-
var df = [];
|
|
129
|
-
|
|
130
|
-
for (const node of preorder(tree)) {
|
|
131
|
-
if (node.parent === null) {
|
|
132
|
-
df.push({
|
|
133
|
-
'parentId': null,
|
|
134
|
-
'parentLabel': null,
|
|
135
|
-
'thisId': node.id,
|
|
136
|
-
'thisLabel': node.label,
|
|
137
|
-
'children': node.children.map(x => x.id),
|
|
138
|
-
'branchLength': 0.,
|
|
139
|
-
'isTip': false,
|
|
140
|
-
'x': node.x,
|
|
141
|
-
'y': node.y,
|
|
142
|
-
'angle': node.angle
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
df.push({
|
|
147
|
-
'parentId': node.parent.id,
|
|
148
|
-
'parentLabel': node.parent.label,
|
|
149
|
-
'thisId': node.id,
|
|
150
|
-
'thisLabel': node.label,
|
|
151
|
-
'children': node.children.map(x => x.id),
|
|
152
|
-
'branchLength': node.branchLength,
|
|
153
|
-
'isTip': (node.children.length == 0),
|
|
154
|
-
'x': node.x,
|
|
155
|
-
'y': node.y,
|
|
156
|
-
'angle': node.angle
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (sort) {
|
|
162
|
-
df = df.sort(function (a, b) {
|
|
163
|
-
return a.thisId - b.thisId;
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
return (df);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Recursive function for breadth-first search of a tree
|
|
171
|
-
* the root node is visited first.
|
|
172
|
-
*/
|
|
173
|
-
|
|
174
|
-
function levelorder(root) {
|
|
175
|
-
// aka breadth-first search
|
|
176
|
-
var queue = [root],
|
|
177
|
-
result = [],
|
|
178
|
-
curnode;
|
|
179
|
-
|
|
180
|
-
while (queue.length > 0) {
|
|
181
|
-
curnode = queue.pop();
|
|
182
|
-
result.push(curnode);
|
|
183
|
-
for (const child of curnode.children) {
|
|
184
|
-
queue.push(child);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return (result);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Count the number of tips that descend from this node
|
|
192
|
-
*/
|
|
193
|
-
|
|
194
|
-
function numTips (thisnode) {
|
|
195
|
-
var result = 0;
|
|
196
|
-
for (const node of levelorder(thisnode)) {
|
|
197
|
-
if (node.children.length == 0) result++;
|
|
198
|
-
}
|
|
199
|
-
return (result);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Iterable mean
|
|
204
|
-
* Poached from https://github.com/d3/d3-array/blob/master/src/mean.js
|
|
205
|
-
* (Other array means buggered up the tree)
|
|
206
|
-
*/
|
|
207
|
-
|
|
208
|
-
function mean (values, valueof) {
|
|
209
|
-
let count = 0;
|
|
210
|
-
let sum = 0;
|
|
211
|
-
if (valueof === undefined) {
|
|
212
|
-
for (let value of values) {
|
|
213
|
-
if (value != null && (value = +value) >= value) {
|
|
214
|
-
++count, sum += value;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
let index = -1;
|
|
219
|
-
for (let value of values) {
|
|
220
|
-
if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {
|
|
221
|
-
++count, sum += value;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (count) return sum / count;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Take a parsed tree and get the important data from them (i.e. radii, arcs).
|
|
230
|
-
*/
|
|
231
|
-
|
|
232
|
-
function radialData (node) {
|
|
233
|
-
// should start out the same as get_horizontal
|
|
234
|
-
// it's very similar in fact, but keeping separate for clarity.
|
|
235
|
-
var pd = fortify(node);
|
|
236
|
-
|
|
237
|
-
var tip_number = numTips(node);
|
|
238
|
-
|
|
239
|
-
// make tip angles
|
|
240
|
-
var tipID = 1;
|
|
241
|
-
for (let i = 0; i < pd.length; i++) {
|
|
242
|
-
if (pd[i].isTip == true) {
|
|
243
|
-
pd[i].angle = (tipID / tip_number) * 2 * Math.PI;
|
|
244
|
-
tipID += 1;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// probably incredibly inefficient for large trees.
|
|
249
|
-
// gets the y values of two child branches by looping through the whole tree...
|
|
250
|
-
function internalNodeAngle(child_1, child_2) {
|
|
251
|
-
for (var i = 0; i < pd.length; i++) {
|
|
252
|
-
if (pd[i].thisId === child_1) {
|
|
253
|
-
var angle_1 = pd[i].angle;
|
|
254
|
-
}
|
|
255
|
-
if (pd[i].thisId === child_2) {
|
|
256
|
-
var angle_2 = pd[i].angle;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return mean([angle_1, angle_2]); // see utils.js
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// if the node is not a tip...
|
|
263
|
-
for (let i = 0; i < pd.length; i++) {
|
|
264
|
-
if (pd[i].isTip === false) {
|
|
265
|
-
// then y0 === y1 and is the mean of the parental nodes
|
|
266
|
-
pd[i].angle = internalNodeAngle(pd[i].children[0], pd[i].children[1]);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// find root
|
|
271
|
-
var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
272
|
-
|
|
273
|
-
// sort the data temporarily in decreasing parentId
|
|
274
|
-
pd.sort((a, b) => b.thisId - a.thisId);
|
|
275
|
-
|
|
276
|
-
// get the branchlength of the parentID
|
|
277
|
-
function getParentBranchLength(current_parentId) {
|
|
278
|
-
for (var i = 0; i < pd.length; i++) {
|
|
279
|
-
if (pd[i].thisId === current_parentId) {
|
|
280
|
-
var branchLength = pd[i].r;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return branchLength;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// assign depths (branch lengths) to radii
|
|
287
|
-
for (let i = 0; i < pd.length; i++) {
|
|
288
|
-
// special cases where parent is the root.
|
|
289
|
-
if (pd[i].parentId === root) {
|
|
290
|
-
|
|
291
|
-
if (pd[i].isTip === true) {
|
|
292
|
-
// radius is branch length at root?
|
|
293
|
-
pd[i].r = pd[i].branchLength;
|
|
294
|
-
pd[i].x = pd[i].branchLength;
|
|
295
|
-
pd[i].y = 0;
|
|
296
|
-
|
|
297
|
-
} else { // it's a node
|
|
298
|
-
// radius is branch length at root?
|
|
299
|
-
pd[i].r = pd[i].branchLength;
|
|
300
|
-
pd[i].x = pd[i].branchLength * Math.cos(pd[i].angle);
|
|
301
|
-
pd[i].y = pd[i].branchLength * Math.sin(pd[i].angle);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
} else {
|
|
305
|
-
// the x0 is that of the parent
|
|
306
|
-
var parent_branchLength = getParentBranchLength(pd[i].parentId);
|
|
307
|
-
// the x1 is the sum of parent and current branchlength
|
|
308
|
-
pd[i].r = parent_branchLength + pd[i].branchLength;
|
|
309
|
-
// at the same time we can make x and y from polar coordinates
|
|
310
|
-
// this is creating some NaN's, may cause problems later.
|
|
311
|
-
pd[i].x = (parent_branchLength + pd[i].branchLength) * Math.cos(pd[i].angle);
|
|
312
|
-
pd[i].y = (parent_branchLength + pd[i].branchLength) * Math.sin(pd[i].angle);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// at this point the first element is the root
|
|
317
|
-
pd[0].r = 0;
|
|
318
|
-
pd[0].x = 0;
|
|
319
|
-
pd[0].y = 0;
|
|
320
|
-
|
|
321
|
-
// return the original sorted data
|
|
322
|
-
//pd.sort((a,b) => a.thisId - b.thisId)
|
|
323
|
-
|
|
324
|
-
return pd;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Take a parsed tree and get the radii of each of the nodes.
|
|
329
|
-
*/
|
|
330
|
-
|
|
331
|
-
function getRadii (node) {
|
|
332
|
-
var data = radialData(node);
|
|
333
|
-
|
|
334
|
-
// for the current iteration of the loop find the matching parentId
|
|
335
|
-
function getRadius(current_node) {
|
|
336
|
-
for (var i = 0; i < data.length; i++) {
|
|
337
|
-
if (data[i].thisId === current_node.parentId) {
|
|
338
|
-
var radius = data[i].r;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return radius;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
var arcs = [];
|
|
345
|
-
// find root
|
|
346
|
-
var root = data.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
347
|
-
|
|
348
|
-
for (var i = 0; i < data.length; i++) {
|
|
349
|
-
if (data[i].thisId !== root) {
|
|
350
|
-
|
|
351
|
-
arcs.push({
|
|
352
|
-
'thisId': data[i].thisId,
|
|
353
|
-
'thisLabel': data[i].thisLabel,
|
|
354
|
-
'x0': data[i].x,
|
|
355
|
-
// radius of the parent * cos(angle)
|
|
356
|
-
'x1': getRadius(data[i]) * Math.cos(data[i].angle),
|
|
357
|
-
'y0': data[i].y,
|
|
358
|
-
// radius of the parent * sin(angle)
|
|
359
|
-
'y1': getRadius(data[i]) * Math.sin(data[i].angle),
|
|
360
|
-
'isTip': data[i].isTip
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return arcs;
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Takes parent data and returns a start value (radians),
|
|
371
|
-
* end value (radians), and radius of circle to draw an
|
|
372
|
-
* arc from.
|
|
373
|
-
* thanks https://codereview.stackexchange.com/questions/187510/angle-reflection-function
|
|
374
|
-
*/
|
|
375
|
-
|
|
376
|
-
function reflectAngle (rad, dir) {
|
|
377
|
-
const c = Math.cos(rad), s = Math.sin(rad);
|
|
378
|
-
const PI_sub = "3.1415";
|
|
379
|
-
|
|
380
|
-
function checkSign(x) {
|
|
381
|
-
if (x.toString().includes(PI_sub)) {
|
|
382
|
-
x = Math.abs(x);
|
|
383
|
-
}
|
|
384
|
-
return x;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return checkSign(Math.atan2(...(dir === "X" ? [s, -c] : [-s, c])));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Takes parent data and returns a start value (radians),
|
|
392
|
-
* end value (radians), and radius of circle to draw an
|
|
393
|
-
* arc from
|
|
394
|
-
*/
|
|
395
|
-
|
|
396
|
-
function getArcs (pd) {
|
|
397
|
-
// must return start of arc and end
|
|
398
|
-
// these are pairs of edges that have the same parent
|
|
399
|
-
// start is the min(angle) of the children and end is the max(angle)
|
|
400
|
-
// radius is the radius of the parent
|
|
401
|
-
// origin is 0, 0
|
|
402
|
-
|
|
403
|
-
var data = [];
|
|
404
|
-
var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// get the branchlength of the parentID
|
|
408
|
-
function sisterAngle(current_parentId) {
|
|
409
|
-
for (var i = 0; i < pd.length; i++) {
|
|
410
|
-
if (pd[i].parentId === current_parentId) {
|
|
411
|
-
var sister_angle = pd[i].angle;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return sister_angle;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function parentRadius(current_parentId) {
|
|
418
|
-
for (var i = 0; i < pd.length; i++) {
|
|
419
|
-
if (pd[i].thisId === current_parentId) {
|
|
420
|
-
var parent_r = pd[i].r;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
return parent_r;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
for (let i = 0; i < pd.length; i++) {
|
|
427
|
-
if (pd[i].thisId !== root) {
|
|
428
|
-
data.push({
|
|
429
|
-
'start': reflectAngle(Math.min(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
430
|
-
'end': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
431
|
-
'radius': parentRadius(pd[i].parentId),
|
|
432
|
-
'thisId': pd[i].thisId,
|
|
433
|
-
'parentId': pd[i].parentId
|
|
434
|
-
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// TODO: understand why this works (and it may not in every case)... test!
|
|
439
|
-
for (let i = 0; i < data.length; i++) {
|
|
440
|
-
if (Math.sign(data[i].start) !== Math.sign(data[i].end)) {
|
|
441
|
-
data[i].end = Math.abs(data[i].end);
|
|
442
|
-
data[i].start = -Math.abs(data[i].start);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
return data.filter(d => d.start !== d.end & d.radius !== 0);
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Simple wrapper function for getting the data,
|
|
452
|
-
* radii, and arcs.
|
|
453
|
-
*/
|
|
454
|
-
|
|
455
|
-
function radialLayout (node) {
|
|
456
|
-
var data = {};
|
|
457
|
-
|
|
458
|
-
// TODO: does radial_data need to be printed out?
|
|
459
|
-
data.data = radialData(node);
|
|
460
|
-
data.radii = getRadii(node);
|
|
461
|
-
data.arcs = getArcs(data.data);
|
|
462
|
-
|
|
463
|
-
return data;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Rectangle layout algorithm.
|
|
468
|
-
* Attempted copy of .layout.rect() function here https://github.com/ArtPoon/ggfree/blob/master/R/tree.R
|
|
469
|
-
*/
|
|
470
|
-
|
|
471
|
-
function getHorizontal(node) {
|
|
472
|
-
const pd = fortify(node);
|
|
473
|
-
|
|
474
|
-
// Fast lookup from id -> pd index
|
|
475
|
-
const idIndex = new Map(pd.map((d, i) => [d.thisId, i]));
|
|
476
|
-
|
|
477
|
-
// 1) Leaf order from the INPUT TREE (respects your child order / ladderize)
|
|
478
|
-
const leafIds = [];
|
|
479
|
-
(function dfs(n) {
|
|
480
|
-
if (!n.children || n.children.length === 0) { leafIds.push(n.id); return; }
|
|
481
|
-
n.children.forEach(dfs);
|
|
482
|
-
})(node);
|
|
483
|
-
|
|
484
|
-
// Map each leaf id to a vertical slot (1..N)
|
|
485
|
-
const tipSlot = new Map(leafIds.map((id, i) => [id, i + 1]));
|
|
486
|
-
|
|
487
|
-
// 2) Set Y for tips directly from that order; compute Y for internal nodes via postorder
|
|
488
|
-
(function setY(n) {
|
|
489
|
-
const i = idIndex.get(n.id);
|
|
490
|
-
if (!n.children || n.children.length === 0) {
|
|
491
|
-
const y = tipSlot.get(n.id);
|
|
492
|
-
pd[i].y0 = y; pd[i].y1 = y;
|
|
493
|
-
return y;
|
|
494
|
-
}
|
|
495
|
-
const ys = n.children.map(setY);
|
|
496
|
-
const y = mean(ys);
|
|
497
|
-
pd[i].y0 = y; pd[i].y1 = y;
|
|
498
|
-
return y;
|
|
499
|
-
})(node);
|
|
500
|
-
|
|
501
|
-
// 3) Set X by accumulating branch lengths down the tree (no sorting-by-id needed)
|
|
502
|
-
(function setX(n, xParent) {
|
|
503
|
-
const i = idIndex.get(n.id);
|
|
504
|
-
const bl = pd[i].branchLength ?? 0;
|
|
505
|
-
const x0 = xParent ?? 0;
|
|
506
|
-
const x1 = x0 + bl;
|
|
507
|
-
pd[i].x0 = x0; pd[i].x1 = x1;
|
|
508
|
-
if (n.children && n.children.length) n.children.forEach(c => setX(c, x1));
|
|
509
|
-
})(node, 0);
|
|
510
|
-
|
|
511
|
-
// Clean up and return
|
|
512
|
-
return pd.map(({ y, x, angle, ...item }) => item);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
function getVertical (node) {
|
|
516
|
-
const data = getHorizontal(node);
|
|
517
|
-
|
|
518
|
-
// Group rows by parentId (children that share a parent)
|
|
519
|
-
const byParent = new Map();
|
|
520
|
-
for (const row of data) {
|
|
521
|
-
if (row.parentId == null) continue;
|
|
522
|
-
const a = byParent.get(row.parentId);
|
|
523
|
-
if (a) a.push(row); else byParent.set(row.parentId, [row]);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const verticals = [];
|
|
527
|
-
for (const [parentId, kids] of byParent.entries()) {
|
|
528
|
-
if (!kids.length) continue;
|
|
529
|
-
// Works for binary and multifurcations:
|
|
530
|
-
const yvals = kids.map(d => d.y0);
|
|
531
|
-
const y0 = Math.min(...yvals);
|
|
532
|
-
const y1 = Math.max(...yvals);
|
|
533
|
-
// All children share the same junction x (their x0)
|
|
534
|
-
const x = kids[0].x0;
|
|
535
|
-
|
|
536
|
-
verticals.push({
|
|
537
|
-
parentId,
|
|
538
|
-
x0: x,
|
|
539
|
-
x1: x,
|
|
540
|
-
y0,
|
|
541
|
-
y1,
|
|
542
|
-
heights: y1 - y0
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return verticals;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Simple wrapper for rectangle layout functions
|
|
551
|
-
*/
|
|
552
|
-
|
|
553
|
-
function rectangleLayout (node) {
|
|
554
|
-
var data = {};
|
|
555
|
-
|
|
556
|
-
data.data = getHorizontal(node); // horizontal_lines
|
|
557
|
-
data.vertical_lines = getVertical(node);
|
|
558
|
-
|
|
559
|
-
return data;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Convert parsed Newick tree from fortify() into data frame of edges
|
|
564
|
-
* this is akin to a "phylo" object in R, where thisID and parentId
|
|
565
|
-
* are the $edge slot. I think.
|
|
566
|
-
*/
|
|
567
|
-
|
|
568
|
-
function edges (df, rectangular = false) {
|
|
569
|
-
var result = [],
|
|
570
|
-
parent;
|
|
571
|
-
|
|
572
|
-
// make sure data frame is sorted
|
|
573
|
-
df.sort(function (a, b) {
|
|
574
|
-
return a.thisId - b.thisId;
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
for (const row of df) {
|
|
578
|
-
if (row.parentId === null) {
|
|
579
|
-
continue; // skip the root
|
|
580
|
-
}
|
|
581
|
-
parent = df[row.parentId];
|
|
582
|
-
if (parent === null || parent === undefined) continue;
|
|
583
|
-
|
|
584
|
-
if (rectangular) {
|
|
585
|
-
var pair1 = {
|
|
586
|
-
x1: row.x,
|
|
587
|
-
y1: row.y,
|
|
588
|
-
id1: row.thisId,
|
|
589
|
-
x2: parent.x,
|
|
590
|
-
y2: row.y,
|
|
591
|
-
id2: undefined
|
|
592
|
-
};
|
|
593
|
-
result.push(pair1);
|
|
594
|
-
var pair2 = {
|
|
595
|
-
x1: parent.x,
|
|
596
|
-
y1: row.y,
|
|
597
|
-
id1: undefined,
|
|
598
|
-
x2: parent.x,
|
|
599
|
-
y2: parent.y,
|
|
600
|
-
id2: row.parentId
|
|
601
|
-
};
|
|
602
|
-
result.push(pair2);
|
|
603
|
-
} else {
|
|
604
|
-
var pair3 = {
|
|
605
|
-
x1: row.x,
|
|
606
|
-
y1: row.y,
|
|
607
|
-
id1: row.thisId,
|
|
608
|
-
x2: parent.x,
|
|
609
|
-
y2: parent.y,
|
|
610
|
-
id2: row.parentId
|
|
611
|
-
};
|
|
612
|
-
result.push(pair3);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
return result;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Equal-angle layout algorithm for unrooted trees.
|
|
620
|
-
* Populates the nodes of a tree object with information on
|
|
621
|
-
* the angles to draw branches such that they do not
|
|
622
|
-
* intersect.
|
|
623
|
-
*/
|
|
624
|
-
|
|
625
|
-
function equalAngleLayout(node) {
|
|
626
|
-
if (node.parent === null) {
|
|
627
|
-
// node is root
|
|
628
|
-
node.start = 0.; // guarantees no arcs overlap 0
|
|
629
|
-
node.end = 2.; // *pi
|
|
630
|
-
node.angle = 0.; // irrelevant
|
|
631
|
-
node.ntips = numTips(node);
|
|
632
|
-
node.x = 0;
|
|
633
|
-
node.y = 0;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
var child, arc, lastStart = node.start;
|
|
637
|
-
|
|
638
|
-
for (var i = 0; i < node.children.length; i++) {
|
|
639
|
-
// the child of the current node
|
|
640
|
-
child = node.children[i];
|
|
641
|
-
// the number of tips the child node has
|
|
642
|
-
child.ntips = numTips(child);
|
|
643
|
-
|
|
644
|
-
// assign proportion of arc to this child
|
|
645
|
-
arc = (node.end - node.start) * child.ntips / node.ntips;
|
|
646
|
-
child.start = lastStart;
|
|
647
|
-
child.end = child.start + arc;
|
|
648
|
-
|
|
649
|
-
// bisect the arc
|
|
650
|
-
child.angle = child.start + (child.end - child.start) / 2.;
|
|
651
|
-
lastStart = child.end;
|
|
652
|
-
|
|
653
|
-
// map to coordinates
|
|
654
|
-
child.x = node.x + child.branchLength * Math.sin(child.angle * Math.PI);
|
|
655
|
-
child.y = node.y + child.branchLength * Math.cos(child.angle * Math.PI);
|
|
656
|
-
|
|
657
|
-
// climb up
|
|
658
|
-
equalAngleLayout(child);
|
|
659
|
-
}
|
|
660
|
-
// had to add this!
|
|
661
|
-
return node;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Simple wrapper function for equalAngleLayout()
|
|
666
|
-
*/
|
|
667
|
-
|
|
668
|
-
function unrooted (node) {
|
|
669
|
-
var data = {};
|
|
670
|
-
// use the Felsenstein equal angle layout algorithm
|
|
671
|
-
var eq = fortify(equalAngleLayout(node));
|
|
672
|
-
data.data = eq;
|
|
673
|
-
// make the edges dataset
|
|
674
|
-
data.edges = edges(eq);
|
|
675
|
-
|
|
676
|
-
return data;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// find the x & y coordinates of the parental species
|
|
680
|
-
function parentFisheye (d, data /* e.g. lwPhylo.unrooted.data */) {
|
|
681
|
-
for (let i = 0; i < data.length; i++) {
|
|
682
|
-
if (d.parentId === data[i].thisId) {
|
|
683
|
-
return {
|
|
684
|
-
px: data[i].fisheye.x,
|
|
685
|
-
py: data[i].fisheye.y
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
* Parse a Newick tree string into a doubly-linked
|
|
693
|
-
* list of JS Objects. Assigns node labels, branch
|
|
694
|
-
* lengths and node IDs (numbering terminal before
|
|
695
|
-
* internal nodes).
|
|
696
|
-
*/
|
|
697
|
-
|
|
698
|
-
function readTree (text) {
|
|
699
|
-
// remove whitespace
|
|
700
|
-
text = text.replace(/ \t/g, '');
|
|
701
|
-
|
|
702
|
-
var tokens = text.split(/(;|\(|\)|,)/),
|
|
703
|
-
root = { 'parent': null, 'children': [] },
|
|
704
|
-
curnode = root,
|
|
705
|
-
nodeId = 0;
|
|
706
|
-
|
|
707
|
-
for (const token of tokens) {
|
|
708
|
-
if (token == "" || token == ';') {
|
|
709
|
-
continue
|
|
710
|
-
}
|
|
711
|
-
if (token == '(') {
|
|
712
|
-
// add a child to current node
|
|
713
|
-
let child = {
|
|
714
|
-
'parent': curnode,
|
|
715
|
-
'children': []
|
|
716
|
-
};
|
|
717
|
-
curnode.children.push(child);
|
|
718
|
-
curnode = child; // climb up
|
|
719
|
-
}
|
|
720
|
-
else if (token == ',') {
|
|
721
|
-
// climb down, add another child to parent
|
|
722
|
-
curnode = curnode.parent;
|
|
723
|
-
let child = {
|
|
724
|
-
'parent': curnode,
|
|
725
|
-
'children': []
|
|
726
|
-
};
|
|
727
|
-
curnode.children.push(child);
|
|
728
|
-
curnode = child; // climb up
|
|
729
|
-
}
|
|
730
|
-
else if (token == ')') {
|
|
731
|
-
// climb down twice
|
|
732
|
-
curnode = curnode.parent;
|
|
733
|
-
if (curnode === null) {
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
var nodeinfo = token.split(':');
|
|
739
|
-
|
|
740
|
-
if (nodeinfo.length == 1) {
|
|
741
|
-
if (token.startsWith(':')) {
|
|
742
|
-
curnode.label = "";
|
|
743
|
-
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
744
|
-
} else {
|
|
745
|
-
curnode.label = nodeinfo[0];
|
|
746
|
-
curnode.branchLength = null;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
else if (nodeinfo.length == 2) {
|
|
750
|
-
curnode.label = nodeinfo[0];
|
|
751
|
-
curnode.branchLength = parseFloat(nodeinfo[1]);
|
|
752
|
-
}
|
|
753
|
-
else {
|
|
754
|
-
// TODO: handle edge cases with >1 ":"
|
|
755
|
-
console.warn(token, "I don't know what to do with two colons!");
|
|
756
|
-
}
|
|
757
|
-
curnode.id = nodeId++; // assign then increment
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
curnode.id = nodeId;
|
|
762
|
-
|
|
763
|
-
return (root);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Subset a tree given a node - i.e. the node of interests and all the descendents
|
|
768
|
-
*/
|
|
769
|
-
|
|
770
|
-
function subTree (tree, node) {
|
|
771
|
-
// Thanks Richard Challis!
|
|
772
|
-
let fullTree = {};
|
|
773
|
-
tree.data.forEach(obj => {
|
|
774
|
-
fullTree[obj.thisId] = { ...obj };
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
let subTree = {};
|
|
778
|
-
const getDescendants = function (rootNodeId) {
|
|
779
|
-
if (fullTree[rootNodeId]) {
|
|
780
|
-
subTree[rootNodeId] = fullTree[rootNodeId];
|
|
781
|
-
if (fullTree[rootNodeId].children) {
|
|
782
|
-
fullTree[rootNodeId].children.forEach(childNodeId => {
|
|
783
|
-
getDescendants(childNodeId);
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
// call the recursive function
|
|
789
|
-
getDescendants(node);
|
|
790
|
-
|
|
791
|
-
// in each of the functions, data contains the children key
|
|
792
|
-
const data = [["data", Object.values(subTree)]];
|
|
793
|
-
|
|
794
|
-
const nodes = data[0][1].map(d => d.thisId);
|
|
795
|
-
|
|
796
|
-
var res = [];
|
|
797
|
-
// in all keys except data, push to new array
|
|
798
|
-
for (const node in tree) {
|
|
799
|
-
if (node === "data") continue;
|
|
800
|
-
res.push([node, tree[node]]);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
var filtered = res.map(d => [
|
|
804
|
-
d[0],
|
|
805
|
-
d[1].filter(d => nodes.includes(d.thisId))
|
|
806
|
-
]);
|
|
807
|
-
|
|
808
|
-
return Object.fromEntries(data.concat(filtered));
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
exports.describeArc = describeArc;
|
|
812
|
-
exports.parentFisheye = parentFisheye;
|
|
813
|
-
exports.phisheye = phisheye;
|
|
814
|
-
exports.radialLayout = radialLayout;
|
|
815
|
-
exports.readTree = readTree;
|
|
816
|
-
exports.rectangleLayout = rectangleLayout;
|
|
817
|
-
exports.subTree = subTree;
|
|
818
|
-
exports.unrooted = unrooted;
|
|
819
|
-
|
|
820
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
821
|
-
|
|
822
|
-
})));
|