react-msaview 8.4.1 → 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 (193) 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/MSAView.js +2 -2
  12. package/dist/components/MSAView.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +1892 -7
  14. package/dist/components/MSAViewer.js +144 -202
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/ResizeHandles.js +4 -3
  17. package/dist/components/ResizeHandles.js.map +1 -1
  18. package/dist/components/Track.js +3 -2
  19. package/dist/components/Track.js.map +1 -1
  20. package/dist/components/controls.d.ts +11 -0
  21. package/dist/components/controls.js +48 -0
  22. package/dist/components/controls.js.map +1 -0
  23. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  24. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  25. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  26. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  27. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  28. package/dist/components/dialogs/HelpDialog.js +30 -0
  29. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  30. package/dist/components/header/FileMenu.js +33 -7
  31. package/dist/components/header/FileMenu.js.map +1 -1
  32. package/dist/components/header/Header.js +4 -6
  33. package/dist/components/header/Header.js.map +1 -1
  34. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -32
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +28 -44
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  40. package/dist/components/header/settingsMenuItems.js +48 -1
  41. package/dist/components/header/settingsMenuItems.js.map +1 -1
  42. package/dist/components/import/data/seq2.d.ts +0 -1
  43. package/dist/components/import/data/seq2.js +0 -17
  44. package/dist/components/import/data/seq2.js.map +1 -1
  45. package/dist/components/msa/MSACanvas.js +2 -1
  46. package/dist/components/msa/MSACanvas.js.map +1 -1
  47. package/dist/components/msa/MSACanvasBlock.js +2 -5
  48. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  49. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  50. package/dist/components/msa/drawFeatureSpans.js +99 -56
  51. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  52. package/dist/components/msa/msaRaster.d.ts +14 -1
  53. package/dist/components/msa/msaRaster.js +19 -9
  54. package/dist/components/msa/msaRaster.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  57. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  58. package/dist/components/msa/renderHighlights.d.ts +2 -2
  59. package/dist/components/msa/renderHighlights.js +2 -3
  60. package/dist/components/msa/renderHighlights.js.map +1 -1
  61. package/dist/components/msa/renderMSAMouseover.js +2 -1
  62. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  63. package/dist/components/renderCtx.d.ts +1 -1
  64. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  65. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  66. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  67. package/dist/components/tracks/drawTracks.d.ts +1 -53
  68. package/dist/components/tracks/drawTracks.js +5 -5
  69. package/dist/components/tracks/drawTracks.js.map +1 -1
  70. package/dist/components/tree/CladeLabels.d.ts +1 -1
  71. package/dist/components/tree/CladeLabels.js +22 -34
  72. package/dist/components/tree/CladeLabels.js.map +1 -1
  73. package/dist/components/tree/TreeCanvas.js +4 -4
  74. package/dist/components/tree/TreeCanvas.js.map +1 -1
  75. package/dist/components/tree/TreeCanvasBlock.js +4 -4
  76. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  77. package/dist/components/tree/TreeOverview.js +10 -5
  78. package/dist/components/tree/TreeOverview.js.map +1 -1
  79. package/dist/components/tree/TreePanel.js +6 -2
  80. package/dist/components/tree/TreePanel.js.map +1 -1
  81. package/dist/components/tree/TreeRuler.js +2 -2
  82. package/dist/components/tree/TreeRuler.js.map +1 -1
  83. package/dist/components/tree/cladeBrackets.d.ts +22 -17
  84. package/dist/components/tree/cladeBrackets.js +40 -9
  85. package/dist/components/tree/cladeBrackets.js.map +1 -1
  86. package/dist/components/tree/clickMap.d.ts +2 -0
  87. package/dist/components/tree/clickMap.js.map +1 -1
  88. package/dist/components/tree/ellipsize.d.ts +8 -0
  89. package/dist/components/tree/ellipsize.js +20 -0
  90. package/dist/components/tree/ellipsize.js.map +1 -0
  91. package/dist/components/tree/renderTreeCanvas.js +30 -32
  92. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  93. package/dist/components/tree/renderTreeMouseover.js +1 -1
  94. package/dist/components/tree/renderTreeMouseover.js.map +1 -1
  95. package/dist/components/tree/renderTreeOverview.js +4 -11
  96. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  97. package/dist/constants.d.ts +6 -3
  98. package/dist/constants.js +16 -6
  99. package/dist/constants.js.map +1 -1
  100. package/dist/element.js +3 -6
  101. package/dist/element.js.map +1 -1
  102. package/dist/exportLayout.d.ts +39 -0
  103. package/dist/exportLayout.js +78 -0
  104. package/dist/exportLayout.js.map +1 -0
  105. package/dist/headlessRenderEnv.d.ts +12 -39
  106. package/dist/headlessRenderEnv.js +18 -12
  107. package/dist/headlessRenderEnv.js.map +1 -1
  108. package/dist/hierarchy.d.ts +5 -8
  109. package/dist/hierarchy.js +5 -79
  110. package/dist/hierarchy.js.map +1 -1
  111. package/dist/index.d.ts +3 -1
  112. package/dist/index.js +4 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/measureTextCanvas.d.ts +7 -0
  115. package/dist/measureTextCanvas.js +10 -8
  116. package/dist/measureTextCanvas.js.map +1 -1
  117. package/dist/model/treeModel.d.ts +14 -25
  118. package/dist/model/treeModel.js +16 -30
  119. package/dist/model/treeModel.js.map +1 -1
  120. package/dist/model.d.ts +108 -79
  121. package/dist/model.js +258 -209
  122. package/dist/model.js.map +1 -1
  123. package/dist/renderToSvg.d.ts +2 -7
  124. package/dist/renderToSvg.js +49 -135
  125. package/dist/renderToSvg.js.map +1 -1
  126. package/dist/svgTestUtil.d.ts +48 -48
  127. package/dist/tipLabels.d.ts +8 -0
  128. package/dist/tipLabels.js +46 -0
  129. package/dist/tipLabels.js.map +1 -0
  130. package/dist/types.d.ts +0 -1
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/viewerSnapshot.d.ts +169 -0
  134. package/dist/viewerSnapshot.js +100 -0
  135. package/dist/viewerSnapshot.js.map +1 -0
  136. package/package.json +3 -3
  137. package/src/colorSchemes.ts +1 -4
  138. package/src/columnCounts.ts +1 -1
  139. package/src/components/Loading.tsx +19 -12
  140. package/src/components/MSAView.tsx +2 -2
  141. package/src/components/MSAViewer.tsx +276 -256
  142. package/src/components/ResizeHandles.tsx +10 -3
  143. package/src/components/Track.tsx +3 -2
  144. package/src/components/controls.ts +61 -0
  145. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  146. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  147. package/src/components/dialogs/HelpDialog.tsx +79 -0
  148. package/src/components/header/FileMenu.tsx +33 -7
  149. package/src/components/header/Header.tsx +4 -6
  150. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  151. package/src/components/header/ZoomControls.tsx +30 -66
  152. package/src/components/header/settingsMenuItems.ts +67 -0
  153. package/src/components/import/data/seq2.ts +0 -18
  154. package/src/components/msa/MSACanvas.tsx +2 -1
  155. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  156. package/src/components/msa/drawFeatureSpans.ts +160 -79
  157. package/src/components/msa/msaRaster.ts +27 -9
  158. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  159. package/src/components/msa/renderHighlights.ts +2 -3
  160. package/src/components/msa/renderMSAMouseover.ts +5 -1
  161. package/src/components/renderCtx.ts +0 -1
  162. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  163. package/src/components/tracks/drawTracks.ts +5 -5
  164. package/src/components/tree/CladeLabels.tsx +30 -54
  165. package/src/components/tree/TreeCanvas.tsx +10 -4
  166. package/src/components/tree/TreeCanvasBlock.tsx +4 -4
  167. package/src/components/tree/TreeOverview.tsx +10 -5
  168. package/src/components/tree/TreePanel.tsx +8 -2
  169. package/src/components/tree/TreeRuler.tsx +7 -3
  170. package/src/components/tree/cladeBrackets.ts +59 -19
  171. package/src/components/tree/clickMap.ts +2 -0
  172. package/src/components/tree/ellipsize.ts +24 -0
  173. package/src/components/tree/renderTreeCanvas.ts +37 -36
  174. package/src/components/tree/renderTreeMouseover.ts +1 -1
  175. package/src/components/tree/renderTreeOverview.ts +7 -14
  176. package/src/constants.ts +17 -6
  177. package/src/element.ts +3 -7
  178. package/src/exportLayout.ts +134 -0
  179. package/src/headlessRenderEnv.ts +24 -11
  180. package/src/hierarchy.ts +5 -88
  181. package/src/index.ts +5 -5
  182. package/src/measureTextCanvas.ts +10 -8
  183. package/src/model/treeModel.ts +17 -34
  184. package/src/model.ts +296 -243
  185. package/src/renderToSvg.tsx +72 -209
  186. package/src/tipLabels.ts +51 -0
  187. package/src/types.ts +0 -1
  188. package/src/version.ts +1 -1
  189. package/src/viewerSnapshot.ts +147 -0
  190. package/dist/components/header/ZoomMenu.d.ts +0 -6
  191. package/dist/components/header/ZoomMenu.js +0 -37
  192. package/dist/components/header/ZoomMenu.js.map +0 -1
  193. package/src/components/header/ZoomMenu.tsx +0 -48
