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
@@ -15,6 +15,7 @@ import {
15
15
  Typography,
16
16
  useTheme,
17
17
  } from '@mui/material'
18
+ import { observer } from 'mobx-react'
18
19
 
19
20
  import Checkbox2 from '../Checkbox2.tsx'
20
21
 
@@ -30,7 +31,7 @@ function afterPaint(fn: () => void) {
30
31
  })
31
32
  }
32
33
 
33
- export default function ExportSVGDialog({
34
+ const ExportSVGDialog = observer(function ({
34
35
  model,
35
36
  onClose,
36
37
  }: {
@@ -181,4 +182,6 @@ export default function ExportSVGDialog({
181
182
  </DialogActions>
182
183
  </Dialog>
183
184
  )
184
- }
185
+ })
186
+
187
+ export default ExportSVGDialog
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import { Dialog } from '@jbrowse/core/ui'
4
4
  import {
@@ -10,18 +10,28 @@ import {
10
10
  TableCell,
11
11
  TableHead,
12
12
  TableRow,
13
+ TextField,
13
14
  } from '@mui/material'
14
15
  import { observer } from 'mobx-react'
15
16
 
16
17
  import type { MsaViewModel } from '../../model.ts'
18
+ import type { Annotation } from '../../types.ts'
17
19
 
18
- const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
19
- // the accessions the file has, not the ones the filter map happens to hold:
20
- // that map now carries only what the reader turned off, and before that it
21
- // accumulated every accession of every file opened in this view
22
- const { annotationTypes } = model
20
+ function matches(type: Annotation, query: string) {
21
+ return [type.accession, type.name, type.description].some(field =>
22
+ field?.toLowerCase().includes(query),
23
+ )
24
+ }
25
+
26
+ const Toggles = observer(function ({
27
+ model,
28
+ accessions,
29
+ }: {
30
+ model: MsaViewModel
31
+ accessions: string[]
32
+ }) {
23
33
  const setAll = (shown: boolean) => {
24
- for (const accession of annotationTypes.keys()) {
34
+ for (const accession of accessions) {
25
35
  model.setFilter(accession, shown)
26
36
  }
27
37
  }
@@ -48,6 +58,11 @@ const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
48
58
 
49
59
  const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
50
60
  const { annotationTypes, annotations, featureColors, fillPalette } = model
61
+ const [query, setQuery] = useState('')
62
+ const needle = query.trim().toLowerCase()
63
+ const types = [...annotationTypes.values()].filter(
64
+ type => !needle || matches(type, needle),
65
+ )
51
66
  const counts = new Map<string, number>()
52
67
  const swatches = new Map<string, string>()
53
68
  for (const annot of annotations) {
@@ -62,7 +77,15 @@ const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
62
77
  }
63
78
  return (
64
79
  <>
65
- <Toggles model={model} />
80
+ <TextField
81
+ label="Filter"
82
+ size="small"
83
+ value={query}
84
+ onChange={event => {
85
+ setQuery(event.target.value)
86
+ }}
87
+ />
88
+ <Toggles model={model} accessions={types.map(t => t.accession)} />
66
89
  <Table size="small">
67
90
  <TableHead>
68
91
  <TableRow>
@@ -75,36 +98,34 @@ const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
75
98
  </TableRow>
76
99
  </TableHead>
77
100
  <TableBody>
78
- {[...annotationTypes.values()].map(
79
- ({ accession, name, description }) => (
80
- <TableRow key={accession}>
81
- <TableCell padding="checkbox">
82
- <Checkbox
83
- checked={!model.turnedOffFeatures.get(accession)}
84
- onChange={() => {
85
- model.setFilter(
86
- accession,
87
- !!model.turnedOffFeatures.get(accession),
88
- )
89
- }}
90
- />
91
- </TableCell>
92
- <TableCell>
93
- <div
94
- style={{
95
- width: 20,
96
- height: 20,
97
- background: swatches.get(accession),
98
- }}
99
- />
100
- </TableCell>
101
- <TableCell>{accession}</TableCell>
102
- <TableCell>{name}</TableCell>
103
- <TableCell>{counts.get(accession)}</TableCell>
104
- <TableCell>{description}</TableCell>
105
- </TableRow>
106
- ),
107
- )}
101
+ {types.map(({ accession, name, description }) => (
102
+ <TableRow key={accession}>
103
+ <TableCell padding="checkbox">
104
+ <Checkbox
105
+ checked={!model.turnedOffFeatures.get(accession)}
106
+ onChange={() => {
107
+ model.setFilter(
108
+ accession,
109
+ !!model.turnedOffFeatures.get(accession),
110
+ )
111
+ }}
112
+ />
113
+ </TableCell>
114
+ <TableCell>
115
+ <div
116
+ style={{
117
+ width: 20,
118
+ height: 20,
119
+ background: swatches.get(accession),
120
+ }}
121
+ />
122
+ </TableCell>
123
+ <TableCell>{accession}</TableCell>
124
+ <TableCell>{name}</TableCell>
125
+ <TableCell>{counts.get(accession)}</TableCell>
126
+ <TableCell>{description}</TableCell>
127
+ </TableRow>
128
+ ))}
108
129
  </TableBody>
109
130
  </Table>
110
131
  </>
@@ -1,6 +1,7 @@
1
1
  import React, { lazy } from 'react'
2
2
 
3
3
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
+ import AccountTree from '@mui/icons-material/AccountTree'
4
5
  import Assignment from '@mui/icons-material/Assignment'
5
6
  import FolderOpen from '@mui/icons-material/FolderOpen'
6
7
  import Menu from '@mui/icons-material/Menu'
@@ -47,6 +48,14 @@ const FileMenu = observer(({ model }: { model: MsaViewModel }) => {
47
48
  model,
48
49
  }),
49
50
  },
51
+ {
52
+ label: 'Export tree (Newick)',
53
+ icon: AccountTree,
54
+ disabled: !!model.tree.noTree,
55
+ onClick: () => {
56
+ model.exportNewick()
57
+ },
58
+ },
50
59
  {
51
60
  label: 'Export SVG',
52
61
  icon: PhotoCamera,
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import { Slider, Typography } from '@mui/material'
4
4
  import { observer } from 'mobx-react'
@@ -11,6 +11,8 @@ const GappynessSlider = observer(function GappynessSlider({
11
11
  model: MsaViewModel
12
12
  }) {
13
13
  const { hideGaps, allowedGappyness } = model
14
+ const [dragValue, setDragValue] = useState<number>()
15
+ const value = dragValue ?? allowedGappyness
14
16
  if (!hideGaps) {
15
17
  return null
16
18
  }
@@ -20,7 +22,7 @@ const GappynessSlider = observer(function GappynessSlider({
20
22
  least ceil(allowedGappyness% of the rows), so at 100 it hides the
21
23
  columns that are entirely gaps rather than nothing */}
22
24
  <Typography style={{ whiteSpace: 'nowrap' }}>
23
- Hide columns w/ &ge;{allowedGappyness}% gaps
25
+ Hide columns w/ &ge;{value}% gaps
24
26
  </Typography>
25
27
  <Slider
26
28
  // In JBrowse other MUI Sliders share the page, so `.MuiSlider-thumb`
@@ -30,13 +32,24 @@ const GappynessSlider = observer(function GappynessSlider({
30
32
  //
31
33
  // Not `slotProps={{ input: ... }}`: that widens `onChange`'s `val` to
32
34
  // `number | number[]`, which setAllowedGappyness rejects.
35
+ //
36
+ // The thumb follows local state while dragging and the model takes the
37
+ // value on release, since each write recomputes the gap columns over
38
+ // the whole alignment.
39
+ //
40
+ // The thumb centers on the track's ends, so the margin keeps its
41
+ // overhang off the label and the row search beside it.
33
42
  data-testid="gappyness_slider"
34
- style={{ width: 100 }}
43
+ style={{ width: 100, marginInline: 10 }}
35
44
  min={1}
36
45
  max={100}
37
- value={allowedGappyness}
46
+ value={value}
38
47
  onChange={(_, val) => {
48
+ setDragValue(val)
49
+ }}
50
+ onChangeCommitted={(_, val) => {
39
51
  model.setAllowedGappyness(val)
52
+ setDragValue(undefined)
40
53
  }}
41
54
  />
42
55
  </div>
@@ -5,14 +5,15 @@ import Help from '@mui/icons-material/Help'
5
5
  import { IconButton, Tooltip } from '@mui/material'
6
6
  import { observer } from 'mobx-react'
7
7
 
8
+ import StatusMessage from '../StatusMessage.tsx'
8
9
  import ColorSchemeMenu from './ColorSchemeMenu.tsx'
9
10
  import FileMenu from './FileMenu.tsx'
10
11
  import GappynessSlider from './GappynessSlider.tsx'
11
12
  import HeaderInfoArea from './HeaderInfoArea.tsx'
12
- import HeaderStatusArea from './HeaderStatusArea.tsx'
13
13
  import LoadWarnings from './LoadWarnings.tsx'
14
14
  import MSASettingsMenu from './MSASettingsMenu.tsx'
15
15
  import MultiAlignmentSelector from './MultiAlignmentSelector.tsx'
16
+ import RowSearch from './RowSearch.tsx'
16
17
  import TreeSettingsMenu from './TreeSettingsMenu.tsx'
17
18
  import UnshareableDataWarning from './UnshareableDataWarning.tsx'
18
19
  import ZoomControls from './ZoomControls.tsx'
@@ -42,14 +43,16 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
42
43
  <ZoomControls model={model} />
43
44
  <ZoomMenu model={model} />
44
45
  <GappynessSlider model={model} />
46
+ <RowSearch model={model} />
45
47
  <div style={{ paddingLeft: 20, margin: 'auto' }}>
46
48
  <MultiAlignmentSelector model={model} />
47
49
  </div>
48
- <HeaderInfoArea model={model} />
49
- <Spacer />
50
+ <div style={{ flex: '1 1 0', minWidth: 0, display: 'flex' }}>
51
+ <HeaderInfoArea model={model} />
52
+ </div>
50
53
  <LoadWarnings model={model} />
51
54
  <UnshareableDataWarning model={model} />
52
- <HeaderStatusArea model={model} />
55
+ <StatusMessage model={model} variant="header" />
53
56
  <Tooltip title="About">
54
57
  <IconButton
55
58
  aria-label="About"
@@ -64,8 +67,4 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
64
67
  )
65
68
  })
66
69
 
67
- function Spacer() {
68
- return <div style={{ flex: 1 }} />
69
- }
70
-
71
70
  export default Header
@@ -0,0 +1,34 @@
1
+ import React from 'react'
2
+
3
+ import { Autocomplete, TextField, createFilterOptions } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+
6
+ import { findableRowNames, revealRow } from './revealRow.ts'
7
+
8
+ import type { MsaViewModel } from '../../model.ts'
9
+
10
+ const filterOptions = createFilterOptions<string>({ limit: 50 })
11
+
12
+ const RowSearch = observer(function ({ model }: { model: MsaViewModel }) {
13
+ const names = findableRowNames(model)
14
+ return names.length > 1 ? (
15
+ <Autocomplete
16
+ size="small"
17
+ options={names}
18
+ filterOptions={filterOptions}
19
+ value={null}
20
+ blurOnSelect
21
+ onChange={(_, name) => {
22
+ if (name) {
23
+ revealRow(model, name)
24
+ }
25
+ }}
26
+ style={{ width: 180, margin: 'auto 8px' }}
27
+ renderInput={params => (
28
+ <TextField {...params} placeholder="Find row" aria-label="Find row" />
29
+ )}
30
+ />
31
+ ) : null
32
+ })
33
+
34
+ export default RowSearch
@@ -0,0 +1,17 @@
1
+ import type { MsaViewModel } from '../../model.ts'
2
+
3
+ export function findableRowNames(model: MsaViewModel) {
4
+ const collapsed = new Set(model.collapsed)
5
+ return model.leaves
6
+ .filter(({ data: { id, name } }) => !(collapsed.has(id) && name === id))
7
+ .map(leaf => leaf.data.name)
8
+ }
9
+
10
+ export function revealRow(model: MsaViewModel, name: string) {
11
+ const index = model.rowNamesSet.get(name)
12
+ if (index !== undefined) {
13
+ const { rowHeight, msaAreaHeight } = model
14
+ model.setScrollY(-(index + 0.5) * rowHeight + msaAreaHeight / 2)
15
+ model.setMousePos(undefined, index)
16
+ }
17
+ }
@@ -1,6 +1,27 @@
1
+ import { treeOrders } from '../../constants.ts'
2
+
1
3
  import type { MsaViewModel } from '../../model.ts'
4
+ import type { TreeOrder } from '../../types.ts'
2
5
  import type { MenuItem } from '@jbrowse/core/ui/Menu'
3
6
 
7
+ function radio(label: string, checked: boolean, onClick: () => void) {
8
+ return { label, type: 'radio' as const, checked, onClick }
9
+ }
10
+
11
+ const treeOrderLabels: Record<TreeOrder, string> = {
12
+ branchLength: 'By branch length',
13
+ input: 'As written',
14
+ ladderize: 'Ladderize, small clades first',
15
+ ladderizeReverse: 'Ladderize, large clades first',
16
+ }
17
+
18
+ function outgroupLabel(outgroup: string[]) {
19
+ const [first, ...rest] = outgroup
20
+ return rest.length === 0
21
+ ? `Outgroup ${first}`
22
+ : `Outgroup ${first} … ${rest.at(-1)}`
23
+ }
24
+
4
25
  function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
5
26
  return {
6
27
  label,
@@ -56,8 +77,53 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
56
77
  drawNodeLabels,
57
78
  drawLabels,
58
79
  showTreeOverview,
80
+ treeOrder,
81
+ treeRoot,
82
+ rotated,
59
83
  } = model
60
84
  return [
85
+ {
86
+ label: 'Order',
87
+ type: 'subMenu' as const,
88
+ subMenu: [
89
+ ...treeOrders.map(order =>
90
+ radio(treeOrderLabels[order], treeOrder === order, () => {
91
+ model.setTreeOrder(order)
92
+ }),
93
+ ),
94
+ { type: 'divider' as const },
95
+ {
96
+ label: 'Undo rotations',
97
+ disabled: rotated.length === 0,
98
+ onClick: () => {
99
+ model.clearRotated()
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ label: 'Root',
106
+ type: 'subMenu' as const,
107
+ subMenu: [
108
+ radio('As written', treeRoot === undefined, () => {
109
+ model.setTreeRoot(undefined)
110
+ }),
111
+ radio('Midpoint', treeRoot === 'midpoint', () => {
112
+ model.setTreeRoot('midpoint')
113
+ }),
114
+ // "Reroot here" in a node's menu sets the outgroup, so this only
115
+ // reports it
116
+ ...(typeof treeRoot === 'object'
117
+ ? [
118
+ {
119
+ ...radio(outgroupLabel(treeRoot.outgroup), true, () => {}),
120
+ disabled: true,
121
+ },
122
+ ]
123
+ : []),
124
+ ],
125
+ },
126
+ { type: 'divider' as const },
61
127
  toggle('Show branch length', showBranchLen, arg => {
62
128
  model.setShowBranchLen(arg)
63
129
  }),
@@ -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,6 +53,13 @@ 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"
56
+ tabIndex={0}
57
+ aria-label="Alignment. Arrow keys scroll, shift pages, + and - zoom, Home and End jump to the first and last column."
58
+ onKeyDown={event => {
59
+ if (onMsaKey(model, event)) {
60
+ event.preventDefault()
61
+ }
62
+ }}
55
63
  onMouseDown={onMouseDown}
56
64
  onMouseUp={onMouseUp}
57
65
  style={{