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.
Files changed (85) hide show
  1. package/bundle/index.js +35 -135
  2. package/bundle/index.js.LICENSE.txt +1 -1
  3. package/bundle/index.js.map +1 -1
  4. package/dist/clustalX.d.ts +5 -0
  5. package/dist/clustalX.js +88 -0
  6. package/dist/clustalX.js.map +1 -0
  7. package/dist/components/SequenceTextArea.js +2 -2
  8. package/dist/components/SequenceTextArea.js.map +1 -1
  9. package/dist/components/TextTrack.js +3 -5
  10. package/dist/components/TextTrack.js.map +1 -1
  11. package/dist/components/Track.d.ts +2 -1
  12. package/dist/components/Track.js +4 -3
  13. package/dist/components/Track.js.map +1 -1
  14. package/dist/components/VerticalScrollbar.js +6 -31
  15. package/dist/components/VerticalScrollbar.js.map +1 -1
  16. package/dist/components/dialogs/InterProScanDialog.js +5 -3
  17. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  18. package/dist/components/header/HeaderStatusArea.js +4 -2
  19. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  20. package/dist/components/header/MSASettingsMenu.js +2 -27
  21. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  22. package/dist/components/header/SettingsMenu.js +4 -69
  23. package/dist/components/header/SettingsMenu.js.map +1 -1
  24. package/dist/components/header/TreeSettingsMenu.js +3 -41
  25. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  26. package/dist/components/header/settingsMenuItems.d.ts +4 -0
  27. package/dist/components/header/settingsMenuItems.js +59 -0
  28. package/dist/components/header/settingsMenuItems.js.map +1 -0
  29. package/dist/components/minimap/Minimap.js +6 -31
  30. package/dist/components/minimap/Minimap.js.map +1 -1
  31. package/dist/components/msa/MSACanvasBlock.js +31 -33
  32. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  33. package/dist/components/msa/renderMSABlock.js +15 -9
  34. package/dist/components/msa/renderMSABlock.js.map +1 -1
  35. package/dist/components/tree/TreeCanvas.js +12 -14
  36. package/dist/components/tree/TreeCanvas.js.map +1 -1
  37. package/dist/components/tree/renderTreeCanvas.d.ts +9 -3
  38. package/dist/components/tree/renderTreeCanvas.js +14 -40
  39. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  40. package/dist/fetchUtils.d.ts +2 -1
  41. package/dist/fetchUtils.js +16 -2
  42. package/dist/fetchUtils.js.map +1 -1
  43. package/dist/hierarchy.d.ts +2 -0
  44. package/dist/hierarchy.js +25 -0
  45. package/dist/hierarchy.js.map +1 -1
  46. package/dist/launchInterProScan.d.ts +2 -1
  47. package/dist/launchInterProScan.js +31 -15
  48. package/dist/launchInterProScan.js.map +1 -1
  49. package/dist/model.d.ts +150 -494
  50. package/dist/model.js +23 -116
  51. package/dist/model.js.map +1 -1
  52. package/dist/useColorContrast.d.ts +10 -0
  53. package/dist/useColorContrast.js +13 -0
  54. package/dist/useColorContrast.js.map +1 -0
  55. package/dist/useDragScroll.d.ts +12 -0
  56. package/dist/useDragScroll.js +47 -0
  57. package/dist/useDragScroll.js.map +1 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/dist/version.js.map +1 -1
  61. package/package.json +6 -6
  62. package/src/clustalX.test.ts +76 -0
  63. package/src/clustalX.ts +131 -0
  64. package/src/components/SequenceTextArea.tsx +2 -2
  65. package/src/components/TextTrack.tsx +3 -8
  66. package/src/components/Track.tsx +6 -9
  67. package/src/components/VerticalScrollbar.tsx +13 -40
  68. package/src/components/dialogs/InterProScanDialog.tsx +7 -1
  69. package/src/components/header/HeaderStatusArea.tsx +6 -1
  70. package/src/components/header/MSASettingsMenu.tsx +3 -27
  71. package/src/components/header/SettingsMenu.tsx +7 -79
  72. package/src/components/header/TreeSettingsMenu.tsx +4 -48
  73. package/src/components/header/settingsMenuItems.ts +68 -0
  74. package/src/components/minimap/Minimap.tsx +13 -38
  75. package/src/components/msa/MSACanvasBlock.tsx +53 -36
  76. package/src/components/msa/renderMSABlock.ts +16 -9
  77. package/src/components/tree/TreeCanvas.tsx +13 -35
  78. package/src/components/tree/renderTreeCanvas.ts +23 -38
  79. package/src/fetchUtils.ts +16 -2
  80. package/src/hierarchy.ts +27 -0
  81. package/src/launchInterProScan.ts +34 -10
  82. package/src/model.ts +32 -143
  83. package/src/useColorContrast.ts +18 -0
  84. package/src/useDragScroll.ts +55 -0
  85. package/src/version.ts +1 -1
