react-msaview 8.1.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/bundle/index.js +104 -117
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +4 -3
  4. package/dist/columnCounts.js +11 -6
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/CopyButton.d.ts +1 -1
  7. package/dist/components/CopyButton.js +1 -1
  8. package/dist/components/CopyButton.js.map +1 -1
  9. package/dist/components/DragHandle.js +14 -2
  10. package/dist/components/DragHandle.js.map +1 -1
  11. package/dist/components/Loading.js +10 -11
  12. package/dist/components/Loading.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +12 -1
  14. package/dist/components/MSAViewer.js +12 -1
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/SequenceTextArea.d.ts +11 -0
  17. package/dist/components/SequenceTextArea.js +40 -8
  18. package/dist/components/SequenceTextArea.js.map +1 -1
  19. package/dist/components/StatusMessage.d.ts +12 -0
  20. package/dist/components/StatusMessage.js +43 -0
  21. package/dist/components/StatusMessage.js.map +1 -0
  22. package/dist/components/Track.js +3 -3
  23. package/dist/components/Track.js.map +1 -1
  24. package/dist/components/VerticalScrollbar.js +6 -1
  25. package/dist/components/VerticalScrollbar.js.map +1 -1
  26. package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
  27. package/dist/components/dialogs/ExportSVGDialog.js +4 -2
  28. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  29. package/dist/components/dialogs/FeatureDialog.js +15 -10
  30. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  31. package/dist/components/getVisibleLeaves.d.ts +1 -1
  32. package/dist/components/header/FileMenu.js +9 -0
  33. package/dist/components/header/FileMenu.js.map +1 -1
  34. package/dist/components/header/GappynessSlider.js +15 -3
  35. package/dist/components/header/GappynessSlider.js.map +1 -1
  36. package/dist/components/header/Header.js +6 -7
  37. package/dist/components/header/Header.js.map +1 -1
  38. package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
  39. package/dist/components/header/RowSearch.js +15 -0
  40. package/dist/components/header/RowSearch.js.map +1 -0
  41. package/dist/components/header/revealRow.d.ts +3 -0
  42. package/dist/components/header/revealRow.js +15 -0
  43. package/dist/components/header/revealRow.js.map +1 -0
  44. package/dist/components/header/settingsMenuItems.js +57 -1
  45. package/dist/components/header/settingsMenuItems.js.map +1 -1
  46. package/dist/components/import/ImportForm.js +10 -4
  47. package/dist/components/import/ImportForm.js.map +1 -1
  48. package/dist/components/import/ImportFormExamples.js +19 -27
  49. package/dist/components/import/ImportFormExamples.js.map +1 -1
  50. package/dist/components/minimap/Minimap.js +6 -1
  51. package/dist/components/minimap/Minimap.js.map +1 -1
  52. package/dist/components/msa/AnnotationLegend.js +11 -5
  53. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  54. package/dist/components/msa/MSACanvas.js +6 -1
  55. package/dist/components/msa/MSACanvas.js.map +1 -1
  56. package/dist/components/msa/MSACanvasBlock.js +3 -7
  57. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  58. package/dist/components/msa/msaKeys.d.ts +5 -0
  59. package/dist/components/msa/msaKeys.js +35 -0
  60. package/dist/components/msa/msaKeys.js.map +1 -0
  61. package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
  62. package/dist/components/msa/useMsaBlockMouse.js +18 -5
  63. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  64. package/dist/components/rowpanels/RowPanelBlock.js +10 -14
  65. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  66. package/dist/components/tracks/drawTracks.d.ts +1 -0
  67. package/dist/components/tracks/drawTracks.js +14 -2
  68. package/dist/components/tracks/drawTracks.js.map +1 -1
  69. package/dist/components/tracks/useTrackHover.d.ts +1 -5
  70. package/dist/components/tracks/useTrackHover.js +5 -5
  71. package/dist/components/tracks/useTrackHover.js.map +1 -1
  72. package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
  73. package/dist/components/tree/ScaleBarSVG.js +11 -0
  74. package/dist/components/tree/ScaleBarSVG.js.map +1 -0
  75. package/dist/components/tree/TreeCanvas.js +8 -26
  76. package/dist/components/tree/TreeCanvas.js.map +1 -1
  77. package/dist/components/tree/TreeCanvasBlock.js +11 -20
  78. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  79. package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
  80. package/dist/components/tree/TreeMenu.js +48 -0
  81. package/dist/components/tree/TreeMenu.js.map +1 -0
  82. package/dist/components/tree/TreeRuler.js +3 -3
  83. package/dist/components/tree/TreeRuler.js.map +1 -1
  84. package/dist/components/tree/renderTreeCanvas.js +19 -9
  85. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  86. package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
  87. package/dist/components/tree/renderTreeMouseover.js +20 -0
  88. package/dist/components/tree/renderTreeMouseover.js.map +1 -0
  89. package/dist/components/tree/useTreeHover.d.ts +3 -8
  90. package/dist/components/tree/useTreeHover.js +8 -10
  91. package/dist/components/tree/useTreeHover.js.map +1 -1
  92. package/dist/components/useHoverAnchor.d.ts +19 -0
  93. package/dist/components/useHoverAnchor.js +18 -0
  94. package/dist/components/useHoverAnchor.js.map +1 -0
  95. package/dist/constants.d.ts +4 -1
  96. package/dist/constants.js +12 -3
  97. package/dist/constants.js.map +1 -1
  98. package/dist/expandSpec.d.ts +46 -0
  99. package/dist/expandSpec.js +127 -0
  100. package/dist/expandSpec.js.map +1 -0
  101. package/dist/exportFileName.d.ts +3 -3
  102. package/dist/exportFileName.js +4 -4
  103. package/dist/exportFileName.js.map +1 -1
  104. package/dist/hierarchy.d.ts +51 -0
  105. package/dist/hierarchy.js +125 -0
  106. package/dist/hierarchy.js.map +1 -1
  107. package/dist/index.d.ts +4 -2
  108. package/dist/index.js +2 -0
  109. package/dist/index.js.map +1 -1
  110. package/dist/model/treeModel.d.ts +12 -0
  111. package/dist/model/treeModel.js +14 -1
  112. package/dist/model/treeModel.js.map +1 -1
  113. package/dist/model.d.ts +188 -47
  114. package/dist/model.js +482 -192
  115. package/dist/model.js.map +1 -1
  116. package/dist/renderToSvg.js +2 -4
  117. package/dist/renderToSvg.js.map +1 -1
  118. package/dist/rerootTree.d.ts +33 -0
  119. package/dist/rerootTree.js +144 -0
  120. package/dist/rerootTree.js.map +1 -0
  121. package/dist/svgTestUtil.d.ts +114 -20
  122. package/dist/types.d.ts +25 -12
  123. package/dist/useMsaSvgFigure.d.ts +2 -1
  124. package/dist/useMsaSvgFigure.js +11 -4
  125. package/dist/useMsaSvgFigure.js.map +1 -1
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/writeNewick.d.ts +8 -0
  129. package/dist/writeNewick.js +34 -0
  130. package/dist/writeNewick.js.map +1 -0
  131. package/package.json +3 -3
  132. package/src/columnCounts.ts +14 -6
  133. package/src/components/CopyButton.tsx +2 -2
  134. package/src/components/DragHandle.tsx +21 -1
  135. package/src/components/Loading.tsx +14 -30
  136. package/src/components/MSAViewer.tsx +26 -0
  137. package/src/components/SequenceTextArea.tsx +54 -8
  138. package/src/components/StatusMessage.tsx +70 -0
  139. package/src/components/Track.tsx +8 -4
  140. package/src/components/VerticalScrollbar.tsx +7 -0
  141. package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
  142. package/src/components/dialogs/FeatureDialog.tsx +59 -38
  143. package/src/components/header/FileMenu.tsx +9 -0
  144. package/src/components/header/GappynessSlider.tsx +17 -4
  145. package/src/components/header/Header.tsx +7 -8
  146. package/src/components/header/RowSearch.tsx +34 -0
  147. package/src/components/header/revealRow.ts +17 -0
  148. package/src/components/header/settingsMenuItems.ts +66 -0
  149. package/src/components/import/ImportForm.tsx +14 -7
  150. package/src/components/import/ImportFormExamples.tsx +20 -28
  151. package/src/components/minimap/Minimap.tsx +7 -0
  152. package/src/components/msa/AnnotationLegend.tsx +12 -6
  153. package/src/components/msa/MSACanvas.tsx +8 -0
  154. package/src/components/msa/MSACanvasBlock.tsx +5 -11
  155. package/src/components/msa/msaKeys.ts +39 -0
  156. package/src/components/msa/useMsaBlockMouse.ts +23 -7
  157. package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
  158. package/src/components/tracks/drawTracks.ts +15 -2
  159. package/src/components/tracks/useTrackHover.ts +5 -5
  160. package/src/components/tree/ScaleBarSVG.tsx +30 -0
  161. package/src/components/tree/TreeCanvas.tsx +9 -40
  162. package/src/components/tree/TreeCanvasBlock.tsx +19 -39
  163. package/src/components/tree/TreeMenu.tsx +129 -0
  164. package/src/components/tree/TreeRuler.tsx +7 -13
  165. package/src/components/tree/renderTreeCanvas.ts +20 -8
  166. package/src/components/tree/renderTreeMouseover.ts +41 -0
  167. package/src/components/tree/useTreeHover.ts +8 -17
  168. package/src/components/useHoverAnchor.ts +31 -0
  169. package/src/constants.ts +14 -3
  170. package/src/expandSpec.ts +196 -0
  171. package/src/exportFileName.ts +4 -3
  172. package/src/hierarchy.ts +151 -4
  173. package/src/index.ts +11 -1
  174. package/src/model/treeModel.ts +22 -0
  175. package/src/model.ts +588 -221
  176. package/src/renderToSvg.tsx +6 -9
  177. package/src/rerootTree.ts +172 -0
  178. package/src/types.ts +34 -12
  179. package/src/useMsaSvgFigure.ts +11 -4
  180. package/src/version.ts +1 -1
  181. package/src/writeNewick.ts +38 -0
  182. package/dist/components/PortalTooltip.d.ts +0 -11
  183. package/dist/components/PortalTooltip.js +0 -29
  184. package/dist/components/PortalTooltip.js.map +0 -1
  185. package/dist/components/header/HeaderStatusArea.js +0 -21
  186. package/dist/components/header/HeaderStatusArea.js.map +0 -1
  187. package/dist/components/tree/TreeBranchMenu.js +0 -29
  188. package/dist/components/tree/TreeBranchMenu.js.map +0 -1
  189. package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
  190. package/dist/components/tree/TreeNodeMenu.js +0 -41
  191. package/dist/components/tree/TreeNodeMenu.js.map +0 -1
  192. package/src/components/PortalTooltip.tsx +0 -47
  193. package/src/components/header/HeaderStatusArea.tsx +0 -32
  194. package/src/components/tree/TreeBranchMenu.tsx +0 -68
  195. package/src/components/tree/TreeNodeMenu.tsx +0 -95
