react-msaview 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +74 -74
  3. package/bundle/index.js.map +4 -4
  4. package/dist/colorSchemes.js +1 -3
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/columnCounts.d.ts +0 -1
  7. package/dist/columnCounts.js +1 -1
  8. package/dist/columnCounts.js.map +1 -1
  9. package/dist/components/Loading.js +5 -5
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +1891 -4
  12. package/dist/components/MSAViewer.js +144 -201
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/ResizeHandles.js +3 -2
  15. package/dist/components/ResizeHandles.js.map +1 -1
  16. package/dist/components/Track.js +2 -1
  17. package/dist/components/Track.js.map +1 -1
  18. package/dist/components/controls.d.ts +11 -0
  19. package/dist/components/controls.js +48 -0
  20. package/dist/components/controls.js.map +1 -0
  21. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  22. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  23. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  24. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  25. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  26. package/dist/components/dialogs/HelpDialog.js +30 -0
  27. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  28. package/dist/components/header/FileMenu.js +33 -7
  29. package/dist/components/header/FileMenu.js.map +1 -1
  30. package/dist/components/header/Header.js +4 -6
  31. package/dist/components/header/Header.js.map +1 -1
  32. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  33. package/dist/components/header/TreeSettingsMenu.js +1 -32
  34. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  35. package/dist/components/header/ZoomControls.js +28 -44
  36. package/dist/components/header/ZoomControls.js.map +1 -1
  37. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  38. package/dist/components/header/settingsMenuItems.js +44 -0
  39. package/dist/components/header/settingsMenuItems.js.map +1 -1
  40. package/dist/components/import/data/seq2.d.ts +0 -1
  41. package/dist/components/import/data/seq2.js +0 -17
  42. package/dist/components/import/data/seq2.js.map +1 -1
  43. package/dist/components/msa/MSACanvas.js +2 -1
  44. package/dist/components/msa/MSACanvas.js.map +1 -1
  45. package/dist/components/msa/MSACanvasBlock.js +2 -5
  46. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  47. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  48. package/dist/components/msa/drawFeatureSpans.js +99 -56
  49. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  50. package/dist/components/msa/msaRaster.d.ts +14 -1
  51. package/dist/components/msa/msaRaster.js +19 -9
  52. package/dist/components/msa/msaRaster.js.map +1 -1
  53. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  54. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  56. package/dist/components/msa/renderHighlights.d.ts +2 -2
  57. package/dist/components/msa/renderHighlights.js +2 -3
  58. package/dist/components/msa/renderHighlights.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +2 -1
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/renderCtx.d.ts +1 -1
  62. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  63. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  64. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  65. package/dist/components/tracks/drawTracks.d.ts +1 -53
  66. package/dist/components/tracks/drawTracks.js +5 -5
  67. package/dist/components/tracks/drawTracks.js.map +1 -1
  68. package/dist/components/tree/CladeLabels.d.ts +1 -1
  69. package/dist/components/tree/CladeLabels.js +22 -34
  70. package/dist/components/tree/CladeLabels.js.map +1 -1
  71. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  72. package/dist/components/tree/cladeBrackets.js +33 -3
  73. package/dist/components/tree/cladeBrackets.js.map +1 -1
  74. package/dist/components/tree/renderTreeCanvas.js +2 -8
  75. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  76. package/dist/components/tree/renderTreeOverview.js +4 -11
  77. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  78. package/dist/element.js +3 -6
  79. package/dist/element.js.map +1 -1
  80. package/dist/exportLayout.d.ts +39 -0
  81. package/dist/exportLayout.js +78 -0
  82. package/dist/exportLayout.js.map +1 -0
  83. package/dist/headlessRenderEnv.d.ts +12 -39
  84. package/dist/headlessRenderEnv.js +18 -12
  85. package/dist/headlessRenderEnv.js.map +1 -1
  86. package/dist/hierarchy.d.ts +5 -8
  87. package/dist/hierarchy.js +5 -79
  88. package/dist/hierarchy.js.map +1 -1
  89. package/dist/index.d.ts +3 -1
  90. package/dist/index.js +4 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/measureTextCanvas.d.ts +7 -0
  93. package/dist/measureTextCanvas.js +8 -6
  94. package/dist/measureTextCanvas.js.map +1 -1
  95. package/dist/model/treeModel.d.ts +0 -5
  96. package/dist/model/treeModel.js +0 -7
  97. package/dist/model/treeModel.js.map +1 -1
  98. package/dist/model.d.ts +38 -44
  99. package/dist/model.js +153 -112
  100. package/dist/model.js.map +1 -1
  101. package/dist/renderToSvg.d.ts +2 -7
  102. package/dist/renderToSvg.js +32 -120
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/svgTestUtil.d.ts +12 -20
  105. package/dist/types.d.ts +0 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/viewerSnapshot.d.ts +169 -0
  109. package/dist/viewerSnapshot.js +100 -0
  110. package/dist/viewerSnapshot.js.map +1 -0
  111. package/package.json +3 -3
  112. package/src/colorSchemes.ts +1 -4
  113. package/src/columnCounts.ts +1 -1
  114. package/src/components/Loading.tsx +19 -12
  115. package/src/components/MSAViewer.tsx +275 -251
  116. package/src/components/ResizeHandles.tsx +9 -2
  117. package/src/components/Track.tsx +2 -1
  118. package/src/components/controls.ts +61 -0
  119. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  120. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  121. package/src/components/dialogs/HelpDialog.tsx +79 -0
  122. package/src/components/header/FileMenu.tsx +33 -7
  123. package/src/components/header/Header.tsx +4 -6
  124. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  125. package/src/components/header/ZoomControls.tsx +30 -66
  126. package/src/components/header/settingsMenuItems.ts +59 -0
  127. package/src/components/import/data/seq2.ts +0 -18
  128. package/src/components/msa/MSACanvas.tsx +2 -1
  129. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  130. package/src/components/msa/drawFeatureSpans.ts +160 -79
  131. package/src/components/msa/msaRaster.ts +27 -9
  132. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  133. package/src/components/msa/renderHighlights.ts +2 -3
  134. package/src/components/msa/renderMSAMouseover.ts +5 -1
  135. package/src/components/renderCtx.ts +0 -1
  136. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  137. package/src/components/tracks/drawTracks.ts +5 -5
  138. package/src/components/tree/CladeLabels.tsx +30 -54
  139. package/src/components/tree/cladeBrackets.ts +50 -3
  140. package/src/components/tree/renderTreeCanvas.ts +2 -13
  141. package/src/components/tree/renderTreeOverview.ts +7 -14
  142. package/src/element.ts +3 -7
  143. package/src/exportLayout.ts +134 -0
  144. package/src/headlessRenderEnv.ts +24 -11
  145. package/src/hierarchy.ts +5 -88
  146. package/src/index.ts +5 -5
  147. package/src/measureTextCanvas.ts +8 -6
  148. package/src/model/treeModel.ts +0 -7
  149. package/src/model.ts +170 -122
  150. package/src/renderToSvg.tsx +55 -194
  151. package/src/types.ts +0 -1
  152. package/src/version.ts +1 -1
  153. package/src/viewerSnapshot.ts +147 -0
  154. package/dist/components/header/ZoomMenu.d.ts +0 -6
  155. package/dist/components/header/ZoomMenu.js +0 -37
  156. package/dist/components/header/ZoomMenu.js.map +0 -1
  157. package/src/components/header/ZoomMenu.tsx +0 -48
