react-msaview 8.2.0 → 8.4.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 (255) hide show
  1. package/bundle/index.js +77 -300
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blockFasta.d.ts +6 -0
  4. package/dist/blockFasta.js +12 -0
  5. package/dist/blockFasta.js.map +1 -0
  6. package/dist/colorSchemes.d.ts +7 -0
  7. package/dist/colorSchemes.js +15 -0
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/columnCounts.d.ts +4 -3
  10. package/dist/columnCounts.js +11 -6
  11. package/dist/columnCounts.js.map +1 -1
  12. package/dist/components/CopyButton.d.ts +1 -1
  13. package/dist/components/CopyButton.js +1 -1
  14. package/dist/components/CopyButton.js.map +1 -1
  15. package/dist/components/DragHandle.js +14 -2
  16. package/dist/components/DragHandle.js.map +1 -1
  17. package/dist/components/Loading.js +10 -11
  18. package/dist/components/Loading.js.map +1 -1
  19. package/dist/components/MSAView.js +15 -9
  20. package/dist/components/MSAView.js.map +1 -1
  21. package/dist/components/MSAViewer.d.ts +39 -7
  22. package/dist/components/MSAViewer.js +72 -22
  23. package/dist/components/MSAViewer.js.map +1 -1
  24. package/dist/components/MetadataTable.d.ts +5 -0
  25. package/dist/components/MetadataTable.js +29 -0
  26. package/dist/components/MetadataTable.js.map +1 -0
  27. package/dist/components/SequenceTextArea.d.ts +11 -0
  28. package/dist/components/SequenceTextArea.js +40 -8
  29. package/dist/components/SequenceTextArea.js.map +1 -1
  30. package/dist/components/StatusMessage.d.ts +12 -0
  31. package/dist/components/StatusMessage.js +43 -0
  32. package/dist/components/StatusMessage.js.map +1 -0
  33. package/dist/components/Track.js +13 -4
  34. package/dist/components/Track.js.map +1 -1
  35. package/dist/components/VerticalScrollbar.js +6 -1
  36. package/dist/components/VerticalScrollbar.js.map +1 -1
  37. package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
  38. package/dist/components/dialogs/ExportSVGDialog.js +4 -2
  39. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  40. package/dist/components/dialogs/FeatureDialog.js +15 -10
  41. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  42. package/dist/components/dialogs/MetadataDialog.js +5 -6
  43. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  44. package/dist/components/getVisibleLeaves.d.ts +1 -1
  45. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  46. package/dist/components/header/ColorSchemeMenu.js +23 -5
  47. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  48. package/dist/components/header/FileMenu.js +9 -0
  49. package/dist/components/header/FileMenu.js.map +1 -1
  50. package/dist/components/header/GappynessSlider.js +15 -3
  51. package/dist/components/header/GappynessSlider.js.map +1 -1
  52. package/dist/components/header/{HeaderStatusArea.d.ts → GoToBox.d.ts} +2 -2
  53. package/dist/components/header/GoToBox.js +21 -0
  54. package/dist/components/header/GoToBox.js.map +1 -0
  55. package/dist/components/header/Header.js +8 -7
  56. package/dist/components/header/Header.js.map +1 -1
  57. package/dist/components/header/SelectionMenu.d.ts +12 -0
  58. package/dist/components/header/SelectionMenu.js +72 -0
  59. package/dist/components/header/SelectionMenu.js.map +1 -0
  60. package/dist/components/header/goTo.d.ts +20 -0
  61. package/dist/components/header/goTo.js +69 -0
  62. package/dist/components/header/goTo.js.map +1 -0
  63. package/dist/components/header/settingsMenuItems.js +57 -1
  64. package/dist/components/header/settingsMenuItems.js.map +1 -1
  65. package/dist/components/import/ImportForm.js +10 -4
  66. package/dist/components/import/ImportForm.js.map +1 -1
  67. package/dist/components/import/ImportFormExamples.js +19 -27
  68. package/dist/components/import/ImportFormExamples.js.map +1 -1
  69. package/dist/components/minimap/Minimap.js +6 -1
  70. package/dist/components/minimap/Minimap.js.map +1 -1
  71. package/dist/components/msa/AnnotationLegend.js +11 -5
  72. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  73. package/dist/components/msa/MSACanvas.js +17 -1
  74. package/dist/components/msa/MSACanvas.js.map +1 -1
  75. package/dist/components/msa/MSACanvasBlock.js +6 -6
  76. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  77. package/dist/components/msa/msaKeys.d.ts +5 -0
  78. package/dist/components/msa/msaKeys.js +41 -0
  79. package/dist/components/msa/msaKeys.js.map +1 -0
  80. package/dist/components/msa/msaRaster.js +1 -0
  81. package/dist/components/msa/msaRaster.js.map +1 -1
  82. package/dist/components/msa/renderHighlights.d.ts +14 -3
  83. package/dist/components/msa/renderHighlights.js +29 -4
  84. package/dist/components/msa/renderHighlights.js.map +1 -1
  85. package/dist/components/msa/tileColor.js +9 -8
  86. package/dist/components/msa/tileColor.js.map +1 -1
  87. package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
  88. package/dist/components/msa/useMsaBlockMouse.js +34 -14
  89. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  90. package/dist/components/overlayColors.d.ts +2 -0
  91. package/dist/components/overlayColors.js +3 -0
  92. package/dist/components/overlayColors.js.map +1 -1
  93. package/dist/components/rowpanels/RowPanelBlock.js +10 -14
  94. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  95. package/dist/components/selectionDrag.d.ts +17 -0
  96. package/dist/components/selectionDrag.js +30 -0
  97. package/dist/components/selectionDrag.js.map +1 -0
  98. package/dist/components/tracks/drawTracks.d.ts +1 -0
  99. package/dist/components/tracks/drawTracks.js +14 -2
  100. package/dist/components/tracks/drawTracks.js.map +1 -1
  101. package/dist/components/tracks/useTrackHover.d.ts +4 -6
  102. package/dist/components/tracks/useTrackHover.js +30 -10
  103. package/dist/components/tracks/useTrackHover.js.map +1 -1
  104. package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
  105. package/dist/components/tree/ScaleBarSVG.js +11 -0
  106. package/dist/components/tree/ScaleBarSVG.js.map +1 -0
  107. package/dist/components/tree/TreeCanvas.js +8 -26
  108. package/dist/components/tree/TreeCanvas.js.map +1 -1
  109. package/dist/components/tree/TreeCanvasBlock.js +11 -20
  110. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  111. package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
  112. package/dist/components/tree/TreeMenu.js +48 -0
  113. package/dist/components/tree/TreeMenu.js.map +1 -0
  114. package/dist/components/tree/TreeRuler.js +3 -3
  115. package/dist/components/tree/TreeRuler.js.map +1 -1
  116. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
  117. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  118. package/dist/components/tree/renderTreeCanvas.js +19 -9
  119. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  120. package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
  121. package/dist/components/tree/renderTreeMouseover.js +20 -0
  122. package/dist/components/tree/renderTreeMouseover.js.map +1 -0
  123. package/dist/components/tree/useTreeHover.d.ts +3 -8
  124. package/dist/components/tree/useTreeHover.js +8 -10
  125. package/dist/components/tree/useTreeHover.js.map +1 -1
  126. package/dist/components/useHoverAnchor.d.ts +19 -0
  127. package/dist/components/useHoverAnchor.js +18 -0
  128. package/dist/components/useHoverAnchor.js.map +1 -0
  129. package/dist/constants.d.ts +3 -0
  130. package/dist/constants.js +12 -3
  131. package/dist/constants.js.map +1 -1
  132. package/dist/element.d.ts +7 -3
  133. package/dist/element.js +32 -4
  134. package/dist/element.js.map +1 -1
  135. package/dist/expandSpec.js +3 -3
  136. package/dist/expandSpec.js.map +1 -1
  137. package/dist/exportFileName.d.ts +3 -3
  138. package/dist/exportFileName.js +4 -4
  139. package/dist/exportFileName.js.map +1 -1
  140. package/dist/featuresLayer.d.ts +8 -0
  141. package/dist/featuresLayer.js +42 -0
  142. package/dist/featuresLayer.js.map +1 -0
  143. package/dist/hierarchy.d.ts +51 -0
  144. package/dist/hierarchy.js +125 -0
  145. package/dist/hierarchy.js.map +1 -1
  146. package/dist/index.d.ts +2 -2
  147. package/dist/model/DataModel.d.ts +1 -1
  148. package/dist/model/DataModel.js +3 -3
  149. package/dist/model/DataModel.js.map +1 -1
  150. package/dist/model/msaModel.d.ts +14 -1
  151. package/dist/model/msaModel.js +17 -1
  152. package/dist/model/msaModel.js.map +1 -1
  153. package/dist/model/treeModel.d.ts +12 -0
  154. package/dist/model/treeModel.js +14 -1
  155. package/dist/model/treeModel.js.map +1 -1
  156. package/dist/model.d.ts +285 -53
  157. package/dist/model.js +693 -231
  158. package/dist/model.js.map +1 -1
  159. package/dist/renderToSvg.js +2 -4
  160. package/dist/renderToSvg.js.map +1 -1
  161. package/dist/rerootTree.d.ts +33 -0
  162. package/dist/rerootTree.js +144 -0
  163. package/dist/rerootTree.js.map +1 -0
  164. package/dist/svgTestUtil.d.ts +172 -26
  165. package/dist/types.d.ts +75 -12
  166. package/dist/useMsaSvgFigure.d.ts +2 -1
  167. package/dist/useMsaSvgFigure.js +11 -4
  168. package/dist/useMsaSvgFigure.js.map +1 -1
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.js +1 -1
  171. package/dist/writeNewick.d.ts +8 -0
  172. package/dist/writeNewick.js +34 -0
  173. package/dist/writeNewick.js.map +1 -0
  174. package/package.json +3 -3
  175. package/src/blockFasta.ts +18 -0
  176. package/src/colorSchemes.ts +20 -0
  177. package/src/columnCounts.ts +14 -6
  178. package/src/components/CopyButton.tsx +2 -2
  179. package/src/components/DragHandle.tsx +21 -1
  180. package/src/components/Loading.tsx +14 -30
  181. package/src/components/MSAView.tsx +16 -9
  182. package/src/components/MSAViewer.tsx +121 -26
  183. package/src/components/MetadataTable.tsx +55 -0
  184. package/src/components/SequenceTextArea.tsx +54 -8
  185. package/src/components/StatusMessage.tsx +70 -0
  186. package/src/components/Track.tsx +21 -4
  187. package/src/components/VerticalScrollbar.tsx +7 -0
  188. package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
  189. package/src/components/dialogs/FeatureDialog.tsx +59 -38
  190. package/src/components/dialogs/MetadataDialog.tsx +9 -8
  191. package/src/components/header/ColorSchemeMenu.tsx +31 -12
  192. package/src/components/header/FileMenu.tsx +9 -0
  193. package/src/components/header/GappynessSlider.tsx +17 -4
  194. package/src/components/header/GoToBox.tsx +50 -0
  195. package/src/components/header/Header.tsx +11 -8
  196. package/src/components/header/SelectionMenu.tsx +117 -0
  197. package/src/components/header/goTo.ts +75 -0
  198. package/src/components/header/settingsMenuItems.ts +66 -0
  199. package/src/components/import/ImportForm.tsx +14 -7
  200. package/src/components/import/ImportFormExamples.tsx +20 -28
  201. package/src/components/minimap/Minimap.tsx +7 -0
  202. package/src/components/msa/AnnotationLegend.tsx +12 -6
  203. package/src/components/msa/MSACanvas.tsx +19 -1
  204. package/src/components/msa/MSACanvasBlock.tsx +9 -11
  205. package/src/components/msa/msaKeys.ts +45 -0
  206. package/src/components/msa/msaRaster.ts +1 -0
  207. package/src/components/msa/renderHighlights.ts +46 -4
  208. package/src/components/msa/tileColor.ts +9 -8
  209. package/src/components/msa/useMsaBlockMouse.ts +44 -31
  210. package/src/components/overlayColors.ts +4 -0
  211. package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
  212. package/src/components/selectionDrag.ts +42 -0
  213. package/src/components/tracks/drawTracks.ts +15 -2
  214. package/src/components/tracks/useTrackHover.ts +32 -10
  215. package/src/components/tree/ScaleBarSVG.tsx +30 -0
  216. package/src/components/tree/TreeCanvas.tsx +9 -40
  217. package/src/components/tree/TreeCanvasBlock.tsx +19 -39
  218. package/src/components/tree/TreeMenu.tsx +129 -0
  219. package/src/components/tree/TreeRuler.tsx +7 -13
  220. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
  221. package/src/components/tree/renderTreeCanvas.ts +20 -8
  222. package/src/components/tree/renderTreeMouseover.ts +41 -0
  223. package/src/components/tree/useTreeHover.ts +8 -17
  224. package/src/components/useHoverAnchor.ts +31 -0
  225. package/src/constants.ts +14 -3
  226. package/src/element.ts +45 -5
  227. package/src/expandSpec.ts +3 -3
  228. package/src/exportFileName.ts +4 -3
  229. package/src/featuresLayer.ts +48 -0
  230. package/src/hierarchy.ts +151 -4
  231. package/src/index.ts +8 -1
  232. package/src/model/DataModel.ts +3 -3
  233. package/src/model/msaModel.ts +19 -1
  234. package/src/model/treeModel.ts +22 -0
  235. package/src/model.ts +841 -272
  236. package/src/renderToSvg.tsx +6 -9
  237. package/src/rerootTree.ts +172 -0
  238. package/src/types.ts +92 -12
  239. package/src/useMsaSvgFigure.ts +11 -4
  240. package/src/version.ts +1 -1
  241. package/src/writeNewick.ts +38 -0
  242. package/dist/components/PortalTooltip.d.ts +0 -11
  243. package/dist/components/PortalTooltip.js +0 -29
  244. package/dist/components/PortalTooltip.js.map +0 -1
  245. package/dist/components/header/HeaderStatusArea.js +0 -21
  246. package/dist/components/header/HeaderStatusArea.js.map +0 -1
  247. package/dist/components/tree/TreeBranchMenu.js +0 -29
  248. package/dist/components/tree/TreeBranchMenu.js.map +0 -1
  249. package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
  250. package/dist/components/tree/TreeNodeMenu.js +0 -41
  251. package/dist/components/tree/TreeNodeMenu.js.map +0 -1
  252. package/src/components/PortalTooltip.tsx +0 -47
  253. package/src/components/header/HeaderStatusArea.tsx +0 -32
  254. package/src/components/tree/TreeBranchMenu.tsx +0 -68
  255. package/src/components/tree/TreeNodeMenu.tsx +0 -95
