react-msaview 5.0.16 → 5.1.1
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 +35 -135
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/clustalX.d.ts +5 -0
- package/dist/clustalX.js +88 -0
- package/dist/clustalX.js.map +1 -0
- package/dist/components/SequenceTextArea.js +2 -2
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/TextTrack.js +3 -5
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.d.ts +2 -1
- package/dist/components/Track.js +4 -3
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +6 -31
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +5 -3
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +4 -2
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.js +2 -27
- package/dist/components/header/MSASettingsMenu.js.map +1 -1
- package/dist/components/header/SettingsMenu.js +4 -69
- package/dist/components/header/SettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +3 -41
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/settingsMenuItems.d.ts +4 -0
- package/dist/components/header/settingsMenuItems.js +59 -0
- package/dist/components/header/settingsMenuItems.js.map +1 -0
- package/dist/components/minimap/Minimap.js +6 -31
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +31 -33
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +15 -9
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +12 -14
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +9 -3
- package/dist/components/tree/renderTreeCanvas.js +14 -40
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/fetchUtils.d.ts +2 -1
- package/dist/fetchUtils.js +16 -2
- package/dist/fetchUtils.js.map +1 -1
- package/dist/hierarchy.d.ts +2 -0
- package/dist/hierarchy.js +25 -0
- package/dist/hierarchy.js.map +1 -1
- package/dist/launchInterProScan.d.ts +2 -1
- package/dist/launchInterProScan.js +31 -15
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model.d.ts +150 -494
- package/dist/model.js +23 -116
- package/dist/model.js.map +1 -1
- package/dist/useColorContrast.d.ts +10 -0
- package/dist/useColorContrast.js +13 -0
- package/dist/useColorContrast.js.map +1 -0
- package/dist/useDragScroll.d.ts +12 -0
- package/dist/useDragScroll.js +47 -0
- package/dist/useDragScroll.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -6
- package/src/clustalX.test.ts +76 -0
- package/src/clustalX.ts +131 -0
- package/src/components/SequenceTextArea.tsx +2 -2
- package/src/components/TextTrack.tsx +3 -8
- package/src/components/Track.tsx +6 -9
- package/src/components/VerticalScrollbar.tsx +13 -40
- package/src/components/dialogs/InterProScanDialog.tsx +7 -1
- package/src/components/header/HeaderStatusArea.tsx +6 -1
- package/src/components/header/MSASettingsMenu.tsx +3 -27
- package/src/components/header/SettingsMenu.tsx +7 -79
- package/src/components/header/TreeSettingsMenu.tsx +4 -48
- package/src/components/header/settingsMenuItems.ts +68 -0
- package/src/components/minimap/Minimap.tsx +13 -38
- package/src/components/msa/MSACanvasBlock.tsx +53 -36
- package/src/components/msa/renderMSABlock.ts +16 -9
- package/src/components/tree/TreeCanvas.tsx +13 -35
- package/src/components/tree/renderTreeCanvas.ts +23 -38
- package/src/fetchUtils.ts +16 -2
- package/src/hierarchy.ts +27 -0
- package/src/launchInterProScan.ts +34 -10
- package/src/model.ts +32 -143
- package/src/useColorContrast.ts +18 -0
- package/src/useDragScroll.ts +55 -0
- package/src/version.ts +1 -1
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useCallback, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { useDragScroll } from '../../useDragScroll.ts'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
clientX: number
|
|
9
|
-
scrollX: number
|
|
10
|
-
}
|
|
7
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
11
8
|
|
|
12
9
|
const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
13
|
-
const [mouseDown, setMouseDown] = useState<ClickCoord>()
|
|
14
10
|
const [hovered, setHovered] = useState(false)
|
|
15
|
-
const scheduled = useRef(false)
|
|
16
11
|
const { scrollX, msaAreaWidth, minimapHeight, colWidth, numColumns } = model
|
|
17
12
|
const unit = msaAreaWidth / numColumns / colWidth
|
|
18
13
|
const left = -scrollX
|
|
@@ -22,32 +17,15 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
17
|
const fill = 'rgba(66, 119, 127, 0.3)'
|
|
23
18
|
const w = Math.max(e - s, 20)
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
scheduled.current = false
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function fn2() {
|
|
40
|
-
setMouseDown(undefined)
|
|
41
|
-
}
|
|
42
|
-
if (mouseDown !== undefined) {
|
|
43
|
-
document.addEventListener('mousemove', fn)
|
|
44
|
-
document.addEventListener('mouseup', fn2)
|
|
45
|
-
return () => {
|
|
46
|
-
document.removeEventListener('mousemove', fn)
|
|
47
|
-
document.removeEventListener('mousemove', fn2)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}, [model, unit, mouseDown])
|
|
20
|
+
const { startDrag } = useDragScroll(
|
|
21
|
+
'x',
|
|
22
|
+
useCallback(
|
|
23
|
+
(delta: number, startScroll: number) => {
|
|
24
|
+
model.setScrollX(startScroll - delta / unit)
|
|
25
|
+
},
|
|
26
|
+
[model, unit],
|
|
27
|
+
),
|
|
28
|
+
)
|
|
51
29
|
|
|
52
30
|
const barHeight = 12
|
|
53
31
|
const polygonHeight = minimapHeight - barHeight
|
|
@@ -84,10 +62,7 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
84
62
|
setHovered(false)
|
|
85
63
|
}}
|
|
86
64
|
onMouseDown={event => {
|
|
87
|
-
|
|
88
|
-
clientX: event.clientX,
|
|
89
|
-
scrollX: model.scrollX,
|
|
90
|
-
})
|
|
65
|
+
startDrag(event, model.scrollX)
|
|
91
66
|
}}
|
|
92
67
|
/>
|
|
93
68
|
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { BaseTooltip } from '@jbrowse/core/ui'
|
|
4
|
-
import { useTheme } from '@mui/material'
|
|
5
4
|
import { autorun } from 'mobx'
|
|
6
5
|
import { observer } from 'mobx-react'
|
|
7
6
|
|
|
8
7
|
import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
|
|
9
8
|
import { renderMSABlock } from './renderMSABlock.ts'
|
|
10
|
-
import {
|
|
9
|
+
import { useColorContrast } from '../../useColorContrast.ts'
|
|
11
10
|
|
|
12
11
|
import type { MsaViewModel } from '../../model.ts'
|
|
13
12
|
|
|
13
|
+
function eventToColRow(
|
|
14
|
+
event: React.MouseEvent,
|
|
15
|
+
el: HTMLCanvasElement,
|
|
16
|
+
offsetX: number,
|
|
17
|
+
offsetY: number,
|
|
18
|
+
colWidth: number,
|
|
19
|
+
rowHeight: number,
|
|
20
|
+
) {
|
|
21
|
+
const { left, top } = el.getBoundingClientRect()
|
|
22
|
+
return {
|
|
23
|
+
col: Math.floor((event.clientX - left + offsetX) / colWidth),
|
|
24
|
+
row: Math.floor((event.clientY - top + offsetY) / rowHeight),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const MSACanvasBlock = observer(function ({
|
|
15
29
|
model,
|
|
16
30
|
offsetX,
|
|
@@ -31,12 +45,7 @@ const MSACanvasBlock = observer(function ({
|
|
|
31
45
|
mouseClickRow,
|
|
32
46
|
highResScaleFactor,
|
|
33
47
|
} = model
|
|
34
|
-
const theme =
|
|
35
|
-
|
|
36
|
-
const contrastScheme = useMemo(
|
|
37
|
-
() => colorContrast(colorScheme, theme),
|
|
38
|
-
[colorScheme, theme],
|
|
39
|
-
)
|
|
48
|
+
const { theme, contrastScheme } = useColorContrast(colorScheme)
|
|
40
49
|
|
|
41
50
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
42
51
|
useEffect(() => {
|
|
@@ -87,36 +96,44 @@ const MSACanvasBlock = observer(function ({
|
|
|
87
96
|
<canvas
|
|
88
97
|
ref={ref}
|
|
89
98
|
onMouseMove={event => {
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
if (ref.current) {
|
|
100
|
+
setMousePosition({ x: event.clientX, y: event.clientY })
|
|
101
|
+
const { col, row } = eventToColRow(
|
|
102
|
+
event,
|
|
103
|
+
ref.current,
|
|
104
|
+
offsetX,
|
|
105
|
+
offsetY,
|
|
106
|
+
colWidth,
|
|
107
|
+
rowHeight,
|
|
108
|
+
)
|
|
109
|
+
// Only set mouse position if within valid MSA bounds
|
|
110
|
+
if (
|
|
111
|
+
col >= 0 &&
|
|
112
|
+
col < model.numColumns &&
|
|
113
|
+
row >= 0 &&
|
|
114
|
+
row < model.numRows
|
|
115
|
+
) {
|
|
116
|
+
model.setMousePos(col, row)
|
|
117
|
+
} else {
|
|
118
|
+
model.setMousePos(undefined, undefined)
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
}}
|
|
107
122
|
onClick={event => {
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
if (ref.current) {
|
|
124
|
+
const { col, row } = eventToColRow(
|
|
125
|
+
event,
|
|
126
|
+
ref.current,
|
|
127
|
+
offsetX,
|
|
128
|
+
offsetY,
|
|
129
|
+
colWidth,
|
|
130
|
+
rowHeight,
|
|
131
|
+
)
|
|
132
|
+
if (col === mouseClickCol && row === mouseClickRow) {
|
|
133
|
+
model.setMouseClickPos(undefined, undefined)
|
|
134
|
+
} else {
|
|
135
|
+
model.setMouseClickPos(col, row)
|
|
136
|
+
}
|
|
120
137
|
}
|
|
121
138
|
}}
|
|
122
139
|
onMouseLeave={() => {
|
|
@@ -41,7 +41,13 @@ export function renderMSABlock({
|
|
|
41
41
|
ctx.scale(k, k)
|
|
42
42
|
ctx.translate(-offsetX, rowHeight / 2 - offsetY)
|
|
43
43
|
ctx.textAlign = 'center'
|
|
44
|
-
|
|
44
|
+
// match an optional existing "bold " so re-renders don't accumulate it
|
|
45
|
+
// (an invalid "bold bold 16px" string would be silently ignored by canvas,
|
|
46
|
+
// freezing the font size against zoom changes)
|
|
47
|
+
ctx.font = ctx.font.replace(
|
|
48
|
+
/(?:bold )?\d+px/,
|
|
49
|
+
`${bgColor ? '' : 'bold '}${fontSize}px`,
|
|
50
|
+
)
|
|
45
51
|
|
|
46
52
|
const yStart = Math.max(0, Math.floor((offsetY - rowHeight) / rowHeight))
|
|
47
53
|
const yEnd = Math.max(0, Math.ceil((offsetY + by + rowHeight) / rowHeight))
|
|
@@ -130,14 +136,15 @@ function drawTiles({
|
|
|
130
136
|
const isMatchingReference =
|
|
131
137
|
referenceSeq && name !== relativeTo && letter === referenceSeq[j]
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
let color: string | undefined
|
|
140
|
+
if (isClustalX) {
|
|
141
|
+
color = model.colClustalX[xStart + j]![letter]
|
|
142
|
+
} else if (isPercentIdentity) {
|
|
143
|
+
const consensus = model.colConsensus[xStart + j]!
|
|
144
|
+
color = letter === consensus.letter ? consensus.color : undefined
|
|
145
|
+
} else {
|
|
146
|
+
color = colorScheme[letter.toUpperCase()]
|
|
147
|
+
}
|
|
141
148
|
if (bgColor || isClustalX || isPercentIdentity) {
|
|
142
149
|
// Use a very light background for matching positions in relative mode
|
|
143
150
|
const finalColor = isMatchingReference
|
|
@@ -43,35 +43,23 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
43
43
|
ctx.resetTransform()
|
|
44
44
|
ctx.clearRect(0, 0, w, h)
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
ctx.fillStyle = referenceColor
|
|
52
|
-
ctx.fillRect(
|
|
53
|
-
0,
|
|
54
|
-
referenceLeaf.x! + sy - rowHeight / 2,
|
|
55
|
-
w,
|
|
56
|
-
rowHeight,
|
|
57
|
-
)
|
|
46
|
+
const leafByName = new Map(leaves.map(leaf => [leaf.data.name, leaf]))
|
|
47
|
+
const fillRow = (name: string) => {
|
|
48
|
+
const leaf = leafByName.get(name)
|
|
49
|
+
if (leaf) {
|
|
50
|
+
ctx.fillRect(0, leaf.x! + sy - rowHeight / 2, w, rowHeight)
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
53
|
|
|
54
|
+
if (relativeTo) {
|
|
55
|
+
ctx.fillStyle = referenceColor
|
|
56
|
+
fillRow(relativeTo)
|
|
57
|
+
}
|
|
58
|
+
|
|
61
59
|
if (hoveredTreeNode) {
|
|
62
60
|
ctx.fillStyle = treeHoverColor
|
|
63
61
|
for (const descendantName of hoveredTreeNode.descendantNames) {
|
|
64
|
-
|
|
65
|
-
leaf => leaf.data.name === descendantName,
|
|
66
|
-
)
|
|
67
|
-
if (matchingLeaf) {
|
|
68
|
-
ctx.fillRect(
|
|
69
|
-
0,
|
|
70
|
-
matchingLeaf.x! + sy - rowHeight / 2,
|
|
71
|
-
w,
|
|
72
|
-
rowHeight,
|
|
73
|
-
)
|
|
74
|
-
}
|
|
62
|
+
fillRow(descendantName)
|
|
75
63
|
}
|
|
76
64
|
}
|
|
77
65
|
|
|
@@ -80,18 +68,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
80
68
|
mouseOverRowName !== relativeTo &&
|
|
81
69
|
!hoveredTreeNode?.descendantNames.includes(mouseOverRowName)
|
|
82
70
|
) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
if (matchingLeaf) {
|
|
87
|
-
ctx.fillStyle = treeHoverColor
|
|
88
|
-
ctx.fillRect(
|
|
89
|
-
0,
|
|
90
|
-
matchingLeaf.x! + sy - rowHeight / 2,
|
|
91
|
-
w,
|
|
92
|
-
rowHeight,
|
|
93
|
-
)
|
|
94
|
-
}
|
|
71
|
+
ctx.fillStyle = treeHoverColor
|
|
72
|
+
fillRow(mouseOverRowName)
|
|
95
73
|
}
|
|
96
74
|
}
|
|
97
75
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { descendants, links } from '../../hierarchy.ts'
|
|
1
|
+
import { calcDepthToLeaf, descendants, links } from '../../hierarchy.ts'
|
|
2
2
|
|
|
3
3
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
4
4
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -10,36 +10,6 @@ const extendBounds = 5
|
|
|
10
10
|
const radius = 2.5
|
|
11
11
|
const d = radius * 2
|
|
12
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
13
|
// Calculate node x-coordinate for both phylogram (with branch lengths) and cladogram (topology only) modes
|
|
44
14
|
// For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
|
|
45
15
|
// This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
|
|
@@ -84,19 +54,21 @@ export function renderTree({
|
|
|
84
54
|
ctx,
|
|
85
55
|
model,
|
|
86
56
|
theme,
|
|
57
|
+
maxBranchLen,
|
|
58
|
+
maxDepthToLeaf,
|
|
87
59
|
blockSizeYOverride,
|
|
88
60
|
}: {
|
|
89
61
|
offsetY: number
|
|
90
62
|
ctx: CanvasRenderingContext2D
|
|
91
63
|
model: MsaViewModel
|
|
92
64
|
theme: Theme
|
|
65
|
+
maxBranchLen: number
|
|
66
|
+
maxDepthToLeaf: number
|
|
93
67
|
blockSizeYOverride?: number
|
|
94
68
|
}) {
|
|
95
69
|
const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
|
|
96
70
|
const by = blockSizeYOverride || blockSize
|
|
97
71
|
ctx.strokeStyle = theme.palette.text.primary
|
|
98
|
-
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
99
|
-
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
100
72
|
for (const link of links(hierarchy)) {
|
|
101
73
|
const { source, target } = link
|
|
102
74
|
const sy = source.x!
|
|
@@ -127,12 +99,16 @@ export function renderNodeBubbles({
|
|
|
127
99
|
clickMap,
|
|
128
100
|
offsetY,
|
|
129
101
|
model,
|
|
102
|
+
maxBranchLen,
|
|
103
|
+
maxDepthToLeaf,
|
|
130
104
|
blockSizeYOverride,
|
|
131
105
|
}: {
|
|
132
106
|
ctx: CanvasRenderingContext2D
|
|
133
107
|
clickMap?: ClickMapIndex
|
|
134
108
|
offsetY: number
|
|
135
109
|
model: MsaViewModel
|
|
110
|
+
maxBranchLen: number
|
|
111
|
+
maxDepthToLeaf: number
|
|
136
112
|
blockSizeYOverride?: number
|
|
137
113
|
}) {
|
|
138
114
|
const {
|
|
@@ -143,8 +119,6 @@ export function renderNodeBubbles({
|
|
|
143
119
|
marginLeft: ml,
|
|
144
120
|
} = model
|
|
145
121
|
const by = blockSizeYOverride || blockSize
|
|
146
|
-
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
147
|
-
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
148
122
|
for (const node of descendants(hierarchy)) {
|
|
149
123
|
const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
|
|
150
124
|
if (x === undefined) {
|
|
@@ -184,6 +158,8 @@ export function renderTreeLabels({
|
|
|
184
158
|
offsetY,
|
|
185
159
|
ctx,
|
|
186
160
|
clickMap,
|
|
161
|
+
maxBranchLen,
|
|
162
|
+
maxDepthToLeaf,
|
|
187
163
|
blockSizeYOverride,
|
|
188
164
|
}: {
|
|
189
165
|
model: MsaViewModel
|
|
@@ -191,6 +167,8 @@ export function renderTreeLabels({
|
|
|
191
167
|
ctx: CanvasRenderingContext2D
|
|
192
168
|
clickMap?: ClickMapIndex
|
|
193
169
|
theme: Theme
|
|
170
|
+
maxBranchLen: number
|
|
171
|
+
maxDepthToLeaf: number
|
|
194
172
|
blockSizeYOverride?: number
|
|
195
173
|
}) {
|
|
196
174
|
const {
|
|
@@ -205,7 +183,6 @@ export function renderTreeLabels({
|
|
|
205
183
|
marginLeft,
|
|
206
184
|
leaves,
|
|
207
185
|
noTree,
|
|
208
|
-
hierarchy,
|
|
209
186
|
} = model
|
|
210
187
|
const by = blockSizeYOverride || blockSize
|
|
211
188
|
const emHeight = ctx.measureText('M').width
|
|
@@ -215,8 +192,6 @@ export function renderTreeLabels({
|
|
|
215
192
|
} else {
|
|
216
193
|
ctx.textAlign = 'start'
|
|
217
194
|
}
|
|
218
|
-
const maxBranchLen = findMaxBranchLen(hierarchy)
|
|
219
|
-
const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
|
|
220
195
|
for (const node of leaves) {
|
|
221
196
|
const {
|
|
222
197
|
data: { name, id },
|
|
@@ -319,12 +294,18 @@ export function renderTreeCanvas({
|
|
|
319
294
|
const font = ctx.font
|
|
320
295
|
ctx.font = font.replace(/\d+px/, `${fontSize}px`)
|
|
321
296
|
|
|
297
|
+
// memoized on the model and shared across the tree/bubble/label passes (and
|
|
298
|
+
// across all tree blocks) rather than re-traversing the hierarchy in each
|
|
299
|
+
const { maxBranchLength: maxBranchLen, maxDepthToLeaf } = model
|
|
300
|
+
|
|
322
301
|
if (!noTree && drawTree) {
|
|
323
302
|
renderTree({
|
|
324
303
|
ctx,
|
|
325
304
|
offsetY,
|
|
326
305
|
model,
|
|
327
306
|
theme,
|
|
307
|
+
maxBranchLen,
|
|
308
|
+
maxDepthToLeaf,
|
|
328
309
|
blockSizeYOverride,
|
|
329
310
|
})
|
|
330
311
|
|
|
@@ -334,6 +315,8 @@ export function renderTreeCanvas({
|
|
|
334
315
|
offsetY,
|
|
335
316
|
clickMap,
|
|
336
317
|
model,
|
|
318
|
+
maxBranchLen,
|
|
319
|
+
maxDepthToLeaf,
|
|
337
320
|
blockSizeYOverride,
|
|
338
321
|
})
|
|
339
322
|
}
|
|
@@ -346,6 +329,8 @@ export function renderTreeCanvas({
|
|
|
346
329
|
model,
|
|
347
330
|
clickMap,
|
|
348
331
|
theme,
|
|
332
|
+
maxBranchLen,
|
|
333
|
+
maxDepthToLeaf,
|
|
349
334
|
blockSizeYOverride,
|
|
350
335
|
})
|
|
351
336
|
}
|
package/src/fetchUtils.ts
CHANGED
|
@@ -20,6 +20,20 @@ export async function jsonfetch<T>(url: string, args?: RequestInit) {
|
|
|
20
20
|
return response.json() as T
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export function timeout(time: number) {
|
|
24
|
-
return new Promise(
|
|
23
|
+
export function timeout(time: number, signal?: AbortSignal) {
|
|
24
|
+
return new Promise<void>((resolve, reject) => {
|
|
25
|
+
if (signal?.aborted) {
|
|
26
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
27
|
+
} else {
|
|
28
|
+
const id = setTimeout(resolve, time)
|
|
29
|
+
signal?.addEventListener('abort', () => {
|
|
30
|
+
clearTimeout(id)
|
|
31
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isAbortError(e: unknown) {
|
|
38
|
+
return e instanceof DOMException && e.name === 'AbortError'
|
|
25
39
|
}
|
package/src/hierarchy.ts
CHANGED
|
@@ -204,6 +204,33 @@ export function collapse<T>(node: HierarchyNode<T>) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// Cladogram positioning based on ape's plot.phylo: uses topological depth (max
|
|
208
|
+
// steps to a tip) instead of branch length so all leaves align at the rightmost
|
|
209
|
+
// x. Memoizes onto node.depthToLeaf since the layout walks the tree repeatedly.
|
|
210
|
+
// See https://github.com/emmanuelparadis/ape/blob/master/R/plot.phylo.R
|
|
211
|
+
export function calcDepthToLeaf(node: HierarchyNode): number {
|
|
212
|
+
if (node.depthToLeaf === undefined) {
|
|
213
|
+
let maxDepth = 0
|
|
214
|
+
if (node.children) {
|
|
215
|
+
for (const child of node.children) {
|
|
216
|
+
maxDepth = Math.max(maxDepth, 1 + calcDepthToLeaf(child))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
node.depthToLeaf = maxDepth
|
|
220
|
+
}
|
|
221
|
+
return node.depthToLeaf
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function findMaxBranchLen(node: HierarchyNode): number {
|
|
225
|
+
let maxLen = node.len || 0
|
|
226
|
+
if (node.children) {
|
|
227
|
+
for (const child of node.children) {
|
|
228
|
+
maxLen = Math.max(maxLen, findMaxBranchLen(child))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return maxLen
|
|
232
|
+
}
|
|
233
|
+
|
|
207
234
|
export function maxLength(d: HierarchyNode): number {
|
|
208
235
|
return (
|
|
209
236
|
(d.data.length || 0) +
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
-
import { jsonfetch, textfetch, timeout } from './fetchUtils.ts'
|
|
3
|
+
import { isAbortError, jsonfetch, textfetch, timeout } from './fetchUtils.ts'
|
|
4
4
|
|
|
5
5
|
import type { MsaViewModel } from './model.ts'
|
|
6
6
|
|
|
@@ -29,12 +29,14 @@ async function runInterProScan({
|
|
|
29
29
|
onJobId,
|
|
30
30
|
programs,
|
|
31
31
|
model,
|
|
32
|
+
signal,
|
|
32
33
|
}: {
|
|
33
34
|
seq: string
|
|
34
35
|
programs: string[]
|
|
35
36
|
onProgress: (arg?: { msg: string; url?: string }) => void
|
|
36
37
|
onJobId?: (arg: string) => void
|
|
37
38
|
model: MsaViewModel
|
|
39
|
+
signal?: AbortSignal
|
|
38
40
|
}) {
|
|
39
41
|
const jobId = await textfetch(`${base}/iprscan5/run`, {
|
|
40
42
|
method: 'POST',
|
|
@@ -43,37 +45,42 @@ async function runInterProScan({
|
|
|
43
45
|
sequence: seq,
|
|
44
46
|
programs: programs.join(','),
|
|
45
47
|
}),
|
|
48
|
+
signal,
|
|
46
49
|
})
|
|
47
50
|
onJobId?.(jobId)
|
|
48
51
|
await wait({
|
|
49
52
|
jobId,
|
|
50
53
|
onProgress,
|
|
54
|
+
signal,
|
|
51
55
|
})
|
|
52
|
-
await loadInterProScanResultsWithStatus({ jobId, model })
|
|
56
|
+
await loadInterProScanResultsWithStatus({ jobId, model, onProgress, signal })
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function loadInterProScanResults(jobId: string) {
|
|
59
|
+
function loadInterProScanResults(jobId: string, signal?: AbortSignal) {
|
|
56
60
|
return jsonfetch<InterProScanResponse>(
|
|
57
61
|
`${base}/iprscan5/result/${jobId}/json`,
|
|
62
|
+
{ signal },
|
|
58
63
|
)
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
async function wait({
|
|
62
67
|
onProgress,
|
|
63
68
|
jobId,
|
|
69
|
+
signal,
|
|
64
70
|
}: {
|
|
65
71
|
jobId: string
|
|
66
72
|
onProgress: (arg?: { msg: string; url?: string }) => void
|
|
73
|
+
signal?: AbortSignal
|
|
67
74
|
}) {
|
|
68
75
|
const url = `${base}/iprscan5/status/${jobId}`
|
|
69
76
|
try {
|
|
70
77
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
71
78
|
while (true) {
|
|
72
79
|
for (let i = 0; i < 10; i++) {
|
|
73
|
-
await timeout(1000)
|
|
80
|
+
await timeout(1000, signal)
|
|
74
81
|
onProgress({ msg: `Checking status ${10 - i}`, url })
|
|
75
82
|
}
|
|
76
|
-
const result = await textfetch(url)
|
|
83
|
+
const result = await textfetch(url, { signal })
|
|
77
84
|
if (result.includes('FINISHED')) {
|
|
78
85
|
break
|
|
79
86
|
}
|
|
@@ -93,6 +100,7 @@ export async function launchInterProScan({
|
|
|
93
100
|
onJobId,
|
|
94
101
|
onProgress,
|
|
95
102
|
model,
|
|
103
|
+
signal,
|
|
96
104
|
}: {
|
|
97
105
|
algorithm: string
|
|
98
106
|
seq: string
|
|
@@ -100,6 +108,7 @@ export async function launchInterProScan({
|
|
|
100
108
|
onProgress: (arg?: { msg: string; url?: string }) => void
|
|
101
109
|
onJobId?: (arg: string) => void
|
|
102
110
|
model: MsaViewModel
|
|
111
|
+
signal?: AbortSignal
|
|
103
112
|
}) {
|
|
104
113
|
try {
|
|
105
114
|
onProgress({ msg: `Launching ${algorithm} MSA` })
|
|
@@ -110,10 +119,17 @@ export async function launchInterProScan({
|
|
|
110
119
|
onProgress,
|
|
111
120
|
programs,
|
|
112
121
|
model,
|
|
122
|
+
signal,
|
|
113
123
|
})
|
|
114
124
|
} else {
|
|
115
125
|
throw new Error('unknown algorithm')
|
|
116
126
|
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (isAbortError(e)) {
|
|
129
|
+
getSession(model).notify('Cancelled InterProScan query', 'info')
|
|
130
|
+
} else {
|
|
131
|
+
throw e
|
|
132
|
+
}
|
|
117
133
|
} finally {
|
|
118
134
|
onProgress()
|
|
119
135
|
}
|
|
@@ -122,25 +138,33 @@ export async function launchInterProScan({
|
|
|
122
138
|
async function loadInterProScanResultsWithStatus({
|
|
123
139
|
jobId,
|
|
124
140
|
model,
|
|
141
|
+
onProgress,
|
|
142
|
+
signal,
|
|
125
143
|
}: {
|
|
126
144
|
jobId: string
|
|
127
145
|
model: MsaViewModel
|
|
146
|
+
onProgress: (arg?: { msg: string; url?: string }) => void
|
|
147
|
+
signal?: AbortSignal
|
|
128
148
|
}) {
|
|
129
149
|
try {
|
|
130
|
-
|
|
150
|
+
onProgress({
|
|
131
151
|
msg: `Downloading results of ${jobId} (for larger sequences this can be slow, click status to download and upload in the manual tab)`,
|
|
132
152
|
url: `https://www.ebi.ac.uk/Tools/services/rest/iprscan5/result/${jobId}/json`,
|
|
133
153
|
})
|
|
134
|
-
const ret = await loadInterProScanResults(jobId)
|
|
154
|
+
const ret = await loadInterProScanResults(jobId, signal)
|
|
135
155
|
model.setInterProAnnotations(
|
|
136
156
|
Object.fromEntries(ret.results.map(r => [r.xref[0]!.id, r])),
|
|
137
157
|
)
|
|
138
158
|
model.setShowDomains(true)
|
|
139
159
|
getSession(model).notify(`Loaded interproscan ${jobId} results`, 'success')
|
|
140
160
|
} catch (e) {
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
if (isAbortError(e)) {
|
|
162
|
+
getSession(model).notify('Cancelled InterProScan query', 'info')
|
|
163
|
+
} else {
|
|
164
|
+
console.error(e)
|
|
165
|
+
getSession(model).notifyError(`${e}`, e)
|
|
166
|
+
}
|
|
143
167
|
} finally {
|
|
144
|
-
|
|
168
|
+
onProgress()
|
|
145
169
|
}
|
|
146
170
|
}
|