react-msaview 9.0.0 → 9.1.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 (157) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +74 -74
  3. package/bundle/index.js.map +4 -4
  4. package/dist/colorSchemes.js +1 -3
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/columnCounts.d.ts +0 -1
  7. package/dist/columnCounts.js +1 -1
  8. package/dist/columnCounts.js.map +1 -1
  9. package/dist/components/Loading.js +5 -5
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +1891 -4
  12. package/dist/components/MSAViewer.js +144 -201
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/ResizeHandles.js +3 -2
  15. package/dist/components/ResizeHandles.js.map +1 -1
  16. package/dist/components/Track.js +2 -1
  17. package/dist/components/Track.js.map +1 -1
  18. package/dist/components/controls.d.ts +11 -0
  19. package/dist/components/controls.js +48 -0
  20. package/dist/components/controls.js.map +1 -0
  21. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  22. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  23. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  24. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  25. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  26. package/dist/components/dialogs/HelpDialog.js +30 -0
  27. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  28. package/dist/components/header/FileMenu.js +33 -7
  29. package/dist/components/header/FileMenu.js.map +1 -1
  30. package/dist/components/header/Header.js +4 -6
  31. package/dist/components/header/Header.js.map +1 -1
  32. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  33. package/dist/components/header/TreeSettingsMenu.js +1 -32
  34. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  35. package/dist/components/header/ZoomControls.js +28 -44
  36. package/dist/components/header/ZoomControls.js.map +1 -1
  37. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  38. package/dist/components/header/settingsMenuItems.js +44 -0
  39. package/dist/components/header/settingsMenuItems.js.map +1 -1
  40. package/dist/components/import/data/seq2.d.ts +0 -1
  41. package/dist/components/import/data/seq2.js +0 -17
  42. package/dist/components/import/data/seq2.js.map +1 -1
  43. package/dist/components/msa/MSACanvas.js +2 -1
  44. package/dist/components/msa/MSACanvas.js.map +1 -1
  45. package/dist/components/msa/MSACanvasBlock.js +2 -5
  46. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  47. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  48. package/dist/components/msa/drawFeatureSpans.js +99 -56
  49. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  50. package/dist/components/msa/msaRaster.d.ts +14 -1
  51. package/dist/components/msa/msaRaster.js +19 -9
  52. package/dist/components/msa/msaRaster.js.map +1 -1
  53. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  54. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  56. package/dist/components/msa/renderHighlights.d.ts +2 -2
  57. package/dist/components/msa/renderHighlights.js +2 -3
  58. package/dist/components/msa/renderHighlights.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +2 -1
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/renderCtx.d.ts +1 -1
  62. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  63. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  64. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  65. package/dist/components/tracks/drawTracks.d.ts +1 -53
  66. package/dist/components/tracks/drawTracks.js +5 -5
  67. package/dist/components/tracks/drawTracks.js.map +1 -1
  68. package/dist/components/tree/CladeLabels.d.ts +1 -1
  69. package/dist/components/tree/CladeLabels.js +22 -34
  70. package/dist/components/tree/CladeLabels.js.map +1 -1
  71. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  72. package/dist/components/tree/cladeBrackets.js +33 -3
  73. package/dist/components/tree/cladeBrackets.js.map +1 -1
  74. package/dist/components/tree/renderTreeCanvas.js +2 -8
  75. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  76. package/dist/components/tree/renderTreeOverview.js +4 -11
  77. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  78. package/dist/element.js +3 -6
  79. package/dist/element.js.map +1 -1
  80. package/dist/exportLayout.d.ts +39 -0
  81. package/dist/exportLayout.js +78 -0
  82. package/dist/exportLayout.js.map +1 -0
  83. package/dist/headlessRenderEnv.d.ts +12 -39
  84. package/dist/headlessRenderEnv.js +18 -12
  85. package/dist/headlessRenderEnv.js.map +1 -1
  86. package/dist/hierarchy.d.ts +5 -8
  87. package/dist/hierarchy.js +5 -79
  88. package/dist/hierarchy.js.map +1 -1
  89. package/dist/index.d.ts +3 -1
  90. package/dist/index.js +4 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/measureTextCanvas.d.ts +7 -0
  93. package/dist/measureTextCanvas.js +8 -6
  94. package/dist/measureTextCanvas.js.map +1 -1
  95. package/dist/model/treeModel.d.ts +0 -5
  96. package/dist/model/treeModel.js +0 -7
  97. package/dist/model/treeModel.js.map +1 -1
  98. package/dist/model.d.ts +38 -44
  99. package/dist/model.js +153 -112
  100. package/dist/model.js.map +1 -1
  101. package/dist/renderToSvg.d.ts +2 -7
  102. package/dist/renderToSvg.js +32 -120
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/svgTestUtil.d.ts +12 -20
  105. package/dist/types.d.ts +0 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/viewerSnapshot.d.ts +169 -0
  109. package/dist/viewerSnapshot.js +100 -0
  110. package/dist/viewerSnapshot.js.map +1 -0
  111. package/package.json +3 -3
  112. package/src/colorSchemes.ts +1 -4
  113. package/src/columnCounts.ts +1 -1
  114. package/src/components/Loading.tsx +19 -12
  115. package/src/components/MSAViewer.tsx +275 -251
  116. package/src/components/ResizeHandles.tsx +9 -2
  117. package/src/components/Track.tsx +2 -1
  118. package/src/components/controls.ts +61 -0
  119. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  120. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  121. package/src/components/dialogs/HelpDialog.tsx +79 -0
  122. package/src/components/header/FileMenu.tsx +33 -7
  123. package/src/components/header/Header.tsx +4 -6
  124. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  125. package/src/components/header/ZoomControls.tsx +30 -66
  126. package/src/components/header/settingsMenuItems.ts +59 -0
  127. package/src/components/import/data/seq2.ts +0 -18
  128. package/src/components/msa/MSACanvas.tsx +2 -1
  129. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  130. package/src/components/msa/drawFeatureSpans.ts +160 -79
  131. package/src/components/msa/msaRaster.ts +27 -9
  132. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  133. package/src/components/msa/renderHighlights.ts +2 -3
  134. package/src/components/msa/renderMSAMouseover.ts +5 -1
  135. package/src/components/renderCtx.ts +0 -1
  136. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  137. package/src/components/tracks/drawTracks.ts +5 -5
  138. package/src/components/tree/CladeLabels.tsx +30 -54
  139. package/src/components/tree/cladeBrackets.ts +50 -3
  140. package/src/components/tree/renderTreeCanvas.ts +2 -13
  141. package/src/components/tree/renderTreeOverview.ts +7 -14
  142. package/src/element.ts +3 -7
  143. package/src/exportLayout.ts +134 -0
  144. package/src/headlessRenderEnv.ts +24 -11
  145. package/src/hierarchy.ts +5 -88
  146. package/src/index.ts +5 -5
  147. package/src/measureTextCanvas.ts +8 -6
  148. package/src/model/treeModel.ts +0 -7
  149. package/src/model.ts +170 -122
  150. package/src/renderToSvg.tsx +55 -194
  151. package/src/types.ts +0 -1
  152. package/src/version.ts +1 -1
  153. package/src/viewerSnapshot.ts +147 -0
  154. package/dist/components/header/ZoomMenu.d.ts +0 -6
  155. package/dist/components/header/ZoomMenu.js +0 -37
  156. package/dist/components/header/ZoomMenu.js.map +0 -1
  157. package/src/components/header/ZoomMenu.tsx +0 -48
