react-msaview 5.0.7 → 5.0.16

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 (174) hide show
  1. package/bundle/index.js +135 -35
  2. package/bundle/index.js.LICENSE.txt +1 -1
  3. package/bundle/index.js.map +1 -1
  4. package/dist/components/Checkbox2.js +3 -6
  5. package/dist/components/Checkbox2.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +14 -0
  7. package/dist/components/MSAViewer.js +34 -0
  8. package/dist/components/MSAViewer.js.map +1 -0
  9. package/dist/components/SequenceTextArea.js +4 -4
  10. package/dist/components/SequenceTextArea.js.map +1 -1
  11. package/dist/components/Track.js +5 -24
  12. package/dist/components/Track.js.map +1 -1
  13. package/dist/components/dialogs/DomainDialog.js +2 -5
  14. package/dist/components/dialogs/DomainDialog.js.map +1 -1
  15. package/dist/components/dialogs/InterProScanDialog.js +7 -7
  16. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  17. package/dist/components/dialogs/SettingsDialog.js +3 -19
  18. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  19. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  20. package/dist/components/header/ColorSchemeMenu.js +19 -0
  21. package/dist/components/header/ColorSchemeMenu.js.map +1 -0
  22. package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
  23. package/dist/components/header/FileMenu.js +71 -0
  24. package/dist/components/header/FileMenu.js.map +1 -0
  25. package/dist/components/header/Header.js +8 -6
  26. package/dist/components/header/Header.js.map +1 -1
  27. package/dist/components/header/HeaderMenu.js +3 -145
  28. package/dist/components/header/HeaderMenu.js.map +1 -1
  29. package/dist/components/header/MSASettingsMenu.d.ts +6 -0
  30. package/dist/components/header/MSASettingsMenu.js +36 -0
  31. package/dist/components/header/MSASettingsMenu.js.map +1 -0
  32. package/dist/components/header/SettingsMenu.js +1 -21
  33. package/dist/components/header/SettingsMenu.js.map +1 -1
  34. package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
  35. package/dist/components/header/TreeSettingsMenu.js +74 -0
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -0
  37. package/dist/components/header/ZoomMenu.js +0 -8
  38. package/dist/components/header/ZoomMenu.js.map +1 -1
  39. package/dist/components/header/getDomainsMenu.d.ts +31 -0
  40. package/dist/components/header/getDomainsMenu.js +75 -0
  41. package/dist/components/header/getDomainsMenu.js.map +1 -0
  42. package/dist/components/import/ImportFormExamples.js +22 -20
  43. package/dist/components/import/ImportFormExamples.js.map +1 -1
  44. package/dist/components/msa/MSACanvas.js +13 -84
  45. package/dist/components/msa/MSACanvas.js.map +1 -1
  46. package/dist/components/msa/MSACanvasBlock.js +1 -3
  47. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  48. package/dist/components/msa/renderMSABlock.js +2 -4
  49. package/dist/components/msa/renderMSABlock.js.map +1 -1
  50. package/dist/components/msa/renderMSAMouseover.js +1 -7
  51. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  52. package/dist/components/tree/TreeCanvas.js +14 -91
  53. package/dist/components/tree/TreeCanvas.js.map +1 -1
  54. package/dist/components/tree/TreeNodeMenu.js +5 -16
  55. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  56. package/dist/components/tree/renderTreeCanvas.js +55 -22
  57. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  58. package/dist/constants.d.ts +0 -2
  59. package/dist/constants.js +0 -2
  60. package/dist/constants.js.map +1 -1
  61. package/dist/fetchUtils.d.ts +0 -1
  62. package/dist/fetchUtils.js +0 -4
  63. package/dist/fetchUtils.js.map +1 -1
  64. package/dist/flatToTree.d.ts +0 -5
  65. package/dist/flatToTree.js +13 -30
  66. package/dist/flatToTree.js.map +1 -1
  67. package/dist/hierarchy.d.ts +29 -0
  68. package/dist/hierarchy.js +164 -0
  69. package/dist/hierarchy.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +1 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/launchInterProScan.d.ts +0 -5
  74. package/dist/launchInterProScan.js +5 -3
  75. package/dist/launchInterProScan.js.map +1 -1
  76. package/dist/model/DataModel.d.ts +9 -0
  77. package/dist/model/DataModel.js +12 -1
  78. package/dist/model/DataModel.js.map +1 -1
  79. package/dist/model/msaModel.d.ts +3 -0
  80. package/dist/model/msaModel.js +0 -1
  81. package/dist/model/msaModel.js.map +1 -1
  82. package/dist/model/treeModel.d.ts +3 -6
  83. package/dist/model/treeModel.js +3 -15
  84. package/dist/model/treeModel.js.map +1 -1
  85. package/dist/model.d.ts +24 -77
  86. package/dist/model.js +118 -239
  87. package/dist/model.js.map +1 -1
  88. package/dist/neighborJoining.js +38 -629
  89. package/dist/neighborJoining.js.map +1 -1
  90. package/dist/parseAsn1.d.ts +0 -12
  91. package/dist/parseAsn1.js +125 -332
  92. package/dist/parseAsn1.js.map +1 -1
  93. package/dist/useWheelScroll.d.ts +8 -0
  94. package/dist/useWheelScroll.js +93 -0
  95. package/dist/useWheelScroll.js.map +1 -0
  96. package/dist/util.d.ts +1 -6
  97. package/dist/util.js +5 -34
  98. package/dist/util.js.map +1 -1
  99. package/dist/vendor/copyToClipboard.d.ts +1 -10
  100. package/dist/vendor/copyToClipboard.js +14 -109
  101. package/dist/vendor/copyToClipboard.js.map +1 -1
  102. package/dist/vendor/fileSaver.d.ts +1 -11
  103. package/dist/vendor/fileSaver.js +7 -76
  104. package/dist/vendor/fileSaver.js.map +1 -1
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/dist/version.js.map +1 -1
  108. package/package.json +10 -13
  109. package/src/collapseLogic.test.ts +115 -0
  110. package/src/components/Checkbox2.tsx +9 -18
  111. package/src/components/MSAViewer.tsx +67 -0
  112. package/src/components/SequenceTextArea.tsx +4 -4
  113. package/src/components/Track.tsx +10 -26
  114. package/src/components/dialogs/DomainDialog.tsx +4 -5
  115. package/src/components/dialogs/InterProScanDialog.tsx +7 -7
  116. package/src/components/dialogs/SettingsDialog.tsx +0 -37
  117. package/src/components/header/ColorSchemeMenu.tsx +35 -0
  118. package/src/components/header/FileMenu.tsx +84 -0
  119. package/src/components/header/Header.tsx +8 -6
  120. package/src/components/header/HeaderMenu.tsx +4 -155
  121. package/src/components/header/MSASettingsMenu.tsx +48 -0
  122. package/src/components/header/SettingsMenu.tsx +0 -23
  123. package/src/components/header/TreeSettingsMenu.tsx +96 -0
  124. package/src/components/header/ZoomMenu.tsx +0 -8
  125. package/src/components/header/getDomainsMenu.ts +83 -0
  126. package/src/components/import/ImportFormExamples.tsx +38 -35
  127. package/src/components/msa/MSACanvas.tsx +21 -91
  128. package/src/components/msa/MSACanvasBlock.tsx +1 -3
  129. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
  130. package/src/components/msa/renderMSABlock.ts +2 -5
  131. package/src/components/msa/renderMSAMouseover.ts +0 -6
  132. package/src/components/tree/TreeCanvas.tsx +35 -100
  133. package/src/components/tree/TreeNodeMenu.tsx +5 -14
  134. package/src/components/tree/renderTreeCanvas.test.ts +205 -0
  135. package/src/components/tree/renderTreeCanvas.ts +64 -27
  136. package/src/constants.ts +0 -2
  137. package/src/fetchUtils.ts +0 -5
  138. package/src/flatToTree.ts +20 -38
  139. package/src/hierarchy.test.ts +120 -0
  140. package/src/hierarchy.ts +221 -0
  141. package/src/index.ts +2 -0
  142. package/src/launchInterProScan.ts +4 -3
  143. package/src/model/DataModel.ts +12 -1
  144. package/src/model/msaModel.ts +0 -2
  145. package/src/model/treeModel.ts +2 -18
  146. package/src/model.ts +180 -278
  147. package/src/neighborJoining.ts +38 -628
  148. package/src/parseAsn1.test.ts +4 -1
  149. package/src/parseAsn1.ts +135 -405
  150. package/src/useWheelScroll.ts +109 -0
  151. package/src/util.ts +5 -50
  152. package/src/vendor/copyToClipboard.ts +14 -122
  153. package/src/vendor/fileSaver.ts +8 -105
  154. package/src/version.ts +1 -1
  155. package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
  156. package/dist/components/dialogs/AddTrackDialog.js +0 -30
  157. package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
  158. package/dist/components/dialogs/TabPanel.d.ts +0 -6
  159. package/dist/components/dialogs/TabPanel.js +0 -6
  160. package/dist/components/dialogs/TabPanel.js.map +0 -1
  161. package/dist/components/header/ZoomStar.js +0 -40
  162. package/dist/components/header/ZoomStar.js.map +0 -1
  163. package/dist/layout.d.ts +0 -26
  164. package/dist/layout.js +0 -74
  165. package/dist/layout.js.map +0 -1
  166. package/dist/reparseTree.d.ts +0 -2
  167. package/dist/reparseTree.js +0 -15
  168. package/dist/reparseTree.js.map +0 -1
  169. package/src/components/dialogs/AddTrackDialog.tsx +0 -85
  170. package/src/components/dialogs/TabPanel.tsx +0 -19
  171. package/src/components/header/ZoomStar.tsx +0 -74
  172. package/src/createPaletteMap.test.ts +0 -57
  173. package/src/layout.ts +0 -118
  174. package/src/reparseTree.ts +0 -18