@@ -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
@@ -158,19 +169,12 @@ export interface MSAViewerProps {
158
169
  onSelectionChange?: (selection: MsaSelection | undefined) => void
159
170
  /**
160
171
  * the model the viewer built, for the model API; called again with the new
161
- * model when msa, tree, msaFilehandle or treeFilehandle changes
172
+ * model when msa, tree, msaUrl, treeUrl, msaFilehandle or treeFilehandle
173
+ * changes
162
174
  */
163
175
  onModel?: (model: MsaViewModel) => void
164
176
  }
165
177
 
166
- function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
167
- return typeof scheme === 'string'
168
- ? { colorSchemeName: scheme }
169
- : scheme
170
- ? { customColorScheme: scheme.map }
171
- : {}
172
- }
173
-
174
178
  type DataSource = Pick<
175
179
  MSAViewerProps,
176
180
  'msa' | 'tree' | 'msaFilehandle' | 'treeFilehandle'
@@ -188,17 +192,22 @@ function sameSource(a: DataSource, b: DataSource) {
188
192
  /**
189
193
  * A new alignment or tree needs a new model, so the viewer remounts when one
190
194
  * of those props changes and keeps its model across every other change,
191
- * including a new annotation file.
195
+ * including a new annotation file. A `ref` holds the current model.
192
196
  */
193
- export default function MSAViewer(props: MSAViewerProps) {
194
- const { msa, tree, msaFilehandle, treeFilehandle } = props
195
- const source = { msa, tree, msaFilehandle, treeFilehandle }
196
- const [shown, setShown] = useState({ source, generation: 0 })
197
- if (!sameSource(shown.source, source)) {
198
- setShown({ source, generation: shown.generation + 1 })
199
- }
200
- return <Viewer key={shown.generation} {...props} />
201
- }
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
202
211
 
203
212
  function useModelReaction<
204
213
  K extends 'hoveredCell' | 'clickedCell' | 'viewport' | 'selection',
@@ -227,87 +236,237 @@ function useModelReaction<
227
236
  )