@@ -1,7 +1,7 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { ErrorMessage, FileSelector } from '@jbrowse/core/ui'
4
- import { Button, Container, Typography } from '@mui/material'
4
+ import { Box, Button, Container, Typography } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
 
7
7
  import ImportFormExamples from './ImportFormExamples.tsx'
@@ -10,6 +10,8 @@ import { load } from './util.ts'
10
10
  import type { MsaViewModel } from '../../model.ts'
11
11
  import type { FileLocation } from '@jbrowse/core/util/types'
12
12
 
13
+ const phone = '@media (max-width: 640px)'
14
+
13
15
  const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
14
16
  const [msaFile, setMsaFile] = useState<FileLocation>()
15
17
  const [treeFile, setTreeFile] = useState<FileLocation>()
@@ -18,7 +20,7 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
18
20
 
19
21
  return (
20
22
  <Container>
21
- <div style={{ width: '50%' }}>
23
+ <Box sx={{ width: '50%', [phone]: { width: '100%' } }}>
22
24
  {error ? <ErrorMessage error={error} /> : null}
23
25
  <Typography>
24
26
  Open an MSA file (FASTA, Stockholm, Clustal, A3M or EMF format) and/or
@@ -28,14 +30,19 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
28
30
  An MSA alone or a tree alone is enough, and a Stockholm file with an
29
31
  embedded tree needs no separate tree file.
30
32
  </Typography>
31
- </div>
33
+ </Box>
32
34
 
33
- <div
34
- style={{
35
+ <Box
36
+ sx={{
35
37
  display: 'flex',
36
38
  justifyContent: 'center',
37
39
  alignItems: 'center',
38
- gap: 40,
40
+ gap: '40px',
41
+ [phone]: {
42
+ flexDirection: 'column',
43
+ alignItems: 'stretch',
44
+ gap: '16px',
45
+ },
39
46
  }}
40
47
  >
41
48
  <div>
@@ -74,7 +81,7 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
74
81
  <Typography>Examples</Typography>
75
82
  <ImportFormExamples model={model} />
76
83
  </div>
77
- </div>
84
+ </Box>
78
85
  </Container>
79
86
  )
80
87
  })