@@ -1,12 +1,23 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, {
2
+ forwardRef,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react'
2
8
 
3
- import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
4
9
  import useMeasure from '@jbrowse/core/util/useMeasure'
5
10
  import { destroy, isAlive } from '@jbrowse/mobx-state-tree'
6
11
  import { ThemeProvider } from '@mui/material/styles'
7
12
  import { compareStructural, reaction, when } from 'mobx'
8
13
 
9
14
  import MSAModelF from '../model.ts'
15
+ import {
16
+ fileLocations,
17
+ resolveTheme,
18
+ viewerHighlights,
19
+ viewerSnapshot,
20
+ } from '../viewerSnapshot.ts'
10
21
  import Loading from './Loading.tsx'
11
22
 
12
23
  import type { MsaViewModel } from '../model.ts'
@@ -30,16 +41,7 @@ import type {
30
41
  } from '../types.ts'
31
42
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
32
43
  import type { ThemeOptions } from '@mui/material/styles'
33
-
34
- function resolveTheme(theme: MSAViewerProps['theme']) {
35
- return createJBrowseTheme(
36
- theme === 'dark'
37
- ? { palette: { mode: 'dark' } }
38
- : typeof theme === 'object'
39
- ? theme
40
- : undefined,
41
- )
42
- }
44
+ import type { ForwardedRef } from 'react'
43
45
 
44
46
  export interface MSAViewerProps {
45
47
  msa?: string
@@ -48,6 +50,12 @@ export interface MSAViewerProps {
48
50
  msaFilehandle?: FileLocationType
49
51
  treeFilehandle?: FileLocationType
50
52
  gffFilehandle?: FileLocationType
53
+ /** a URL the viewer fetches the alignment from; `msaFilehandle` wins */
54
+ msaUrl?: string
55
+ /** a URL the viewer fetches the Newick tree from; `treeFilehandle` wins */
56
+ treeUrl?: string
57
+ /** a URL the viewer fetches the GFF from; `gffFilehandle` wins */
58
+ gffUrl?: string
51
59
  /**
52
60
  * a scheme name from the built-in table, or `{map}`, a color per residue
53
61
  * letter that leaves every other letter uncolored
@@ -62,7 +70,10 @@ export interface MSAViewerProps {
62
70
  * hide columns at least this percent gaps (default 100, i.e. hide nothing)
63
71
  */
64
72
  allowedGappyness?: number
65
- /** alignment columns (0-based) to highlight with a persistent overlay */
73
+ /**
74
+ * alignment columns, 1-based like `highlights`, drawn as one `{start, end}`
75
+ * highlight per run of adjacent columns
76
+ */
66
77
  highlightColumns?: number[]
67
78
  /**
68
79
  * labeled highlights in 1-based inclusive coordinates: `{start, end}` for
@@ -105,10 +116,8 @@ export interface MSAViewerProps {
105
116
  rowPanels?: RowPanelSpec[]
106
117
  /** draw the phylogenetic tree (default true); false leaves a labels-only gutter */
107
118
  drawTree?: boolean
108
- /** fixed width (px) of the tree/label area */
119
+ /** fixed width (px) of the tree/label area; unset fits it to the labels and the tree */
109
120
  treeAreaWidth?: number
110
- /** auto-size the tree/label area to the labels (used when drawTree is false) */
111
- autoTreeAreaWidth?: boolean
112
121
  /** draw branch lengths (default true); false draws a cladogram */
113
122
  showBranchLen?: boolean
114
123
  /**
@@ -160,19 +169,12 @@ export interface MSAViewerProps {
160
169
  onSelectionChange?: (selection: MsaSelection | undefined) => void
161
170
  /**
162
171
  * the model the viewer built, for the model API; called again with the new
163
- * model when msa, tree, msaFilehandle or treeFilehandle changes
172
+ * model when msa, tree, msaUrl, treeUrl, msaFilehandle or treeFilehandle
173
+ * changes
164
174
  */
165
175
  onModel?: (model: MsaViewModel) => void
166
176
  }
167
177
 
168
- function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
169
- return typeof scheme === 'string'
170
- ? { colorSchemeName: scheme }
171
- : scheme
172
- ? { customColorScheme: scheme.map }
173
- : {}
174
- }
175
-
176
178
  type DataSource = Pick<
177
179
  MSAViewerProps,
178
180
  'msa' | 'tree' | 'msaFilehandle' | 'treeFilehandle'
@@ -190,17 +192,22 @@ function sameSource(a: DataSource, b: DataSource) {
190
192
  /**
191
193
  * A new alignment or tree needs a new model, so the viewer remounts when one
192
194
  * of those props changes and keeps its model across every other change,
193
- * including a new annotation file.
195
+ * including a new annotation file. A `ref` holds the current model.
194
196
  */
195
- export default function MSAViewer(props: MSAViewerProps) {
196
- const { msa, tree, msaFilehandle, treeFilehandle } = props
197
- const source = { msa, tree, msaFilehandle, treeFilehandle }
198
- const [shown, setShown] = useState({ source, generation: 0 })
199
- if (!sameSource(shown.source, source)) {
200
- setShown({ source, generation: shown.generation + 1 })
201
- }
202
- return <Viewer key={shown.generation} {...props} />
203
- }
197
+ const MSAViewer = forwardRef<MsaViewModel, MSAViewerProps>(
198
+ function MSAViewer(props, ref) {
199
+ const resolved = { ...props, ...fileLocations(props) }
200
+ const { msa, tree, msaFilehandle, treeFilehandle } = resolved
201
+ const source = { msa, tree, msaFilehandle, treeFilehandle }
202
+ const [shown, setShown] = useState({ source, generation: 0 })
203
+ if (!sameSource(shown.source, source)) {
204
+ setShown({ source, generation: shown.generation + 1 })
205
+ }
206
+ return <Viewer key={shown.generation} modelRef={ref} {...resolved} />
207
+ },
208
+ )
209
+
210
+ export default MSAViewer
204
211
 
205
212
  function useModelReaction<
206
213
  K extends 'hoveredCell' | 'clickedCell' | 'viewport' | 'selection',
@@ -229,89 +236,237 @@ function useModelReaction<
229
236
  )
230
237
  }