228
237
  }
229
238
 
230
- function Viewer({
231
- msa,
232
- tree,
233
- gff,
234
- msaFilehandle,
235
- treeFilehandle,
236
- gffFilehandle,
237
- colorScheme,
238
- height,
239
- colWidth,
240
- rowHeight,
241
- allowedGappyness,
242
- highlightColumns,
243
- highlights,
244
- features,
245
- clades,
246
- relativeTo,
247
- columnTracks,
248
- residueMappings,
249
- rowData,
250
- encodings,
251
- rowPanels,
252
- drawTree,
253
- residueEncoding,
254
- treeAreaWidth,
255
- showBranchLen,
256
- treeOrder,
257
- treeRoot,
258
- region,
259
- selection,
260
- hideHeader,
261
- theme,
262
- onCellHover,
263
- onCellClick,
264
- onViewportChange,
265
- onSelectionChange,
266
- onModel,
267
- }: MSAViewerProps) {
268
- const [model] = useState(() =>
269
- MSAModelF().create({
270
- type: 'MsaView',
271
- ...(msa || tree || gff || rowData
272
- ? {
273
- data: {
274
- msa: msa ?? '',
275
- tree: tree ?? '',
276
- gff,
277
- // the row table is stored as this JSON string, which is what a
278
- // shared URL and the size limit already cover
279
- ...(rowData ? { treeMetadata: JSON.stringify(rowData) } : {}),
280
- },
281
- }
282
- : {}),
283
- ...(msaFilehandle ? { msaFilehandle } : {}),
284
- ...(treeFilehandle ? { treeFilehandle } : {}),
285
- ...(gffFilehandle ? { gffFilehandle } : {}),
286
- ...colorSchemeSnapshot(colorScheme),
287
- ...(height ? { height } : {}),
288
- ...(colWidth ? { colWidth } : {}),
289
- ...(rowHeight ? { rowHeight } : {}),
290
- ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
291
- ...(highlightColumns ? { highlightColumns } : {}),
292
- ...(highlights ? { highlights } : {}),
293
- ...(features ? { features } : {}),
294
- ...(selection ? { selection } : {}),
295
- ...(clades ? { clades } : {}),
296
- ...(relativeTo ? { relativeTo } : {}),
297
- ...(columnTracks ? { columnTracks } : {}),
298
- ...(residueMappings ? { residueMappings } : {}),
299
- ...(encodings ? { encodings } : {}),
300
- ...(rowPanels ? { rowPanels } : {}),
301
- ...(drawTree !== undefined ? { drawTree } : {}),
302
- ...(treeAreaWidth ? { treeAreaWidth } : {}),
303
- ...(showBranchLen !== undefined ? { showBranchLen } : {}),
304
- ...(treeOrder ? { treeOrder } : {}),
305
- ...(treeRoot ? { treeRoot } : {}),
306
- ...(residueEncoding !== undefined
307
- ? { bgColor: residueEncoding === 'fill' }
308
- : {}),
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)
309
274
  }),
310
- )
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
+ })
311
470
 
