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
@@ -13,13 +13,19 @@ 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,
27
+ TreeOrder,
28
+ TreeRoot,
23
29
  Viewport,
24
30
  } from '../types.ts'
25
31
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
@@ -42,7 +48,11 @@ export interface MSAViewerProps {
42
48
  msaFilehandle?: FileLocationType
43
49
  treeFilehandle?: FileLocationType
44
50
  gffFilehandle?: FileLocationType
45
- 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
46
56
  height?: number
47
57
  /** initial per-column pixel width (zoom level) */
48
58
  colWidth?: number
@@ -60,6 +70,13 @@ export interface MSAViewerProps {
60
70
  * for whole rows, each with an optional `label` and `color`
61
71
  */
62
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[]
63
80
  /**
64
81
  * clades of the tree with a mark drawn over them: `{mrca, tips, mark}` or
65
82
  * `{range, tips, mark}`, where `mark` is `highlight`, `bracket`, `collapse`
@@ -73,7 +90,7 @@ export interface MSAViewerProps {
73
90
  /** which residue of which structure each row's residues are (see docs/layers.md) */
74
91
  residueMappings?: ResidueMapping[]
75
92
  /** extra fields per row name, such as a lineage or a host (see docs/layers.md) */
76
- rowData?: Record<string, Record<string, string>>
93
+ rowData?: RowDataInput
77
94
  /**
78
95
  * what the viewer's marks read from a table: `{channel, field, scale?}`,
79
96
  * where `channel` is `tipLabel`, `rowTint` or `branch` over `rowData`, or
@@ -94,6 +111,17 @@ export interface MSAViewerProps {
94
111
  autoTreeAreaWidth?: boolean
95
112
  /** draw branch lengths (default true); false draws a cladogram */
96
113
  showBranchLen?: boolean
114
+ /**
115
+ * the order each node's children draw in: `branchLength` (default,
116
+ * shortest first), `input` (as the file gives them), `ladderize` (fewest
117
+ * tips first) or `ladderizeReverse` (most tips first)
118
+ */
119
+ treeOrder?: TreeOrder
120
+ /**
121
+ * reroot the tree: `midpoint`, or `{outgroup: [tip names]}` on the branch
122
+ * above those tips' common ancestor
123
+ */
124
+ treeRoot?: TreeRoot
97
125
  /**
98
126
  * which channel `colorScheme` paints: `fill` (default) colors the background
99
127
  * of the cell a residue sits in, `color` colors the letter itself and leaves
@@ -107,6 +135,11 @@ export interface MSAViewerProps {
107
135
  * in columns, 1-based and inclusive
108
136
  */
109
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
110
143
  /** leave out the toolbar, for a page drawing its own controls */
111
144
  hideHeader?: boolean
112
145
  /**
@@ -120,43 +153,48 @@ export interface MSAViewerProps {
120
153
  onCellClick?: (cell: Cell | undefined) => void
121
154
  /** the alignment columns on screen, after every scroll, zoom and resize */
122
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
123
161
  /**
124
162
  * the model the viewer built, for the model API; called again with the new
125
- * model when msa, tree, gff or a filehandle changes
163
+ * model when msa, tree, msaFilehandle or treeFilehandle changes
126
164
  */
127
165
  onModel?: (model: MsaViewModel) => void
128
166
  }
129
167
 
168
+ function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
169
+ return typeof scheme === 'string'
170
+ ? { colorSchemeName: scheme }
171
+ : scheme
172
+ ? { customColorScheme: scheme.map }
173
+ : {}
174
+ }
175
+
130
176
  type DataSource = Pick<
131
177
  MSAViewerProps,
132
- 'msa' | 'tree' | 'gff' | 'msaFilehandle' | 'treeFilehandle' | 'gffFilehandle'
178
+ 'msa' | 'tree' | 'msaFilehandle' | 'treeFilehandle'
133
179
  >
134
180
 
135
181
  function sameSource(a: DataSource, b: DataSource) {
136
182
  return (
137
183
  a.msa === b.msa &&
138
184
  a.tree === b.tree &&
139
- a.gff === b.gff &&
140
- JSON.stringify([a.msaFilehandle, a.treeFilehandle, a.gffFilehandle]) ===
141
- JSON.stringify([b.msaFilehandle, b.treeFilehandle, b.gffFilehandle])
185
+ JSON.stringify([a.msaFilehandle, a.treeFilehandle]) ===
186
+ JSON.stringify([b.msaFilehandle, b.treeFilehandle])
142
187
  )
143
188
  }
144
189
 
145
190
  /**
146
- * A new alignment, tree or annotation file needs a new model, so the viewer
147
- * remounts when one of those props changes and keeps its model across every
148
- * 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.
149
194
  */
