react-msaview 5.7.3 → 5.9.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/bundle/index.js +99 -99
- package/bundle/index.js.map +4 -4
- package/dist/components/SequenceTextArea.js +2 -2
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js +6 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
- package/dist/components/header/getDomainsMenu.js +1 -1
- package/dist/components/header/getDomainsMenu.js.map +1 -1
- package/dist/components/import/ImportForm.js +7 -11
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +7 -11
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/import/util.d.ts +1 -1
- package/dist/components/import/util.js +1 -1
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/msa/DomainLegend.js +4 -5
- package/dist/components/msa/DomainLegend.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +2 -2
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +9 -9
- package/dist/components/tree/renderTreeCanvas.js +36 -24
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/flatToTree.d.ts +3 -1
- package/dist/flatToTree.js +16 -9
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +12 -17
- package/dist/hierarchy.js +12 -145
- package/dist/hierarchy.js.map +1 -1
- package/dist/model.d.ts +17 -1
- package/dist/model.js +34 -4
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +11 -2
- package/dist/neighborJoining.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/src/colSpace.test.ts +40 -0
- package/src/components/SequenceTextArea.tsx +2 -2
- package/src/components/dialogs/UserProvidedDomainsDialog.tsx +8 -1
- package/src/components/header/getDomainsMenu.ts +1 -1
- package/src/components/import/ImportForm.tsx +6 -9
- package/src/components/import/ImportFormExamples.tsx +11 -14
- package/src/components/import/util.ts +1 -1
- package/src/components/msa/DomainLegend.tsx +4 -3
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +3 -2
- package/src/components/tree/renderTreeCanvas.test.ts +54 -1
- package/src/components/tree/renderTreeCanvas.ts +41 -28
- package/src/constants.ts +1 -0
- package/src/flatToTree.test.ts +21 -0
- package/src/flatToTree.ts +21 -10
- package/src/hierarchy.ts +44 -179
- package/src/largeTree.test.ts +32 -0
- package/src/model.ts +34 -3
- package/src/neighborJoining.test.ts +21 -6
- package/src/neighborJoining.ts +12 -2
- package/src/parseAsn1.test.ts +16 -0
- package/src/stripDefaultSnapshot.test.ts +30 -0
- package/src/version.ts +1 -1
package/src/flatToTree.ts
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
interface FlatItem {
|
|
2
2
|
id: number
|
|
3
3
|
parent?: number
|
|
4
|
+
// NCBI BioTreeContainer features, already un-remapped by parseAsn1. Both are
|
|
5
|
+
// strings there: the ASN.1 encodes every feature value as one.
|
|
6
|
+
label?: string
|
|
7
|
+
dist?: string
|
|
4
8
|
}
|
|
5
9
|
|
|
6
10
|
interface TreeNode {
|
|
7
11
|
id: string
|
|
8
12
|
name: string
|
|
9
|
-
|
|
13
|
+
length?: number
|
|
10
14
|
children: TreeNode[]
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export function flatToTree(items: FlatItem[]): TreeNode {
|
|
14
18
|
const nodeMap = new Map(
|
|
15
|
-
items.map(item =>
|
|
16
|
-
item.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
items.map(item => {
|
|
20
|
+
const length = item.dist === undefined ? undefined : Number(item.dist)
|
|
21
|
+
return [
|
|
22
|
+
item.id,
|
|
23
|
+
{
|
|
24
|
+
id: `${item.id}`,
|
|
25
|
+
// the label is what NCBI's own tree viewer shows; falling back to the
|
|
26
|
+
// node id would render a tree of bare numbers
|
|
27
|
+
name: item.label || `${item.id}`,
|
|
28
|
+
...(length !== undefined && Number.isFinite(length)
|
|
29
|
+
? { length }
|
|
30
|
+
: {}),
|
|
31
|
+
children: [] as TreeNode[],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
}),
|
|
24
35
|
)
|
|
25
36
|
|
|
26
37
|
let root: TreeNode | undefined
|
package/src/hierarchy.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
descendants,
|
|
3
|
+
find,
|
|
4
|
+
forEachDescendant,
|
|
5
|
+
forEachLink,
|
|
6
|
+
hierarchy as coreHierarchy,
|
|
7
|
+
leaves,
|
|
8
|
+
links,
|
|
9
|
+
sort,
|
|
10
|
+
sum,
|
|
11
|
+
} from '@gmod/newick'
|
|
2
12
|
|
|
3
|
-
|
|
4
|
-
|
|
13
|
+
import type { NodeWithIds } from './types.ts'
|
|
14
|
+
import type { HierarchyNode as CoreHierarchyNode } from '@gmod/newick'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A hierarchy node plus the fields this viewer hangs off it during layout.
|
|
18
|
+
*
|
|
19
|
+
* The traversals in `@gmod/newick` are generic over the node type rather than
|
|
20
|
+
* over its data, so they take and return this extended node rather than the base
|
|
21
|
+
* one, and no call site has to cast.
|
|
22
|
+
*/
|
|
23
|
+
export interface HierarchyNode<T = NodeWithIds> extends CoreHierarchyNode<T> {
|
|
5
24
|
children: HierarchyNode<T>[] | null
|
|
6
25
|
parent: HierarchyNode<T> | null
|
|
7
|
-
depth: number
|
|
8
|
-
height: number
|
|
9
|
-
value?: number
|
|
10
26
|
x?: number
|
|
11
27
|
y?: number
|
|
12
28
|
len?: number
|
|
@@ -18,183 +34,32 @@ export interface HierarchyNode<T = NodeWithIds> {
|
|
|
18
34
|
collapsedTipXFar?: number
|
|
19
35
|
}
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return result
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function computeHeight<T>(node: HierarchyNode<T>) {
|
|
49
|
-
const nodes = descendants(node)
|
|
50
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
51
|
-
const n = nodes[i]!
|
|
52
|
-
let h = 0
|
|
53
|
-
if (n.children) {
|
|
54
|
-
for (const child of n.children) {
|
|
55
|
-
if (child.height + 1 > h) {
|
|
56
|
-
h = child.height + 1
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
n.height = h
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
37
|
+
// The generic traversals live in @gmod/newick now, shared with the tree sidebar
|
|
38
|
+
// in jbrowse-components. They are iterative there for the reason they were here:
|
|
39
|
+
// a phylogenetic tree can be a caterpillar, whose depth equals its leaf count,
|
|
40
|
+
// and the recursive form overflows the stack somewhere past 5000 tips.
|
|
41
|
+
export {
|
|
42
|
+
descendants,
|
|
43
|
+
find,
|
|
44
|
+
forEachDescendant,
|
|
45
|
+
forEachLink,
|
|
46
|
+
leaves,
|
|
47
|
+
links,
|
|
48
|
+
sort,
|
|
49
|
+
sum,
|
|
50
|
+
}
|
|
51
|
+
export type { HierarchyLink } from '@gmod/newick'
|
|
52
|
+
|
|
53
|
+
// coreHierarchy builds base nodes, and the layout fields above are written onto
|
|
54
|
+
// them afterwards by this package. Every one of those fields is optional, so the
|
|
55
|
+
// two node types are mutually assignable and this needs no cast -- it exists
|
|
56
|
+
// only to declare the wider return type, which is what lets those later
|
|
57
|
+
// assignments typecheck.
|
|
64
58
|
export function hierarchy<T>(
|
|
65
59
|
data: T,
|
|
66
60
|
childrenAccessor: (d: T) => T[] | undefined,
|
|
67
61
|
): HierarchyNode<T> {
|
|
68
|
-
|
|
69
|
-
data,
|
|
70
|
-
children: null,
|
|
71
|
-
parent: null,
|
|
72
|
-
depth: 0,
|
|
73
|
-
height: 0,
|
|
74
|
-
}
|
|
75
|
-
const stack = [root]
|
|
76
|
-
while (stack.length > 0) {
|
|
77
|
-
const node = stack.pop()!
|
|
78
|
-
const kids = childrenAccessor(node.data)
|
|
79
|
-
if (kids?.length) {
|
|
80
|
-
node.children = kids.map(d => ({
|
|
81
|
-
data: d,
|
|
82
|
-
children: null,
|
|
83
|
-
parent: node,
|
|
84
|
-
depth: node.depth + 1,
|
|
85
|
-
height: 0,
|
|
86
|
-
}))
|
|
87
|
-
for (const child of node.children) {
|
|
88
|
-
stack.push(child)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
computeHeight(root)
|
|
93
|
-
return root
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function sum<T>(
|
|
97
|
-
node: HierarchyNode<T>,
|
|
98
|
-
valueFn: (d: T) => number,
|
|
99
|
-
): HierarchyNode<T> {
|
|
100
|
-
const nodes = descendants(node)
|
|
101
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
102
|
-
const n = nodes[i]!
|
|
103
|
-
let s = valueFn(n.data)
|
|
104
|
-
if (n.children) {
|
|
105
|
-
for (const child of n.children) {
|
|
106
|
-
s += child.value!
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
n.value = s
|
|
110
|
-
}
|
|
111
|
-
return node
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function sort<T>(
|
|
115
|
-
node: HierarchyNode<T>,
|
|
116
|
-
compareFn: (a: HierarchyNode<T>, b: HierarchyNode<T>) => number,
|
|
117
|
-
): HierarchyNode<T> {
|
|
118
|
-
const stack = [node]
|
|
119
|
-
while (stack.length > 0) {
|
|
120
|
-
const n = stack.pop()!
|
|
121
|
-
if (n.children) {
|
|
122
|
-
n.children.sort(compareFn)
|
|
123
|
-
for (const child of n.children) {
|
|
124
|
-
stack.push(child)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return node
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function find<T>(
|
|
132
|
-
node: HierarchyNode<T>,
|
|
133
|
-
predicate: (n: HierarchyNode<T>) => boolean,
|
|
134
|
-
): HierarchyNode<T> | undefined {
|
|
135
|
-
const stack = [node]
|
|
136
|
-
let found: HierarchyNode<T> | undefined
|
|
137
|
-
while (stack.length > 0 && found === undefined) {
|
|
138
|
-
const n = stack.pop()!
|
|
139
|
-
if (predicate(n)) {
|
|
140
|
-
found = n
|
|
141
|
-
} else if (n.children) {
|
|
142
|
-
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
143
|
-
stack.push(n.children[i]!)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return found
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function leaves<T>(node: HierarchyNode<T>): HierarchyNode<T>[] {
|
|
151
|
-
const result: HierarchyNode<T>[] = []
|
|
152
|
-
const stack = [node]
|
|
153
|
-
while (stack.length > 0) {
|
|
154
|
-
const n = stack.pop()!
|
|
155
|
-
if (n.children) {
|
|
156
|
-
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
157
|
-
stack.push(n.children[i]!)
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
result.push(n)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return result
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function links<T>(node: HierarchyNode<T>): HierarchyLink<T>[] {
|
|
167
|
-
const result: HierarchyLink<T>[] = []
|
|
168
|
-
forEachLink(node, (source, target) => {
|
|
169
|
-
result.push({ source, target })
|
|
170
|
-
})
|
|
171
|
-
return result
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function forEachLink<T>(
|
|
175
|
-
node: HierarchyNode<T>,
|
|
176
|
-
cb: (source: HierarchyNode<T>, target: HierarchyNode<T>) => void,
|
|
177
|
-
) {
|
|
178
|
-
const stack = [node]
|
|
179
|
-
while (stack.length > 0) {
|
|
180
|
-
const n = stack.pop()!
|
|
181
|
-
if (n.children) {
|
|
182
|
-
for (let i = n.children.length - 1; i >= 0; i--) {
|
|
183
|
-
const child = n.children[i]!
|
|
184
|
-
cb(n, child)
|
|
185
|
-
stack.push(child)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function forEachDescendant<T>(
|
|
192
|
-
node: HierarchyNode<T>,
|
|
193
|
-
cb: (n: HierarchyNode<T>) => void,
|
|
194
|
-
) {
|
|
195
|
-
for (const n of descendants(node)) {
|
|
196
|
-
cb(n)
|
|
197
|
-
}
|
|
62
|
+
return coreHierarchy(data, childrenAccessor)
|
|
198
63
|
}
|
|
199
64
|
|
|
200
65
|
export function clusterLayout<T>(
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import MSAModelF from './model.ts'
|
|
5
|
+
|
|
6
|
+
// jsdom has no 2d context, and measureTextCanvas throws without one. Only the
|
|
7
|
+
// width matters here, so a stub keeps the test about tree size.
|
|
8
|
+
HTMLCanvasElement.prototype.getContext = (() => ({
|
|
9
|
+
font: '12px sans-serif',
|
|
10
|
+
measureText: (t: string) => ({ width: t.length * 7 }),
|
|
11
|
+
})) as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
12
|
+
|
|
13
|
+
// The import form ships a "230k COVID-19 samples (tree only)" example, so a tree
|
|
14
|
+
// this size is a supported input rather than a hypothetical. Every leaf gets a
|
|
15
|
+
// measured label, and anything that then reduces over them per row has to do it
|
|
16
|
+
// without passing one argument per row.
|
|
17
|
+
function flatNewick(n: number) {
|
|
18
|
+
return `(${Array.from({ length: n }, (_, i) => `s${i}:0.1`).join(',')});`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('a tree far past the argument limit still lays out', () => {
|
|
22
|
+
const model = MSAModelF().create({
|
|
23
|
+
type: 'MsaView',
|
|
24
|
+
data: { tree: flatNewick(200_000) },
|
|
25
|
+
})
|
|
26
|
+
model.setWidth(1000)
|
|
27
|
+
|
|
28
|
+
expect(model.numRows).toBe(200_000)
|
|
29
|
+
// 's199999' is the longest name, at 7 chars * 7px
|
|
30
|
+
expect(model.labelsWidth).toBe(49)
|
|
31
|
+
expect(model.treeWidth).toBeGreaterThan(0)
|
|
32
|
+
})
|
package/src/model.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
defaultScrollX,
|
|
32
32
|
defaultScrollY,
|
|
33
33
|
defaultScrollZoom,
|
|
34
|
+
defaultShowDomainLegend,
|
|
34
35
|
defaultShowDomains,
|
|
35
36
|
defaultSubFeatureRows,
|
|
36
37
|
maxCellSize,
|
|
@@ -132,6 +133,15 @@ function stateModelFactory() {
|
|
|
132
133
|
* #property
|
|
133
134
|
*/
|
|
134
135
|
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
136
|
+
/**
|
|
137
|
+
* #property
|
|
138
|
+
* whether the domain legend is expanded. The legend floats over the
|
|
139
|
+
* top-right of the alignment, so on a tall panel it covers real
|
|
140
|
+
* residues -- persisting the state is what lets a reader collapse it
|
|
141
|
+
* and keep it collapsed, and what lets a session or a figure open with
|
|
142
|
+
* it already out of the way.
|
|
143
|
+
*/
|
|
144
|
+
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
135
145
|
/**
|
|
136
146
|
* #property
|
|
137
147
|
*/
|
|
@@ -475,6 +485,13 @@ function stateModelFactory() {
|
|
|
475
485
|
setShowDomains(arg: boolean) {
|
|
476
486
|
self.showDomains = arg
|
|
477
487
|
},
|
|
488
|
+
/**
|
|
489
|
+
* #action
|
|
490
|
+
* expand or collapse the domain legend that floats over the alignment
|
|
491
|
+
*/
|
|
492
|
+
setShowDomainLegend(arg: boolean) {
|
|
493
|
+
self.showDomainLegend = arg
|
|
494
|
+
},
|
|
478
495
|
/**
|
|
479
496
|
* #action
|
|
480
497
|
*/
|
|
@@ -1438,15 +1455,28 @@ function stateModelFactory() {
|
|
|
1438
1455
|
: new Map(
|
|
1439
1456
|
leaves.map(node => {
|
|
1440
1457
|
const { name } = node.data
|
|
1441
|
-
|
|
1458
|
+
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1459
|
+
// to the row name, and measuring '' would size the gutter (and
|
|
1460
|
+
// the label's click target) to nothing
|
|
1461
|
+
const displayName = treeMetadata[name]?.genome || name
|
|
1442
1462
|
return [name, measureTextCanvas(displayName, fontSize)] as const
|
|
1443
1463
|
}),
|
|
1444
1464
|
)
|
|
1445
1465
|
},
|
|
1446
1466
|
|
|
1447
1467
|
get labelsWidth() {
|
|
1448
|
-
|
|
1449
|
-
|
|
1468
|
+
// a loop, not Math.max(...widths.values()): spreading a map of every
|
|
1469
|
+
// leaf passes one argument per row, and the argument limit is somewhere
|
|
1470
|
+
// around 125k -- so the bundled 230k-tip COVID tree threw
|
|
1471
|
+
// "RangeError: Maximum call stack size exceeded" out of a getter the
|
|
1472
|
+
// treeWidth autorun reads on load
|
|
1473
|
+
let max = 0
|
|
1474
|
+
for (const width of this.labelWidthMap.values()) {
|
|
1475
|
+
if (width > max) {
|
|
1476
|
+
max = width
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return max
|
|
1450
1480
|
},
|
|
1451
1481
|
|
|
1452
1482
|
/**
|
|
@@ -2298,6 +2328,7 @@ function stateModelFactory() {
|
|
|
2298
2328
|
...(rest.treeMetadataFilehandle
|
|
2299
2329
|
? {}
|
|
2300
2330
|
: { treeMetadata: data.treeMetadata }),
|
|
2331
|
+
...(rest.gffFilehandle ? {} : { gff: data.gff }),
|
|
2301
2332
|
},
|
|
2302
2333
|
}))
|
|
2303
2334
|
}
|
|
@@ -3,6 +3,8 @@ import { describe, expect, test } from 'vitest'
|
|
|
3
3
|
|
|
4
4
|
import { calculateNeighborJoiningTree } from './neighborJoining.ts'
|
|
5
5
|
|
|
6
|
+
import type { NewickNode } from '@gmod/newick'
|
|
7
|
+
|
|
6
8
|
describe('calculateNeighborJoiningTree', () => {
|
|
7
9
|
test('generates valid Newick tree for 2 sequences', () => {
|
|
8
10
|
const rows: [string, string][] = [
|
|
@@ -81,6 +83,21 @@ describe('calculateNeighborJoiningTree', () => {
|
|
|
81
83
|
expect(tree).toContain('seq3')
|
|
82
84
|
})
|
|
83
85
|
|
|
86
|
+
test('treats a dot gap the same as a dash gap', () => {
|
|
87
|
+
// stockholm and a3m pad with '.', so the two spellings have to score alike
|
|
88
|
+
const withDashes: [string, string][] = [
|
|
89
|
+
['seq1', 'MK-AYLSMFG'],
|
|
90
|
+
['seq2', 'MKAAYLSMFG'],
|
|
91
|
+
['seq3', 'MKA-YLSMFG'],
|
|
92
|
+
]
|
|
93
|
+
const withDots = withDashes.map(
|
|
94
|
+
([name, seq]) => [name, seq.replaceAll('-', '.')] as [string, string],
|
|
95
|
+
)
|
|
96
|
+
expect(calculateNeighborJoiningTree(withDots)).toBe(
|
|
97
|
+
calculateNeighborJoiningTree(withDashes),
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
84
101
|
test('names with special characters round-trip through Newick parsing', () => {
|
|
85
102
|
const rows: [string, string][] = [
|
|
86
103
|
['EU105457.1|chr09:67680268..67675529_LTR/Copia', 'MKAA'],
|
|
@@ -89,12 +106,10 @@ describe('calculateNeighborJoiningTree', () => {
|
|
|
89
106
|
const newick = calculateNeighborJoiningTree(rows)
|
|
90
107
|
const tree = parseNewick(newick)
|
|
91
108
|
|
|
92
|
-
function getLeafNames(node:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return children.flatMap(c => getLeafNames(c))
|
|
109
|
+
function getLeafNames(node: NewickNode): (string | undefined)[] {
|
|
110
|
+
return node.children?.length
|
|
111
|
+
? node.children.flatMap(c => getLeafNames(c))
|
|
112
|
+
: [node.name]
|
|
98
113
|
}
|
|
99
114
|
const names = getLeafNames(tree)
|
|
100
115
|
expect(names).toContain('EU105457.1|chr09:67680268..67675529_LTR/Copia')
|
package/src/neighborJoining.ts
CHANGED
|
@@ -44,6 +44,14 @@ function getBlosum62Score(a: string, b: string) {
|
|
|
44
44
|
return BLOSUM62.get(a.toUpperCase())?.get(b.toUpperCase()) ?? -4
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// stockholm and a3m write gaps as '.', not just '-'. Scoring a '.' as a residue
|
|
48
|
+
// charges the pair the unknown-symbol penalty on every padded column, so two
|
|
49
|
+
// sequences that differ only in where an unrelated row forced padding come out
|
|
50
|
+
// as divergent.
|
|
51
|
+
function isGap(c: string) {
|
|
52
|
+
return c === '-' || c === '.'
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
function computePairwiseDistance(seq1: string, seq2: string) {
|
|
48
56
|
if (seq1.length !== seq2.length) {
|
|
49
57
|
throw new Error('Sequences must have the same length (aligned)')
|
|
@@ -58,11 +66,13 @@ function computePairwiseDistance(seq1: string, seq2: string) {
|
|
|
58
66
|
const a = seq1[i]!
|
|
59
67
|
const b = seq2[i]!
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
const aGap = isGap(a)
|
|
70
|
+
const bGap = isGap(b)
|
|
71
|
+
if (aGap && bGap) {
|
|
62
72
|
continue
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
if (
|
|
75
|
+
if (aGap || bGap) {
|
|
66
76
|
mismatches++
|
|
67
77
|
continue
|
|
68
78
|
}
|
package/src/parseAsn1.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
|
|
3
3
|
import { expect, test } from 'vitest'
|
|
4
4
|
|
|
5
|
+
import { flatToTree } from './flatToTree.ts'
|
|
6
|
+
import MSAModelF from './model.ts'
|
|
5
7
|
import { parseAsn1 } from './parseAsn1.ts'
|
|
6
8
|
|
|
7
9
|
const r = readFileSync(
|
|
@@ -18,3 +20,17 @@ test('throws a clear error on input missing required sections', () => {
|
|
|
18
20
|
/missing/,
|
|
19
21
|
)
|
|
20
22
|
})
|
|
23
|
+
|
|
24
|
+
test('the tree keeps the labels and branch lengths the file carries', () => {
|
|
25
|
+
// the parsed features have to survive into the tree the viewer draws, or a
|
|
26
|
+
// BLAST tree renders as a lengthless list of node ids
|
|
27
|
+
const model = MSAModelF().create({ type: 'MsaView', data: { tree: r } })
|
|
28
|
+
model.setWidth(1000)
|
|
29
|
+
|
|
30
|
+
expect(model.numRows).toBe(101)
|
|
31
|
+
expect(model.rowNames[0]).toBe(
|
|
32
|
+
'sodium/glucose cotransporter 4 [Gouania willdenowi]',
|
|
33
|
+
)
|
|
34
|
+
expect(model.allBranchesLength0).toBe(false)
|
|
35
|
+
expect(flatToTree(parseAsn1(r)).length).toBeUndefined()
|
|
36
|
+
})
|
|
@@ -55,3 +55,33 @@ test('inline data is stripped when a filehandle can refetch it', () => {
|
|
|
55
55
|
expect(snap.data.msa).toBeUndefined()
|
|
56
56
|
expect(snap.data.tree).toBe('(a,b);')
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
test('inline gff survives the snapshot when no gffFilehandle can refetch it', () => {
|
|
60
|
+
const gff = 's1\tx\tprotein_match\t1\t3\t.\t.\t.\tName=PF1'
|
|
61
|
+
const withInlineGff = MsaView.create({
|
|
62
|
+
type: 'MsaView',
|
|
63
|
+
data: { msa: '>s1\nACGT', gff },
|
|
64
|
+
})
|
|
65
|
+
expect(getSnapshot(withInlineGff).data.gff).toBe(gff)
|
|
66
|
+
|
|
67
|
+
const withFilehandle = MsaView.create({
|
|
68
|
+
type: 'MsaView',
|
|
69
|
+
data: { msa: '>s1\nACGT', gff },
|
|
70
|
+
gffFilehandle: {
|
|
71
|
+
uri: 'http://example.com/x.gff',
|
|
72
|
+
locationType: 'UriLocation',
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
expect(getSnapshot(withFilehandle).data.gff).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// The legend floats over the top-right of the alignment, so collapsing it is a
|
|
79
|
+
// way of seeing the residues under it. As component state that choice was lost
|
|
80
|
+
// on every reload and could not be set by a session or a figure at all.
|
|
81
|
+
test('a collapsed domain legend survives into the snapshot', () => {
|
|
82
|
+
const model = MsaView.create({ type: 'MsaView' })
|
|
83
|
+
expect(getSnapshot(model).showDomainLegend).toBeUndefined()
|
|
84
|
+
model.setShowDomainLegend(false)
|
|
85
|
+
expect(getSnapshot(model).showDomainLegend).toBe(false)
|
|
86
|
+
expect(MsaView.create(getSnapshot(model)).showDomainLegend).toBe(false)
|
|
87
|
+
})
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '5.
|
|
1
|
+
export const version = '5.9.0'
|