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,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,11 +1,10 @@
1
1
  import React from 'react'
2
2
 
3
- import Attributes from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/Attributes'
4
- import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard'
5
3
  import { Dialog } from '@jbrowse/core/ui'
6
- import { DialogContent } from '@mui/material'
4
+ import { DialogContent, Typography } from '@mui/material'
7
5
  import { observer } from 'mobx-react'
8
6
 
7
+ import MetadataTable, { isEmpty } from '../MetadataTable.tsx'
9
8
  import SequenceTextArea from '../SequenceTextArea.tsx'
10
9
 
11
10
  import type { MsaViewModel } from '../../model.ts'
@@ -18,7 +17,6 @@ const MetadataDialog = observer(function ({
18
17
  onClose: () => void
19
18
  }) {
20
19
  const { header } = model
21
-
22
20
  return (
23
21
  <Dialog
24
22
  onClose={() => {
@@ -29,10 +27,13 @@ const MetadataDialog = observer(function ({
29
27
  maxWidth="xl"
30
28
  >
31
29
  <DialogContent>
32
- <Attributes attributes={header} />
33
- <BaseCard title="sequence">
34
- <SequenceTextArea str={model.rows} />
35
- </BaseCard>
30
+ {Object.values(header).every(isEmpty) ? (
31
+ <Typography>This alignment has no metadata.</Typography>
32
+ ) : (
33
+ <MetadataTable record={header} />
34
+ )}
35
+ <Typography variant="h6">Sequences</Typography>
36
+ <SequenceTextArea str={model.rows} />
36
37
  </DialogContent>
37
38
  </Dialog>
38
39
  )
@@ -8,8 +8,37 @@ import colorSchemes from '../../colorSchemes.ts'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
11
+ // While a `customColorScheme` map is set, a checked Custom entry stands for it
12
+ // and no named scheme is checked. Picking a named scheme clears the map.
13
+ export function colorSchemeMenuItems(model: MsaViewModel) {
14
+ const { colorSchemeName, customColorScheme } = model
15
+ const custom = customColorScheme
16
+ ? [
17
+ {
18
+ label: 'Custom',
19
+ type: 'radio',
20
+ checked: true,
21
+ onClick: () => {},
22
+ } as const,
23
+ ]
24
+ : []
25
+ return [
26
+ ...custom,
27
+ ...Object.keys(colorSchemes).map(
28
+ option =>
29
+ ({
30
+ label: option,
31
+ type: 'radio',
32
+ checked: !customColorScheme && colorSchemeName === option,
33
+ onClick: () => {
34
+ model.setColorSchemeName(option)
35
+ },
36
+ }) as const,
37
+ ),
38
+ ]
39
+ }
40
+
11
41
  const ColorSchemeMenu = observer(function ({ model }: { model: MsaViewModel }) {
12
- const { colorSchemeName } = model
13
42
  return (
14
43
  <CascadingMenuButton
15
44
  data-testid="color_scheme_menu"
@@ -17,17 +46,7 @@ const ColorSchemeMenu = observer(function ({ model }: { model: MsaViewModel }) {
17
46
  closeAfterItemClick
18
47
  anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
19
48
  transformOrigin={{ vertical: 'top', horizontal: 'left' }}
20
- menuItems={Object.keys(colorSchemes).map(
21
- option =>
22
- ({
23
- label: option,
24
- type: 'radio',
25
- checked: colorSchemeName === option,
26
- onClick: () => {
27
- model.setColorSchemeName(option)
28
- },
29
- }) as const,
30
- )}
49
+ menuItems={colorSchemeMenuItems(model)}
31
50
  >
32
51
  <Palette />
33
52
  </CascadingMenuButton>
@@ -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>
@@ -0,0 +1,50 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import { Autocomplete, TextField } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+
6
+ import { findableRowNames, goTo, rowNameSuggestions } from './goTo.ts'
7
+
8
+ import type { MsaViewModel } from '../../model.ts'
9
+
10
+ const label = 'Go to row or column'
11
+
12
+ const GoToBox = observer(function ({ model }: { model: MsaViewModel }) {
13
+ const [missed, setMissed] = useState(false)
14
+ const names = findableRowNames(model)
15
+ return names.length > 1 || model.numColumns > 0 ? (
16
+ <Autocomplete
17
+ freeSolo
18
+ size="small"
19
+ options={names}
20
+ filterOptions={(options, { inputValue }) =>
21
+ rowNameSuggestions(options, inputValue)
22
+ }
23
+ value={null}
24
+ blurOnSelect
25
+ onInputChange={() => {
26
+ setMissed(false)
27
+ }}
28
+ onChange={(_, text) => {
29
+ if (text) {
30
+ setMissed(!goTo(model, text))
31
+ }
32
+ }}
33
+ style={{ width: 180, margin: 'auto 8px' }}
34
+ renderInput={params => (
35
+ <TextField
36
+ {...params}
37
+ error={missed}
38
+ placeholder={label}
39
+ title="A row name, a column number, or name:residue"
40
+ slotProps={{
41
+ ...params.slotProps,
42
+ htmlInput: { ...params.slotProps.htmlInput, 'aria-label': label },
43
+ }}
44
+ />
45
+ )}
46
+ />
47
+ ) : null
48
+ })
49
+
50
+ export default GoToBox
@@ -5,14 +5,16 @@ 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'
12
+ import GoToBox from './GoToBox.tsx'
11
13
  import HeaderInfoArea from './HeaderInfoArea.tsx'
12
- import HeaderStatusArea from './HeaderStatusArea.tsx'
13
14
  import LoadWarnings from './LoadWarnings.tsx'
14
15
  import MSASettingsMenu from './MSASettingsMenu.tsx'
15
16
  import MultiAlignmentSelector from './MultiAlignmentSelector.tsx'
17
+ import SelectionMenu from './SelectionMenu.tsx'
16
18
  import TreeSettingsMenu from './TreeSettingsMenu.tsx'
17
19
  import UnshareableDataWarning from './UnshareableDataWarning.tsx'
18
20
  import ZoomControls from './ZoomControls.tsx'
@@ -42,14 +44,19 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
42
44
  <ZoomControls model={model} />
43
45
  <ZoomMenu model={model} />
44
46
  <GappynessSlider model={model} />
47
+ <GoToBox model={model} />
45
48
  <div style={{ paddingLeft: 20, margin: 'auto' }}>
46
49
  <MultiAlignmentSelector model={model} />
47
50
  </div>
48
- <HeaderInfoArea model={model} />
49
- <Spacer />
51
+ {/* zero flex-basis, so what appears here on a selection or a hover
52
+ shrinks into the header's spare width and never wraps it */}
53
+ <div style={{ flex: '1 1 0', minWidth: 0, display: 'flex' }}>
54
+ <SelectionMenu model={model} />
55
+ <HeaderInfoArea model={model} />
56
+ </div>
50
57
  <LoadWarnings model={model} />
51
58
  <UnshareableDataWarning model={model} />
52
- <HeaderStatusArea model={model} />
59
+ <StatusMessage model={model} variant="header" />
53
60
  <Tooltip title="About">
54
61
  <IconButton
55
62
  aria-label="About"
@@ -64,8 +71,4 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
64
71
  )
65
72
  })
66
73
 
67
- function Spacer() {
68
- return <div style={{ flex: 1 }} />
69
- }
70
-
71
74
  export default Header
@@ -0,0 +1,117 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import ArrowDropDown from '@mui/icons-material/ArrowDropDown'
4
+ import Clear from '@mui/icons-material/Clear'
5
+ import ContentCopy from '@mui/icons-material/ContentCopy'
6
+ import ZoomIn from '@mui/icons-material/ZoomIn'
7
+ import {
8
+ Button,
9
+ ListItemIcon,
10
+ ListItemText,
11
+ Menu,
12
+ MenuItem,
13
+ Tooltip,
14
+ } from '@mui/material'
15
+ import { observer } from 'mobx-react'
16
+
17
+ import copy from '../../vendor/copyToClipboard.ts'
18
+
19
+ import type { MsaViewModel } from '../../model.ts'
20
+
21
+ function count(n: number, noun: string) {
22
+ return `${n.toLocaleString('en-US')} ${noun}${n === 1 ? '' : 's'}`
23
+ }
24
+
25
+ /**
26
+ * The selected block's size, as a button opening what to do with it. It
27
+ * renders only while there is a selection. Header.tsx puts it in the slot
28
+ * with no width of its own, so the label ellipsizes before it could wrap the
29
+ * header onto a second line.
30
+ */
31
+ const SelectionMenu = observer(function ({ model }: { model: MsaViewModel }) {
32
+ const [anchorEl, setAnchorEl] = useState<HTMLElement>()
33
+ const { selectionSize, selectionFileSpan } = model
34
+ if (!selectionSize || !selectionFileSpan) {
35
+ return null
36
+ }
37
+ const label = `${count(selectionSize.columns, 'column')} × ${count(selectionSize.rows, 'row')}`
38
+ const items = [
39
+ {
40
+ text: 'Copy as FASTA',
41
+ Icon: ContentCopy,
42
+ act: () => {
43
+ copy(model.selectionFasta)
44
+ },
45
+ },
46
+ {
47
+ text: 'Zoom to selection',
48
+ Icon: ZoomIn,
49
+ act: () => {
50
+ model.zoomToSelection()
51
+ },
52
+ },
53
+ {
54
+ text: 'Clear selection',
55
+ Icon: Clear,
56
+ act: () => {
57
+ model.clearSelection()
58
+ },
59
+ },
60
+ ]
61
+ return (
62
+ <>
63
+ <Tooltip
64
+ title={`Columns ${selectionFileSpan.start}-${selectionFileSpan.end} selected. Shift-drag the alignment to select, Escape to clear`}
65
+ >
66
+ <Button
67
+ size="small"
68
+ aria-label={`Selection: ${label}`}
69
+ endIcon={<ArrowDropDown />}
70
+ onClick={event => {
71
+ setAnchorEl(event.currentTarget)
72
+ }}
73
+ style={{
74
+ margin: 'auto 0 auto 8px',
75
+ flexShrink: 0,
76
+ maxWidth: '100%',
77
+ minWidth: 0,
78
+ textTransform: 'none',
79
+ whiteSpace: 'nowrap',
80
+ }}
81
+ >
82
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
83
+ {label}
84
+ </span>
85
+ </Button>
86
+ </Tooltip>
87
+ {anchorEl ? (
88
+ <Menu
89
+ anchorEl={anchorEl}
90
+ open
91
+ transitionDuration={0}
92
+ onClose={() => {
93
+ setAnchorEl(undefined)
94
+ }}
95
+ >
96
+ {items.map(({ text, Icon, act }) => (
97
+ <MenuItem
98
+ key={text}
99
+ dense
100
+ onClick={() => {
101
+ act()
102
+ setAnchorEl(undefined)
103
+ }}
104
+ >
105
+ <ListItemIcon>
106
+ <Icon fontSize="small" />
107
+ </ListItemIcon>
108
+ <ListItemText>{text}</ListItemText>
109
+ </MenuItem>
110
+ ))}
111
+ </Menu>
112
+ ) : null}
113
+ </>
114
+ )
115
+ })
116
+
117
+ export default SelectionMenu
@@ -0,0 +1,75 @@
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
+ // not MUI's createFilterOptions: JBrowse hosts do not re-export it, so
11
+ // jbrowse-plugin-msaview would fail to load
12
+ export function rowNameSuggestions(names: string[], input: string, limit = 50) {
13
+ const query = input.toLowerCase()
14
+ return names
15
+ .filter(name => name.toLowerCase().includes(query))
16
+ .slice(0, limit)
17
+ }
18
+
19
+ /**
20
+ * The visible row and column a "Go to" entry names: a row by its name, the
21
+ * column the ruler numbers N, or residue N of a row written `name:N`. A name
22
+ * is tried whole first, so a row called `chr1:100` stays reachable.
23
+ */
24
+ export function resolveGoTo(model: MsaViewModel, text: string) {
25
+ const query = text.trim()
26
+ const row = model.rowNamesSet.get(query)
27
+ if (row !== undefined) {
28
+ return { row }
29
+ }
30
+ const residue = /^(.+):(\d+)$/.exec(query)
31
+ if (residue) {
32
+ const [, name = '', pos] = residue
33
+ const rowIndex = model.rowNamesSet.get(name)
34
+ const col =
35
+ rowIndex === undefined ? undefined : columnOfResidue(model, name, +pos!)
36
+ return col === undefined ? undefined : { row: rowIndex, col }
37
+ }
38
+ if (/^\d+$/.test(query)) {
39
+ const n = +query
40
+ const { relativeTo, numColumns } = model
41
+ const col = relativeTo
42
+ ? columnOfResidue(model, relativeTo, n)
43
+ : n >= 1 && n <= numColumns
44
+ ? n - 1
45
+ : undefined
46
+ return col === undefined ? undefined : { col }
47
+ }
48
+ return undefined
49
+ }
50
+
51
+ // seqPosToVisibleCol maps a residue past the row's end to the column after
52
+ // it; here that residue names nothing
53
+ function columnOfResidue(model: MsaViewModel, name: string, pos: number) {
54
+ const globalCol = model.seqPosIndex(name)?.[pos - 1]
55
+ return globalCol === undefined
56
+ ? undefined
57
+ : model.globalColToVisibleCol(globalCol)
58
+ }
59
+
60
+ /** centers the view on the entry's target and lights it; false if none */
61
+ export function goTo(model: MsaViewModel, text: string) {
62
+ const target = resolveGoTo(model, text)
63
+ if (target) {
64
+ const { row, col } = target
65
+ const { rowHeight, colWidth, msaAreaHeight, msaAreaWidth } = model
66
+ if (row !== undefined) {
67
+ model.setScrollY(-(row + 0.5) * rowHeight + msaAreaHeight / 2)
68
+ }
69
+ if (col !== undefined) {
70
+ model.setScrollX(-(col + 0.5) * colWidth + msaAreaWidth / 2)
71
+ }
72
+ model.setMousePos(col, row)
73
+ }
74
+ return target !== undefined
75
+ }
@@ -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
  }),