@@ -1,9 +1,10 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useCallback, useRef } from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
5
  import Loading from './Loading.tsx'
6
6
  import MSACanvasBlock from './MSACanvasBlock.tsx'
7
+ import { useWheelScroll } from '../../useWheelScroll.ts'
7
8
 
8
9
  import type { MsaViewModel } from '../../model.ts'
9
10
 
@@ -17,100 +18,29 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
17
18
  blocks2d,
18
19
  } = model
19
20
  const ref = useRef<HTMLDivElement>(null)
20
- // wheel
21
- const scheduled = useRef(false)
22
- const deltaX = useRef(0)
23
- const deltaY = useRef(0)
24
- // mouse click-and-drag scrolling
25
- const prevX = useRef(0)
26
- const prevY = useRef(0)
27
- const [mouseDragging, setMouseDragging] = useState(false)
28
- useEffect(() => {
29
- const curr = ref.current
30
- if (!curr) {
31
- return
32
- }
33
- function onWheel(event: WheelEvent) {
34
- deltaX.current += event.deltaX
35
- deltaY.current += event.deltaY
36
-
37
- if (!scheduled.current) {
38
- scheduled.current = true
39
- requestAnimationFrame(() => {
40
- model.doScrollX(-deltaX.current)
41
- model.doScrollY(-deltaY.current)
42
- deltaX.current = 0
43
- deltaY.current = 0
44
- scheduled.current = false
45
- })
46
- }
47
- event.preventDefault()
48
- event.stopPropagation()
49
- }
50
- curr.addEventListener('wheel', onWheel, { passive: false })
51
- return () => {
52
- curr.removeEventListener('wheel', onWheel)
53
- }
54
- }, [model])
55
-
56
- useEffect(() => {
57
- if (mouseDragging) {
58
- function globalMouseMove(event: MouseEvent) {
59
- event.preventDefault()
60
- const currX = event.clientX
61
- const currY = event.clientY
62
- const distanceX = currX - prevX.current
63
- const distanceY = currY - prevY.current
64
- if (distanceX || distanceY) {
65
- if (!scheduled.current) {
66
- scheduled.current = true
67
- window.requestAnimationFrame(() => {
68
- model.doScrollX(distanceX)
69
- model.doScrollY(distanceY)
70
- scheduled.current = false
71
- prevX.current = event.clientX
72
- prevY.current = event.clientY
73
- })
74
- }
75
- }
76
- }
77
-
78
- function globalMouseUp() {
79
- prevX.current = 0
80
- setMouseDragging(false)
81
- }
82
-
83
- window.addEventListener('mousemove', globalMouseMove, true)
84
- window.addEventListener('mouseup', globalMouseUp, true)
85
- return () => {
86
- window.removeEventListener('mousemove', globalMouseMove, true)
87
- window.removeEventListener('mouseup', globalMouseUp, true)
88
- }
89
- }
90
- return undefined
91
- }, [model, mouseDragging])
21
+ const onScrollX = useCallback(
22
+ (d: number) => {
23
+ model.doScrollX(d)
24
+ },
25
+ [model],
26
+ )
27
+ const onScrollY = useCallback(
28
+ (d: number) => {
29
+ model.doScrollY(d)
30
+ },
31
+ [model],
32
+ )
33
+ const { onMouseDown, onMouseUp } = useWheelScroll({
34
+ ref,
35
+ onScrollX,
36
+ onScrollY,
37
+ })
92
38
 