@@ -0,0 +1,61 @@
1
+ export interface Control {
2
+ label: string
3
+ action: string
4
+ }
5
+
6
+ export interface KeyControl extends Control {
7
+ keys: string[]
8
+ shiftKey?: boolean
9
+ }
10
+
11
+ const arrows = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']
12
+
13
+ export const keyboardControls: KeyControl[] = [
14
+ { label: 'Arrow keys', action: 'Scroll one cell', keys: arrows },
15
+ {
16
+ label: 'Shift + arrow keys',
17
+ action: 'Scroll one page',
18
+ keys: arrows,
19
+ shiftKey: true,
20
+ },
21
+ { label: '+ and -', action: 'Zoom in and out', keys: ['+', '=', '-'] },
22
+ {
23
+ label: 'Home and End',
24
+ action: 'Jump to the first and last column',
25
+ keys: ['Home', 'End'],
26
+ },
27
+ { label: 'Escape', action: 'Clear the selection', keys: ['Escape'] },
28
+ ]
29
+
30
+ const selectCells: Control = {
31
+ label: 'Shift + drag',
32
+ action: 'Select a block of cells',
33
+ }
34
+
35
+ export const mouseControls: Control[] = [
36
+ { label: 'Drag', action: 'Pan the alignment' },
37
+ {
38
+ label: 'Wheel',
39
+ action: 'Scroll, or zoom as set in Alignment settings → Mouse wheel',
40
+ },
41
+ { label: 'Ctrl + wheel', action: 'Zoom at the cursor' },
42
+ selectCells,
43
+ {
44
+ label: 'Drag along a track',
45
+ action: 'Select a range of columns across every row',
46
+ },
47
+ {
48
+ label: 'Click a tree node',
49
+ action: 'Collapse, rotate, reroot or show only that clade',
50
+ },
51
+ {
52
+ label: 'Click the tree overview',
53
+ action: 'Show only the clade under the pointer',
54
+ },
55
+ { label: 'Drag a divider', action: 'Resize the tree area or a track' },
56
+ ]
57
+
58
+ export const alignmentAriaLabel = [
59
+ 'Alignment',
60
+ ...[...keyboardControls, selectCells].map(c => `${c.label}: ${c.action}`),
61
+ ].join('. ')
@@ -13,25 +13,9 @@ import {
13
13
  Typography,
14
14
  } from '@mui/material'