312
471
  const destroyTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
313
472
  undefined,
@@ -320,144 +479,7 @@ function Viewer({
320
479
  }
321
480
  }, [model, width])
322
481
 
323
- // Each effect depends only on its own prop, so a change made inside the
324
- // viewer survives the host's next render.
325
- useEffect(() => {
326
- if (height !== undefined) {
327
- model.setHeight(height)
328
- }
329
- }, [model, height])
330
- useEffect(() => {
331
- if (colWidth !== undefined) {
332
- model.setColWidth(colWidth)
333
- }
334
- }, [model, colWidth])
335
- useEffect(() => {
336
- if (rowHeight !== undefined) {
337
- model.setRowHeight(rowHeight)
338
- }
339
- }, [model, rowHeight])
340
- useEffect(() => {
341
- if (allowedGappyness !== undefined) {
342
- model.setAllowedGappyness(allowedGappyness)
343
- }
344
- }, [model, allowedGappyness])
345
- useEffect(() => {
346
- if (drawTree !== undefined) {
347
- model.setDrawTree(drawTree)
348
- }
349
- }, [model, drawTree])
350
- useEffect(() => {
351
- if (treeAreaWidth !== undefined) {
352
- model.setTreeAreaWidth(treeAreaWidth)
353
- }
354
- }, [model, treeAreaWidth])
355
- useEffect(() => {
356
- if (showBranchLen !== undefined) {
357
- model.setShowBranchLen(showBranchLen)
358
- }
359
- }, [model, showBranchLen])
360
- useEffect(() => {
361
- if (treeOrder !== undefined) {
362
- model.setTreeOrder(treeOrder)
363
- }
364
- }, [model, treeOrder])
365
- useEffect(() => {
366
- if (residueEncoding !== undefined) {
367
- model.setBgColor(residueEncoding === 'fill')
368
- }
369
- }, [model, residueEncoding])
370
- useEffect(() => {
371
- model.setHideHeader(!!hideHeader)
372
- }, [model, hideHeader])
373
- // unguarded, so removing the prop turns the reference diff off
374
- useEffect(() => {
375
- model.drawRelativeTo(relativeTo)
376
- }, [model, relativeTo])
377
-
378
- // The snapshot already holds the first gff and gffFilehandle, so these
379
- // effects apply only a later change. Removing either one sets the text back
380
- // to the gff prop, which clears the annotations when that is absent too.
381
- const appliedGff = useRef(gff)
382
- const gffFilehandleKey = JSON.stringify(gffFilehandle ?? null)
383
- const appliedGffFilehandle = useRef(gffFilehandleKey)
384
- useEffect(() => {
385
- if (appliedGffFilehandle.current !== gffFilehandleKey) {
386
- appliedGffFilehandle.current = gffFilehandleKey
387
- const location = JSON.parse(gffFilehandleKey) as FileLocationType | null
388
- if (location) {
389
- model.setGFFFilehandle(location)
390
- } else {
391
- model.setGFF(appliedGff.current)
392
- }
393
- }
394
- }, [model, gffFilehandleKey])
395
- useEffect(() => {
396
- if (appliedGff.current !== gff) {
397
- appliedGff.current = gff
398
- model.setGFF(gff)
399
- }
400
- }, [model, gff])
401
-
402
- // keyed by content, since a host computing a layer inline passes a new array
403
- // on every render
404
- const colorSchemeKey = JSON.stringify(colorScheme ?? null)
405
- useEffect(() => {
406
- const scheme = JSON.parse(colorSchemeKey) as ColorScheme | null
407
- if (typeof scheme === 'string') {
408
- model.setColorSchemeName(scheme)
409
- } else if (scheme) {
410
- model.setCustomColorScheme(scheme.map)
411
- }
412
- }, [model, colorSchemeKey])
413
- const highlightsKey = JSON.stringify(highlights ?? [])
414
- useEffect(() => {
415
- model.setHighlights(JSON.parse(highlightsKey))
416
- }, [model, highlightsKey])
417
- const featuresKey = JSON.stringify(features ?? [])
418
- useEffect(() => {
419
- model.setFeatures(JSON.parse(featuresKey))
420
- }, [model, featuresKey])
421
- const selectionKey = JSON.stringify(selection ?? null)
422
- useEffect(() => {
423
- model.setSelection(JSON.parse(selectionKey) ?? undefined)
424
- }, [model, selectionKey])
425
- const treeRootKey = JSON.stringify(treeRoot ?? null)
426
- useEffect(() => {
427
- model.setTreeRoot(JSON.parse(treeRootKey) ?? undefined)
428
- }, [model, treeRootKey])
429
- const cladesKey = JSON.stringify(clades ?? [])
430
- useEffect(() => {
431
- model.setClades(JSON.parse(cladesKey))
432
- }, [model, cladesKey])
433
- const columnTracksKey = JSON.stringify(columnTracks ?? [])
434
- useEffect(() => {
435
- model.setColumnTracks(JSON.parse(columnTracksKey))
436
- }, [model, columnTracksKey])
437
- const residueMappingsKey = JSON.stringify(residueMappings ?? [])
438
- useEffect(() => {
439
- model.setResidueMappings(JSON.parse(residueMappingsKey))
440
- }, [model, residueMappingsKey])
441
- const rowDataKey = JSON.stringify(rowData ?? {})
442
- useEffect(() => {
443
- const table = JSON.parse(rowDataKey)
444
- // an empty table leaves whatever the snapshot or a filehandle loaded
445
- if (Object.keys(table).length) {
446
- model.setRowData(table)
447
- }
448
- }, [model, rowDataKey])
449
- const encodingsKey = JSON.stringify(encodings ?? [])
450
- useEffect(() => {
451
- model.setEncodings(JSON.parse(encodingsKey))
452
- }, [model, encodingsKey])
453
- const rowPanelsKey = JSON.stringify(rowPanels ?? [])
454
- useEffect(() => {
455
- model.setRowPanels(JSON.parse(rowPanelsKey))
456
- }, [model, rowPanelsKey])
457
- const highlightColumnsKey = JSON.stringify(highlightColumns ?? null)
458
- useEffect(() => {
459
- model.setHighlightedColumns(JSON.parse(highlightColumnsKey) ?? undefined)
460
- }, [model, highlightColumnsKey])
482
+ useSyncedProps(model, props)
461
483
 
462
484
  const regionKey = JSON.stringify(region ?? null)
463
485
  useEffect(() => {
@@ -466,7 +488,7 @@ function Viewer({
466
488
  ? when(
467
489
  () => model.viewInitialized && model.numColumns > 0,
468
490
  () => {
469
- model.zoomToRegion(target)
491
+ model.revealRegion(target)
470
492
  },
471
493
  )
472
494
  : undefined
@@ -477,6 +499,8 @@ function Viewer({
477
499
  useModelReaction(model, 'viewport', onViewportChange)
478
500
  useModelReaction(model, 'selection', onSelectionChange)
479
501
 
502
+ useImperativeHandle(modelRef, () => model, [model])
503
+
480
504
  const latestOnModel = useRef(onModel)
481
505
  useEffect(() => {
482
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
  )
@@ -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
 
@@ -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