react-msaview 5.6.1 → 5.6.3
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 +95 -95
- package/bundle/index.js.map +4 -4
- package/dist/clustalX.d.ts +4 -3
- package/dist/clustalX.js +25 -4
- package/dist/clustalX.js.map +1 -1
- package/dist/columnCounts.d.ts +37 -0
- package/dist/columnCounts.js +148 -0
- package/dist/columnCounts.js.map +1 -0
- package/dist/components/MSAView.js +1 -8
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +6 -0
- package/dist/components/getVisibleLeaves.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +17 -54
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +97 -96
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -32
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
- package/dist/components/msa/useMsaBlockMouse.js +45 -0
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +14 -19
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +40 -111
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
- package/dist/components/tree/renderTreeCanvas.js +15 -3
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/useTreeHover.d.ts +24 -0
- package/dist/components/tree/useTreeHover.js +44 -0
- package/dist/components/tree/useTreeHover.js.map +1 -0
- package/dist/hierarchy.js +15 -9
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/model/msaModel.js +4 -3
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.js +9 -8
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +52 -10
- package/dist/model.js +208 -93
- package/dist/model.js.map +1 -1
- package/dist/propertyConservation.d.ts +2 -1
- package/dist/propertyConservation.js +36 -25
- package/dist/propertyConservation.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +7 -0
- package/dist/seqPosToGlobalCol.js +17 -0
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/stripDefault.d.ts +21 -0
- package/dist/stripDefault.js +26 -0
- package/dist/stripDefault.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/clustalX.test.ts +9 -11
- package/src/clustalX.ts +27 -28
- package/src/columnCounts.test.ts +58 -0
- package/src/columnCounts.ts +170 -0
- package/src/components/MSAView.tsx +1 -9
- package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
- package/src/components/msa/MSACanvasBlock.tsx +42 -110
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
- package/src/components/msa/renderMSABlock.ts +113 -125
- package/src/components/msa/renderMSAMouseover.ts +11 -34
- package/src/components/msa/useMsaBlockMouse.ts +81 -0
- package/src/components/tree/TreeCanvas.tsx +16 -23
- package/src/components/tree/TreeCanvasBlock.tsx +62 -143
- package/src/components/tree/renderTreeCanvas.test.ts +100 -190
- package/src/components/tree/renderTreeCanvas.ts +14 -3
- package/src/components/tree/useTreeHover.ts +69 -0
- package/src/domainBands.test.ts +98 -0
- package/src/hierarchy.ts +15 -9
- package/src/index.ts +7 -0
- package/src/model/msaModel.ts +4 -6
- package/src/model/treeModel.ts +9 -14
- package/src/model.ts +241 -112
- package/src/propertyConservation.test.ts +2 -13
- package/src/propertyConservation.ts +40 -31
- package/src/rowSpace.test.ts +46 -0
- package/src/seqPosToGlobalCol.ts +18 -0
- package/src/stripDefault.test.ts +48 -0
- package/src/stripDefault.ts +35 -0
- package/src/types.ts +23 -0
- package/src/version.ts +1 -1
- package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
- package/dist/components/msa/getVisibleLeaves.js.map +0 -1
- package/dist/useDevicePixelRatio.d.ts +0 -1
- package/dist/useDevicePixelRatio.js +0 -27
- package/dist/useDevicePixelRatio.js.map +0 -1
- package/src/useDevicePixelRatio.ts +0 -29
- /package/dist/components/{msa/getVisibleLeaves.js → getVisibleLeaves.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { makeStyles } from '@jbrowse/core/util/tss-react'
|
|
4
4
|
import { useTheme } from '@mui/material'
|
|
@@ -6,11 +6,10 @@ import { observer } from 'mobx-react'
|
|
|
6
6
|
|
|
7
7
|
import TreeBranchMenu from './TreeBranchMenu.tsx'
|
|
8
8
|
import TreeNodeMenu from './TreeNodeMenu.tsx'
|
|
9
|
-
import { ClickMapIndex } from './clickMap.ts'
|
|
10
9
|
import { padding, renderTreeCanvas } from './renderTreeCanvas.ts'
|
|
10
|
+
import { useTreeHover } from './useTreeHover.ts'
|
|
11
11
|
import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
|
|
12
12
|
|
|
13
|
-
import type { ClickEntry } from './clickMap.ts'
|
|
14
13
|
import type { MsaViewModel } from '../../model.ts'
|
|
15
14
|
|
|
16
15
|
const useStyles = makeStyles()(theme => ({
|
|
@@ -28,9 +27,15 @@ const useStyles = makeStyles()(theme => ({
|
|
|
28
27
|
overflow: 'hidden',
|
|
29
28
|
textOverflow: 'ellipsis',
|
|
30
29
|
},
|
|
30
|
+
hover: {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
pointerEvents: 'none',
|
|
33
|
+
zIndex: 100,
|
|
34
|
+
background: 'rgba(0,0,0,0.1)',
|
|
35
|
+
},
|
|
31
36
|
}))
|
|
32
37
|
|
|
33
|
-
interface
|
|
38
|
+
interface MenuData {
|
|
34
39
|
name: string
|
|
35
40
|
id: string
|
|
36
41
|
x: number
|
|
@@ -46,88 +51,35 @@ const TreeCanvasBlock = observer(function ({
|
|
|
46
51
|
}) {
|
|
47
52
|
const { classes } = useStyles()
|
|
48
53
|
const theme = useTheme()
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const [hoverElt, setHoverElt] = useState<ClickEntry>()
|
|
54
|
-
const [tooltipInfo, setTooltipInfo] = useState<{
|
|
55
|
-
name: string
|
|
56
|
-
x: number
|
|
57
|
-
y: number
|
|
58
|
-
}>()
|
|
54
|
+
const [branchMenu, setBranchMenu] = useState<MenuData>()
|
|
55
|
+
const [nodeMenu, setNodeMenu] = useState<MenuData>()
|
|
56
|
+
const { clickMap, hovered, hitTest, onMouseMove, onMouseLeave } =
|
|
57
|
+
useTreeHover({ model, offsetY })
|
|
59
58
|
|
|
60
59
|
const { scrollY, treeAreaWidth, blockSize, highResScaleFactor } = model
|
|
61
|
-
|
|
62
60
|
const width = treeAreaWidth + padding
|
|
63
61
|
const height = blockSize
|
|
64
|
-
const w2 = width * highResScaleFactor
|
|
65
|
-
const h2 = height * highResScaleFactor
|
|
66
62
|
|
|
67
63
|
const ref = useCanvasAutorun(
|
|
68
64
|
ctx => {
|
|
69
65
|
ctx.resetTransform()
|
|
70
|
-
ctx.clearRect(
|
|
66
|
+
ctx.clearRect(
|
|
67
|
+
0,
|
|
68
|
+
0,
|
|
69
|
+
width * highResScaleFactor,
|
|
70
|
+
height * highResScaleFactor,
|
|
71
|
+
)
|
|
71
72
|
renderTreeCanvas({
|
|
72
73
|
ctx,
|
|
73
74
|
model,
|
|
74
75
|
offsetY,
|
|
75
|
-
clickMap
|
|
76
|
+
clickMap,
|
|
76
77
|
theme,
|
|
77
78
|
})
|
|
78
79
|
},
|
|
79
|
-
[model,
|
|
80
|
+
[model, clickMap, offsetY, theme],
|
|
80
81
|
)
|
|
81
82
|
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
const ctx = mouseoverRef.current?.getContext('2d')
|
|
84
|
-
if (!ctx) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
ctx.resetTransform()
|
|
89
|
-
ctx.clearRect(0, 0, treeAreaWidth + padding, blockSize)
|
|
90
|
-
ctx.translate(0, -offsetY)
|
|
91
|
-
|
|
92
|
-
// Highlight tree element being directly hovered
|
|
93
|
-
if (hoverElt) {
|
|
94
|
-
const { minX, maxX, minY, maxY } = hoverElt
|
|
95
|
-
|
|
96
|
-
ctx.fillStyle = 'rgba(0,0,0,0.1)'
|
|
97
|
-
ctx.fillRect(minX, minY, maxX - minX, maxY - minY)
|
|
98
|
-
}
|
|
99
|
-
}, [hoverElt, offsetY, blockSize, treeAreaWidth])
|
|
100
|
-
|
|
101
|
-
function hoverBranchClickMap(event: React.MouseEvent) {
|
|
102
|
-
const x = event.nativeEvent.offsetX
|
|
103
|
-
const y = event.nativeEvent.offsetY
|
|
104
|
-
|
|
105
|
-
const [entry] = clickMap.current.search({
|
|
106
|
-
minX: x,
|
|
107
|
-
maxX: x + 1,
|
|
108
|
-
minY: y + offsetY,
|
|
109
|
-
maxY: y + 1 + offsetY,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
return entry?.branch
|
|
113
|
-
? { ...entry, x: event.clientX, y: event.clientY }
|
|
114
|
-
: undefined
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function hoverNameClickMap(event: React.MouseEvent) {
|
|
118
|
-
const x = event.nativeEvent.offsetX
|
|
119
|
-
const y = event.nativeEvent.offsetY
|
|
120
|
-
const [entry] = clickMap.current.search({
|
|
121
|
-
minX: x,
|
|
122
|
-
maxX: x + 1,
|
|
123
|
-
minY: y + offsetY,
|
|
124
|
-
maxY: y + 1 + offsetY,
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
return entry && !entry.branch
|
|
128
|
-
? { ...entry, x: event.clientX, y: event.clientY }
|
|
129
|
-
: undefined
|
|
130
|
-
}
|
|
131
83
|
const style = {
|
|
132
84
|
width,
|
|
133
85
|
height,
|
|
@@ -135,9 +87,10 @@ const TreeCanvasBlock = observer(function ({
|
|
|
135
87
|
left: 0,
|
|
136
88
|
position: 'absolute',
|
|
137
89
|
} as const
|
|
90
|
+
|
|
138
91
|
return (
|
|
139
92
|
<>
|
|
140
|
-
{branchMenu
|
|
93
|
+
{branchMenu ? (
|
|
141
94
|
<TreeBranchMenu
|
|
142
95
|
node={branchMenu}
|
|
143
96
|
model={model}
|
|
@@ -147,98 +100,64 @@ const TreeCanvasBlock = observer(function ({
|
|
|
147
100
|
/>
|
|
148
101
|
) : null}
|
|
149
102
|
|
|
150
|
-
{
|
|
103
|
+
{nodeMenu ? (
|
|
151
104
|
<TreeNodeMenu
|
|
152
|
-
node={
|
|
105
|
+
node={nodeMenu}
|
|
153
106
|
model={model}
|
|
154
107
|
onClose={() => {
|
|
155
|
-
|
|
108
|
+
setNodeMenu(undefined)
|
|
156
109
|
}}
|
|
157
110
|
/>
|
|
158
111
|
) : null}
|
|
159
112
|
|
|
160
113
|
<canvas
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
114
|
+
ref={ref}
|
|
115
|
+
width={width * highResScaleFactor}
|
|
116
|
+
height={height * highResScaleFactor}
|
|
117
|
+
style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
|
|
164
118
|
onMouseMove={event => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
setHoverElt(hoveredLeaf) // Only show direct hover highlight for leaf nodes
|
|
175
|
-
|
|
176
|
-
// Set tooltip info
|
|
177
|
-
if (hoveredAny) {
|
|
178
|
-
setTooltipInfo({
|
|
179
|
-
name: hoveredAny.name,
|
|
119
|
+
onMouseMove(event)
|
|
120
|
+
}}
|
|
121
|
+
onMouseLeave={() => {
|
|
122
|
+
onMouseLeave()
|
|
123
|
+
}}
|
|
124
|
+
onClick={event => {
|
|
125
|
+
const entry = hitTest(event)
|
|
126
|
+
if (entry) {
|
|
127
|
+
const menu = {
|
|
180
128
|
x: event.clientX,
|
|
181
129
|
y: event.clientY,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
model.setHoveredTreeNode(hoveredAny.id)
|
|
190
|
-
|
|
191
|
-
// For leaf nodes, also set single row highlight for backward compatibility
|
|
192
|
-
if (hoveredLeaf?.name) {
|
|
193
|
-
const rowIndex = model.rowNamesSet.get(hoveredLeaf.name)
|
|
194
|
-
if (rowIndex !== undefined) {
|
|
195
|
-
model.setMousePos(undefined, rowIndex)
|
|
196
|
-
}
|
|
130
|
+
id: entry.id,
|
|
131
|
+
name: entry.name,
|
|
132
|
+
}
|
|
133
|
+
if (entry.branch) {
|
|
134
|
+
setBranchMenu(menu)
|
|
135
|
+
} else {
|
|
136
|
+
setNodeMenu(menu)
|
|
197
137
|
}
|
|
198
|
-
} else {
|
|
199
|
-
// Clear all highlighting when not hovering over any tree node
|
|
200
|
-
model.setHoveredTreeNode(undefined)
|
|
201
|
-
model.setMousePos(undefined, undefined)
|
|
202
138
|
}
|
|
203
139
|
}}
|
|
204
|
-
|
|
205
|
-
const { clientX: x, clientY: y } = event
|
|
140
|
+
/>
|
|
206
141
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
142
|
+
{/* direct-hover highlight, drawn only for leaf labels */}
|
|
143
|
+
{hovered && !hovered.branch ? (
|
|
144
|
+
<div
|
|
145
|
+
className={classes.hover}
|
|
146
|
+
style={{
|
|
147
|
+
left: hovered.minX,
|
|
148
|
+
top: hovered.minY + scrollY,
|
|
149
|
+
width: hovered.maxX - hovered.minX,
|
|
150
|
+
height: hovered.maxY - hovered.minY,
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
) : null}
|
|
211
154
|
|
|
212
|
-
|
|
213
|
-
if (data2?.id) {
|
|
214
|
-
setToggleNodeMenu({ ...data2, x, y })
|
|
215
|
-
}
|
|
216
|
-
}}
|
|
217
|
-
onMouseLeave={() => {
|
|
218
|
-
setHoverElt(undefined)
|
|
219
|
-
setTooltipInfo(undefined)
|
|
220
|
-
// Clear all highlighting when leaving tree area
|
|
221
|
-
model.setHoveredTreeNode(undefined)
|
|
222
|
-
model.setMousePos(undefined, undefined)
|
|
223
|
-
}}
|
|
224
|
-
ref={ref}
|
|
225
|
-
/>
|
|
226
|
-
<canvas
|
|
227
|
-
style={{
|
|
228
|
-
...style,
|
|
229
|
-
pointerEvents: 'none',
|
|
230
|
-
zIndex: 100,
|
|
231
|
-
}}
|
|
232
|
-
width={width}
|
|
233
|
-
height={height}
|
|
234
|
-
ref={mouseoverRef}
|
|
235
|
-
/>
|
|
236
|
-
{tooltipInfo ? (
|
|
155
|
+
{hovered ? (
|
|
237
156
|
<div
|
|
238
157
|
className={classes.tooltip}
|
|
239
|
-
style={{ left:
|
|
158
|
+
style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
|
|
240
159
|
>
|
|
241
|
-
{
|
|
160
|
+
{hovered.name}
|
|
242
161
|
</div>
|
|
243
162
|
) : null}
|
|
244
163
|
</>
|
|
@@ -1,205 +1,115 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { getNodeX } from './renderTreeCanvas.ts'
|
|
4
|
+
import { calcDepthToLeaf, findMaxBranchLen } from '../../hierarchy.ts'
|
|
5
|
+
|
|
6
|
+
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
7
|
+
|
|
8
|
+
function leaf(id: string, len?: number): HierarchyNode {
|
|
9
|
+
return {
|
|
10
|
+
data: { id, name: id, children: [] },
|
|
11
|
+
children: null,
|
|
12
|
+
parent: null,
|
|
13
|
+
depth: 0,
|
|
14
|
+
height: 0,
|
|
15
|
+
len,
|
|
6
16
|
}
|
|
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
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
function internal(
|
|
20
|
+
id: string,
|
|
21
|
+
children: HierarchyNode[],
|
|
22
|
+
len?: number,
|
|
23
|
+
): HierarchyNode {
|
|
24
|
+
const node: HierarchyNode = {
|
|
25
|
+
data: { id, name: id, children: children.map(c => c.data) },
|
|
26
|
+
children,
|
|
27
|
+
parent: null,
|
|
28
|
+
depth: 0,
|
|
29
|
+
height: 1,
|
|
30
|
+
len,
|
|
25
31
|
}
|
|
26
|
-
|
|
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
|
|
32
|
+
for (const child of children) {
|
|
33
|
+
child.parent = node
|
|
37
34
|
}
|
|
38
|
-
|
|
39
|
-
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
|
|
35
|
+
return node
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
describe('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
38
|
+
describe('calcDepthToLeaf', () => {
|
|
39
|
+
it('is 0 for a leaf', () => {
|
|
40
|
+
expect(calcDepthToLeaf(leaf('a'))).toBe(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('counts steps to the deepest tip', () => {
|
|
44
|
+
const tips = [leaf('a'), leaf('b')]
|
|
45
|
+
const intermediate = internal('int', tips)
|
|
46
|
+
const root = internal('root', [intermediate])
|
|
47
|
+
|
|
48
|
+
expect(calcDepthToLeaf(tips[0]!)).toBe(0)
|
|
49
|
+
expect(calcDepthToLeaf(intermediate)).toBe(1)
|
|
50
|
+
expect(calcDepthToLeaf(root)).toBe(2)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('memoizes without re-walking the subtree', () => {
|
|
54
|
+
// the renderer asks for every node's depth on every pass, so the memo has to
|
|
55
|
+
// short-circuit the traversal itself, not just the arithmetic. A sentinel
|
|
56
|
+
// planted on an already-computed child must survive a second root call.
|
|
57
|
+
const child = leaf('a')
|
|
58
|
+
const root = internal('root', [child])
|
|
59
|
+
expect(calcDepthToLeaf(root)).toBe(1)
|
|
60
|
+
|
|
61
|
+
child.depthToLeaf = 99
|
|
62
|
+
expect(calcDepthToLeaf(root)).toBe(1)
|
|
63
|
+
expect(child.depthToLeaf).toBe(99)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('findMaxBranchLen', () => {
|
|
68
|
+
it('uses the node itself when it is a leaf', () => {
|
|
69
|
+
expect(findMaxBranchLen(leaf('a', 1.5))).toBe(1.5)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('takes the max across descendants', () => {
|
|
73
|
+
expect(
|
|
74
|
+
findMaxBranchLen(internal('p', [leaf('a', 0.5), leaf('b', 1.5)], 0.3)),
|
|
75
|
+
).toBe(1.5)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('treats a missing len as 0', () => {
|
|
79
|
+
expect(findMaxBranchLen(leaf('a'))).toBe(0)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('getNodeX cladogram positioning', () => {
|
|
84
|
+
it('aligns every tip at the rightmost x', () => {
|
|
85
|
+
const tips = [leaf('a'), leaf('b')]
|
|
86
|
+
calcDepthToLeaf(internal('root', tips))
|
|
87
|
+
|
|
88
|
+
const xs = tips.map(tip => getNodeX(tip, false, 100, 1))
|
|
89
|
+
expect(xs).toEqual([100, 100])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('puts the root at the leftmost x and internal nodes in between', () => {
|
|
93
|
+
const tips = [leaf('a'), leaf('b')]
|
|
94
|
+
const intermediate = internal('int', tips)
|
|
95
|
+
const root = internal('root', [intermediate])
|
|
96
|
+
calcDepthToLeaf(root)
|
|
97
|
+
|
|
98
|
+
const xRoot = getNodeX(root, false, 100, 2)!
|
|
99
|
+
const xInt = getNodeX(intermediate, false, 100, 2)!
|
|
100
|
+
const xTip = getNodeX(tips[0]!, false, 100, 2)!
|
|
101
|
+
|
|
102
|
+
expect(xRoot).toBe(0)
|
|
103
|
+
expect(xInt).toBeGreaterThan(xRoot)
|
|
104
|
+
expect(xTip).toBeGreaterThan(xInt)
|
|
105
|
+
expect(xTip).toBe(100)
|
|
86
106
|
})
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
})
|
|
108
|
+
it('collapses to the root x when there is no topological depth', () => {
|
|
109
|
+
expect(getNodeX(leaf('a'), false, 100, 0)).toBe(0)
|
|
125
110
|
})
|
|
126
111
|
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
})
|
|
112
|
+
it('uses branch length in phylogram mode', () => {
|
|
113
|
+
expect(getNodeX(leaf('a', 2.5), true, 100, 1)).toBe(2.5)
|
|
204
114
|
})
|
|
205
115
|
})
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
forEachLink,
|
|
5
5
|
} from '../../hierarchy.ts'
|
|
6
6
|
import { setFontSize } from '../../setFontSize.ts'
|
|
7
|
+
import { getVisibleLeaves } from '../getVisibleLeaves.ts'
|
|
7
8
|
|
|
8
9
|
import type { ClickMapIndex } from './clickMap.ts'
|
|
9
10
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -27,7 +28,7 @@ function inYBlock(y: number, offsetY: number, by: number) {
|
|
|
27
28
|
// For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
|
|
28
29
|
// This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
|
|
29
30
|
// Matches ape's: xx <- max(xx) - xx (where xx is depth from each node to tips)
|
|
30
|
-
function getNodeX(
|
|
31
|
+
export function getNodeX(
|
|
31
32
|
node: HierarchyNode,
|
|
32
33
|
showBranchLen: boolean,
|
|
33
34
|
maxBranchLen: number,
|
|
@@ -253,20 +254,27 @@ export function renderTreeLabels({
|
|
|
253
254
|
treeAreaWidth,
|
|
254
255
|
treeAreaWidthMinusMargin,
|
|
255
256
|
marginLeft,
|
|
256
|
-
leaves,
|
|
257
257
|
noTree,
|
|
258
258
|
labelWidthMap,
|
|
259
259
|
collapsed,
|
|
260
260
|
} = model
|
|
261
|
+
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
262
|
+
// same slice the MSA renderer uses instead of walking every tip per block
|
|
263
|
+
const visibleLeaves = getVisibleLeaves({
|
|
264
|
+
model,
|
|
265
|
+
offsetY,
|
|
266
|
+
blockSizeY: blockSizeYOverride ?? model.blockSize,
|
|
267
|
+
})
|
|
261
268
|
const by = blockSizeYOverride ?? blockSize
|
|
262
269
|
const emHeight = ctx.measureText('M').width
|
|
263
270
|
if (labelsAlignRight) {
|
|
264
271
|
ctx.textAlign = 'right'
|
|
265
272
|
ctx.setLineDash([1, 3])
|
|
273
|
+
ctx.strokeStyle = theme.palette.text.primary
|
|
266
274
|
} else {
|
|
267
275
|
ctx.textAlign = 'start'
|
|
268
276
|
}
|
|
269
|
-
for (const node of
|
|
277
|
+
for (const node of visibleLeaves) {
|
|
270
278
|
const {
|
|
271
279
|
data: { name, id },
|
|
272
280
|
} = node
|
|
@@ -292,6 +300,9 @@ export function renderTreeLabels({
|
|
|
292
300
|
const smallPadding = 2
|
|
293
301
|
const offset = treeAreaWidthMinusMargin - smallPadding
|
|
294
302
|
if (drawTree && !noTree) {
|
|
303
|
+
// beginPath is load-bearing: without it every leader line re-strokes
|
|
304
|
+
// all the ones before it, which is quadratic in the label count
|
|
305
|
+
ctx.beginPath()
|
|
295
306
|
ctx.moveTo(xp + radius + 2, y)
|
|
296
307
|
ctx.lineTo(offset - smallPadding - width, y)
|
|
297
308
|
ctx.stroke()
|