15
15
  import { observer } from 'mobx-react'
16
- import {
17
- annotationsToGFF,
18
- interProScanResponseToAnnotations,
19
- } from 'msa-parsers'
16
+ import { annotationTextToGFF } from 'msa-parsers'
20
17
 
21
18
  import type { MsaViewModel } from '../../model.ts'
22
- import type { InterProScanResponse } from 'msa-parsers'
23
-
24
- // GFF3 as the CLI writes it, or the JSON an InterProScan run returns. Both
25
- // arrive as text and the model keeps GFF, so the JSON is converted on the way
26
- // in rather than given a second path through the model
27
- function toGFF(text: string) {
28
- const trimmed = text.trimStart()
29
- if (!trimmed.startsWith('{')) {
30
- return text
31
- }
32
- const response: InterProScanResponse = JSON.parse(trimmed)
33
- return annotationsToGFF(interProScanResponseToAnnotations(response))
34
- }
35
19
 
36
20
  async function fetchText(url: string) {
37
21
  const response = await fetch(url)
@@ -137,10 +121,10 @@ const AnnotationFileDialog = observer(function ({
137
121
  ? file.text()
138
122
  : fetchText(url))
139
123
  // through the model's own GFF text, so the annotations travel
140
- // in the snapshot and the shared URL like every other layer.
141
- // Setting them straight onto the volatile left a view whose
142
- // link opened without them
143
- model.setGFF(toGFF(text))
124
+ // in the snapshot and the shared URL like every other layer. A
125
+ // filehandle left set would make the snapshot drop that text
126
+ model.setGFFFilehandle(undefined)
127
+ model.setGFF(annotationTextToGFF(text))
144
128
  model.setShowDomains(true)
145
129
  handleClose()
146
130
  } catch (e) {
@@ -17,6 +17,7 @@ import {
17
17
  } from '@mui/material'
18
18
  import { observer } from 'mobx-react'
19
19
 
20
+ import { exportPageSize, getLayout } from '../../exportLayout.ts'
20
21
  import Checkbox2 from '../Checkbox2.tsx'
21
22
 
22
23
  import type { MsaViewModel } from '../../model.ts'
@@ -46,24 +47,12 @@ const ExportSVGDialog = observer(function ({
46
47
  const [error, setError] = useState<unknown>()
47
48
  const [exporting, setExporting] = useState(false)
48
49
  const theme = useTheme()
49
- const {
50
- totalWidth,
51
- totalHeight,
52
- effectiveTreeAreaWidth,
53
- rowPanelsWidth,
54
- turnedOnTracks,
55
- totalTrackAreaHeight,
56
- numColumns,
57
- leaves,
58
- showMsaLetters,
59
- treeOverviewHeight,
60
- } = model
50
+ const { turnedOnTracks, numColumns, leaves, showMsaLetters } = model
61
51
  const hasTracks = turnedOnTracks.length > 0
62
- const entireWidth = totalWidth + effectiveTreeAreaWidth + rowPanelsWidth
63
- const entireHeight =
64
- totalHeight +
65
- (includeTracks ? totalTrackAreaHeight : 0) +
66
- treeOverviewHeight
52
+ const entire = exportPageSize(
53
+ model,
54
+ getLayout(model, { theme, exportType: 'entire', includeTracks }),
55
+ )
67
56
  // the background is one raster image whatever its size, so what the figure
68
57
  // actually costs is its residue letters: each is an svg element of its own.
69
58
  // Past roughly this many the export runs to tens of seconds and can exhaust
@@ -126,8 +115,8 @@ const ExportSVGDialog = observer(function ({
126
115
  </div>
127
116
  {isLargeExport ? (
128
117
  <Alert severity="warning" style={{ marginTop: 8 }}>
129
- The entire MSA is {Math.round(entireWidth)}x
130
- {Math.round(entireHeight)} pixels and draws{' '}
118
+ The entire MSA is {Math.round(entire.width)}x
119
+ {Math.round(entire.height)} pixels and draws{' '}
131
120
  {Math.round(glyphs / 1000)}k residue letters, each its own SVG
132
121
  element. Export may be slow or fail — zooming out far enough to hide
133
122
  the letters exports the same alignment as an image instead.
@@ -0,0 +1,79 @@
1
+ import React, { lazy } from 'react'
2
+
3
+ import { Dialog } from '@jbrowse/core/ui'
4
+ import {
5
+ Button,
6
+ DialogActions,
7
+ DialogContent,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableRow,
12
+ Typography,
13
+ } from '@mui/material'
14
+
15
+ import { version } from '../../version.ts'
16
+ import { keyboardControls, mouseControls } from '../controls.ts'
17
+
18
+ import type { MsaViewModel } from '../../model.ts'
19
+ import type { Control } from '../controls.ts'
20
+
21
+ const AboutDialog = lazy(() => import('./AboutDialog.tsx'))
22
+
23
+ function ControlTable({ title, rows }: { title: string; rows: Control[] }) {
24
+ return (
25
+ <>
26
+ <Typography variant="subtitle2" sx={{ mt: 1 }}>
27
+ {title}
28
+ </Typography>
29
+ <Table size="small" aria-label={title}>
30
+ <TableBody>
31
+ {rows.map(({ label, action }) => (
32
+ <TableRow key={label}>
33
+ <TableCell sx={{ whiteSpace: 'nowrap', fontWeight: 500 }}>
34
+ {label}
35
+ </TableCell>
36
+ <TableCell>{action}</TableCell>
37
+ </TableRow>
38
+ ))}
39
+ </TableBody>
40
+ </Table>
41
+ </>
42
+ )
43
+ }
44
+
45
+ export default function HelpDialog({
46
+ model,
47
+ onClose,
48
+ }: {
49
+ model: MsaViewModel
50
+ onClose: () => void
51
+ }) {
52
+ return (
53
+ <Dialog open title="Mouse & keyboard" onClose={onClose}>
54
+ <DialogContent>
55
+ <ControlTable title="Mouse" rows={mouseControls} />
56
+ <ControlTable
57
+ title="Keyboard (click the alignment first)"
58
+ rows={keyboardControls}
59
+ />
60
+ </DialogContent>
61
+ <DialogActions>
62
+ <Typography variant="caption" sx={{ mr: 'auto', pl: 1 }}>
63
+ MSAView {version}
64
+ </Typography>
65
+ <Button
66
+ onClick={() => {
67
+ model.queueDialog(done => [AboutDialog, { onClose: done }])
68
+ onClose()
69
+ }}
70
+ >
71
+ About & credits
72
+ </Button>
73
+ <Button variant="contained" onClick={onClose}>
74
+ Close
75
+ </Button>
76
+ </DialogActions>
77
+ </Dialog>
78
+ )
79
+ }
@@ -8,6 +8,7 @@ import Menu from '@mui/icons-material/Menu'
8
8
  import PhotoCamera from '@mui/icons-material/PhotoCamera'
9
9
  import { observer } from 'mobx-react'
10
10
 
11
+ import { maxNeighborJoiningRows } from '../../constants.ts'
11
12
  import { getAnnotationsMenu } from './getAnnotationsMenu.ts'
12
13
 
13
14
  import type { MsaViewModel } from '../../model.ts'
@@ -15,19 +16,43 @@ import type { MsaViewModel } from '../../model.ts'
15
16
  const MetadataDialog = lazy(() => import('../dialogs/MetadataDialog.tsx'))
16
17
  const ExportSVGDialog = lazy(() => import('../dialogs/ExportSVGDialog.tsx'))
17
18
 
19
+ function neighborJoiningItem(model: MsaViewModel) {
20
+ const rows = model.rowMap.size
21
+ const tooManyRows = rows > maxNeighborJoiningRows
22
+ return {
23
+ label: tooManyRows
24
+ ? `Calculate neighbor joining tree (over ${maxNeighborJoiningRows} rows, use FastTree)`
25
+ : 'Calculate neighbor joining tree (BLOSUM62)',
26
+ icon: AccountTree,
27
+ disabled: tooManyRows || rows < 2,
28
+ onClick: () => {
29
+ try {
30
+ model.calculateNeighborJoiningTreeFromMSA()
31
+ } catch (e) {
32
+ console.error('Failed to calculate NJ tree:', e)
33
+ model.addWarning(e instanceof Error ? e.message : String(e))
34
+ }
35
+ },
36
+ }
37
+ }
38
+
18
39
  const FileMenu = observer(({ model }: { model: MsaViewModel }) => {
19
40
  return (
20
41
  <CascadingMenuButton
21
42
  data-testid="file_menu"
22
43
  tooltip="File"
23
44
  menuItems={[
24
- {
25
- label: 'Return to import form',
26
- icon: FolderOpen,
27
- onClick: () => {
28
- model.reset()
29
- },
30
- },
45
+ ...(model.importForm
46
+ ? [
47
+ {
48
+ label: 'Return to import form',
49
+ icon: FolderOpen,
50
+ onClick: () => {
51
+ model.reset()
52
+ },
53
+ },
54
+ ]
55
+ : []),
31
56
  {
32
57
  label: 'Metadata',
33
58
  icon: Assignment,
@@ -48,6 +73,7 @@ const FileMenu = observer(({ model }: { model: MsaViewModel }) => {
48
73
  model,
49
74
  }),
50
75
  },
76
+ neighborJoiningItem(model),
51
77
  {
52
78
  label: 'Export tree (Newick)',
53
79
  icon: AccountTree,
@@ -18,11 +18,10 @@ import SelectionMenu from './SelectionMenu.tsx'
18
18
  import TreeSettingsMenu from './TreeSettingsMenu.tsx'
19
19
  import UnshareableDataWarning from './UnshareableDataWarning.tsx'
20
20
  import ZoomControls from './ZoomControls.tsx'
21
- import ZoomMenu from './ZoomMenu.tsx'
22
21
 
23
22
  import type { MsaViewModel } from '../../model.ts'
24
23
 
25
- const AboutDialog = lazy(() => import('../dialogs/AboutDialog.tsx'))
24
+ const HelpDialog = lazy(() => import('../dialogs/HelpDialog.tsx'))
26
25
 
27
26
  const Header = observer(function ({ model }: { model: MsaViewModel }) {
28
27
  const [ref, { height }] = useMeasure()
@@ -42,7 +41,6 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
42
41
  <TreeSettingsMenu model={model} />
43
42
  <MSASettingsMenu model={model} />
44
43
  <ZoomControls model={model} />
45
- <ZoomMenu model={model} />
46
44
  <GappynessSlider model={model} />
47
45
  <GoToBox model={model} />
48
46
  <div style={{ paddingLeft: 20, margin: 'auto' }}>
@@ -57,11 +55,11 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
57
55
  <LoadWarnings model={model} />
58
56
  <UnshareableDataWarning model={model} />
59
57
  <StatusMessage model={model} variant="header" />
60
- <Tooltip title="About">
58
+ <Tooltip title="Mouse & keyboard help">
61
59
  <IconButton
62
- aria-label="About"
60
+ aria-label="Mouse & keyboard help"
63
61
  onClick={() => {
64
- model.queueDialog(onClose => [AboutDialog, { onClose }])
62
+ model.queueDialog(onClose => [HelpDialog, { model, onClose }])
65
63
  }}
66
64
  >
67
65
  <Help />
@@ -4,7 +4,6 @@ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
4
  import AccountTree from '@mui/icons-material/AccountTree'
5
5
  import { observer } from 'mobx-react'
6
6
 
7
- import { maxNeighborJoiningRows } from '../../constants.ts'
8
7
  import { treeSettingsMenuItems } from './settingsMenuItems.ts'
9
8
 
10
9
  import type { MsaViewModel } from '../../model.ts'
@@ -14,43 +13,14 @@ const TreeSettingsMenu = observer(function ({
14
13
  }: {
15
14
  model: MsaViewModel
16
15
  }) {
17
- const { rows } = model
18
- const tooManyRows = rows.length > maxNeighborJoiningRows
19
- return (
16
+ return model.noTree ? null : (
20
17
  <CascadingMenuButton
21
18
  data-testid="tree_settings_menu"
22
19
  tooltip="Tree settings"
23
20
  closeAfterItemClick={false}
24
21
  anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
25
22
  transformOrigin={{ vertical: 'top', horizontal: 'left' }}
26
- menuItems={[
27
- ...treeSettingsMenuItems(model),
28
- ...(rows.length >= 2
29
- ? [
30
- {
31
- label: 'Advanced',
32
- type: 'subMenu' as const,
33
- subMenu: [
34
- {
35
- // the label shows the cap before a click would freeze the tab
36
- label: tooManyRows
37
- ? `Calculate neighbor joining tree (over ${maxNeighborJoiningRows} rows, use FastTree)`
38
- : 'Calculate neighbor joining tree (BLOSUM62)',
39
- disabled: tooManyRows,
40
- onClick: () => {
41
- try {
42
- model.calculateNeighborJoiningTreeFromMSA()
43
- } catch (e) {
44
- console.error('Failed to calculate NJ tree:', e)
45
- model.setError(e)
46
- }
47
- },
48
- },
49
- ],
50
- },
51
- ]
52
- : []),
53
- ]}
23
+ menuItems={treeSettingsMenuItems(model)}
54
24
  >
55
25
  <AccountTree />
56
26
  </CascadingMenuButton>
@@ -1,67 +1,20 @@
1
1
  import React from 'react'
2
2
 
3
3
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
+ import FitScreen from '@mui/icons-material/FitScreen'
5
+ import RestartAlt from '@mui/icons-material/RestartAlt'
4
6
  import ZoomIn from '@mui/icons-material/ZoomIn'
5
7
  import ZoomOut from '@mui/icons-material/ZoomOut'
6
8
  import { IconButton, Tooltip } from '@mui/material'
7
9
  import { observer } from 'mobx-react'
8
10
 
9
- import {
10
- MouseScroll,
11
- MouseZoomBoth,
12
- MouseZoomHorizontal,
13
- MouseZoomVertical,
14
- } from './ScrollZoomIcons.tsx'
15
-
16
- import type { ScrollZoomAxis } from '../../constants.ts'
17
11
  import type { MsaViewModel } from '../../model.ts'
18
12
 
19
- interface WheelMode {
20
- label: string
21
- // undefined is the wheel scrolling the alignment, which is the default
22
- axis?: ScrollZoomAxis
23
- Icon: typeof MouseScroll
24
- helpText?: string
25
- }
26
-
27
- const scrollMode: WheelMode = {
28
- label: 'Scrolls the alignment',
29
- Icon: MouseScroll,
30
- helpText: 'Ctrl+wheel zooms both axes at the cursor.',
31
- }
32
-
33
- const wheelModes: WheelMode[] = [
34
- scrollMode,
35
- {
36
- label: 'Zooms both axes',
37
- axis: 'both',
38
- Icon: MouseZoomBoth,
39
- helpText: 'Hold shift to pan while the wheel zooms.',
40
- },
41
- {
42
- label: 'Zooms columns only',
43
- axis: 'horizontal',
44
- Icon: MouseZoomHorizontal,
45
- helpText:
46
- 'Holds the row height, so the labels and letters stay at their size while the alignment compresses.',
47
- },
48
- {
49
- label: 'Zooms rows only',
50
- axis: 'vertical',
51
- Icon: MouseZoomVertical,
52
- helpText:
53
- 'Holds the column width, so the alignment stays as wide while the rows grow or shrink.',
54
- },
55
- ]
56
-
57
13
  const ZoomControls = observer(function ZoomControls({
58
14
  model,
59
15
  }: {
60
16
  model: MsaViewModel
61
17
  }) {
62
- const { scrollZoom, scrollZoomAxis } = model
63
- const activeAxis = scrollZoom ? scrollZoomAxis : undefined
64
- const current = wheelModes.find(m => m.axis === activeAxis) ?? scrollMode
65
18
  return (
66
19
  <>
67
20
  <Tooltip title="Zoom in">
@@ -85,30 +38,41 @@ const ZoomControls = observer(function ZoomControls({
85
38
  </IconButton>
86
39
  </Tooltip>
87
40
  <CascadingMenuButton
88
- data-testid="scroll_zoom_menu"
89
- tooltip={`Mouse wheel: ${current.label.toLowerCase()} (ctrl+wheel always zooms)`}
90
- anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
91
- transformOrigin={{ vertical: 'top', horizontal: 'left' }}
41
+ data-testid="fit_menu"
42
+ tooltip="Fit or reset zoom"
92
43
  menuItems={[
93
- { type: 'subHeader', label: 'Mouse wheel' },
94
- ...wheelModes.map(({ label, axis, Icon, helpText }) => ({
95
- label,
96
- icon: Icon,
97
- helpText,
98
- type: 'radio' as const,
99
- checked: current.label === label,
44
+ {
45
+ label: 'Fit to window',
46
+ onClick: () => {
47
+ model.fit()
48
+ },
49
+ },
50
+ {
51
+ label: 'Fit horizontally',
100
52
  onClick: () => {
101
- model.setScrollZoom(axis !== undefined)
102
- if (axis !== undefined) {
103
- model.setScrollZoomAxis(axis)
104
- }
53
+ model.fitHorizontally()
105
54
  },
106
- })),
55
+ },
56
+ {
57
+ label: 'Fit vertically',
58
+ onClick: () => {
59
+ model.fitVertically()
60
+ },
61
+ },
62
+ { type: 'divider' },
63
+ {
64
+ label: 'Reset zoom',
65
+ icon: RestartAlt,
66
+ onClick: () => {
67
+ model.resetZoom()
68
+ },
69
+ },
107
70
  ]}
108
71
  >
109
- <current.Icon />
72
+ <FitScreen />
110
73
  </CascadingMenuButton>
111
74
  </>
112
75
  )
113
76
  })
77
+
114
78
  export default ZoomControls
@@ -1,5 +1,12 @@
1
1
  import { treeOrders } from '../../constants.ts'
2
+ import {
3
+ MouseScroll,
4
+ MouseZoomBoth,
5
+ MouseZoomHorizontal,
6
+ MouseZoomVertical,
7
+ } from './ScrollZoomIcons.tsx'
2
8
 
9
+ import type { ScrollZoomAxis } from '../../constants.ts'
3
10
  import type { MsaViewModel } from '../../model.ts'
4
11
  import type { TreeOrder } from '../../types.ts'
5
12
  import type { MenuItem } from '@jbrowse/core/ui/Menu'
@@ -43,6 +50,53 @@ function tracksSubMenu(model: MsaViewModel): MenuItem[] {
43
50
  )
44
51
  }
45
52
 
53
+ const wheelModes: {
54
+ label: string
55
+ axis?: ScrollZoomAxis
56
+ icon: typeof MouseScroll
57
+ helpText: string
58
+ }[] = [
59
+ {
60
+ label: 'Scrolls the alignment',
61
+ icon: MouseScroll,
62
+ helpText: 'Ctrl+wheel zooms both axes at the cursor.',
63
+ },
64
+ {
65
+ label: 'Zooms both axes',
66
+ axis: 'both',
67
+ icon: MouseZoomBoth,
68
+ helpText: 'Hold shift to pan while the wheel zooms.',
69
+ },
70
+ {
71
+ label: 'Zooms columns only',
72
+ axis: 'horizontal',
73
+ icon: MouseZoomHorizontal,
74
+ helpText:
75
+ 'Holds the row height, so the labels and letters stay at their size while the alignment compresses.',
76
+ },
77
+ {
78
+ label: 'Zooms rows only',
79
+ axis: 'vertical',
80
+ icon: MouseZoomVertical,
81
+ helpText:
82
+ 'Holds the column width, so the alignment stays as wide while the rows grow or shrink.',
83
+ },
84
+ ]
85
+
86
+ export function wheelSubMenu(model: MsaViewModel): MenuItem[] {
87
+ const active = model.scrollZoom ? model.scrollZoomAxis : undefined
88
+ return wheelModes.map(({ label, axis, icon, helpText }) => ({
89
+ ...radio(label, axis === active, () => {
90
+ model.setScrollZoom(axis !== undefined)
91
+ if (axis !== undefined) {
92
+ model.setScrollZoomAxis(axis)
93
+ }
94
+ }),
95
+ icon,
96
+ helpText,
97
+ }))
98
+ }
99
+
46
100
  export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
47
101
  const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
48
102
  return [
@@ -51,6 +105,11 @@ export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
51
105
  type: 'subMenu' as const,
52
106
  subMenu: tracksSubMenu(model),
53
107
  },
108
+ {
109
+ label: 'Mouse wheel',
110
+ type: 'subMenu' as const,
111
+ subMenu: wheelSubMenu(model),
112
+ },
54
113
  { type: 'divider' as const },
55
114
  toggle('Show track values on hover', showColumnStats, arg => {
56
115
  model.setShowColumnStats(arg)
@@ -13,23 +13,5 @@ UniProt/Swiss-Prot|Q7JFM4|IL2_AOTVO MLKPPLPLRSLLFLQLPLLGVGLNPKFLTPSGNEDIG
13
13
  UniProt/Swiss-Prot|Q29416|IL2_CANFA --------------LFLQLSLLG-------------------------------------
14
14
  `
15
15
 
16
- export const smallMSAOnly = `CLUSTAL O(1.2.4) multiple sequence alignment
17
-
18
-
19
- sp|P69905|HBA_HUMAN MVLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHFDLSHGSAQVKGHG 60
20
- sp|P01942|HBA_MOUSE MVLSGEDKSNIKAAWGKIGGHGAEYGAEALERMFASFPTTKTYFPHFDVSHGSAQVKGHG 60
21
- sp|P13786|HBAZ_CAPHI MSLTRTERTIILSLWSKISTQADVIGTETLERLFSCYPQAKTYFPHFDLHSGSAQLRAHG 60
22
- * *: ::: : : *.*:. :. *:*:***:* .:* :********: ****::.**
23
-
24
- sp|P69905|HBA_HUMAN KKVADALTNAVAHVDDMPNALSALSDLHAHKLRVDPVNFKLLSHCLLVTLAAHLPAEFTP 120
25
- sp|P01942|HBA_MOUSE KKVADALASAAGHLDDLPGALSALSDLHAHKLRVDPVNFKLLSHCLLVTLASHHPADFTP 120
26
- sp|P13786|HBAZ_CAPHI SKVVAAVGDAVKSIDNVTSALSKLSELHAYVLRVDPVNFKFLSHCLLVTLASHFPADFTA 120
27
- .**. *: .*. :*:: .*** **:***: *********:**********:* **:**
28
-
29
- sp|P69905|HBA_HUMAN AVHASLDKFLASVSTVLTSKYR 142
30
- sp|P01942|HBA_MOUSE AVHASLDKFLASVSTVLTSKYR 142
31
- sp|P13786|HBAZ_CAPHI DAHAAWDKFLSIVSGVLTEKYR 142
32
- .**: ****: ** ***.***`
33
-
34
16
  export const smallTree =
35
17
  '(((UniProt/Swiss-Prot|P26898|IL2RA_SHEEP:0.24036,(UniProt/Swiss-Prot|P41690|IL2RA_FELCA:0.17737,(UniProt/Swiss-Prot|P01589|IL2RA_HUMAN:0.03906,UniProt/Swiss-Prot|Q5MNY4|IL2RA_MACMU:0.03787):0.13033):0.04964):0.02189,UniProt/Swiss-Prot|P01590|IL2RA_MOUSE:0.23072):0.06814,(((UniProt/Swiss-Prot|Q95118|IL2RG_BOVIN:0.09600,UniProt/Swiss-Prot|P40321|IL2RG_CANFA:0.09845):0.25333,UniProt/Swiss-Prot|Q29416|IL2_CANFA:-0.35055):0.10231,(UniProt/Swiss-Prot|P26896|IL2RB_RAT:0.33631,UniProt/Swiss-Prot|Q7JFM4|IL2_AOTVO:-0.33631):0.10166):0.01607,(UniProt/Swiss-Prot|Q8BZM1|GLMN_MOUSE:0.32378,UniProt/Swiss-Prot|P36835|IL2_CAPHI:-0.32378):0.09999)'
@@ -3,6 +3,7 @@ import React, { useCallback, useRef } from 'react'
3
3
  import { observer } from 'mobx-react'
4
4
 
5
5
  import { useWheelScroll } from '../../useWheelScroll.ts'
6
+ import { alignmentAriaLabel } from '../controls.ts'
6
7
  import Loading from './Loading.tsx'
7
8
  import MSACanvasBlock from './MSACanvasBlock.tsx'
8
9
  import { onMsaKey } from './msaKeys.ts'
@@ -54,7 +55,7 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
54
55
  // col*colWidth + scrollX, row*rowHeight + scrollY
55
56
  data-testid="msa_canvas"
56
57
  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
+ aria-label={alignmentAriaLabel}
58
59
  onKeyDown={event => {
59
60
  if (onMsaKey(model, event)) {
60
61
  event.preventDefault()
@@ -5,7 +5,7 @@ import { useTheme } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
6
 
7
7
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
- import { drawMsaRaster, rasterSupported } from './msaRaster.ts'
8
+ import { drawMsaRaster, rasterApplies } from './msaRaster.ts'
9
9
  import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
10
10
  import { renderMSABlock } from './renderMSABlock.ts'
11
11
  import { useMsaBlockMouse } from './useMsaBlockMouse.ts'
@@ -38,11 +38,7 @@ const MSACanvasBlock = observer(function ({
38
38
  offsetY,
39
39
  model,
40
40
  })
41
- // the alignment background comes from zoom-independent raster tiles; the
42
- // domain overlay paints its own boxes instead, and letter-color mode has
43
- // no background to paint
44
- const rasterTiles =
45
- !model.actuallyShowDomains && model.bgColor && rasterSupported()
41
+ const rasterTiles = rasterApplies(model)
46
42
  if (rasterTiles) {
47
43
  drawMsaRaster({ ctx, model, theme, offsetX, offsetY })
48
44
  }