react-msaview 8.3.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 (124) hide show
  1. package/bundle/index.js +77 -287
  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/components/MSAView.js +15 -9
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +28 -7
  12. package/dist/components/MSAViewer.js +61 -22
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/MetadataTable.d.ts +5 -0
  15. package/dist/components/MetadataTable.js +29 -0
  16. package/dist/components/MetadataTable.js.map +1 -0
  17. package/dist/components/Track.js +11 -2
  18. package/dist/components/Track.js.map +1 -1
  19. package/dist/components/dialogs/MetadataDialog.js +5 -6
  20. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  22. package/dist/components/header/ColorSchemeMenu.js +23 -5
  23. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  24. package/dist/components/header/{RowSearch.d.ts → GoToBox.d.ts} +2 -2
  25. package/dist/components/header/GoToBox.js +21 -0
  26. package/dist/components/header/GoToBox.js.map +1 -0
  27. package/dist/components/header/Header.js +4 -2
  28. package/dist/components/header/Header.js.map +1 -1
  29. package/dist/components/header/SelectionMenu.d.ts +12 -0
  30. package/dist/components/header/SelectionMenu.js +72 -0
  31. package/dist/components/header/SelectionMenu.js.map +1 -0
  32. package/dist/components/header/goTo.d.ts +20 -0
  33. package/dist/components/header/goTo.js +69 -0
  34. package/dist/components/header/goTo.js.map +1 -0
  35. package/dist/components/msa/MSACanvas.js +13 -2
  36. package/dist/components/msa/MSACanvas.js.map +1 -1
  37. package/dist/components/msa/MSACanvasBlock.js +5 -1
  38. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  39. package/dist/components/msa/msaKeys.js +6 -0
  40. package/dist/components/msa/msaKeys.js.map +1 -1
  41. package/dist/components/msa/msaRaster.js +1 -0
  42. package/dist/components/msa/msaRaster.js.map +1 -1
  43. package/dist/components/msa/renderHighlights.d.ts +14 -3
  44. package/dist/components/msa/renderHighlights.js +29 -4
  45. package/dist/components/msa/renderHighlights.js.map +1 -1
  46. package/dist/components/msa/tileColor.js +9 -8
  47. package/dist/components/msa/tileColor.js.map +1 -1
  48. package/dist/components/msa/useMsaBlockMouse.d.ts +1 -1
  49. package/dist/components/msa/useMsaBlockMouse.js +18 -11
  50. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  51. package/dist/components/overlayColors.d.ts +2 -0
  52. package/dist/components/overlayColors.js +3 -0
  53. package/dist/components/overlayColors.js.map +1 -1
  54. package/dist/components/selectionDrag.d.ts +17 -0
  55. package/dist/components/selectionDrag.js +30 -0
  56. package/dist/components/selectionDrag.js.map +1 -0
  57. package/dist/components/tracks/useTrackHover.d.ts +3 -1
  58. package/dist/components/tracks/useTrackHover.js +26 -6
  59. package/dist/components/tracks/useTrackHover.js.map +1 -1
  60. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
  61. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  62. package/dist/constants.d.ts +1 -1
  63. package/dist/constants.js +6 -6
  64. package/dist/constants.js.map +1 -1
  65. package/dist/element.d.ts +7 -3
  66. package/dist/element.js +32 -4
  67. package/dist/element.js.map +1 -1
  68. package/dist/featuresLayer.d.ts +8 -0
  69. package/dist/featuresLayer.js +42 -0
  70. package/dist/featuresLayer.js.map +1 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/model/DataModel.d.ts +1 -1
  73. package/dist/model/DataModel.js +3 -3
  74. package/dist/model/DataModel.js.map +1 -1
  75. package/dist/model/msaModel.d.ts +14 -1
  76. package/dist/model/msaModel.js +17 -1
  77. package/dist/model/msaModel.js.map +1 -1
  78. package/dist/model.d.ts +122 -11
  79. package/dist/model.js +260 -41
  80. package/dist/model.js.map +1 -1
  81. package/dist/svgTestUtil.d.ts +68 -8
  82. package/dist/types.d.ts +50 -0
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/package.json +3 -3
  86. package/src/blockFasta.ts +18 -0
  87. package/src/colorSchemes.ts +20 -0
  88. package/src/components/MSAView.tsx +16 -9
  89. package/src/components/MSAViewer.tsx +95 -26
  90. package/src/components/MetadataTable.tsx +55 -0
  91. package/src/components/Track.tsx +14 -1
  92. package/src/components/dialogs/MetadataDialog.tsx +9 -8
  93. package/src/components/header/ColorSchemeMenu.tsx +31 -12
  94. package/src/components/header/GoToBox.tsx +50 -0
  95. package/src/components/header/Header.tsx +6 -2
  96. package/src/components/header/SelectionMenu.tsx +117 -0
  97. package/src/components/header/goTo.ts +75 -0
  98. package/src/components/msa/MSACanvas.tsx +12 -2
  99. package/src/components/msa/MSACanvasBlock.tsx +5 -1
  100. package/src/components/msa/msaKeys.ts +6 -0
  101. package/src/components/msa/msaRaster.ts +1 -0
  102. package/src/components/msa/renderHighlights.ts +46 -4
  103. package/src/components/msa/tileColor.ts +9 -8
  104. package/src/components/msa/useMsaBlockMouse.ts +24 -27
  105. package/src/components/overlayColors.ts +4 -0
  106. package/src/components/selectionDrag.ts +42 -0
  107. package/src/components/tracks/useTrackHover.ts +28 -6
  108. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
  109. package/src/constants.ts +6 -6
  110. package/src/element.ts +45 -5
  111. package/src/featuresLayer.ts +48 -0
  112. package/src/index.ts +5 -0
  113. package/src/model/DataModel.ts +3 -3
  114. package/src/model/msaModel.ts +19 -1
  115. package/src/model.ts +309 -51
  116. package/src/types.ts +58 -0
  117. package/src/version.ts +1 -1
  118. package/dist/components/header/RowSearch.js +0 -15
  119. package/dist/components/header/RowSearch.js.map +0 -1
  120. package/dist/components/header/revealRow.d.ts +0 -3
  121. package/dist/components/header/revealRow.js +0 -15
  122. package/dist/components/header/revealRow.js.map +0 -1
  123. package/src/components/header/RowSearch.tsx +0 -34
  124. package/src/components/header/revealRow.ts +0 -17
