react-msaview 5.0.13 → 5.0.16
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/bundle/index.js +135 -35
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/components/SequenceTextArea.js +4 -4
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +1 -1
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +52 -11
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/hierarchy.d.ts +1 -0
- package/dist/hierarchy.js.map +1 -1
- package/dist/model.js +1 -0
- package/dist/model.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/src/components/SequenceTextArea.tsx +4 -4
- package/src/components/import/ImportFormExamples.tsx +1 -1
- package/src/components/tree/renderTreeCanvas.test.ts +205 -0
- package/src/components/tree/renderTreeCanvas.ts +60 -10
- package/src/hierarchy.ts +1 -0
- package/src/model.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
function calcDepthToLeaf(node: any): number {
|
|
4
|
+
if (node.depthToLeaf !== undefined) {
|
|
5
|
+
return node.depthToLeaf
|
|
6
|
+
}
|
|
7
|
+
if (!node.children || node.children.length === 0) {
|
|
8
|
+
node.depthToLeaf = 0
|
|
9
|
+
} else {
|
|
10
|
+
let maxDepth = 0
|
|
11
|
+
for (const child of node.children) {
|
|
12
|
+
maxDepth = Math.max(maxDepth, 1 + calcDepthToLeaf(child))
|
|
13
|
+
}
|
|
14
|
+
node.depthToLeaf = maxDepth
|
|
15
|
+
}
|
|
16
|
+
return node.depthToLeaf
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findMaxBranchLen(node: any): number {
|
|
20
|
+
let maxLen = node.len || 0
|
|
21
|
+
if (node.children) {
|
|
22
|
+
for (const child of node.children) {
|
|
23
|
+
maxLen = Math.max(maxLen, findMaxBranchLen(child))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return maxLen
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getNodeX(
|
|
30
|
+
node: any,
|
|
31
|
+
showBranchLen: boolean,
|
|
32
|
+
maxBranchLen: number,
|
|
33
|
+
maxDepthToLeaf: number,
|
|
34
|
+
): number | undefined {
|
|
35
|
+
if (showBranchLen) {
|
|
36
|
+
return node.len
|
|
37
|
+
}
|
|
38
|
+
const depthToLeaf = calcDepthToLeaf(node)
|
|
39
|
+
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('Tree rendering positioning', () => {
|
|
43
|
+
describe('calcDepthToLeaf', () => {
|
|
44
|
+
it('should return 0 for leaf nodes', () => {
|
|
45
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
46
|
+
expect(calcDepthToLeaf(leaf)).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should return 1 for nodes with only leaf children', () => {
|
|
50
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
51
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
52
|
+
const parent: any = {
|
|
53
|
+
id: 'parent',
|
|
54
|
+
data: { id: 'parent' },
|
|
55
|
+
children: [leaf1, leaf2],
|
|
56
|
+
}
|
|
57
|
+
expect(calcDepthToLeaf(parent)).toBe(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should return correct depth for nested tree', () => {
|
|
61
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
62
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
63
|
+
const intermediate: any = {
|
|
64
|
+
id: 'int',
|
|
65
|
+
data: { id: 'int' },
|
|
66
|
+
children: [leaf1, leaf2],
|
|
67
|
+
}
|
|
68
|
+
const root: any = {
|
|
69
|
+
id: 'root',
|
|
70
|
+
data: { id: 'root' },
|
|
71
|
+
children: [intermediate],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(calcDepthToLeaf(leaf1)).toBe(0)
|
|
75
|
+
expect(calcDepthToLeaf(intermediate)).toBe(1)
|
|
76
|
+
expect(calcDepthToLeaf(root)).toBe(2)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should cache result', () => {
|
|
80
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
81
|
+
const depth1 = calcDepthToLeaf(leaf)
|
|
82
|
+
const depth2 = calcDepthToLeaf(leaf)
|
|
83
|
+
expect(depth1).toBe(depth2)
|
|
84
|
+
expect(leaf.depthToLeaf).toBeDefined()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('findMaxBranchLen', () => {
|
|
89
|
+
it('should return node len for leaf', () => {
|
|
90
|
+
const leaf: any = {
|
|
91
|
+
id: 'leaf',
|
|
92
|
+
data: { id: 'leaf' },
|
|
93
|
+
len: 1.5,
|
|
94
|
+
children: null,
|
|
95
|
+
}
|
|
96
|
+
expect(findMaxBranchLen(leaf)).toBe(1.5)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should return max len from descendants', () => {
|
|
100
|
+
const leaf1: any = {
|
|
101
|
+
id: 'leaf1',
|
|
102
|
+
data: { id: 'leaf1' },
|
|
103
|
+
len: 0.5,
|
|
104
|
+
children: null,
|
|
105
|
+
}
|
|
106
|
+
const leaf2: any = {
|
|
107
|
+
id: 'leaf2',
|
|
108
|
+
data: { id: 'leaf2' },
|
|
109
|
+
len: 1.5,
|
|
110
|
+
children: null,
|
|
111
|
+
}
|
|
112
|
+
const parent: any = {
|
|
113
|
+
id: 'parent',
|
|
114
|
+
data: { id: 'parent' },
|
|
115
|
+
len: 0.3,
|
|
116
|
+
children: [leaf1, leaf2],
|
|
117
|
+
}
|
|
118
|
+
expect(findMaxBranchLen(parent)).toBe(1.5)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should handle undefined len', () => {
|
|
122
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
123
|
+
expect(findMaxBranchLen(leaf)).toBe(0)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('getNodeX cladogram positioning', () => {
|
|
128
|
+
it('should position all leaves at rightmost for cladogram', () => {
|
|
129
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
130
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
131
|
+
const root: any = {
|
|
132
|
+
id: 'root',
|
|
133
|
+
data: { id: 'root' },
|
|
134
|
+
children: [leaf1, leaf2],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
calcDepthToLeaf(root)
|
|
138
|
+
const maxBranchLen = 100
|
|
139
|
+
const maxDepthToLeaf = 1
|
|
140
|
+
|
|
141
|
+
const x1 = getNodeX(leaf1, false, maxBranchLen, maxDepthToLeaf)
|
|
142
|
+
const x2 = getNodeX(leaf2, false, maxBranchLen, maxDepthToLeaf)
|
|
143
|
+
|
|
144
|
+
expect(x1).toBe(maxBranchLen)
|
|
145
|
+
expect(x2).toBe(maxBranchLen)
|
|
146
|
+
expect(x1).toBe(x2)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should position root at leftmost for cladogram', () => {
|
|
150
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
151
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
152
|
+
const root: any = {
|
|
153
|
+
id: 'root',
|
|
154
|
+
data: { id: 'root' },
|
|
155
|
+
children: [leaf1, leaf2],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
calcDepthToLeaf(root)
|
|
159
|
+
const maxBranchLen = 100
|
|
160
|
+
const maxDepthToLeaf = 1
|
|
161
|
+
|
|
162
|
+
const xRoot = getNodeX(root, false, maxBranchLen, maxDepthToLeaf)
|
|
163
|
+
expect(xRoot).toBe(0)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should position internal nodes between root and leaves', () => {
|
|
167
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
168
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
169
|
+
const intermediate: any = {
|
|
170
|
+
id: 'int',
|
|
171
|
+
data: { id: 'int' },
|
|
172
|
+
children: [leaf1, leaf2],
|
|
173
|
+
}
|
|
174
|
+
const root: any = {
|
|
175
|
+
id: 'root',
|
|
176
|
+
data: { id: 'root' },
|
|
177
|
+
children: [intermediate],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
calcDepthToLeaf(root)
|
|
181
|
+
const maxBranchLen = 100
|
|
182
|
+
const maxDepthToLeaf = 2
|
|
183
|
+
|
|
184
|
+
const xRoot = getNodeX(root, false, maxBranchLen, maxDepthToLeaf)!
|
|
185
|
+
const xInt = getNodeX(intermediate, false, maxBranchLen, maxDepthToLeaf)!
|
|
186
|
+
const xLeaf = getNodeX(leaf1, false, maxBranchLen, maxDepthToLeaf)!
|
|
187
|
+
|
|
188
|
+
expect(xRoot).toBe(0)
|
|
189
|
+
expect(xInt).toBeGreaterThan(xRoot)
|
|
190
|
+
expect(xLeaf).toBeGreaterThan(xInt)
|
|
191
|
+
expect(xLeaf).toBe(maxBranchLen)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should use branch length when showBranchLen is true', () => {
|
|
195
|
+
const leaf: any = {
|
|
196
|
+
id: 'leaf',
|
|
197
|
+
data: { id: 'leaf' },
|
|
198
|
+
len: 2.5,
|
|
199
|
+
children: null,
|
|
200
|
+
}
|
|
201
|
+
const x = getNodeX(leaf, true, 100, 1)
|
|
202
|
+
expect(x).toBe(2.5)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { descendants, links } from '../../hierarchy.ts'
|
|
2
2
|
|
|
3
|
+
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
3
4
|
import type { MsaViewModel } from '../../model.ts'
|
|
4
5
|
import type { Theme } from '@mui/material'
|
|
5
6
|
|
|
@@ -9,6 +10,53 @@ const extendBounds = 5
|
|
|
9
10
|
const radius = 2.5
|
|
10
11
|
const d = radius * 2
|
|
11
12
|
|
|
13
|
+
// Cladogram positioning algorithm based on ape package's plot.phylo
|
|
14
|
+
// Uses topological depth (steps to tips) instead of branch length for x-positioning
|
|
15
|
+
// This ensures all leaf nodes align at the same x-coordinate (rightmost position)
|
|
16
|
+
// See: https://github.com/emmanuelparadis/ape/blob/master/R/plot.phylo.R
|
|
17
|
+
function calcDepthToLeaf(node: HierarchyNode): number {
|
|
18
|
+
if (node.depthToLeaf !== undefined) {
|
|
19
|
+
return node.depthToLeaf
|
|
20
|
+
}
|
|
21
|
+
if (!node.children || node.children.length === 0) {
|
|
22
|
+
node.depthToLeaf = 0
|
|
23
|
+
} else {
|
|
24
|
+
let maxDepth = 0
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
maxDepth = Math.max(maxDepth, 1 + calcDepthToLeaf(child))
|
|
27
|
+
}
|
|
28
|
+
node.depthToLeaf = maxDepth
|
|
29
|
+
}
|
|
30
|
+
return node.depthToLeaf
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findMaxBranchLen(node: HierarchyNode): number {
|
|
34
|
+
let maxLen = node.len || 0
|
|
35
|
+
if (node.children) {
|
|
36
|
+
for (const child of node.children) {
|
|
37
|
+
maxLen = Math.max(maxLen, findMaxBranchLen(child))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return maxLen
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Calculate node x-coordinate for both phylogram (with branch lengths) and cladogram (topology only) modes
|
|
44
|
+
// For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
|
|
45
|
+
// This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
|
|
46
|
+
// Matches ape's: xx <- max(xx) - xx (where xx is depth from each node to tips)
|
|
47
|
+
function getNodeX(
|
|
48
|
+
node: HierarchyNode,
|
|
49
|
+
showBranchLen: boolean,
|
|
50
|
+
maxBranchLen: number,
|
|
51
|
+
maxDepthToLeaf: number,
|
|
52
|
+
): number | undefined {
|
|
53
|
+
if (showBranchLen) {
|
|
54
|
+
return node.len
|
|
55
|
+
}
|
|
56
|
+
const depthToLeaf = calcDepthToLeaf(node)
|
|
57
|
+
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
interface ClickEntry {
|
|
13
61
|
name: string
|
|
14
62
|
id: string
|
|
@@ -47,15 +95,14 @@ export function renderTree({
|
|
|
47
95
|
const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
|
|
48
96
|
const by = blockSizeYOverride || blockSize
|
|
49
97
|
ctx.strokeStyle = theme.palette.text.primary
|
|
98
|
+
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
99
|
+
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
50
100
|
for (const link of links(hierarchy)) {
|
|
51
101
|
const { source, target } = link
|
|
52
|
-
if (target.height === 0 && !showBranchLen) {
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
102
|
const sy = source.x!
|
|
56
103
|
const ty = target.x!
|
|
57
|
-
const tx = showBranchLen
|
|
58
|
-
const sx = showBranchLen
|
|
104
|
+
const tx = getNodeX(target, showBranchLen, maxBranchLen, maxDepthToLeaf)
|
|
105
|
+
const sx = getNodeX(source, showBranchLen, maxBranchLen, maxDepthToLeaf)
|
|
59
106
|
if (tx === undefined || sx === undefined) {
|
|
60
107
|
continue
|
|
61
108
|
}
|
|
@@ -96,8 +143,10 @@ export function renderNodeBubbles({
|
|
|
96
143
|
marginLeft: ml,
|
|
97
144
|
} = model
|
|
98
145
|
const by = blockSizeYOverride || blockSize
|
|
146
|
+
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
147
|
+
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
99
148
|
for (const node of descendants(hierarchy)) {
|
|
100
|
-
const x = showBranchLen
|
|
149
|
+
const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
|
|
101
150
|
if (x === undefined) {
|
|
102
151
|
continue
|
|
103
152
|
}
|
|
@@ -105,7 +154,7 @@ export function renderNodeBubbles({
|
|
|
105
154
|
const y = node.x!
|
|
106
155
|
const { id, name } = data
|
|
107
156
|
if (
|
|
108
|
-
node.height
|
|
157
|
+
node.height >= 1 &&
|
|
109
158
|
y > offsetY - extendBounds &&
|
|
110
159
|
y < offsetY + by + extendBounds
|
|
111
160
|
) {
|
|
@@ -156,6 +205,7 @@ export function renderTreeLabels({
|
|
|
156
205
|
marginLeft,
|
|
157
206
|
leaves,
|
|
158
207
|
noTree,
|
|
208
|
+
hierarchy,
|
|
159
209
|
} = model
|
|
160
210
|
const by = blockSizeYOverride || blockSize
|
|
161
211
|
const emHeight = ctx.measureText('M').width
|
|
@@ -165,13 +215,13 @@ export function renderTreeLabels({
|
|
|
165
215
|
} else {
|
|
166
216
|
ctx.textAlign = 'start'
|
|
167
217
|
}
|
|
218
|
+
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
219
|
+
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
168
220
|
for (const node of leaves) {
|
|
169
221
|
const {
|
|
170
222
|
data: { name, id },
|
|
171
223
|
} = node
|
|
172
|
-
const len = node.len
|
|
173
224
|
const y = node.x!
|
|
174
|
-
const x = node.y!
|
|
175
225
|
|
|
176
226
|
const displayName = treeMetadata[name]?.genome || name
|
|
177
227
|
if (y > offsetY - extendBounds && y < offsetY + by + extendBounds) {
|
|
@@ -179,7 +229,7 @@ export function renderTreeLabels({
|
|
|
179
229
|
const yp = y + fontSize / 4
|
|
180
230
|
let xp = 0
|
|
181
231
|
if (!noTree) {
|
|
182
|
-
xp = (showBranchLen
|
|
232
|
+
xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) || 0
|
|
183
233
|
}
|
|
184
234
|
|
|
185
235
|
const { width } = ctx.measureText(displayName)
|
package/src/hierarchy.ts
CHANGED
package/src/model.ts
CHANGED
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.0.
|
|
1
|
+
export const version = '5.0.16'
|