231
238
 
232
- function Viewer({
233
- msa,
234
- tree,
235
- gff,
236
- msaFilehandle,
237
- treeFilehandle,
238
- gffFilehandle,
239
- colorScheme,
240
- height,
241
- colWidth,
242
- rowHeight,
243
- allowedGappyness,
244
- highlightColumns,
245
- highlights,
246
- features,
247
- clades,
248
- relativeTo,
249
- columnTracks,
250
- residueMappings,
251
- rowData,
252
- encodings,
253
- rowPanels,
254
- drawTree,
255
- residueEncoding,
256
- treeAreaWidth,
257
- autoTreeAreaWidth,
258
- showBranchLen,
259
- treeOrder,
260
- treeRoot,
261
- region,
262
- selection,
263
- hideHeader,
264
- theme,
265
- onCellHover,
266
- onCellClick,
267
- onViewportChange,
268
- onSelectionChange,
269
- onModel,
270
- }: MSAViewerProps) {
271
- const [model] = useState(() =>
272
- MSAModelF().create({
273
- type: 'MsaView',
274
- ...(msa || tree || gff || rowData
275
- ? {
276
- data: {
277
- msa: msa ?? '',
278
- tree: tree ?? '',
279
- gff,
280
- // the row table is stored as this JSON string, which is what a
281
- // shared URL and the size limit already cover
282
- ...(rowData ? { treeMetadata: JSON.stringify(rowData) } : {}),
283
- },
284
- }
285
- : {}),
286
- ...(msaFilehandle ? { msaFilehandle } : {}),
287
- ...(treeFilehandle ? { treeFilehandle } : {}),
288
- ...(gffFilehandle ? { gffFilehandle } : {}),
289
- ...colorSchemeSnapshot(colorScheme),
290
- ...(height ? { height } : {}),
291
- ...(colWidth ? { colWidth } : {}),
292
- ...(rowHeight ? { rowHeight } : {}),
293
- ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
294
- ...(highlightColumns ? { highlightColumns } : {}),
295
- ...(highlights ? { highlights } : {}),
296
- ...(features ? { features } : {}),
297
- ...(selection ? { selection } : {}),
298
- ...(clades ? { clades } : {}),
299
- ...(relativeTo ? { relativeTo } : {}),
300
- ...(columnTracks ? { columnTracks } : {}),
301
- ...(residueMappings ? { residueMappings } : {}),
302
- ...(encodings ? { encodings } : {}),
303
- ...(rowPanels ? { rowPanels } : {}),
304
- ...(drawTree !== undefined ? { drawTree } : {}),
305
- ...(treeAreaWidth ? { treeAreaWidth } : {}),
306
- ...(autoTreeAreaWidth ? { autoTreeAreaWidth } : {}),
307
- ...(showBranchLen !== undefined ? { showBranchLen } : {}),
308
- ...(treeOrder ? { treeOrder } : {}),
309
- ...(treeRoot ? { treeRoot } : {}),
310
- ...(residueEncoding !== undefined
311
- ? { bgColor: residueEncoding === 'fill' }
312
- : {}),
239
+ interface SyncedProp {
240
+ read: (props: MSAViewerProps) => unknown
241
+ apply: (model: MsaViewModel, value: unknown, props: MSAViewerProps) => void
242
+ inSnapshot?: boolean
243
+ }
244
+
245
+ function synced<T>(
246
+ read: (props: MSAViewerProps) => T,
247
+ apply: (model: MsaViewModel, value: T, props: MSAViewerProps) => void,
248
+ { inSnapshot = false } = {},
249
+ ): SyncedProp {
250
+ return {
251
+ read,
252
+ apply: apply as SyncedProp['apply'],
253
+ inSnapshot,
254
+ }
255
+ }
256
+
257
+ function ifSet<T>(set: (model: MsaViewModel, value: T) => void) {
258
+ return (model: MsaViewModel, value: T | undefined) => {
259
+ if (value !== undefined) {
260
+ set(model, value)
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * How each prop reaches the mounted model. `inSnapshot` marks a prop the
267
+ * snapshot already applied, so only a later change reaches its setter.
268
+ */
269
+ const syncedProps = [
270
+ synced(
271
+ p => p.height,
272
+ ifSet((m, v) => {
273
+ m.setHeight(v)
313
274
  }),
314
- )
275
+ ),
276
+ synced(
277
+ p => p.colWidth,
278
+ ifSet((m, v) => {
279
+ m.setColWidth(v)
280
+ }),
281
+ ),
282
+ synced(
283
+ p => p.rowHeight,
284
+ ifSet((m, v) => {
285
+ m.setRowHeight(v)
286
+ }),
287
+ ),
288
+ synced(
289
+ p => p.allowedGappyness,
290
+ ifSet((m, v) => {
291
+ m.setAllowedGappyness(v)
292
+ }),
293
+ ),
294
+ synced(
295
+ p => p.drawTree,
296
+ ifSet((m, v) => {
297
+ m.setDrawTree(v)
298
+ }),
299
+ ),
300
+ synced(
301
+ p => p.treeAreaWidth,
302
+ ifSet((m, v) => {
303
+ m.setTreeAreaWidth(v)
304
+ }),
305
+ ),
306
+ synced(
307
+ p => p.showBranchLen,
308
+ ifSet((m, v) => {
309
+ m.setShowBranchLen(v)
310
+ }),
311
+ ),
312
+ synced(
313
+ p => p.treeOrder,
314
+ ifSet((m, v) => {
315
+ m.setTreeOrder(v)
316
+ }),
317
+ ),
318
+ synced(
319
+ p => p.residueEncoding,
320
+ ifSet((m, v) => {
321
+ m.setBgColor(v === 'fill')
322
+ }),
323
+ ),
324
+ synced(
325
+ p => !!p.hideHeader,
326
+ (m, v) => {
327
+ m.setHideHeader(v)
328
+ },
329
+ ),
330
+ synced(
331
+ p => p.relativeTo,
332
+ (m, v) => {
333
+ m.drawRelativeTo(v)
334
+ },
335
+ ),
336
+ synced(
337
+ p => p.gffFilehandle,
338
+ (m, location, { gff }) => {
339
+ if (location) {
340
+ m.setGFFFilehandle(location)
341
+ } else {
342
+ m.setGFF(gff)
343
+ }
344
+ },
345
+ { inSnapshot: true },
346
+ ),
347
+ synced(
348
+ p => p.gff,
349
+ (m, v) => {
350
+ m.setGFF(v)
351
+ },
352
+ { inSnapshot: true },
353
+ ),
354
+ synced(
355
+ p => p.colorScheme,
356
+ (m, scheme) => {
357
+ if (typeof scheme === 'string') {
358
+ m.setColorSchemeName(scheme)
359
+ } else if (scheme) {
360
+ m.setCustomColorScheme(scheme.map)
361
+ }
362
+ },
363
+ ),
364
+ synced(viewerHighlights, (m, v) => {
365
+ m.setHighlights(v)
366
+ }),
367
+ synced(
368
+ p => p.features ?? [],
369
+ (m, v) => {
370
+ m.setFeatures(v)
371
+ },
372
+ ),
373
+ synced(
374
+ p => p.selection,
375
+ (m, v) => {
376
+ m.setSelection(v)
377
+ },
378
+ ),
379
+ synced(
380
+ p => p.treeRoot,
381
+ (m, v) => {
382
+ m.setTreeRoot(v)
383
+ },
384
+ ),
385
+ synced(
386
+ p => p.clades ?? [],
387
+ (m, v) => {
388
+ m.setClades(v)
389
+ },
390
+ ),
391
+ synced(
392
+ p => p.columnTracks ?? [],
393
+ (m, v) => {
394
+ m.setColumnTracks(v)
395
+ },
396
+ ),
397
+ synced(
398
+ p => p.residueMappings ?? [],
399
+ (m, v) => {
400
+ m.setResidueMappings(v)
401
+ },
402
+ ),
403
+ synced(
404
+ p => p.rowData,
405
+ (m, table) => {
406
+ if (table && Object.keys(table).length) {
407
+ m.setRowData(table)
408
+ }
409
+ },
410
+ ),
411
+ synced(
412
+ p => p.encodings ?? [],
413
+ (m, v) => {
414
+ m.setEncodings(v)
415
+ },
416
+ ),
417
+ synced(
418
+ p => p.rowPanels ?? [],
419
+ (m, v) => {
420
+ m.setRowPanels(v)
421
+ },
422
+ ),
423
+ ]
424
+
425
+ /**
426
+ * Applies each prop whose content changed since the last render. Comparing
427
+ * JSON keys lets a host pass a freshly computed array every render, and
428
+ * applying only a changed prop keeps a change made inside the viewer across
429
+ * the host's next render. The model receives a parsed copy, never the host's
430
+ * own object.
431
+ */
432
+ function useSyncedProps(model: MsaViewModel, props: MSAViewerProps) {
433
+ const applied = useRef<string[] | undefined>(undefined)
434
+ useEffect(() => {
435
+ const keys = syncedProps.map(({ read }) =>
436
+ JSON.stringify(read(props) ?? null),
437
+ )
438
+ applied.current ??= syncedProps.map(({ inSnapshot }, i) =>
439
+ inSnapshot ? keys[i]! : '',
440
+ )
441
+ const last = applied.current
442
+ syncedProps.forEach(({ apply }, i) => {
443
+ const key = keys[i]!
444
+ if (last[i] !== key) {
445
+ last[i] = key
446
+ apply(model, JSON.parse(key) ?? undefined, props)
447
+ }
448
+ })
449
+ })
450
+ }
451
+
452
+ function Viewer({
453
+ modelRef,
454
+ ...props
455
+ }: MSAViewerProps & { modelRef: ForwardedRef<MsaViewModel> }) {
456
+ const {
457
+ region,
458
+ theme,
459
+ onCellHover,
460
+ onCellClick,
461
+ onViewportChange,
462
+ onSelectionChange,
463
+ onModel,
464
+ } = props
465
+ const [model] = useState(() => {
466
+ const model = MSAModelF().create(viewerSnapshot(props))
467
+ model.setImportForm(false)
468
+ return model
469
+ })
315
470
 
316
471
  const destroyTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
317
472
  undefined,
@@ -324,144 +479,7 @@ function Viewer({
324
479
  }
325
480
  }, [model, width])
326
481
 
327
- // Each effect depends only on its own prop, so a change made inside the
328
- // viewer survives the host's next render.
329
- useEffect(() => {
330
- if (height !== undefined) {
331
- model.setHeight(height)
332
- }
333
- }, [model, height])
334
- useEffect(() => {
335
- if (colWidth !== undefined) {
336
- model.setColWidth(colWidth)
337
- }
338
- }, [model, colWidth])
339
- useEffect(() => {
340
- if (rowHeight !== undefined) {
341
- model.setRowHeight(rowHeight)
342
- }
343
- }, [model, rowHeight])
344
- useEffect(() => {
345
- if (allowedGappyness !== undefined) {
346
- model.setAllowedGappyness(allowedGappyness)
347
- }
348
- }, [model, allowedGappyness])
349
- useEffect(() => {
350
- if (drawTree !== undefined) {
351
- model.setDrawTree(drawTree)
352
- }
353
- }, [model, drawTree])
354
- useEffect(() => {
355
- if (treeAreaWidth !== undefined) {
356
- model.setTreeAreaWidth(treeAreaWidth)
357
- }
358
- }, [model, treeAreaWidth])
359
- useEffect(() => {
360
- if (showBranchLen !== undefined) {
361
- model.setShowBranchLen(showBranchLen)
362
- }
363
- }, [model, showBranchLen])
364
- useEffect(() => {
365
- if (treeOrder !== undefined) {
366
- model.setTreeOrder(treeOrder)
367
- }
368
- }, [model, treeOrder])
369
- useEffect(() => {
370
- if (residueEncoding !== undefined) {
371
- model.setBgColor(residueEncoding === 'fill')
372
- }
373
- }, [model, residueEncoding])
374
- useEffect(() => {
375
- model.setHideHeader(!!hideHeader)
376
- }, [model, hideHeader])
377
- // unguarded, so removing the prop turns the reference diff off
378
- useEffect(() => {
379
- model.drawRelativeTo(relativeTo)
380
- }, [model, relativeTo])
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
-
406
- // keyed by content, since a host computing a layer inline passes a new array
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])
417
- const highlightsKey = JSON.stringify(highlights ?? [])
418
- useEffect(() => {
419
- model.setHighlights(JSON.parse(highlightsKey))
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])
433
- const cladesKey = JSON.stringify(clades ?? [])
434
- useEffect(() => {
435
- model.setClades(JSON.parse(cladesKey))
436
- }, [model, cladesKey])
437
- const columnTracksKey = JSON.stringify(columnTracks ?? [])
438
- useEffect(() => {
439
- model.setColumnTracks(JSON.parse(columnTracksKey))
440
- }, [model, columnTracksKey])
441
- const residueMappingsKey = JSON.stringify(residueMappings ?? [])
442
- useEffect(() => {
443
- model.setResidueMappings(JSON.parse(residueMappingsKey))
444
- }, [model, residueMappingsKey])
445
- const rowDataKey = JSON.stringify(rowData ?? {})
446
- useEffect(() => {
447
- const table = JSON.parse(rowDataKey)
448
- // an empty table leaves whatever the snapshot or a filehandle loaded
449
- if (Object.keys(table).length) {
450
- model.setRowData(table)
451
- }
452
- }, [model, rowDataKey])
453
- const encodingsKey = JSON.stringify(encodings ?? [])
454
- useEffect(() => {
455
- model.setEncodings(JSON.parse(encodingsKey))
456
- }, [model, encodingsKey])
457
- const rowPanelsKey = JSON.stringify(rowPanels ?? [])
458
- useEffect(() => {
459
- model.setRowPanels(JSON.parse(rowPanelsKey))
460
- }, [model, rowPanelsKey])
461
- const highlightColumnsKey = JSON.stringify(highlightColumns ?? null)
462
- useEffect(() => {
463
- model.setHighlightedColumns(JSON.parse(highlightColumnsKey) ?? undefined)
464
- }, [model, highlightColumnsKey])
482
+ useSyncedProps(model, props)
465
483
 
466
484
  const regionKey = JSON.stringify(region ?? null)
467
485
  useEffect(() => {
@@ -470,7 +488,7 @@ function Viewer({
470
488
  ? when(
471
489
  () => model.viewInitialized && model.numColumns > 0,
472
490
  () => {
473
- model.zoomToRegion(target)
491
+ model.revealRegion(target)
474
492
  },
475
493
  )
476
494
  : undefined
@@ -481,6 +499,8 @@ function Viewer({
481
499
  useModelReaction(model, 'viewport', onViewportChange)
482
500
  useModelReaction(model, 'selection', onSelectionChange)
483
501
 
502
+ useImperativeHandle(modelRef, () => model, [model])
503
+
484
504
  const latestOnModel = useRef(onModel)
485
505
  useEffect(() => {
486
506
  latestOnModel.current = onModel
@@ -7,6 +7,8 @@ import DragHandle from './DragHandle.tsx'
7
7
  import type { MsaViewModel } from '../model.ts'
8
8
  import type { BasicTrack } from '../types.ts'
9
9
 
10
+ const minViewHeight = 50
11
+
10
12
  export const VerticalResizeHandle = observer(function ({
11
13
  model,
12
14
  }: {
@@ -14,7 +16,12 @@ export const VerticalResizeHandle = observer(function ({
14
16
  }) {
15
17
  const onDrag = useCallback(
16
18
  (delta: number, startWidth: number) => {
17
- model.setTreeAreaWidth(startWidth + delta)
19
+ model.setTreeAreaWidth(
20
+ Math.min(
21
+ Math.max(0, startWidth + delta),
22
+ model.width - model.resizeHandleWidth,
23
+ ),
24
+ )
18
25
  },
19
26
  [model],
20
27
  )
@@ -22,7 +29,7 @@ export const VerticalResizeHandle = observer(function ({
22
29
  <DragHandle
23
30
  axis="x"
24
31
  variant="resizer"
25
- getStart={() => model.treeAreaWidth}
32
+ getStart={() => model.effectiveTreeAreaWidth}
26
33
  onDrag={onDrag}
27
34
  style={{ width: model.resizeHandleWidth }}
28
35
  />
@@ -36,7 +43,7 @@ export const HorizontalResizeHandle = observer(function ({
36
43
  }) {
37
44
  const onDrag = useCallback(
38
45
  (delta: number, startHeight: number) => {
39
- model.setHeight(startHeight + delta)
46
+ model.setHeight(Math.max(minViewHeight, startHeight + delta))
40
47
  },
41
48
  [model],
42
49
  )
@@ -8,6 +8,7 @@ import { observer } from 'mobx-react'
8
8
 
9
9
  import { useWheelScroll } from '../useWheelScroll.ts'
10
10
  import { onMsaKey } from './msa/msaKeys.ts'
11
+ import TrackBlocks from './tracks/TrackBlocks.tsx'
11
12
  import TrackTooltipContent from './tracks/TrackTooltipContent.tsx'
12
13
  import { useTrackHover } from './tracks/useTrackHover.ts'
13
14
 
@@ -44,7 +45,7 @@ const TrackLabel = observer(function TrackLabel({
44
45
  const closeMenu = () => {
45
46
  setAnchorEl(undefined)
46
47
  }
47
- const { drawLabels, fontSize, treeAreaWidth: width } = model
48
+ const { drawLabels, fontSize, effectiveTreeAreaWidth: width } = model
48
49
  const {
49
50
  model: { name, height },
50
51
  } = track
@@ -184,7 +185,7 @@ const Track = observer(function ({
184
185
  onMouseLeave()
185
186
  }}
186
187
  >
187
- <track.ReactComponent model={model} track={track} />
188
+ <TrackBlocks model={model} track={track} />
188
189
  {anchor ? (
189
190
  <BaseTooltip clientPoint={anchor.clientPoint}>
190
191
  <TrackTooltipContent