@@ -3,16 +3,17 @@ import React from 'react'
3
3
  import { Link, Typography } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
5
5
 
6
- import { smallMSA, smallMSAOnly, smallTree } from './data/seq2.ts'
6
+ import { smallMSA, smallTree } from './data/seq2.ts'
7
7
  import { load } from './util.ts'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
11
11
  const BASE = 'https://jbrowse.org/genomes/multiple_sequence_alignments'
12
12
  const TREES = 'https://jbrowse.org/genomes/newick_trees'
13
+ const DEMO = 'https://gmod.org/JBrowseMSA/demo/data'
13
14
 
14
15
  // each example points at remote files (msa/tree/gff urls) or carries its data
15
- // inline, as the two small bundled ones do
16
+ // inline
16
17
  interface Example {
17
18
  label: string
18
19
  msa?: string
@@ -23,42 +24,33 @@ interface Example {
23
24
 
24
25
  const examples: Example[] = [
25
26
  {
26
- label: '230k COVID-19 samples (tree only)',
27
- tree: `${TREES}/sarscov2phylo.pub.ft.nh`,
28
- },
29
- {
30
- label: 'Small protein MSA+tree',
31
- inline: { msa: smallMSA, tree: smallTree },
32
- },
33
- {
34
- label: 'Small MSA only',
35
- inline: { msa: smallMSAOnly },
36
- },
37
- {
38
- label: 'PFAM SARS-CoV2 multi-stockholm with domains',
39
- msa: `${BASE}/pfam-cov2.stock`,
40
- gff: `${BASE}/pfam-cov2-domains.gff`,
27
+ label: 'NLRP1 in 12 mammals, with Pfam domains',
28
+ msa: `${DEMO}/nlrp1.aln`,
29
+ tree: `${DEMO}/nlrp1.nh`,
30
+ gff: `${DEMO}/nlrp1-domains.gff`,
41
31
  },
42
32
  {
43
- label: 'Lysine stockholm file',
44
- msa: `${BASE}/Lysine.stock`,
33
+ label: '474 human protein kinase domains, with a FastTree tree',
34
+ msa: `${DEMO}/kinase-pocket/kinase-pocket.afa`,
35
+ tree: `${DEMO}/kinase-pocket/kinase-pocket.nwk`,
45
36
  },
46
37
  {
47
- label: 'PF01601 stockholm file (SARS-CoV2 spike protein)',
48
- msa: `${BASE}/PF01601_full.txt`,
38
+ label: 'RSV-A G gene in 1,840 genomes, with the Nextstrain tree',
39
+ msa: `${DEMO}/scale/rsv-full.aln`,
40
+ tree: `${DEMO}/scale/rsv-full.nh`,
49
41
  },
50
42
  {
51
- label: 'Europe COVID full genomes (LR883044.1 and 199 other sequences)',
52
- msa: `${BASE}/europe_covid.fa`,
43
+ label: '230k COVID-19 samples (tree only)',
44
+ tree: `${TREES}/sarscov2phylo.pub.ft.nh`,
53
45
  },
54
46
  {
55
- label: 'MAFFT+VeryFastTree(17.9k samples)',
56
- msa: `${BASE}/rhv_test-only.aligned_with_mafft_auto.fa`,
57
- tree: `${BASE}/rhv_test-only.aligned_with_mafft_auto.nh`,
47
+ label: 'Pfam SARS-CoV-2 families, multi-Stockholm with domains',
48
+ msa: `${BASE}/pfam-cov2.stock`,
49
+ gff: `${BASE}/pfam-cov2-domains.gff`,
58
50
  },
59
51
  {
60
- label: 'Human BLAST results mfa',
61
- msa: 'https://jbrowse.org/demos/ttc39a.mfa',
52
+ label: 'Small protein alignment and tree',
53
+ inline: { msa: smallMSA, tree: smallTree },
62
54
  },
63
55
  ]
64
56
 
@@ -70,6 +70,13 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
70
70
  ref={ref}
71
71
  width={barWidth}
72
72
  height={barHeight}
73
+ onMouseDown={event => {
74
+ if (event.button === 0 && unit > 0) {
75
+ const x =
76
+ event.clientX - event.currentTarget.getBoundingClientRect().left
77
+ model.setScrollX(-x / unit + msaCanvasWidth / 2)
78
+ }
79
+ }}
73
80
  style={{
74
81
  display: 'block',
75
82
  width: msaCanvasWidth,
@@ -32,16 +32,17 @@ const AnnotationLegend = observer(function ({
32
32
  display: 'flex',
33
33
  flexDirection: 'column',
34
34
  opacity: 0.95,
35
+ pointerEvents: 'none',
35
36
  }}
36
37
  >
37
38
  <IconButton
38
39
  size="small"
39
40
  title={expanded ? 'Collapse key' : 'Expand key'}
40
- style={
41
- expanded
42
- ? { position: 'absolute', top: 0, right: 0, padding: 1 }
43
- : { padding: 1 }
44
- }
41
+ style={{
42
+ padding: 1,
43
+ pointerEvents: 'auto',
44
+ ...(expanded && { position: 'absolute', top: 0, right: 0 }),
45
+ }}
45
46
  onClick={() => {
46
47
  model.setShowDomainLegend(!expanded)
47
48
  }}
@@ -59,7 +60,6 @@ const AnnotationLegend = observer(function ({
59
60
  gap: 4,
60
61
  marginRight: i === 0 ? 18 : 0,
61
62
  }}
62
- title={row.hint}
63
63
  >
64
64
  {row.color ? (
65
65
  <div
@@ -74,10 +74,16 @@ const AnnotationLegend = observer(function ({
74
74
  <Typography
75
75
  variant="caption"
76
76
  noWrap
77
+ title={
78
+ row.hint && row.hint !== row.label
79
+ ? `${row.label} (${row.hint})`
80
+ : row.label
81
+ }
77
82
  style={{
78
83
  fontSize: 10,
79
84
  lineHeight: 1.4,
80
85
  fontWeight: row.color ? undefined : 'bold',
86
+ pointerEvents: 'auto',
81
87
  }}
82
88
  >
83
89
  {row.label}
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react'
5
5
  import { useWheelScroll } from '../../useWheelScroll.ts'
6
6
  import Loading from './Loading.tsx'
7
7
  import MSACanvasBlock from './MSACanvasBlock.tsx'
8
+ import { onMsaKey } from './msaKeys.ts'
8
9
 
9
10
  import type { MsaViewModel } from '../../model.ts'
10
11
 
@@ -52,7 +53,24 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
52
53
  // screenshot callouts anchor to columns from this rect's origin:
53
54
  // col*colWidth + scrollX, row*rowHeight + scrollY
54
55
  data-testid="msa_canvas"
55
- onMouseDown={onMouseDown}
56
+ tabIndex={0}
57
+ aria-label="Alignment. Arrow keys scroll, shift pages, + and - zoom, Home and End jump to the first and last column. Shift-drag selects cells, and Escape clears the selection."
58
+ onKeyDown={event => {
59
+ if (onMsaKey(model, event)) {
60
+ event.preventDefault()
61
+ }
62
+ }}
63
+ onMouseDown={event => {
64
+ // a shifted press selects cells (useMsaBlockMouse), so it takes the
65
+ // focus Escape clears from, and leaves out the pan and the page's own
66
+ // text selection
67
+ if (event.shiftKey) {
68
+ event.preventDefault()
69
+ ref.current?.focus({ preventScroll: true })
70
+ } else {
71
+ onMouseDown(event)
72
+ }
73
+ }}
56
74
  onMouseUp={onMouseUp}
57
75
  style={{
58
76
  position: 'relative',
@@ -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,11 @@ const MSACanvasBlock = observer(function ({
76
71
  onMouseMove(event, ref.current)
77
72
  }
78
73
  }}
74
+ onMouseDown={event => {
75
+ if (ref.current) {
76
+ onMouseDown(event, ref.current)
77
+ }
78
+ }}
79
79
  onClick={event => {
80
80
  if (ref.current) {
81
81
  onClick(event, ref.current)
@@ -94,10 +94,8 @@ const MSACanvasBlock = observer(function ({
94
94
  height: blockSize,
95
95
  }}
96
96
  />
97
- {tooltipPoint ? (
98
- <BaseTooltip
99
- clientPoint={{ x: tooltipPoint.x, y: tooltipPoint.y + 15 }}
100
- >
97
+ {anchor ? (
98
+ <BaseTooltip clientPoint={anchor.clientPoint}>
101
99
  {hoveredInsertion ? (
102
100
  <>
103
101
  Insertion ({hoveredInsertion.letters.length}
@@ -0,0 +1,45 @@
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
+ case 'Escape':
37
+ if (model.selection) {
38
+ model.clearSelection()
39
+ return true
40
+ }
41
+ return false
42
+ default:
43
+ return false
44
+ }
45
+ }
@@ -297,6 +297,7 @@ function rasterKeys(model: MsaViewModel, theme: Theme) {
297
297
  model.columns,
298
298
  model.leaves,
299
299
  model.colorSchemeName,
300
+ model.customColorScheme,
300
301
  tileColorTable(model),
301
302
  model.relativeTo,
302
303
  theme.palette.action.hover,
@@ -1,5 +1,9 @@
1
1
  import { setFontSize } from '../../setFontSize.ts'
2
- import { referenceColor } from '../overlayColors.ts'
2
+ import {
3
+ referenceColor,
4
+ selectionBorder,
5
+ selectionFill,
6
+ } from '../overlayColors.ts'
3
7
 
4
8
  import type { MsaViewModel } from '../../model.ts'
5
9
  import type { RenderCtx } from '../renderCtx.ts'
@@ -172,11 +176,48 @@ export function renderHighlights({
172
176
  }
173
177
  }
174
178
 
179
+ /**
180
+ * The selected block, a bordered band per run of consecutive selected rows,
181
+ * culled to the rows this block covers
182
+ */
183
+ export function renderSelection({
184
+ ctx,
185
+ model,
186
+ offsetX,
187
+ offsetY,
188
+ height,
189
+ }: {
190
+ ctx: RenderCtx
191
+ model: MsaViewModel
192
+ offsetX: number
193
+ offsetY: number
194
+ height: number
195
+ }) {
196
+ const { resolvedSelection, colWidth, rowHeight } = model
197
+ if (!resolvedSelection) {
198
+ return
199
+ }
200
+ const { startCol, endCol, rowRuns } = resolvedSelection
201
+ const x = startCol * colWidth - offsetX
202
+ const w = (endCol - startCol + 1) * colWidth
203
+ ctx.lineWidth = 1
204
+ for (const [first, last] of rowRuns) {
205
+ const y = first * rowHeight - offsetY
206
+ const h = (last - first + 1) * rowHeight
207
+ if (y < height && y + h > 0) {
208
+ ctx.fillStyle = selectionFill
209
+ ctx.fillRect(x, y, w, h)
210
+ ctx.strokeStyle = selectionBorder
211
+ ctx.strokeRect(x, y, w, h)
212
+ }
213
+ }
214
+ }
215
+
175
216
  /**
176
217
  * The overlay parts that come from the snapshot: the reference row's tint, a
177
- * bordered band per run of highlighted columns, and the `highlights` layer. The
178
- * live overlay draws these under its hover and click bands; the SVG export
179
- * draws only these.
218
+ * bordered band per run of highlighted columns, the `highlights` layer and the
219
+ * selected block. The live overlay draws these under its hover and click
220
+ * bands; the SVG export draws only these.
180
221
  */
181
222
  export function renderPersistentHighlights({
182
223
  ctx,
@@ -214,4 +255,5 @@ export function renderPersistentHighlights({
214
255
  }
215
256
 
216
257
  renderHighlights({ ctx, model, theme, offsetX, offsetY, width, height })
258
+ renderSelection({ ctx, model, offsetX, offsetY, height })
217
259
  }
@@ -4,12 +4,12 @@ import type { MsaViewModel } from '../../model.ts'
4
4
  // only the active scheme's column table leaves the other per-column table
5
5
  // uncomputed.
6
6
  export function tileColorFn(model: MsaViewModel) {
7
- const { colorSchemeName } = model
8
- if (colorSchemeName === 'clustalx_protein_dynamic') {
7
+ const { dynamicColorSchemeName } = model
8
+ if (dynamicColorSchemeName === 'clustalx_protein_dynamic') {
9
9
  const { colClustalX } = model
10
10
  return (col: number, letter: string) => colClustalX[col]![letter]
11
11
  }
12
- if (colorSchemeName === 'percent_identity_dynamic') {
12
+ if (dynamicColorSchemeName === 'percent_identity_dynamic') {
13
13
  const { colConsensus } = model
14
14
  return (col: number, letter: string) => {
15
15
  const consensus = colConsensus[col]!
@@ -20,14 +20,15 @@ export function tileColorFn(model: MsaViewModel) {
20
20
  return (_col: number, letter: string) => colorScheme[letter]
21
21
  }
22
22
 
23
- // The column table the active scheme colors from, or undefined for the static
24
- // schemes. A cache of rendered colors keys on its identity plus the scheme name.
23
+ // The table the active scheme colors from: a dynamic scheme's per-column table,
24
+ // or the letter table of a static scheme or a `customColorScheme` map. A cache
25
+ // of rendered colors keys on its identity plus the scheme name.
25
26
  export function tileColorTable(model: MsaViewModel) {
26
- const { colorSchemeName } = model
27
- if (colorSchemeName === 'clustalx_protein_dynamic') {
27
+ const { dynamicColorSchemeName } = model
28
+ if (dynamicColorSchemeName === 'clustalx_protein_dynamic') {
28
29
  return model.colClustalX
29
30
  }
30
- if (colorSchemeName === 'percent_identity_dynamic') {
31
+ if (dynamicColorSchemeName === 'percent_identity_dynamic') {
31
32
  return model.colConsensus
32
33
  }
33
34
  return model.colorScheme
@@ -1,30 +1,12 @@
1
- import { useState } from 'react'
1
+ import { useRef } from 'react'
2
+
3
+ import { dragSelection, maxClickTravel } from '../selectionDrag.ts'
4
+ import { useHoverAnchor } from '../useHoverAnchor.ts'
2
5
 
3
6
  import type { MsaViewModel } from '../../model.ts'
7
+ import type { ClientPoint } from '../selectionDrag.ts'
4
8
  import type React from 'react'
5
9
 
6
- function eventToColRow({
7
- event,
8
- el,
9
- offsetX,
10
- offsetY,
11
- colWidth,
12
- rowHeight,
13
- }: {
14
- event: React.MouseEvent
15
- el: HTMLElement
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
-
28
10
  /**
29
11
  * Translates pointer events over one MSA block into model column/row hover and
30
12
  * click state, and reports where a tooltip should be anchored.
@@ -44,11 +26,26 @@ export function useMsaBlockMouse({
44
26
  offsetX: number
45
27
  offsetY: number
46
28
  }) {
47
- const [tooltipPoint, setTooltipPoint] = useState<{ x: number; y: number }>()
29
+ const { anchor, hoverAt, clearAnchor } = useHoverAnchor<true>()
30
+ const downAt = useRef<{ x: number; y: number }>(undefined)
31
+
32
+ // The block's rect places its cells on the page. A drag keeps the mapping
33
+ // it started with, so each point subtracts the scroll since then.
34
+ function cellMapper(el: HTMLElement) {
35
+ const { left, top } = el.getBoundingClientRect()
36
+ const { scrollX, scrollY } = model
37
+ return ({ clientX, clientY }: ClientPoint) => ({
38
+ col: Math.floor(
39
+ (clientX - left + offsetX - (model.scrollX - scrollX)) / model.colWidth,
40
+ ),
41
+ row: Math.floor(
42
+ (clientY - top + offsetY - (model.scrollY - scrollY)) / model.rowHeight,
43
+ ),
44
+ })
45
+ }
48
46
 
49
47
  function colRow(event: React.MouseEvent, el: HTMLElement) {
50
- const { colWidth, rowHeight } = model
51
- return eventToColRow({ event, el, offsetX, offsetY, colWidth, rowHeight })
48
+ return cellMapper(el)(event)
52
49
  }
53
50
 
54
51
  // a block can extend past the alignment (the last block is a full tile wide,
@@ -65,12 +62,28 @@ export function useMsaBlockMouse({
65
62
 
66
63
  const hasTooltip =
67
64
  !!model.hoveredInsertion || model.mouseOverDomains.length > 0
68
- setTooltipPoint(
69
- hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
70
- )
65
+ hoverAt(event, hasTooltip || undefined)
66
+ }
67
+
68
+ // shift+drag selects a block; MSACanvas leaves a shifted press out of its
69
+ // pan
70
+ function onMouseDown(event: React.MouseEvent, el: HTMLElement) {
71
+ downAt.current = { x: event.clientX, y: event.clientY }
72
+ if (event.shiftKey && event.button === 0) {
73
+ dragSelection(model, event, cellMapper(el))
74
+ }
71
75
  }
72
76
 
73
77
  function onClick(event: React.MouseEvent, el: HTMLElement) {
78
+ const down = downAt.current
79
+ downAt.current = undefined
80
+ if (
81
+ down &&
82
+ Math.hypot(event.clientX - down.x, event.clientY - down.y) >
83
+ maxClickTravel
84
+ ) {
85
+ return
86
+ }
74
87
  const pos = colRow(event, el)
75
88
  const { col, row } = pos
76
89
  const { mouseClickCol, mouseClickRow } = model
@@ -83,8 +96,8 @@ export function useMsaBlockMouse({
83
96
 
84
97
  function onMouseLeave() {
85
98
  model.setMousePos()
86
- setTooltipPoint(undefined)
99
+ clearAnchor()
87
100
  }
88
101
 
89
- return { tooltipPoint, onMouseMove, onClick, onMouseLeave }
102
+ return { anchor, onMouseMove, onMouseDown, onClick, onMouseLeave }
90
103
  }
@@ -16,3 +16,7 @@ export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
16
16
  // match or the crosshair appears to change color as it crosses the tracks.
17
17
  export const hoverColor = 'rgba(0,0,0,0.15)'
18
18
  export const clickColor = 'rgba(128,128,0,0.2)'
19
+
20
+ // the selected block, a blue that reads apart from the orange highlights
21
+ export const selectionFill = 'rgba(25,118,210,0.22)'
22
+ export const selectionBorder = 'rgba(25,118,210,0.95)'
@@ -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
  )
@@ -0,0 +1,42 @@
1
+ import { isAlive } from '@jbrowse/mobx-state-tree'
2
+
3
+ import type { MsaViewModel } from '../model.ts'
4
+
5
+ // a press that travels further than this, in px, is a drag and not a click
6
+ export const maxClickTravel = 3
7
+
8
+ export interface ClientPoint {
9
+ clientX: number
10
+ clientY: number
11
+ }
12
+
13
+ /**
14
+ * Follows a press through the window until its release, selecting the block
15
+ * between the cell it started on and the cell under the pointer. The selection
16
+ * starts once the pointer travels past `maxClickTravel`, so a press that stays
17
+ * put reaches the click handlers unchanged. `cellAt` maps a client point to a
18
+ * visible column and row index, or a column alone to select across every row.
19
+ */
20
+ export function dragSelection(
21
+ model: MsaViewModel,
22
+ start: ClientPoint,
23
+ cellAt: (point: ClientPoint) => { col: number; row?: number },
24
+ ) {
25
+ const anchor = cellAt(start)
26
+ const { clientX, clientY } = start
27
+ let dragging = false
28
+ function onMove(event: MouseEvent) {
29
+ dragging ||=
30
+ Math.hypot(event.clientX - clientX, event.clientY - clientY) >
31
+ maxClickTravel
32
+ if (dragging && isAlive(model)) {
33
+ model.selectBlock(anchor, cellAt(event))
34
+ }
35
+ }
36
+ function onUp() {
37
+ window.removeEventListener('mousemove', onMove)
38
+ window.removeEventListener('mouseup', onUp)
39
+ }
40
+ window.addEventListener('mousemove', onMove)
41
+ window.addEventListener('mouseup', onUp)
42
+ }