@@ -1,18 +1,13 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useCallback, useState } from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- import type { MsaViewModel } from '../../model.ts'
5
+ import { useDragScroll } from '../../useDragScroll.ts'
6
6
 
7
- interface ClickCoord {
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
- useEffect(() => {
26
- function fn(event: MouseEvent) {
27
- if (mouseDown !== undefined) {
28
- if (!scheduled.current) {
29
- scheduled.current = true
30
- window.requestAnimationFrame(() => {
31
- model.setScrollX(
32
- mouseDown.scrollX - (event.clientX - mouseDown.clientX) / unit,
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
- setMouseDown({
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, useMemo, useRef, useState } from 'react'
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 { colorContrast } from '../../util.ts'
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 = useTheme()
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 (!ref.current) {
91
- return
92
- }
93
- setMousePosition({ x: event.clientX, y: event.clientY })
94
- const { left, top } = ref.current.getBoundingClientRect()
95
- const mouseX = event.clientX - left + offsetX
96
- const mouseY = event.clientY - top + offsetY
97
- const x = Math.floor(mouseX / colWidth)
98
- const y = Math.floor(mouseY / rowHeight)
99
-
100
- // Only set mouse position if within valid MSA bounds
101
- if (x >= 0 && x < model.numColumns && y >= 0 && y < model.numRows) {
102
- model.setMousePos(x, y)
103
- } else {
104
- model.setMousePos(undefined, undefined)
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 (!ref.current) {
109
- return
110
- }
111
- const { left, top } = ref.current.getBoundingClientRect()
112
- const mouseX = event.clientX - left + offsetX
113
- const mouseY = event.clientY - top + offsetY
114
- const x = Math.floor(mouseX / colWidth)
115
- const y = Math.floor(mouseY / rowHeight)
116
- if (x === mouseClickCol && y === mouseClickRow) {
117
- model.setMouseClickPos(undefined, undefined)
118
- } else {
119
- model.setMouseClickPos(x, y)
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
- ctx.font = ctx.font.replace(/\d+px/, `${bgColor ? '' : 'bold '}${fontSize}px`)
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
- const color = isClustalX
134
- ? model.colClustalX[xStart + j]![letter]
135
- : isPercentIdentity
136
- ? (() => {
137
- const consensus = model.colConsensus[xStart + j]!
138
- return letter === consensus.letter ? consensus.color : undefined
139
- })()
140
- : colorScheme[letter.toUpperCase()]
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
- if (relativeTo) {
47
- const referenceLeaf = leaves.find(
48
- leaf => leaf.data.name === relativeTo,
49
- )
50
- if (referenceLeaf) {
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
- const matchingLeaf = leaves.find(
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
- const matchingLeaf = leaves.find(
84
- leaf => leaf.data.name === mouseOverRowName,
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(res => setTimeout(res, time))
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
- model.setStatus({
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
- console.error(e)
142
- getSession(model).notifyError(`${e}`, e)
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
- model.setStatus()
168
+ onProgress()
145
169
  }
146
170
  }