@@ -23,13 +23,8 @@ const MSACanvasBlock = observer(function ({
23
23
  }) {
24
24
  const { blockSize, highResScaleFactor } = model
25
25
  const theme = useTheme()
26
- const { tooltipPoint, onMouseMove, onClick, onMouseLeave } = useMsaBlockMouse(
27
- {
28
- model,
29
- offsetX,
30
- offsetY,
31
- },
32
- )
26
+ const { anchor, onMouseMove, onMouseDown, onClick, onMouseLeave } =
27
+ useMsaBlockMouse({ model, offsetX, offsetY })
33
28
 
34
29
  const canvasSize = blockSize * highResScaleFactor
35
30
  const ref = useCanvasAutorun({
@@ -76,6 +71,7 @@ const MSACanvasBlock = observer(function ({
76
71
  onMouseMove(event, ref.current)
77
72
  }
78
73
  }}
74
+ onMouseDown={onMouseDown}
79
75
  onClick={event => {
80
76
  if (ref.current) {
81
77
  onClick(event, ref.current)
@@ -94,10 +90,8 @@ const MSACanvasBlock = observer(function ({
94
90
  height: blockSize,
95
91
  }}
96
92
  />
97
- {tooltipPoint ? (
98
- <BaseTooltip
99
- clientPoint={{ x: tooltipPoint.x, y: tooltipPoint.y + 15 }}
100
- >
93
+ {anchor ? (
94
+ <BaseTooltip clientPoint={anchor.clientPoint}>
101
95
  {hoveredInsertion ? (
102
96
  <>
103
97
  Insertion ({hoveredInsertion.letters.length}
@@ -0,0 +1,39 @@
1
+ import type { MsaViewModel } from '../../model.ts'
2
+
3
+ export function onMsaKey(
4
+ model: MsaViewModel,
5
+ { key, shiftKey }: { key: string; shiftKey: boolean },
6
+ ) {
7
+ const { colWidth, rowHeight, msaCanvasWidth, msaAreaHeight } = model
8
+ const dx = shiftKey ? msaCanvasWidth : colWidth
9
+ const dy = shiftKey ? msaAreaHeight : rowHeight
10
+ switch (key) {
11
+ case 'ArrowLeft':
12
+ model.doScrollX(dx)
13
+ return true
14
+ case 'ArrowRight':
15
+ model.doScrollX(-dx)
16
+ return true
17
+ case 'ArrowUp':
18
+ model.doScrollY(dy)
19
+ return true
20
+ case 'ArrowDown':
21
+ model.doScrollY(-dy)
22
+ return true
23
+ case '+':
24
+ case '=':
25
+ model.zoomIn()
26
+ return true
27
+ case '-':
28
+ model.zoomOut()
29
+ return true
30
+ case 'Home':
31
+ model.setScrollX(0)
32
+ return true
33
+ case 'End':
34
+ model.setScrollX(model.maxScrollX)
35
+ return true
36
+ default:
37
+ return false
38
+ }
39
+ }
@@ -1,8 +1,12 @@
1
- import { useState } from 'react'
1
+ import { useRef } from 'react'
2
+
3
+ import { useHoverAnchor } from '../useHoverAnchor.ts'
2
4
 
3
5
  import type { MsaViewModel } from '../../model.ts'
4
6
  import type React from 'react'
5
7
 
8
+ const maxClickTravel = 3
9
+
6
10
  function eventToColRow({
7
11
  event,
8
12
  el,
@@ -44,7 +48,8 @@ export function useMsaBlockMouse({
44
48
  offsetX: number
45
49
  offsetY: number
46
50
  }) {
47
- const [tooltipPoint, setTooltipPoint] = useState<{ x: number; y: number }>()
51
+ const { anchor, hoverAt, clearAnchor } = useHoverAnchor<true>()
52
+ const downAt = useRef<{ x: number; y: number }>(undefined)
48
53
 
49
54
  function colRow(event: React.MouseEvent, el: HTMLElement) {
50
55
  const { colWidth, rowHeight } = model
@@ -65,12 +70,23 @@ export function useMsaBlockMouse({
65
70
 
66
71
  const hasTooltip =
67
72
  !!model.hoveredInsertion || model.mouseOverDomains.length > 0
68
- setTooltipPoint(
69
- hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
70
- )
73
+ hoverAt(event, hasTooltip || undefined)
74
+ }
75
+
76
+ function onMouseDown(event: React.MouseEvent) {
77
+ downAt.current = { x: event.clientX, y: event.clientY }
71
78
  }
72
79
 
73
80
  function onClick(event: React.MouseEvent, el: HTMLElement) {
81
+ const down = downAt.current
82
+ downAt.current = undefined
83
+ if (
84
+ down &&
85
+ Math.hypot(event.clientX - down.x, event.clientY - down.y) >
86
+ maxClickTravel
87
+ ) {
88
+ return
89
+ }
74
90
  const pos = colRow(event, el)
75
91
  const { col, row } = pos
76
92
  const { mouseClickCol, mouseClickRow } = model
@@ -83,8 +99,8 @@ export function useMsaBlockMouse({
83
99
 
84
100
  function onMouseLeave() {
85
101
  model.setMousePos()
86
- setTooltipPoint(undefined)
102
+ clearAnchor()
87
103
  }
88
104
 
89
- return { tooltipPoint, onMouseMove, onClick, onMouseLeave }
105
+ return { anchor, onMouseMove, onMouseDown, onClick, onMouseLeave }
90
106
  }
@@ -1,23 +1,17 @@
1
- import React, { useState } from 'react'
1
+ import React from 'react'
2
2
 
3
+ import BaseTooltip from '@jbrowse/core/ui/BaseTooltip'
3
4
  import { useTheme } from '@mui/material'
4
5
  import { observer } from 'mobx-react'
5
6
 
6
7
  import { featureField, featureName } from '../../featureFields.ts'
7
8
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
- import PortalTooltip from '../PortalTooltip.tsx'
9
+ import { useHoverAnchor } from '../useHoverAnchor.ts'
9
10
  import { renderRowPanel } from './renderRowPanel.ts'
10
11
 
11
12
  import type { MsaViewModel } from '../../model.ts'
12
13
  import type { ResolvedRowPanel } from '../../types.ts'
13
14
 
14
- interface Hover {
15
- name: string
16
- lines: string[]
17
- clientX: number
18
- clientY: number
19
- }
20
-
21
15
  // what the cell or span under the pointer reads, which is the field a strip
22
16
  // colors by, and a span's name, its coordinates and its encoded field
23
17
  function hoverLines({
@@ -66,7 +60,10 @@ const RowPanelBlock = observer(function ({
66
60
  panel: ResolvedRowPanel
67
61
  offsetY: number
68
62
  }) {
69
- const [hovered, setHovered] = useState<Hover>()
63
+ const { anchor, hoverAt, clearAnchor } = useHoverAnchor<{
64
+ name: string
65
+ lines: string[]
66
+ }>()
70
67
  const theme = useTheme()
71
68
  const { blockSize, highResScaleFactor, rowHeight } = model
72
69
  const width = panel.width
@@ -107,30 +104,26 @@ const RowPanelBlock = observer(function ({
107
104
  name,
108
105
  x: event.clientX - left,
109
106
  })
110
- setHovered(
107
+ hoverAt(
108
+ event,
111
109
  name === undefined || lines.length === 0
112
110
  ? undefined
113
- : {
114
- name,
115
- lines,
116
- clientX: event.clientX,
117
- clientY: event.clientY,
118
- },
111
+ : { name, lines },
119
112
  )
120
113
  model.setMousePos(undefined, name === undefined ? undefined : row)
121
114
  }}
122
115
  onMouseLeave={() => {
123
- setHovered(undefined)
116
+ clearAnchor()
124
117
  model.setMousePos(undefined, undefined)
125
118
  }}
126
119
  />
127
- {hovered ? (
128
- <PortalTooltip clientX={hovered.clientX} clientY={hovered.clientY}>
129
- <div>{hovered.name}</div>
130
- {hovered.lines.map(line => (
120
+ {anchor ? (
121
+ <BaseTooltip clientPoint={anchor.clientPoint}>
122
+ <div>{anchor.value.name}</div>
123
+ {anchor.value.lines.map(line => (
131
124
  <div key={line}>{line}</div>
132
125
  ))}
133
- </PortalTooltip>
126
+ </BaseTooltip>
134
127
  ) : null}
135
128
  </>
136
129
  )
@@ -159,6 +159,20 @@ export function rulerStep(colWidth: number, minPixels = 55) {
159
159
  return step
160
160
  }
161
161
 
162
+ // The 0-based columns in [xStart, end) to label: column 1, then each column
163
+ // whose 1-based number is a multiple of the step. A step of 2 or more puts the
164
+ // first multiple at least half a step's width past column 1, which clears the
165
+ // two labels.
166
+ export function rulerCols(xStart: number, end: number, colWidth: number) {
167
+ const step = rulerStep(colWidth)
168
+ const cols = step > 1 && xStart <= 0 && end > 0 ? [0] : []
169
+ const first = Math.max(1, Math.ceil((xStart + 1) / step)) * step
170
+ for (let n = first; n <= end; n += step) {
171
+ cols.push(n - 1)
172
+ }
173
+ return cols
174
+ }
175
+
162
176
  const RULER_FONT_SIZE = 10
163
177
  const TICK_HEIGHT = 4
164
178
 
@@ -186,7 +200,6 @@ export function drawColumnRuler({
186
200
  blockWidth: blockSize,
187
201
  colWidth,
188
202
  })
189
- const step = rulerStep(colWidth)
190
203
  const end = Math.min(xEnd, numColumns)
191
204
  setFontSize(ctx, RULER_FONT_SIZE)
192
205
  ctx.textAlign = 'center'
@@ -195,7 +208,7 @@ export function drawColumnRuler({
195
208
  ctx.strokeStyle = textColor
196
209
  ctx.lineWidth = 1
197
210
 
198
- for (let col = Math.ceil(xStart / step) * step; col < end; col += step) {
211
+ for (const col of rulerCols(xStart, end, colWidth)) {
199
212
  const text = label(col)
200
213
  if (text === undefined) {
201
214
  continue
@@ -1,4 +1,4 @@
1
- import { useState } from 'react'
1
+ import { useHoverAnchor } from '../useHoverAnchor.ts'
2
2
 
3
3
  import type { MsaViewModel } from '../../model.ts'
4
4
  import type React from 'react'
@@ -18,7 +18,7 @@ export function useTrackHover({
18
18
  model: MsaViewModel
19
19
  tooltip: boolean
20
20
  }) {
21
- const [hover, setHover] = useState<{ col: number; x: number; y: number }>()
21
+ const { anchor, hoverAt, clearAnchor } = useHoverAnchor<number>()
22
22
 
23
23
  // the blocks are laid out in column space and scrolled by a transform on
24
24
  // their container, so the pointer's column comes off the untransformed row
@@ -29,14 +29,14 @@ export function useTrackHover({
29
29
  const hit = col >= 0 && col < numColumns
30
30
  model.setMousePos(hit ? col : undefined, undefined)
31
31
  if (tooltip) {
32
- setHover(hit ? { col, x: event.clientX, y: event.clientY } : undefined)
32
+ hoverAt(event, hit ? col : undefined)
33
33
  }
34
34
  }
35
35
 
36
36
  function onMouseLeave() {
37
- setHover(undefined)
37
+ clearAnchor()
38
38
  model.setMousePos(undefined, undefined)
39
39
  }
40
40
 
41
- return { hover: tooltip ? hover : undefined, onMouseMove, onMouseLeave }
41
+ return { anchor: tooltip ? anchor : undefined, onMouseMove, onMouseLeave }
42
42
  }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ /**
4
+ * The branch-length scale bar: its label over a bracket `bar.px` wide, both
5
+ * resting on `y`.
6
+ */
7
+ export default function ScaleBarSVG({
8
+ bar,
9
+ marginLeft,
10
+ y,
11
+ color,
12
+ }: {
13
+ bar: { label: string; px: number }
14
+ marginLeft: number
15
+ y: number
16
+ color: string
17
+ }) {
18
+ return (
19
+ <>
20
+ <text x={marginLeft} y={y - 9} fontSize={10} fill={color}>
21
+ {bar.label}
22
+ </text>
23
+ <path
24
+ d={`M${marginLeft} ${y - 7} v6 h${bar.px} v-6`}
25
+ fill="none"
26
+ stroke={color}
27
+ />
28
+ </>
29
+ )
30
+ }
@@ -5,15 +5,15 @@ import { observer } from 'mobx-react'
5
5
 
6
6
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
7
7
  import { useWheelScroll } from '../../useWheelScroll.ts'
8
- import { referenceColor, treeHoverColor } from '../overlayColors.ts'
9
8
  import CladeLabels from './CladeLabels.tsx'
10
9
  import TreeCanvasBlock from './TreeCanvasBlock.tsx'
10
+ import { renderTreeMouseover } from './renderTreeMouseover.ts'
11
11
 
12
12
  import type { MsaViewModel } from '../../model.ts'
13
13
 
14
14
  const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
15
15
  const ref = useRef<HTMLDivElement>(null)
16
- const { height, blocksY, treeAreaWidth, scrollY } = model
16
+ const { height, blocksY, treeAreaWidth, scrollY, highResScaleFactor } = model
17
17
  const onScrollY = useCallback(
18
18
  (d: number) => {
19
19
  model.doScrollY(d)
@@ -22,47 +22,16 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
22
22
  )
23
23
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
24
24
 
25
+ const canvasWidth = treeAreaWidth * highResScaleFactor
26
+ const canvasHeight = height * highResScaleFactor
25
27
  const mouseoverRef = useCanvasAutorun({
26
28
  draw: ctx => {
27
29
  if (isAlive(model)) {
28
- const {
29
- rowHeight,
30
- treeAreaWidth: w,
31
- height: h,
32
- scrollY: sy,
33
- mouseRow,
34
- referenceRowIndex,
35
- hoveredRowIndices,
36
- } = model
37
- ctx.resetTransform()
38
- ctx.clearRect(0, 0, w, h)
39
-
40
- // rows are laid out at a fixed pitch, so a row index is all that is
41
- // needed to place its band (leaf.x is rowHeight*(index+0.5))
42
- const fillRow = (index: number) => {
43
- ctx.fillRect(0, index * rowHeight + sy, w, rowHeight)
44
- }
45
-
46
- if (referenceRowIndex !== undefined) {
47
- ctx.fillStyle = referenceColor
48
- fillRow(referenceRowIndex)
49
- }
50
-
51
- ctx.fillStyle = treeHoverColor
52
- for (const index of hoveredRowIndices) {
53
- fillRow(index)
54
- }
55
- if (
56
- mouseRow !== undefined &&
57
- mouseRow !== referenceRowIndex &&
58
- !hoveredRowIndices.includes(mouseRow)
59
- ) {
60
- fillRow(mouseRow)
61
- }
30
+ renderTreeMouseover({ ctx, model })
62
31
  }
63
32
  },
64
- width: treeAreaWidth,
65
- height,
33
+ width: canvasWidth,
34
+ height: canvasHeight,
66
35
  deps: [model],
67
36
  })
68
37
 
@@ -95,8 +64,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
95
64
  </div>
96
65
  <canvas
97
66
  ref={mouseoverRef}
98
- width={treeAreaWidth}
99
- height={height}
67
+ width={canvasWidth}
68
+ height={canvasHeight}
100
69
  style={{
101
70
  position: 'absolute',
102
71
  top: 0,
@@ -1,17 +1,17 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
+ import BaseTooltip from '@jbrowse/core/ui/BaseTooltip'
3
4
  import { makeStyles } from '@jbrowse/core/util/tss-react'
4
5
  import { useTheme } from '@mui/material'
5
6
  import { observer } from 'mobx-react'
6
7
 
7
8
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
- import PortalTooltip from '../PortalTooltip.tsx'
9
- import TreeBranchMenu from './TreeBranchMenu.tsx'
10
- import TreeNodeMenu from './TreeNodeMenu.tsx'
9
+ import TreeMenu from './TreeMenu.tsx'
11
10
  import { renderTreeCanvas } from './renderTreeCanvas.ts'
12
11
  import { useTreeHover } from './useTreeHover.ts'
13
12
 
14
13
  import type { MsaViewModel } from '../../model.ts'
14
+ import type { TreeMenuTarget } from './TreeMenu.tsx'
15
15
 
16
16
  const useStyles = makeStyles()(theme => ({
17
17
  hover: {
@@ -22,13 +22,6 @@ const useStyles = makeStyles()(theme => ({
22
22
  },
23
23
  }))
24
24
 
25
- interface MenuData {
26
- name: string
27
- id: string
28
- x: number
29
- y: number
30
- }
31
-
32
25
  const TreeCanvasBlock = observer(function ({
33
26
  model,
34
27
  offsetY,
@@ -38,10 +31,11 @@ const TreeCanvasBlock = observer(function ({
38
31
  }) {
39
32
  const { classes } = useStyles()
40
33
  const theme = useTheme()
41
- const [branchMenu, setBranchMenu] = useState<MenuData>()
42
- const [nodeMenu, setNodeMenu] = useState<MenuData>()
43
- const { clickMap, hovered, hitTest, onMouseMove, onMouseLeave } =
44
- useTreeHover({ model, offsetY })
34
+ const [menu, setMenu] = useState<TreeMenuTarget>()
35
+ const { clickMap, anchor, hitTest, onMouseMove, onMouseLeave } = useTreeHover(
36
+ { model, offsetY },
37
+ )
38
+ const hovered = anchor?.value
45
39
 
46
40
  const { treeAreaWidth, blockSize, highResScaleFactor } = model
47
41
  // TreePanel clips to treeAreaWidth, so a wider canvas is wasted backing store
@@ -77,22 +71,12 @@ const TreeCanvasBlock = observer(function ({
77
71
 
78
72
  return (
79
73
  <>
80
- {branchMenu ? (
81
- <TreeBranchMenu
82
- node={branchMenu}
83
- model={model}
84
- onClose={() => {
85
- setBranchMenu(undefined)
86
- }}
87
- />
88
- ) : null}
89
-
90
- {nodeMenu ? (
91
- <TreeNodeMenu
92
- node={nodeMenu}
74
+ {menu ? (
75
+ <TreeMenu
76
+ node={menu}
93
77
  model={model}
94
78
  onClose={() => {
95
- setNodeMenu(undefined)
79
+ setMenu(undefined)
96
80
  }}
97
81
  />
98
82
  ) : null}
@@ -111,17 +95,13 @@ const TreeCanvasBlock = observer(function ({
111
95
  onClick={event => {
112
96
  const entry = hitTest(event)
113
97
  if (entry) {
114
- const menu = {
98
+ setMenu({
115
99
  x: event.clientX,
116
100
  y: event.clientY,
117
101
  id: entry.id,
118
102
  name: entry.name,
119
- }
120
- if (entry.branch) {
121
- setBranchMenu(menu)
122
- } else {
123
- setNodeMenu(menu)
124
- }
103
+ leaf: !entry.branch,
104
+ })
125
105
  }
126
106
  }}
127
107
  />
@@ -139,10 +119,10 @@ const TreeCanvasBlock = observer(function ({
139
119
  />
140
120
  ) : null}
141
121
 
142
- {hovered ? (
143
- <PortalTooltip clientX={hovered.clientX} clientY={hovered.clientY}>
144
- {hovered.name}
145
- </PortalTooltip>
122
+ {anchor ? (
123
+ <BaseTooltip clientPoint={anchor.clientPoint}>
124
+ {anchor.value.name}
125
+ </BaseTooltip>
146
126
  ) : null}
147
127
  </>
148
128
  )
@@ -0,0 +1,129 @@
1
+ import React, { lazy } from 'react'
2
+
3
+ import { Menu, MenuItem } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+
6
+ import type { MsaViewModel } from '../../model.ts'
7
+
8
+ const TreeNodeInfoDialog = lazy(
9
+ () => import('./dialogs/TreeNodeInfoDialog.tsx'),
10
+ )
11
+
12
+ export interface TreeMenuTarget {
13
+ x: number
14
+ y: number
15
+ name: string
16
+ id: string
17
+ leaf: boolean
18
+ }
19
+
20
+ const TreeMenu = observer(function ({
21
+ node,
22
+ model,
23
+ onClose,
24
+ }: {
25
+ node: TreeMenuTarget
26
+ model: MsaViewModel
27
+ onClose: () => void
28
+ }) {
29
+ const { collapsed, showOnly, relativeTo } = model
30
+ const { name, id, leaf } = node
31
+ const isCollapsed = collapsed.includes(id)
32
+ const isRoot = id === model.tree.id
33
+ const noun = leaf ? 'subtree' : 'this node'
34
+ return (
35
+ <Menu
36
+ anchorReference="anchorPosition"
37
+ anchorPosition={{ left: node.x, top: node.y }}
38
+ transitionDuration={0}
39
+ open
40
+ onClose={onClose}
41
+ >
42
+ <MenuItem dense disabled>
43
+ {name}
44
+ </MenuItem>
45
+ {leaf ? (
46
+ <MenuItem
47
+ dense
48
+ onClick={() => {
49
+ model.queueDialog(onClose => [
50
+ TreeNodeInfoDialog,
51
+ { info: model.getRowData(name), model, nodeName: name, onClose },
52
+ ])
53
+ onClose()
54
+ }}
55
+ >
56
+ More info...
57
+ </MenuItem>
58
+ ) : null}
59
+ <MenuItem
60
+ dense
61
+ onClick={() => {
62
+ model.toggleCollapsed(id)
63
+ onClose()
64
+ }}
65
+ >
66
+ {isCollapsed ? `Expand ${noun}` : `Collapse ${noun}`}
67
+ </MenuItem>
68
+ {leaf ? null : (
69
+ <MenuItem
70
+ dense
71
+ onClick={() => {
72
+ model.setShowOnly(showOnly === id ? undefined : id)
73
+ onClose()
74
+ }}
75
+ >
76
+ {showOnly === id
77
+ ? 'Disable show only this node'
78
+ : 'Show only this node'}
79
+ </MenuItem>
80
+ )}
81
+ {leaf || isCollapsed ? null : (
82
+ <MenuItem
83
+ dense
84
+ onClick={() => {
85
+ model.toggleRotated(id)
86
+ onClose()
87
+ }}
88
+ >
89
+ Rotate this node
90
+ </MenuItem>
91
+ )}
92
+ {isRoot ? null : (
93
+ <MenuItem
94
+ dense
95
+ onClick={() => {
96
+ model.rerootAt(id)
97
+ onClose()
98
+ }}
99
+ >
100
+ Reroot here
101
+ </MenuItem>
102
+ )}
103
+ {leaf ? (
104
+ <MenuItem
105
+ dense
106
+ onClick={() => {
107
+ model.drawRelativeTo(name)
108
+ onClose()
109
+ }}
110
+ >
111
+ Indicate differences from this row
112
+ </MenuItem>
113
+ ) : null}
114
+ {leaf && relativeTo ? (
115
+ <MenuItem
116
+ dense
117
+ onClick={() => {
118
+ model.drawRelativeTo(undefined)
119
+ onClose()
120
+ }}
121
+ >
122
+ Clear reference row
123
+ </MenuItem>
124
+ ) : null}
125
+ </Menu>
126
+ )
127
+ })
128
+
129
+ export default TreeMenu
@@ -4,6 +4,7 @@ import { useTheme } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
5
5
 
6
6
  import { treeScaleBarHeight as h } from '../../constants.ts'
7
+ import ScaleBarSVG from './ScaleBarSVG.tsx'
7
8
 
8
9
  import type { MsaViewModel } from '../../model.ts'
9
10
 
@@ -18,19 +19,12 @@ const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
18
19
  return (
19
20
  <div style={{ flexShrink: 0, width: treeAreaWidth }}>
20
21
  {bar ? (
21
- <svg
22
- width={treeAreaWidth}
23
- height={h}
24
- style={{ display: 'block' }}
25
- color={theme.palette.text.primary}
26
- >
27
- <text x={marginLeft} y={h - 9} fontSize={10} fill="currentColor">
28
- {bar.label}
29
- </text>
30
- <path
31
- d={`M${marginLeft} ${h - 7} v6 h${bar.px} v-6`}
32
- fill="none"
33
- stroke="currentColor"
22
+ <svg width={treeAreaWidth} height={h} style={{ display: 'block' }}>
23
+ <ScaleBarSVG
24
+ bar={bar}
25
+ marginLeft={marginLeft}
26
+ y={h}
27
+ color={theme.palette.text.primary}
34
28
  />
35
29
  </svg>
36
30
  ) : null}