@@ -13,12 +13,16 @@ import type { MsaViewModel } from '../model.ts'
13
13
  import type {
14
14
  Cell,
15
15
  Clade,
16
+ ColorScheme,
16
17
  ColumnTrackSpec,
17
18
  Encoding,
19
+ Feature,
18
20
  Highlight,
21
+ MsaSelection,
19
22
  Region,
20
23
  ResidueEncoding,
21
24
  ResidueMapping,
25
+ RowDataInput,
22
26
  RowPanelSpec,
23
27
  TreeOrder,
24
28
  TreeRoot,
@@ -44,7 +48,11 @@ export interface MSAViewerProps {
44
48
  msaFilehandle?: FileLocationType
45
49
  treeFilehandle?: FileLocationType
46
50
  gffFilehandle?: FileLocationType
47
- colorScheme?: string
51
+ /**
52
+ * a scheme name from the built-in table, or `{map}`, a color per residue
53
+ * letter that leaves every other letter uncolored
54
+ */
55
+ colorScheme?: ColorScheme
48
56
  height?: number
49
57
  /** initial per-column pixel width (zoom level) */
50
58
  colWidth?: number
@@ -62,6 +70,13 @@ export interface MSAViewerProps {
62
70
  * for whole rows, each with an optional `label` and `color`
63
71
  */
64
72
  highlights?: Highlight[]
73
+ /**
74
+ * features on the rows as data, drawn with the GFF's: `{row, start, end}` in
75
+ * residues of that row, 1-based and inclusive, with an optional `name`,
76
+ * `description`, `type`, `strand` and `color`, and any other field an
77
+ * encoding reads (see docs/layers.md)
78
+ */
79
+ features?: Feature[]
65
80
  /**
66
81
  * clades of the tree with a mark drawn over them: `{mrca, tips, mark}` or
67
82
  * `{range, tips, mark}`, where `mark` is `highlight`, `bracket`, `collapse`
@@ -75,7 +90,7 @@ export interface MSAViewerProps {
75
90
  /** which residue of which structure each row's residues are (see docs/layers.md) */
76
91
  residueMappings?: ResidueMapping[]
77
92
  /** extra fields per row name, such as a lineage or a host (see docs/layers.md) */
78
- rowData?: Record<string, Record<string, string>>
93
+ rowData?: RowDataInput
79
94
  /**
80
95
  * what the viewer's marks read from a table: `{channel, field, scale?}`,
81
96
  * where `channel` is `tipLabel`, `rowTint` or `branch` over `rowData`, or
@@ -120,6 +135,11 @@ export interface MSAViewerProps {
120
135
  * in columns, 1-based and inclusive
121
136
  */
122
137
  region?: Region
138
+ /**
139
+ * the selected block: `{start, end}` columns of the file, 1-based and
140
+ * inclusive, and `rows` by name, every row where absent
141
+ */
142
+ selection?: MsaSelection
123
143
  /** leave out the toolbar, for a page drawing its own controls */
124
144
  hideHeader?: boolean
125
145
  /**
@@ -133,43 +153,48 @@ export interface MSAViewerProps {
133
153
  onCellClick?: (cell: Cell | undefined) => void
134
154
  /** the alignment columns on screen, after every scroll, zoom and resize */
135
155
  onViewportChange?: (viewport: Viewport | undefined) => void
156
+ /**
157
+ * the selected block, after each change a drag, the header menu, Escape or
158
+ * the `selection` prop makes, or undefined when it clears
159
+ */
160
+ onSelectionChange?: (selection: MsaSelection | undefined) => void
136
161
  /**
137
162
  * the model the viewer built, for the model API; called again with the new
138
- * model when msa, tree, gff or a filehandle changes
163
+ * model when msa, tree, msaFilehandle or treeFilehandle changes
139
164
  */
140
165
  onModel?: (model: MsaViewModel) => void
141
166
  }
142
167
 
168
+ function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
169
+ return typeof scheme === 'string'
170
+ ? { colorSchemeName: scheme }
171
+ : scheme
172
+ ? { customColorScheme: scheme.map }
173
+ : {}
174
+ }
175
+
143
176
  type DataSource = Pick<
144
177
  MSAViewerProps,
145
- 'msa' | 'tree' | 'gff' | 'msaFilehandle' | 'treeFilehandle' | 'gffFilehandle'
178
+ 'msa' | 'tree' | 'msaFilehandle' | 'treeFilehandle'
146
179
  >
147
180
 
148
181
  function sameSource(a: DataSource, b: DataSource) {
149
182
  return (
150
183
  a.msa === b.msa &&
151
184
  a.tree === b.tree &&
152
- a.gff === b.gff &&
153
- JSON.stringify([a.msaFilehandle, a.treeFilehandle, a.gffFilehandle]) ===
154
- JSON.stringify([b.msaFilehandle, b.treeFilehandle, b.gffFilehandle])
185
+ JSON.stringify([a.msaFilehandle, a.treeFilehandle]) ===
186
+ JSON.stringify([b.msaFilehandle, b.treeFilehandle])
155
187
  )
156
188
  }
157
189
 
158
190
  /**
159
- * A new alignment, tree or annotation file needs a new model, so the viewer
160
- * remounts when one of those props changes and keeps its model across every
161
- * other change.
191
+ * A new alignment or tree needs a new model, so the viewer remounts when one
192
+ * of those props changes and keeps its model across every other change,
193
+ * including a new annotation file.
162
194
  */
163
195
  export default function MSAViewer(props: MSAViewerProps) {
164
- const { msa, tree, gff, msaFilehandle, treeFilehandle, gffFilehandle } = props
165
- const source = {
166
- msa,
167
- tree,
168
- gff,
169
- msaFilehandle,
170
- treeFilehandle,
171
- gffFilehandle,
172
- }
196
+ const { msa, tree, msaFilehandle, treeFilehandle } = props
197
+ const source = { msa, tree, msaFilehandle, treeFilehandle }
173
198
  const [shown, setShown] = useState({ source, generation: 0 })
174
199
  if (!sameSource(shown.source, source)) {
175
200
  setShown({ source, generation: shown.generation + 1 })
@@ -177,7 +202,9 @@ export default function MSAViewer(props: MSAViewerProps) {
177
202
  return <Viewer key={shown.generation} {...props} />
178
203
  }
179
204
 
180
- function useModelReaction<K extends 'hoveredCell' | 'clickedCell' | 'viewport'>(
205
+ function useModelReaction<
206
+ K extends 'hoveredCell' | 'clickedCell' | 'viewport' | 'selection',
207
+ >(
181
208
  model: MsaViewModel,
182
209
  key: K,
183
210
  handler: ((value: MsaViewModel[K]) => void) | undefined,
@@ -216,6 +243,7 @@ function Viewer({
216
243
  allowedGappyness,
217
244
  highlightColumns,
218
245
  highlights,
246
+ features,
219
247
  clades,
220
248
  relativeTo,
221
249
  columnTracks,
@@ -231,11 +259,13 @@ function Viewer({
231
259
  treeOrder,
232
260
  treeRoot,
233
261
  region,
262
+ selection,
234
263
  hideHeader,
235
264
  theme,
236
265
  onCellHover,
237
266
  onCellClick,
238
267
  onViewportChange,
268
+ onSelectionChange,
239
269
  onModel,
240
270
  }: MSAViewerProps) {
241
271
  const [model] = useState(() =>
@@ -256,13 +286,15 @@ function Viewer({
256
286
  ...(msaFilehandle ? { msaFilehandle } : {}),
257
287
  ...(treeFilehandle ? { treeFilehandle } : {}),
258
288
  ...(gffFilehandle ? { gffFilehandle } : {}),
259
- ...(colorScheme ? { colorSchemeName: colorScheme } : {}),
289
+ ...colorSchemeSnapshot(colorScheme),
260
290
  ...(height ? { height } : {}),
261
291
  ...(colWidth ? { colWidth } : {}),
262
292
  ...(rowHeight ? { rowHeight } : {}),
263
293
  ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
264
294
  ...(highlightColumns ? { highlightColumns } : {}),
265
295
  ...(highlights ? { highlights } : {}),
296
+ ...(features ? { features } : {}),
297
+ ...(selection ? { selection } : {}),
266
298
  ...(clades ? { clades } : {}),
267
299
  ...(relativeTo ? { relativeTo } : {}),
268
300
  ...(columnTracks ? { columnTracks } : {}),
@@ -299,11 +331,6 @@ function Viewer({
299
331
  model.setHeight(height)
300
332
  }
301
333
  }, [model, height])
302
- useEffect(() => {
303
- if (colorScheme !== undefined) {
304
- model.setColorSchemeName(colorScheme)
305
- }
306
- }, [model, colorScheme])
307
334
  useEffect(() => {
308
335
  if (colWidth !== undefined) {
309
336
  model.setColWidth(colWidth)
@@ -352,12 +379,53 @@ function Viewer({
352
379
  model.drawRelativeTo(relativeTo)
353
380
  }, [model, relativeTo])
354
381
 
382
+ // The snapshot already holds the first gff and gffFilehandle, so these
383
+ // effects apply only a later change. Removing either one sets the text back
384
+ // to the gff prop, which clears the annotations when that is absent too.
385
+ const appliedGff = useRef(gff)
386
+ const gffFilehandleKey = JSON.stringify(gffFilehandle ?? null)
387
+ const appliedGffFilehandle = useRef(gffFilehandleKey)
388
+ useEffect(() => {
389
+ if (appliedGffFilehandle.current !== gffFilehandleKey) {
390
+ appliedGffFilehandle.current = gffFilehandleKey
391
+ const location = JSON.parse(gffFilehandleKey) as FileLocationType | null
392
+ if (location) {
393
+ model.setGFFFilehandle(location)
394
+ } else {
395
+ model.setGFF(appliedGff.current)
396
+ }
397
+ }
398
+ }, [model, gffFilehandleKey])
399
+ useEffect(() => {
400
+ if (appliedGff.current !== gff) {
401
+ appliedGff.current = gff
402
+ model.setGFF(gff)
403
+ }
404
+ }, [model, gff])
405
+
355
406
  // keyed by content, since a host computing a layer inline passes a new array
356
407
  // on every render
408
+ const colorSchemeKey = JSON.stringify(colorScheme ?? null)
409
+ useEffect(() => {
410
+ const scheme = JSON.parse(colorSchemeKey) as ColorScheme | null
411
+ if (typeof scheme === 'string') {
412
+ model.setColorSchemeName(scheme)
413
+ } else if (scheme) {
414
+ model.setCustomColorScheme(scheme.map)
415
+ }
416
+ }, [model, colorSchemeKey])
357
417
  const highlightsKey = JSON.stringify(highlights ?? [])
358
418
  useEffect(() => {
359
419
  model.setHighlights(JSON.parse(highlightsKey))
360
420
  }, [model, highlightsKey])
421
+ const featuresKey = JSON.stringify(features ?? [])
422
+ useEffect(() => {
423
+ model.setFeatures(JSON.parse(featuresKey))
424
+ }, [model, featuresKey])
425
+ const selectionKey = JSON.stringify(selection ?? null)
426
+ useEffect(() => {
427
+ model.setSelection(JSON.parse(selectionKey) ?? undefined)
428
+ }, [model, selectionKey])
361
429
  const treeRootKey = JSON.stringify(treeRoot ?? null)
362
430
  useEffect(() => {
363
431
  model.setTreeRoot(JSON.parse(treeRootKey) ?? undefined)
@@ -411,6 +479,7 @@ function Viewer({
411
479
  useModelReaction(model, 'hoveredCell', onCellHover)
412
480
  useModelReaction(model, 'clickedCell', onCellClick)
413
481
  useModelReaction(model, 'viewport', onViewportChange)
482
+ useModelReaction(model, 'selection', onSelectionChange)
414
483
 
415
484
  const latestOnModel = useRef(onModel)
416
485
  useEffect(() => {
@@ -0,0 +1,55 @@
1
+ import React from 'react'
2
+
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react'
4
+ import { Table, TableBody, TableCell, TableRow } from '@mui/material'
5
+
6
+ const useStyles = makeStyles()({
7
+ key: {
8
+ fontWeight: 'bold',
9
+ verticalAlign: 'top',
10
+ },
11
+ value: {
12
+ whiteSpace: 'pre-wrap',
13
+ wordBreak: 'break-word',
14
+ },
15
+ })
16
+
17
+ function isRecord(value: unknown): value is Record<string, unknown> {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
19
+ }
20
+
21
+ export function isEmpty(value: unknown) {
22
+ return isRecord(value) && Object.keys(value).length === 0
23
+ }
24
+
25
+ export default function MetadataTable({
26
+ record,
27
+ }: {
28
+ record: Record<string, unknown>
29
+ }) {
30
+ const { classes } = useStyles()
31
+ return (
32
+ <Table size="small">
33
+ <TableBody>
34
+ {Object.entries(record)
35
+ .filter(([, value]) => value != null && !isEmpty(value))
36
+ .map(([key, value]) => (
37
+ <TableRow key={key}>
38
+ <TableCell component="th" className={classes.key}>
39
+ {key}
40
+ </TableCell>
41
+ <TableCell className={classes.value}>
42
+ {isRecord(value) ? (
43
+ <MetadataTable record={value} />
44
+ ) : Array.isArray(value) ? (
45
+ value.join('\n')
46
+ ) : (
47
+ String(value)
48
+ )}
49
+ </TableCell>
50
+ </TableRow>
51
+ ))}
52
+ </TableBody>
53
+ </Table>
54
+ )
55
+ }
@@ -7,6 +7,7 @@ import { IconButton, Menu, MenuItem } from '@mui/material'
7
7
  import { observer } from 'mobx-react'
8
8
 
9
9
  import { useWheelScroll } from '../useWheelScroll.ts'
10
+ import { onMsaKey } from './msa/msaKeys.ts'
10
11
  import TrackTooltipContent from './tracks/TrackTooltipContent.tsx'
11
12
  import { useTrackHover } from './tracks/useTrackHover.ts'
12
13
 
@@ -146,7 +147,7 @@ const Track = observer(function ({
146
147
  )
147
148
  useWheelScroll({ ref, onScrollX })
148
149
  const { classes } = useStyles()
149
- const { anchor, onMouseMove, onMouseLeave } = useTrackHover({
150
+ const { anchor, onMouseMove, onMouseDown, onMouseLeave } = useTrackHover({
150
151
  model,
151
152
  tooltip: showColumnStats,
152
153
  })
@@ -162,11 +163,23 @@ const Track = observer(function ({
162
163
  <div
163
164
  ref={ref}
164
165
  data-testid={`track_${track.model.id}`}
166
+ tabIndex={-1}
167
+ style={{ outline: 'none' }}
168
+ onKeyDown={event => {
169
+ if (onMsaKey(model, event)) {
170
+ event.preventDefault()
171
+ }
172
+ }}
165
173
  onMouseMove={event => {
166
174
  if (ref.current) {
167
175
  onMouseMove(event, ref.current)
168
176
  }
169
177
  }}
178
+ onMouseDown={event => {
179
+ if (ref.current) {
180
+ onMouseDown(event, ref.current)
181
+ }
182
+ }}
170
183
  onMouseLeave={() => {
171
184
  onMouseLeave()
172
185
  }}
@@ -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>
@@ -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
@@ -9,11 +9,12 @@ import StatusMessage from '../StatusMessage.tsx'
9
9
  import ColorSchemeMenu from './ColorSchemeMenu.tsx'
10
10
  import FileMenu from './FileMenu.tsx'
11
11
  import GappynessSlider from './GappynessSlider.tsx'
12
+ import GoToBox from './GoToBox.tsx'
12
13
  import HeaderInfoArea from './HeaderInfoArea.tsx'
13
14
  import LoadWarnings from './LoadWarnings.tsx'
14
15
  import MSASettingsMenu from './MSASettingsMenu.tsx'
15
16
  import MultiAlignmentSelector from './MultiAlignmentSelector.tsx'
16
- import RowSearch from './RowSearch.tsx'
17
+ import SelectionMenu from './SelectionMenu.tsx'
17
18
  import TreeSettingsMenu from './TreeSettingsMenu.tsx'
18
19
  import UnshareableDataWarning from './UnshareableDataWarning.tsx'
19
20
  import ZoomControls from './ZoomControls.tsx'
@@ -43,11 +44,14 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
43
44
  <ZoomControls model={model} />
44
45
  <ZoomMenu model={model} />
45
46
  <GappynessSlider model={model} />
46
- <RowSearch model={model} />
47
+ <GoToBox model={model} />
47
48
  <div style={{ paddingLeft: 20, margin: 'auto' }}>
48
49
  <MultiAlignmentSelector model={model} />
49
50
  </div>
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 */}
50
53
  <div style={{ flex: '1 1 0', minWidth: 0, display: 'flex' }}>
54
+ <SelectionMenu model={model} />
51
55
  <HeaderInfoArea model={model} />
52
56
  </div>
53
57
  <LoadWarnings model={model} />
@@ -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