react-msaview 5.7.2 → 5.8.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 -101
- 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/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/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 +1 -0
- package/dist/model.js +17 -3
- 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/renderBoxFeatureCanvasBlock.ts +3 -2
- package/src/components/tree/renderTreeCanvas.test.ts +54 -1
- package/src/components/tree/renderTreeCanvas.ts +41 -28
- 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 +17 -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 +19 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import MSAModelF from './model.ts'
|
|
5
|
+
|
|
6
|
+
// a ragged alignment: row `a` stops early, so the alignment is as wide as `b`.
|
|
7
|
+
// Column space has to follow the widest row -- the gap analysis already does --
|
|
8
|
+
// or every column past the first row's end becomes unreachable: unscrollable,
|
|
9
|
+
// unhoverable, and missing from the minimap.
|
|
10
|
+
const msa = `>a
|
|
11
|
+
ACGT
|
|
12
|
+
>b
|
|
13
|
+
ACGTACGTAC`
|
|
14
|
+
|
|
15
|
+
function makeModel() {
|
|
16
|
+
const model = MSAModelF().create({
|
|
17
|
+
id: 'col-space-test',
|
|
18
|
+
type: 'MsaView',
|
|
19
|
+
msaFormat: 'fasta',
|
|
20
|
+
data: { msa },
|
|
21
|
+
})
|
|
22
|
+
model.setWidth(800)
|
|
23
|
+
return model
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('column space spans the widest row', () => {
|
|
27
|
+
const model = makeModel()
|
|
28
|
+
expect(model.numColumns).toBe(10)
|
|
29
|
+
expect(model.totalWidth).toBe(10 * model.colWidth)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('the last column agrees with the column statistics', () => {
|
|
33
|
+
const model = makeModel()
|
|
34
|
+
expect(model.colStats.numColumns).toBe(model.numColumns)
|
|
35
|
+
expect(model.conservation).toHaveLength(model.numColumns)
|
|
36
|
+
|
|
37
|
+
model.setMousePos(model.numColumns - 1, 1)
|
|
38
|
+
expect(model.mouseOverColumnStats?.col).toBe(9)
|
|
39
|
+
expect(model.visibleColToRowLetter('b', 9)).toBe('C')
|
|
40
|
+
})
|
|
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react'
|
|
4
4
|
import { Button, TextField } from '@mui/material'
|
|
5
|
+
import { getUngappedSequence } from 'msa-parsers'
|
|
5
6
|
|
|
6
7
|
import copy from '../vendor/copyToClipboard.ts'
|
|
7
8
|
import Checkbox2 from './Checkbox2.tsx'
|
|
@@ -24,9 +25,8 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
|
|
|
24
25
|
const [showGaps, setShowGaps] = useState(false)
|
|
25
26
|
const [showEmpty, setShowEmpty] = useState(false)
|
|
26
27
|
|
|
27
|
-
const removeGaps = (s: string) => s.replaceAll('-', '').replaceAll('.', '')
|
|
28
28
|
const disp = str
|
|
29
|
-
.map(([s1, s2]) => [s1, showGaps ? s2 :
|
|
29
|
+
.map(([s1, s2]) => [s1, showGaps ? s2 : getUngappedSequence(s2)] as const)
|
|
30
30
|
.filter(([, s2]) => showEmpty || !!s2)
|
|
31
31
|
.map(([s1, s2]) => `>${s1}\n${s2}`)
|
|
32
32
|
.join('\n')
|
|
@@ -112,8 +112,15 @@ const UserProvidedDomainsDialog = observer(function ({
|
|
|
112
112
|
? JSON.parse(await file.text())
|
|
113
113
|
: await jsonfetch(interProURL)
|
|
114
114
|
|
|
115
|
+
// key each result by its xref id; a result without one has no
|
|
116
|
+
// row to attach to, and keying it `undefined` would draw a
|
|
117
|
+
// phantom row's worth of domains
|
|
115
118
|
model.setDomains(
|
|
116
|
-
Object.fromEntries(
|
|
119
|
+
Object.fromEntries(
|
|
120
|
+
ret.results
|
|
121
|
+
.map(r => [r.xref[0]?.id, r] as const)
|
|
122
|
+
.filter(e => e[0] !== undefined),
|
|
123
|
+
),
|
|
117
124
|
)
|
|
118
125
|
getSession(model).notify(
|
|
119
126
|
'Loaded interproscan results',
|
|
@@ -49,7 +49,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
|
|
|
49
49
|
label: `Show domains${noDomains ? ' (no domains loaded)' : ''}`,
|
|
50
50
|
disabled: noDomains,
|
|
51
51
|
icon: Visibility,
|
|
52
|
-
checked: actuallyShowDomains
|
|
52
|
+
checked: actuallyShowDomains,
|
|
53
53
|
type: 'checkbox' as const,
|
|
54
54
|
onClick: () => {
|
|
55
55
|
model.setShowDomains(!showDomains)
|
|
@@ -57,15 +57,12 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
57
57
|
<div>
|
|
58
58
|
<Button
|
|
59
59
|
onClick={() => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
model.setError(e)
|
|
67
|
-
}
|
|
68
|
-
})()
|
|
60
|
+
try {
|
|
61
|
+
load(model, msaFile, treeFile, gffFile)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(e)
|
|
64
|
+
model.setError(e)
|
|
65
|
+
}
|
|
69
66
|
}}
|
|
70
67
|
variant="contained"
|
|
71
68
|
color="primary"
|
|
@@ -49,20 +49,17 @@ const ImportFormExamples = observer(function ({
|
|
|
49
49
|
tree?: string
|
|
50
50
|
gff?: string
|
|
51
51
|
}) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
model.setError(e)
|
|
64
|
-
}
|
|
65
|
-
})()
|
|
52
|
+
try {
|
|
53
|
+
load(
|
|
54
|
+
model,
|
|
55
|
+
msa ? { uri: msa, locationType: 'UriLocation' } : undefined,
|
|
56
|
+
tree ? { uri: tree, locationType: 'UriLocation' } : undefined,
|
|
57
|
+
gff ? { uri: gff, locationType: 'UriLocation' } : undefined,
|
|
58
|
+
)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(e)
|
|
61
|
+
model.setError(e)
|
|
62
|
+
}
|
|
66
63
|
}
|
|
67
64
|
return (
|
|
68
65
|
<ul>
|
|
@@ -23,8 +23,9 @@ export function renderBoxFeatureCanvasBlock({
|
|
|
23
23
|
blockSizeXOverride?: number
|
|
24
24
|
blockSizeYOverride?: number
|
|
25
25
|
}) {
|
|
26
|
-
const { blockSize, rowHeight, highResScaleFactor,
|
|
27
|
-
|
|
26
|
+
const { blockSize, rowHeight, highResScaleFactor, actuallyShowDomains } =
|
|
27
|
+
model
|
|
28
|
+
if (actuallyShowDomains) {
|
|
28
29
|
const k = highResScaleFactorOverride ?? highResScaleFactor
|
|
29
30
|
const bx = blockSizeXOverride ?? blockSize
|
|
30
31
|
const by = blockSizeYOverride ?? blockSize
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { calcDepthToLeaf, findMaxBranchLen } from '../../hierarchy.ts'
|
|
4
|
-
import
|
|
4
|
+
import stateModelFactory from '../../model.ts'
|
|
5
|
+
import { getNodeX, renderTreeCanvas } from './renderTreeCanvas.ts'
|
|
5
6
|
|
|
6
7
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
8
|
+
import type { RenderCtx } from '../renderCtx.ts'
|
|
9
|
+
import type { Theme } from '@mui/material'
|
|
7
10
|
|
|
8
11
|
function leaf(id: string, len?: number): HierarchyNode {
|
|
9
12
|
return {
|
|
@@ -113,3 +116,53 @@ describe('getNodeX cladogram positioning', () => {
|
|
|
113
116
|
expect(getNodeX(leaf('a', 2.5), true, 100, 1)).toBe(2.5)
|
|
114
117
|
})
|
|
115
118
|
})
|
|
119
|
+
|
|
120
|
+
// the x values the tree pass actually strokes, for a model built from newick
|
|
121
|
+
function drawnTreeXs(newick: string) {
|
|
122
|
+
const model = stateModelFactory().create({
|
|
123
|
+
type: 'MsaView',
|
|
124
|
+
data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
|
|
125
|
+
})
|
|
126
|
+
model.setWidth(1000)
|
|
127
|
+
// labels would need a canvas to measure; the branch geometry is the subject
|
|
128
|
+
model.setDrawLabels(false)
|
|
129
|
+
model.setDrawNodeBubbles(false)
|
|
130
|
+
|
|
131
|
+
const xs: number[] = []
|
|
132
|
+
const ctx = {
|
|
133
|
+
font: '12px sans-serif',
|
|
134
|
+
beginPath() {},
|
|
135
|
+
stroke() {},
|
|
136
|
+
resetTransform() {},
|
|
137
|
+
scale() {},
|
|
138
|
+
translate() {},
|
|
139
|
+
moveTo(x: number) {
|
|
140
|
+
xs.push(x)
|
|
141
|
+
},
|
|
142
|
+
lineTo(x: number) {
|
|
143
|
+
xs.push(x)
|
|
144
|
+
},
|
|
145
|
+
} as unknown as RenderCtx
|
|
146
|
+
|
|
147
|
+
renderTreeCanvas({
|
|
148
|
+
model,
|
|
149
|
+
ctx,
|
|
150
|
+
offsetY: 0,
|
|
151
|
+
theme: { palette: { text: { primary: '#000' } } } as Theme,
|
|
152
|
+
})
|
|
153
|
+
return xs
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe('renderTreeCanvas horizontal extent', () => {
|
|
157
|
+
it('spreads a cladogram across the tree area when branch lengths are absent', () => {
|
|
158
|
+
// a lengthless newick forces cladogram mode; scaling it by the (zero) max
|
|
159
|
+
// branch length would stack every node on x=0 as one vertical line
|
|
160
|
+
const xs = drawnTreeXs('((a,b),(c,d));')
|
|
161
|
+
expect(Math.max(...xs)).toBe(300)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('scales a phylogram by branch length', () => {
|
|
165
|
+
const xs = drawnTreeXs('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
|
|
166
|
+
expect(Math.max(...xs)).toBeCloseTo(300)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -24,14 +24,15 @@ function inYBlock(y: number, offsetY: number, by: number) {
|
|
|
24
24
|
return y > offsetY - extendBounds && y < offsetY + by + extendBounds
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Calculate node x-coordinate for both phylogram (with branch lengths) and
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// Calculate node x-coordinate for both phylogram (with branch lengths) and
|
|
28
|
+
// cladogram (topology only) modes. tipX is the pixel x the tips sit at.
|
|
29
|
+
// For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * tipX
|
|
30
|
+
// This positions: leaves at tipX (rightmost), root at 0 (leftmost), internal nodes proportionally in between
|
|
30
31
|
// Matches ape's: xx <- max(xx) - xx (where xx is depth from each node to tips)
|
|
31
32
|
export function getNodeX(
|
|
32
33
|
node: HierarchyNode,
|
|
33
34
|
showBranchLen: boolean,
|
|
34
|
-
|
|
35
|
+
tipX: number,
|
|
35
36
|
maxDepthToLeaf: number,
|
|
36
37
|
): number | undefined {
|
|
37
38
|
if (showBranchLen) {
|
|
@@ -41,7 +42,7 @@ export function getNodeX(
|
|
|
41
42
|
return 0
|
|
42
43
|
}
|
|
43
44
|
const depthToLeaf = calcDepthToLeaf(node)
|
|
44
|
-
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) *
|
|
45
|
+
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export function renderTree({
|
|
@@ -49,7 +50,7 @@ export function renderTree({
|
|
|
49
50
|
ctx,
|
|
50
51
|
model,
|
|
51
52
|
theme,
|
|
52
|
-
|
|
53
|
+
tipX,
|
|
53
54
|
maxDepthToLeaf,
|
|
54
55
|
blockSizeYOverride,
|
|
55
56
|
}: {
|
|
@@ -57,7 +58,7 @@ export function renderTree({
|
|
|
57
58
|
ctx: RenderCtx
|
|
58
59
|
model: MsaViewModel
|
|
59
60
|
theme: Theme
|
|
60
|
-
|
|
61
|
+
tipX: number
|
|
61
62
|
maxDepthToLeaf: number
|
|
62
63
|
blockSizeYOverride?: number
|
|
63
64
|
}) {
|
|
@@ -67,8 +68,8 @@ export function renderTree({
|
|
|
67
68
|
forEachLink(hierarchy, (source, target) => {
|
|
68
69
|
const sy = source.x!
|
|
69
70
|
const ty = target.x!
|
|
70
|
-
const tx = getNodeX(target, showBranchLen,
|
|
71
|
-
const sx = getNodeX(source, showBranchLen,
|
|
71
|
+
const tx = getNodeX(target, showBranchLen, tipX, maxDepthToLeaf)
|
|
72
|
+
const sx = getNodeX(source, showBranchLen, tipX, maxDepthToLeaf)
|
|
72
73
|
if (tx === undefined || sx === undefined) {
|
|
73
74
|
return
|
|
74
75
|
}
|
|
@@ -94,7 +95,7 @@ export function renderCollapsedTriangles({
|
|
|
94
95
|
offsetY,
|
|
95
96
|
model,
|
|
96
97
|
theme,
|
|
97
|
-
|
|
98
|
+
tipX,
|
|
98
99
|
maxDepthToLeaf,
|
|
99
100
|
blockSizeYOverride,
|
|
100
101
|
}: {
|
|
@@ -103,7 +104,7 @@ export function renderCollapsedTriangles({
|
|
|
103
104
|
offsetY: number
|
|
104
105
|
model: MsaViewModel
|
|
105
106
|
theme: Theme
|
|
106
|
-
|
|
107
|
+
tipX: number
|
|
107
108
|
maxDepthToLeaf: number
|
|
108
109
|
blockSizeYOverride?: number
|
|
109
110
|
}) {
|
|
@@ -116,19 +117,23 @@ export function renderCollapsedTriangles({
|
|
|
116
117
|
fontSize,
|
|
117
118
|
marginLeft: ml,
|
|
118
119
|
} = model
|
|
120
|
+
// nothing collapsed is the common case, and the traversal below visits every
|
|
121
|
+
// node in the tree on every block of every redraw
|
|
122
|
+
if (collapsed.length === 0) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
const collapsedSet = new Set(collapsed)
|
|
119
126
|
const by = blockSizeYOverride ?? blockSize
|
|
120
127
|
const halfHeight = Math.max(2, rowHeight * 0.42)
|
|
121
128
|
forEachDescendant(hierarchy, node => {
|
|
122
129
|
const { id, name } = node.data
|
|
123
|
-
if (
|
|
124
|
-
const apexX = getNodeX(node, showBranchLen,
|
|
130
|
+
if (collapsedSet.has(id)) {
|
|
131
|
+
const apexX = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
125
132
|
const y = node.x!
|
|
126
133
|
const inBlock = inYBlock(y, offsetY, by)
|
|
127
134
|
// in cladogram mode the hidden tips align at the right edge, otherwise use
|
|
128
135
|
// the branch-length extent recorded in the model's hierarchy getter
|
|
129
|
-
const baseX = showBranchLen
|
|
130
|
-
? (node.collapsedTipXFar ?? maxBranchLen)
|
|
131
|
-
: maxBranchLen
|
|
136
|
+
const baseX = showBranchLen ? (node.collapsedTipXFar ?? tipX) : tipX
|
|
132
137
|
if (apexX !== undefined && inBlock && baseX > apexX) {
|
|
133
138
|
ctx.beginPath()
|
|
134
139
|
ctx.moveTo(apexX, y)
|
|
@@ -171,7 +176,7 @@ export function renderNodeBubbles({
|
|
|
171
176
|
clickMap,
|
|
172
177
|
offsetY,
|
|
173
178
|
model,
|
|
174
|
-
|
|
179
|
+
tipX,
|
|
175
180
|
maxDepthToLeaf,
|
|
176
181
|
blockSizeYOverride,
|
|
177
182
|
}: {
|
|
@@ -179,7 +184,7 @@ export function renderNodeBubbles({
|
|
|
179
184
|
clickMap?: ClickMapIndex
|
|
180
185
|
offsetY: number
|
|
181
186
|
model: MsaViewModel
|
|
182
|
-
|
|
187
|
+
tipX: number
|
|
183
188
|
maxDepthToLeaf: number
|
|
184
189
|
blockSizeYOverride?: number
|
|
185
190
|
}) {
|
|
@@ -191,8 +196,9 @@ export function renderNodeBubbles({
|
|
|
191
196
|
marginLeft: ml,
|
|
192
197
|
} = model
|
|
193
198
|
const by = blockSizeYOverride ?? blockSize
|
|
199
|
+
const collapsedSet = new Set(collapsed)
|
|
194
200
|
forEachDescendant(hierarchy, node => {
|
|
195
|
-
const x = getNodeX(node, showBranchLen,
|
|
201
|
+
const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
196
202
|
if (x === undefined) {
|
|
197
203
|
return
|
|
198
204
|
}
|
|
@@ -200,7 +206,7 @@ export function renderNodeBubbles({
|
|
|
200
206
|
const y = node.x!
|
|
201
207
|
const { id, name } = data
|
|
202
208
|
if (node.height >= 1 && inYBlock(y, offsetY, by)) {
|
|
203
|
-
const isCollapsed =
|
|
209
|
+
const isCollapsed = collapsedSet.has(id)
|
|
204
210
|
ctx.strokeStyle = 'black'
|
|
205
211
|
ctx.fillStyle = isCollapsed ? 'black' : 'white'
|
|
206
212
|
ctx.beginPath()
|
|
@@ -231,7 +237,7 @@ export function renderTreeLabels({
|
|
|
231
237
|
offsetY,
|
|
232
238
|
ctx,
|
|
233
239
|
clickMap,
|
|
234
|
-
|
|
240
|
+
tipX,
|
|
235
241
|
maxDepthToLeaf,
|
|
236
242
|
blockSizeYOverride,
|
|
237
243
|
}: {
|
|
@@ -240,7 +246,7 @@ export function renderTreeLabels({
|
|
|
240
246
|
ctx: RenderCtx
|
|
241
247
|
clickMap?: ClickMapIndex
|
|
242
248
|
theme: Theme
|
|
243
|
-
|
|
249
|
+
tipX: number
|
|
244
250
|
maxDepthToLeaf: number
|
|
245
251
|
blockSizeYOverride?: number
|
|
246
252
|
}) {
|
|
@@ -289,7 +295,7 @@ export function renderTreeLabels({
|
|
|
289
295
|
const yp = y + fontSize / 4
|
|
290
296
|
let xp = 0
|
|
291
297
|
if (!noTree) {
|
|
292
|
-
xp = getNodeX(node, showBranchLen,
|
|
298
|
+
xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
const width =
|
|
@@ -375,7 +381,14 @@ export function renderTreeCanvas({
|
|
|
375
381
|
|
|
376
382
|
// memoized on the model and shared across the tree/bubble/label passes (and
|
|
377
383
|
// across all tree blocks) rather than re-traversing the hierarchy in each
|
|
378
|
-
const { maxBranchLength
|
|
384
|
+
const { maxBranchLength, maxDepthToLeaf, showBranchLenEffective, treeWidth } =
|
|
385
|
+
model
|
|
386
|
+
// pixel x of the tips. A phylogram scales branch lengths so the longest
|
|
387
|
+
// root-to-tip path lands at maxBranchLength; a cladogram ignores lengths and
|
|
388
|
+
// spreads topological depth across the whole tree area instead. Reusing
|
|
389
|
+
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
390
|
+
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
391
|
+
const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
|
|
379
392
|
|
|
380
393
|
if (!noTree && drawTree) {
|
|
381
394
|
renderTree({
|
|
@@ -383,7 +396,7 @@ export function renderTreeCanvas({
|
|
|
383
396
|
offsetY,
|
|
384
397
|
model,
|
|
385
398
|
theme,
|
|
386
|
-
|
|
399
|
+
tipX,
|
|
387
400
|
maxDepthToLeaf,
|
|
388
401
|
blockSizeYOverride,
|
|
389
402
|
})
|
|
@@ -394,7 +407,7 @@ export function renderTreeCanvas({
|
|
|
394
407
|
offsetY,
|
|
395
408
|
model,
|
|
396
409
|
theme,
|
|
397
|
-
|
|
410
|
+
tipX,
|
|
398
411
|
maxDepthToLeaf,
|
|
399
412
|
blockSizeYOverride,
|
|
400
413
|
})
|
|
@@ -405,7 +418,7 @@ export function renderTreeCanvas({
|
|
|
405
418
|
offsetY,
|
|
406
419
|
clickMap,
|
|
407
420
|
model,
|
|
408
|
-
|
|
421
|
+
tipX,
|
|
409
422
|
maxDepthToLeaf,
|
|
410
423
|
blockSizeYOverride,
|
|
411
424
|
})
|
|
@@ -419,7 +432,7 @@ export function renderTreeCanvas({
|
|
|
419
432
|
model,
|
|
420
433
|
clickMap,
|
|
421
434
|
theme,
|
|
422
|
-
|
|
435
|
+
tipX,
|
|
423
436
|
maxDepthToLeaf,
|
|
424
437
|
blockSizeYOverride,
|
|
425
438
|
})
|
package/src/flatToTree.test.ts
CHANGED
|
@@ -15,6 +15,27 @@ describe('flatToTree', () => {
|
|
|
15
15
|
expect(root.children[0]!.children.map(c => c.id)).toEqual(['3'])
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
+
test('carries the label and branch length of a BioTreeContainer node', () => {
|
|
19
|
+
const root = flatToTree([
|
|
20
|
+
{ id: 0, label: 'Multiple organisms' },
|
|
21
|
+
{
|
|
22
|
+
id: 1,
|
|
23
|
+
parent: 0,
|
|
24
|
+
label: 'sodium/glucose cotransporter 4',
|
|
25
|
+
dist: '0.5',
|
|
26
|
+
},
|
|
27
|
+
{ id: 2, parent: 0 },
|
|
28
|
+
])
|
|
29
|
+
expect(root.name).toBe('Multiple organisms')
|
|
30
|
+
expect(root.children[0]).toMatchObject({
|
|
31
|
+
name: 'sodium/glucose cotransporter 4',
|
|
32
|
+
length: 0.5,
|
|
33
|
+
})
|
|
34
|
+
// no label falls back to the id, and no dist leaves the length unset
|
|
35
|
+
expect(root.children[1]!.name).toBe('2')
|
|
36
|
+
expect(root.children[1]!.length).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
18
39
|
test('throws on empty input rather than returning undefined', () => {
|
|
19
40
|
expect(() => flatToTree([])).toThrow(/no root/)
|
|
20
41
|
})
|
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
|