150
195
  export default function MSAViewer(props: MSAViewerProps) {
151
- const { msa, tree, gff, msaFilehandle, treeFilehandle, gffFilehandle } = props
152
- const source = {
153
- msa,
154
- tree,
155
- gff,
156
- msaFilehandle,
157
- treeFilehandle,
158
- gffFilehandle,
159
- }
196
+ const { msa, tree, msaFilehandle, treeFilehandle } = props
197
+ const source = { msa, tree, msaFilehandle, treeFilehandle }
160
198
  const [shown, setShown] = useState({ source, generation: 0 })
161
199
  if (!sameSource(shown.source, source)) {
162
200
  setShown({ source, generation: shown.generation + 1 })
@@ -164,7 +202,9 @@ export default function MSAViewer(props: MSAViewerProps) {
164
202
  return <Viewer key={shown.generation} {...props} />
165
203
  }
166
204
 
167
- function useModelReaction<K extends 'hoveredCell' | 'clickedCell' | 'viewport'>(
205
+ function useModelReaction<
206
+ K extends 'hoveredCell' | 'clickedCell' | 'viewport' | 'selection',
207
+ >(
168
208
  model: MsaViewModel,
169
209
  key: K,
170
210
  handler: ((value: MsaViewModel[K]) => void) | undefined,
@@ -203,6 +243,7 @@ function Viewer({
203
243
  allowedGappyness,
204
244
  highlightColumns,
205
245
  highlights,
246
+ features,
206
247
  clades,
207
248
  relativeTo,
208
249
  columnTracks,
@@ -215,12 +256,16 @@ function Viewer({
215
256
  treeAreaWidth,
216
257
  autoTreeAreaWidth,
217
258
  showBranchLen,
259
+ treeOrder,
260
+ treeRoot,
218
261
  region,
262
+ selection,
219
263
  hideHeader,
220
264
  theme,
221
265
  onCellHover,
222
266
  onCellClick,
223
267
  onViewportChange,
268
+ onSelectionChange,
224
269
  onModel,
225
270
  }: MSAViewerProps) {
226
271
  const [model] = useState(() =>
@@ -241,13 +286,15 @@ function Viewer({
241
286
  ...(msaFilehandle ? { msaFilehandle } : {}),
242
287
  ...(treeFilehandle ? { treeFilehandle } : {}),
243
288
  ...(gffFilehandle ? { gffFilehandle } : {}),
244
- ...(colorScheme ? { colorSchemeName: colorScheme } : {}),
289
+ ...colorSchemeSnapshot(colorScheme),
245
290
  ...(height ? { height } : {}),
246
291
  ...(colWidth ? { colWidth } : {}),
247
292
  ...(rowHeight ? { rowHeight } : {}),
248
293
  ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
249
294
  ...(highlightColumns ? { highlightColumns } : {}),
250
295
  ...(highlights ? { highlights } : {}),
296
+ ...(features ? { features } : {}),
297
+ ...(selection ? { selection } : {}),
251
298
  ...(clades ? { clades } : {}),
252
299
  ...(relativeTo ? { relativeTo } : {}),
253
300
  ...(columnTracks ? { columnTracks } : {}),
@@ -258,6 +305,8 @@ function Viewer({
258
305
  ...(treeAreaWidth ? { treeAreaWidth } : {}),
259
306
  ...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
260
307
  ...(showBranchLen !== undefined ? { showBranchLen } : {}),
308
+ ...(treeOrder ? { treeOrder } : {}),
309
+ ...(treeRoot ? { treeRoot } : {}),
261
310
  ...(residueEncoding !== undefined
262
311
  ? { bgColor: residueEncoding === 'fill' }
263
312
  : {}),
@@ -282,11 +331,6 @@ function Viewer({
282
331
  model.setHeight(height)
283
332
  }
284
333
  }, [model, height])
285
- useEffect(() => {
286
- if (colorScheme !== undefined) {
287
- model.setColorSchemeName(colorScheme)
288
- }
289
- }, [model, colorScheme])
290
334
  useEffect(() => {
291
335
  if (colWidth !== undefined) {
292
336
  model.setColWidth(colWidth)
@@ -317,6 +361,11 @@ function Viewer({
317
361
  model.setShowBranchLen(showBranchLen)
318
362
  }
319
363
  }, [model, showBranchLen])
364
+ useEffect(() => {
365
+ if (treeOrder !== undefined) {
366
+ model.setTreeOrder(treeOrder)
367
+ }
368
+ }, [model, treeOrder])
320
369
  useEffect(() => {
321
370
  if (residueEncoding !== undefined) {
322
371
  model.setBgColor(residueEncoding === 'fill')
@@ -330,12 +379,57 @@ function Viewer({
330
379
  model.drawRelativeTo(relativeTo)
331
380
  }, [model, relativeTo])
332
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
+
333
406
  // keyed by content, since a host computing a layer inline passes a new array
334
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])
335
417
  const highlightsKey = JSON.stringify(highlights ?? [])
336
418
  useEffect(() => {
337
419
  model.setHighlights(JSON.parse(highlightsKey))
338
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])
429
+ const treeRootKey = JSON.stringify(treeRoot ?? null)
430
+ useEffect(() => {
431
+ model.setTreeRoot(JSON.parse(treeRootKey) ?? undefined)
432
+ }, [model, treeRootKey])
339
433
  const cladesKey = JSON.stringify(clades ?? [])
340
434
  useEffect(() => {
341
435
  model.setClades(JSON.parse(cladesKey))
@@ -385,6 +479,7 @@ function Viewer({
385
479
  useModelReaction(model, 'hoveredCell', onCellHover)
386
480
  useModelReaction(model, 'clickedCell', onCellClick)
387
481
  useModelReaction(model, 'viewport', onViewportChange)
482
+ useModelReaction(model, 'selection', onSelectionChange)
388
483
 
389
484
  const latestOnModel = useRef(onModel)
390
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
+ }
@@ -1,9 +1,10 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react'
4
- import { TextField } from '@mui/material'
4
+ import { Button, TextField } from '@mui/material'
5
5
  import { getUngappedSequence } from 'msa-parsers'
6
6
 
7
+ import { saveAs } from '../vendor/fileSaver.ts'
7
8
  import Checkbox2 from './Checkbox2.tsx'
8
9
  import CopyButton from './CopyButton.tsx'
9
10
 
@@ -18,19 +19,59 @@ const useStyles = makeStyles()(theme => ({
18
19
  },
19
20
  }))
20
21
 
22
+ export const maxShownRecords = 100
23
+ export const maxShownChars = 200_000
24
+
25
+ function fastaRecord([name, seq]: [string, string], showGaps: boolean) {
26
+ return `>${name}\n${showGaps ? seq : getUngappedSequence(seq)}`
27
+ }
28
+
29
+ function isAllGaps(seq: string) {
30
+ return !/[^.-]/.test(seq)
31
+ }
32
+
33
+ /**
34
+ * The first records of `rows` as FASTA, up to `maxShownRecords` records or
35
+ * `maxShownChars` characters, whichever comes first. A textarea holding every
36
+ * row of a large alignment freezes the page.
37
+ */
38
+ export function fastaPreview(rows: [string, string][], showGaps: boolean) {
39
+ const shown: string[] = []
40
+ let chars = 0
41
+ for (const row of rows) {
42
+ if (shown.length >= maxShownRecords || chars >= maxShownChars) {
43
+ break
44
+ }
45
+ const record = fastaRecord(row, showGaps)
46
+ shown.push(record)
47
+ chars += record.length + 1
48
+ }
49
+ return { text: shown.join('\n'), hidden: rows.length - shown.length }
50
+ }
51
+
21
52
  export default function SequenceTextArea({ str }: { str: [string, string][] }) {
22
53
  const { classes } = useStyles()
23
54
  const [showGaps, setShowGaps] = useState(false)
24
55
  const [showEmpty, setShowEmpty] = useState(false)
25
56
 
26
- const disp = str
27
- .map(([s1, s2]) => [s1, showGaps ? s2 : getUngappedSequence(s2)] as const)
28
- .filter(([, s2]) => showEmpty || !!s2)
29
- .map(([s1, s2]) => `>${s1}\n${s2}`)
30
- .join('\n')
57
+ const rows = showEmpty
58
+ ? str
59
+ : str.filter(([, seq]) => (showGaps ? !!seq : !isAllGaps(seq)))
60
+ const { text, hidden } = fastaPreview(rows, showGaps)
61
+ const fullText = () => rows.map(row => fastaRecord(row, showGaps)).join('\n')
31
62
  return (
32
63
  <>
33
- <CopyButton text={disp} />
64
+ <CopyButton text={fullText} />
65
+ <Button
66
+ onClick={() => {
67
+ saveAs(
68
+ new Blob([fullText()], { type: 'text/plain' }),
69
+ 'sequences.fasta',
70
+ )
71
+ }}
72
+ >
73
+ Download
74
+ </Button>
34
75
  <Checkbox2
35
76
  label="Show gaps"
36
77
  checked={showGaps}
@@ -52,7 +93,12 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
52
93
  minRows={5}
53
94
  maxRows={10}
54
95
  fullWidth
55
- value={disp}
96
+ value={text}
97
+ helperText={
98
+ hidden > 0
99
+ ? `Showing ${rows.length - hidden} of ${rows.length.toLocaleString()} sequences. ${hidden.toLocaleString()} more are in the copy and the download.`
100
+ : undefined
101
+ }
56
102
  slotProps={{
57
103
  input: {
58
104
  readOnly: true,
@@ -0,0 +1,70 @@
1
+ import React from 'react'
2
+
3
+ import { LoadingEllipses } from '@jbrowse/core/ui'
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react'
5
+ import { Button, CircularProgress, Typography } from '@mui/material'
6
+ import { observer } from 'mobx-react'
7
+
8
+ import type { MsaViewModel } from '../model.ts'
9
+
10
+ const useStyles = makeStyles()({
11
+ page: {
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: 12,
15
+ padding: 20,
16
+ },
17
+ header: {
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: 4,
21
+ margin: 'auto',
22
+ marginLeft: 10,
23
+ },
24
+ })
25
+
26
+ /**
27
+ * The model's status line with its Cancel button: a spinner and a heading in
28
+ * place of the view while it loads, or a line in the header afterwards, which
29
+ * disappears when there is no status.
30
+ */
31
+ const StatusMessage = observer(function ({
32
+ model,
33
+ variant,
34
+ }: {
35
+ model: MsaViewModel
36
+ variant: 'page' | 'header'
37
+ }) {
38
+ const { status } = model
39
+ const { classes } = useStyles()
40
+ if (variant === 'header' && !status) {
41
+ return null
42
+ }
43
+ const message = status?.msg ?? 'Loading...'
44
+ const onCancel = status?.onCancel
45
+ return (
46
+ <div className={classes[variant]}>
47
+ {variant === 'page' ? (
48
+ <>
49
+ <CircularProgress size={24} />
50
+ <Typography variant="h6">{message}</Typography>
51
+ </>
52
+ ) : (
53
+ <LoadingEllipses message={message} component="span" />
54
+ )}
55
+ {onCancel ? (
56
+ <Button
57
+ variant={variant === 'page' ? 'outlined' : 'text'}
58
+ size="small"
59
+ onClick={() => {
60
+ onCancel()
61
+ }}
62
+ >
63
+ Cancel
64
+ </Button>
65
+ ) : null}
66
+ </div>
67
+ )
68
+ })
69
+
70
+ export default StatusMessage
@@ -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 { hover, onMouseMove, onMouseLeave } = useTrackHover({
150
+ const { anchor, onMouseMove, onMouseDown, onMouseLeave } = useTrackHover({
150
151
  model,
151
152
  tooltip: showColumnStats,
152
153
  })
@@ -162,19 +163,35 @@ 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
  }}
173
186
  >
174
187
  <track.ReactComponent model={model} track={track} />
175
- {hover ? (
176
- <BaseTooltip clientPoint={{ x: hover.x, y: hover.y + 15 }}>
177
- <TrackTooltipContent model={model} track={track} col={hover.col} />
188
+ {anchor ? (
189
+ <BaseTooltip clientPoint={anchor.clientPoint}>
190
+ <TrackTooltipContent
191
+ model={model}
192
+ track={track}
193
+ col={anchor.value}
194
+ />
178
195
  </BaseTooltip>
179
196
  ) : null}
180
197
  </div>
@@ -23,6 +23,13 @@ const VerticalScrollbar = observer(({ model }: { model: MsaViewModel }) => {
23
23
 
24
24
  return (
25
25
  <div
26
+ onMouseDown={event => {
27
+ if (event.button === 0 && event.target === event.currentTarget) {
28
+ const y =
29
+ event.clientY - event.currentTarget.getBoundingClientRect().top
30
+ model.setScrollY(-y / unit + msaAreaHeight / 2)
31
+ }
32
+ }}
26
33
  style={{
27
34
  position: 'relative',
28
35
  width,
@@ -15,6 +15,7 @@ import {
15
15
  Typography,
16
16
  useTheme,
17
17
  } from '@mui/material'
18
+ import { observer } from 'mobx-react'
18
19
 
19
20
  import Checkbox2 from '../Checkbox2.tsx'
20
21
 
@@ -30,7 +31,7 @@ function afterPaint(fn: () => void) {
30
31
  })
31
32
  }
32
33
 
33
- export default function ExportSVGDialog({
34
+ const ExportSVGDialog = observer(function ({
34
35
  model,
35
36
  onClose,
36
37
  }: {
@@ -181,4 +182,6 @@ export default function ExportSVGDialog({
181
182
  </DialogActions>
182
183
  </Dialog>
183
184
  )
184
- }
185
+ })
186
+
187
+ export default ExportSVGDialog