93
39
  return (
94
40
  <div
95
41
  ref={ref}
96
- onMouseDown={event => {
97
- // check if clicking a draggable element or a resize handle
98
- const target = event.target as HTMLElement
99
- if (target.draggable || target.dataset.resizer) {
100
- return
101
- }
102
-
103
- // otherwise do click and drag scroll
104
- if (event.button === 0) {
105
- prevX.current = event.clientX
106
- prevY.current = event.clientY
107
- setMouseDragging(true)
108
- }
109
- }}
110
- onMouseUp={event => {
111
- event.preventDefault()
112
- setMouseDragging(false)
113
- }}
42
+ onMouseDown={onMouseDown}
43
+ onMouseUp={onMouseUp}
114
44
  onMouseLeave={event => {
115
45
  event.preventDefault()
116
46
  }}
@@ -52,8 +52,7 @@ const MSACanvasBlock = observer(function ({
52
52
  blockSize * highResScaleFactor,
53
53
  blockSize * highResScaleFactor,
54
54
  )
55
- const { actuallyShowDomains } = model
56
- if (actuallyShowDomains) {
55
+ if (model.actuallyShowDomains) {
57
56
  renderBoxFeatureCanvasBlock({
58
57
  ctx,
59
58
  offsetX,
@@ -102,7 +101,6 @@ const MSACanvasBlock = observer(function ({
102
101
  if (x >= 0 && x < model.numColumns && y >= 0 && y < model.numRows) {
103
102
  model.setMousePos(x, y)
104
103
  } else {
105
- // Clear mouse position when outside bounds
106
104
  model.setMousePos(undefined, undefined)
107
105
  }
108
106
  }}
@@ -1,6 +1,6 @@
1
+ import type { HierarchyNode } from '../../hierarchy.ts'
1
2
  import type { MsaViewModel } from '../../model.ts'
2
3
  import type { NodeWithIdsAndLength } from '../../types.ts'
3
- import type { HierarchyNode } from 'd3-hierarchy'
4
4
 
5
5
  export function renderBoxFeatureCanvasBlock({
6
6
  model,
@@ -1,7 +1,7 @@
1
+ import type { HierarchyNode } from '../../hierarchy.ts'
1
2
  import type { MsaViewModel } from '../../model.ts'
2
3
  import type { NodeWithIdsAndLength } from '../../types.ts'
3
4
  import type { Theme } from '@mui/material'
4
- import type { HierarchyNode } from 'd3-hierarchy'
5
5
 
6
6
  export function renderMSABlock({
7
7
  model,
@@ -180,7 +180,6 @@ function drawText({
180
180
  colorScheme,
181
181
  columns,
182
182
  colWidth,
183
- contrastLettering,
184
183
  rowHeight,
185
184
  relativeTo,
186
185
  } = model
@@ -214,9 +213,7 @@ function drawText({
214
213
  const displayLetter = isMatchingReference ? '.' : letter
215
214
 
216
215
  const color = colorScheme[letter.toUpperCase()]
217
- const contrast = contrastLettering
218
- ? contrastScheme[letter.toUpperCase()] || 'black'
219
- : 'black'
216
+ const contrast = contrastScheme[letter.toUpperCase()] || 'black'
220
217
 
221
218
  // note: -rowHeight/4 matches +rowHeight/4 in tree
222
219
  ctx.fillStyle = actuallyShowDomains
@@ -22,8 +22,6 @@ export function renderMouseover({
22
22
  scrollX,
23
23
  scrollY,
24
24
  mouseRow,
25
- // @ts-expect-error
26
- mouseCol2,
27
25
  mouseClickRow,
28
26
  mouseClickCol,
29
27
  relativeTo,
@@ -79,8 +77,4 @@ export function renderMouseover({
79
77
  ctx.fillStyle = highlightColor
80
78
  ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
81
79
  }
82
- if (mouseCol2 !== undefined) {
83
- ctx.fillStyle = highlightColor
84
- ctx.fillRect(mouseCol2 * colWidth + scrollX, 0, colWidth, height)
85
- }
86
80
  }
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useCallback, useEffect, useRef } from 'react'
2
2
 
3
3
  import { isAlive } from '@jbrowse/mobx-state-tree'
4
4
  import { autorun } from 'mobx'
@@ -6,79 +6,25 @@ import { observer } from 'mobx-react'
6
6
 
7
7
  import TreeCanvasBlock from './TreeCanvasBlock.tsx'
8
8
  import { padding } from './renderTreeCanvas.ts'
9
+ import { useWheelScroll } from '../../useWheelScroll.ts'
9
10
 
10
11
  import type { MsaViewModel } from '../../model.ts'
11
12
 
13
+ const referenceColor = 'rgba(0,128,255,0.3)'
14
+ const treeHoverColor = 'rgba(255,165,0,0.2)'
15
+
12
16
  const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
13
17
  const ref = useRef<HTMLDivElement>(null)
14
18
  const mouseoverRef = useRef<HTMLCanvasElement>(null)
15
- const scheduled = useRef(false)
16
- const deltaY = useRef(0)
17
- const prevY = useRef(0)
18
19
  const { treeWidth, height, blocksY, treeAreaWidth } = model
19
- const [mouseDragging, setMouseDragging] = useState(false)
20
-
21
- useEffect(() => {
22
- const curr = ref.current
23
- if (!curr) {
24
- return
25
- }
26
- function onWheel(event: WheelEvent) {
27
- deltaY.current += event.deltaY
28
-
29
- if (!scheduled.current) {
30
- scheduled.current = true
31
- requestAnimationFrame(() => {
32
- model.doScrollY(-deltaY.current)
33
- deltaY.current = 0
34
- scheduled.current = false
35
- })
36
- }
37
- event.preventDefault()
38
- event.stopPropagation()
39
- }
40
- curr.addEventListener('wheel', onWheel, { passive: false })
41
- return () => {
42
- curr.removeEventListener('wheel', onWheel)
43
- }
44
- }, [model])
45
-
46
- useEffect(() => {
47
- if (mouseDragging) {
48
- function globalMouseMove(event: MouseEvent) {
49
- event.preventDefault()
50
- const currY = event.clientY
51
- const distanceY = currY - prevY.current
52
- if (distanceY) {
53
- if (!scheduled.current) {
54
- scheduled.current = true
55
- window.requestAnimationFrame(() => {
56
- model.doScrollY(distanceY)
57
- scheduled.current = false
58
- prevY.current = event.clientY
59
- })
60
- }
61
- }
62
- }
63
-
64
- function globalMouseUp() {
65
- prevY.current = 0
66
- setMouseDragging(false)
67
- }
68
-
69
- window.addEventListener('mousemove', globalMouseMove, true)
70
- window.addEventListener('mouseup', globalMouseUp, true)
71
- return () => {
72
- window.removeEventListener('mousemove', globalMouseMove, true)
73
- window.removeEventListener('mouseup', globalMouseUp, true)
74
- }
75
- }
76
- return undefined
77
- }, [model, mouseDragging])
20
+ const onScrollY = useCallback(
21
+ (d: number) => {
22
+ model.doScrollY(d)
23
+ },
24
+ [model],
25
+ )
26
+ const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
78
27
 
79
- // Global tree mouseover effect. Only [model] is needed in the dependency
80
- // array because autorun internally tracks all accessed observables
81
- // (treeAreaWidth, height, scrollY, etc.) and re-runs when they change
82
28
  useEffect(() => {
83
29
  const ctx = mouseoverRef.current?.getContext('2d')
84
30
  return ctx
@@ -97,33 +43,38 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
97
43
  ctx.resetTransform()
98
44
  ctx.clearRect(0, 0, w, h)
99
45
 
100
- // Highlight reference row (relativeTo) persistently
101
46
  if (relativeTo) {
102
47
  const referenceLeaf = leaves.find(
103
48
  leaf => leaf.data.name === relativeTo,
104
49
  )
105
50
  if (referenceLeaf) {
106
- const y = referenceLeaf.x! + sy
107
- ctx.fillStyle = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
108
- ctx.fillRect(0, y - rowHeight / 2, w, rowHeight)
51
+ ctx.fillStyle = referenceColor
52
+ ctx.fillRect(
53
+ 0,
54
+ referenceLeaf.x! + sy - rowHeight / 2,
55
+ w,
56
+ rowHeight,
57
+ )
109
58
  }
110
59
  }
111
60
 
112
- // Highlight multiple rows when hovering over tree nodes
113
61
  if (hoveredTreeNode) {
114
- ctx.fillStyle = 'rgba(255,165,0,0.2)' // Orange highlight for tree hover
62
+ ctx.fillStyle = treeHoverColor
115
63
  for (const descendantName of hoveredTreeNode.descendantNames) {
116
64
  const matchingLeaf = leaves.find(
117
65
  leaf => leaf.data.name === descendantName,
118
66
  )
119
67
  if (matchingLeaf) {
120
- const y = matchingLeaf.x! + sy
121
- ctx.fillRect(0, y - rowHeight / 2, w, rowHeight)
68
+ ctx.fillRect(
69
+ 0,
70
+ matchingLeaf.x! + sy - rowHeight / 2,
71
+ w,
72
+ rowHeight,
73
+ )
122
74
  }
123
75
  }
124
76
  }
125
77
 
126
- // Highlight single tree row corresponding to MSA mouseover (if not part of multi-row hover)
127
78
  if (
128
79
  mouseOverRowName &&
129
80
  mouseOverRowName !== relativeTo &&
@@ -133,9 +84,13 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
133
84
  leaf => leaf.data.name === mouseOverRowName,
134
85
  )
135
86
  if (matchingLeaf) {
136
- const y = matchingLeaf.x! + sy
137
- ctx.fillStyle = 'rgba(255,165,0,0.2)' // Orange highlight for MSA sync
138
- ctx.fillRect(0, y - rowHeight / 2, w, rowHeight)
87
+ ctx.fillStyle = treeHoverColor
88
+ ctx.fillRect(
89
+ 0,
90
+ matchingLeaf.x! + sy - rowHeight / 2,
91
+ w,
92
+ rowHeight,
93
+ )
139
94
  }
140
95
  }
141
96
  }
@@ -143,31 +98,11 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
143
98
  : undefined
144
99
  }, [model])
145
100
 
146
- function mouseDown(event: React.MouseEvent) {
147
- // check if clicking a draggable element or a resize handle
148
- const target = event.target as HTMLElement
149
- if (target.draggable || target.dataset.resizer) {
150
- return
151
- }
152
-
153
- // otherwise do click and drag scroll
154
- if (event.button === 0) {
155
- prevY.current = event.clientY
156
- setMouseDragging(true)
157
- }
158
- }
159
-
160
101
  return (
161
102
  <div
162
103
  ref={ref}
163
- onMouseDown={mouseDown}
164
- onMouseUp={event => {
165
- // this local mouseup is used in addition to the global because
166
- // sometimes the global add/remove are not called in time, resulting in
167
- // issue #533
168
- event.preventDefault()
169
- setMouseDragging(false)
170
- }}
104
+ onMouseDown={onMouseDown}
105
+ onMouseUp={onMouseUp}
171
106
  onMouseLeave={event => {
172
107
  event.preventDefault()
173
108
  }}
@@ -24,8 +24,9 @@ const TreeMenu = observer(function ({
24
24
  model: MsaViewModel
25
25
  onClose: () => void
26
26
  }) {
27
- const { collapsed, collapsedLeaves } = model
28
- const { name } = node
27
+ const { collapsed } = model
28
+ const { name, id } = node
29
+ const isCollapsed = collapsed.includes(id)
29
30
  return (
30
31
  <Menu
31
32
  anchorReference="anchorPosition"
@@ -62,21 +63,11 @@ const TreeMenu = observer(function ({
62
63
  <MenuItem
63
64
  dense
64
65
  onClick={() => {
65
- if (collapsed.includes(node.id)) {
66
- model.toggleCollapsed(node.id)
67
- } else {
68
- if (node.id.endsWith('-leafnode')) {
69
- model.toggleCollapsedLeaf(node.id)
70
- } else {
71
- model.toggleCollapsedLeaf(`${node.id}-leafnode`)
72
- }
73
- }
66
+ model.toggleCollapsed(id)
74
67
  onClose()
75
68
  }}
76
69
  >
77
- {collapsed.includes(node.id) || collapsedLeaves.includes(node.id)
78
- ? 'Show node'
79
- : 'Hide node'}
70
+ {isCollapsed ? 'Show node' : 'Hide node'}
80
71
  </MenuItem>
81
72
  <MenuItem
82
73
  dense
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ function calcDepthToLeaf(node: any): number {
4
+ if (node.depthToLeaf !== undefined) {
5
+ return node.depthToLeaf
6
+ }
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
+ }
18
+
19
+ function findMaxBranchLen(node: any): number {
20
+ let maxLen = node.len || 0
21
+ if (node.children) {
22
+ for (const child of node.children) {
23
+ maxLen = Math.max(maxLen, findMaxBranchLen(child))
24
+ }
25
+ }
26
+ return maxLen
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
37
+ }
38
+ const depthToLeaf = calcDepthToLeaf(node)
39
+ return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
40
+ }
41
+
42
+ describe('Tree rendering positioning', () => {
43
+ describe('calcDepthToLeaf', () => {
44
+ it('should return 0 for leaf nodes', () => {
45
+ const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
46
+ expect(calcDepthToLeaf(leaf)).toBe(0)
47
+ })
48
+
49
+ it('should return 1 for nodes with only leaf children', () => {
50
+ const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
51
+ const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
52
+ const parent: any = {
53
+ id: 'parent',
54
+ data: { id: 'parent' },
55
+ children: [leaf1, leaf2],
56
+ }
57
+ expect(calcDepthToLeaf(parent)).toBe(1)
58
+ })
59
+
60
+ it('should return correct depth for nested tree', () => {
61
+ const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
62
+ const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
63
+ const intermediate: any = {
64
+ id: 'int',
65
+ data: { id: 'int' },
66
+ children: [leaf1, leaf2],
67
+ }
68
+ const root: any = {
69
+ id: 'root',
70
+ data: { id: 'root' },
71
+ children: [intermediate],
72
+ }
73
+
74
+ expect(calcDepthToLeaf(leaf1)).toBe(0)
75
+ expect(calcDepthToLeaf(intermediate)).toBe(1)
76
+ expect(calcDepthToLeaf(root)).toBe(2)
77
+ })
78
+
79
+ it('should cache result', () => {
80
+ const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
81
+ const depth1 = calcDepthToLeaf(leaf)
82
+ const depth2 = calcDepthToLeaf(leaf)
83
+ expect(depth1).toBe(depth2)
84
+ expect(leaf.depthToLeaf).toBeDefined()
85
+ })
86
+ })
87
+
88
+ describe('findMaxBranchLen', () => {
89
+ it('should return node len for leaf', () => {
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
+ })
125
+ })
126
+
127
+ describe('getNodeX cladogram positioning', () => {
128
+ it('should position all leaves at rightmost for cladogram', () => {
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
+ })
204
+ })
205
+ })