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.
- package/README.md +19 -10
- package/bundle/index.js +74 -74
- package/bundle/index.js.map +4 -4
- package/dist/colorSchemes.js +1 -3
- package/dist/colorSchemes.js.map +1 -1
- package/dist/columnCounts.d.ts +0 -1
- package/dist/columnCounts.js +1 -1
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/Loading.js +5 -5
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +1891 -4
- package/dist/components/MSAViewer.js +144 -201
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.js +3 -2
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/Track.js +2 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/controls.d.ts +11 -0
- package/dist/components/controls.js +48 -0
- package/dist/components/controls.js.map +1 -0
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +5 -7
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/HelpDialog.d.ts +6 -0
- package/dist/components/dialogs/HelpDialog.js +30 -0
- package/dist/components/dialogs/HelpDialog.js.map +1 -0
- package/dist/components/header/FileMenu.js +33 -7
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/Header.js +4 -6
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -32
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +28 -44
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/settingsMenuItems.d.ts +1 -0
- package/dist/components/header/settingsMenuItems.js +44 -0
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/data/seq2.d.ts +0 -1
- package/dist/components/import/data/seq2.js +0 -17
- package/dist/components/import/data/seq2.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +2 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +2 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
- package/dist/components/msa/drawFeatureSpans.js +99 -56
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +14 -1
- package/dist/components/msa/msaRaster.js +19 -9
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +2 -2
- package/dist/components/msa/renderHighlights.js +2 -3
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +2 -1
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
- package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
- package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -53
- package/dist/components/tracks/drawTracks.js +5 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/CladeLabels.d.ts +1 -1
- package/dist/components/tree/CladeLabels.js +22 -34
- package/dist/components/tree/CladeLabels.js.map +1 -1
- package/dist/components/tree/cladeBrackets.d.ts +17 -10
- package/dist/components/tree/cladeBrackets.js +33 -3
- package/dist/components/tree/cladeBrackets.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -8
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeOverview.js +4 -11
- package/dist/components/tree/renderTreeOverview.js.map +1 -1
- package/dist/element.js +3 -6
- package/dist/element.js.map +1 -1
- package/dist/exportLayout.d.ts +39 -0
- package/dist/exportLayout.js +78 -0
- package/dist/exportLayout.js.map +1 -0
- package/dist/headlessRenderEnv.d.ts +12 -39
- package/dist/headlessRenderEnv.js +18 -12
- package/dist/headlessRenderEnv.js.map +1 -1
- package/dist/hierarchy.d.ts +5 -8
- package/dist/hierarchy.js +5 -79
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/measureTextCanvas.d.ts +7 -0
- package/dist/measureTextCanvas.js +8 -6
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/treeModel.d.ts +0 -5
- package/dist/model/treeModel.js +0 -7
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +38 -44
- package/dist/model.js +153 -112
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.d.ts +2 -7
- package/dist/renderToSvg.js +32 -120
- package/dist/renderToSvg.js.map +1 -1
- package/dist/svgTestUtil.d.ts +12 -20
- package/dist/types.d.ts +0 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewerSnapshot.d.ts +169 -0
- package/dist/viewerSnapshot.js +100 -0
- package/dist/viewerSnapshot.js.map +1 -0
- package/package.json +3 -3
- package/src/colorSchemes.ts +1 -4
- package/src/columnCounts.ts +1 -1
- package/src/components/Loading.tsx +19 -12
- package/src/components/MSAViewer.tsx +275 -251
- package/src/components/ResizeHandles.tsx +9 -2
- package/src/components/Track.tsx +2 -1
- package/src/components/controls.ts +61 -0
- package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
- package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
- package/src/components/dialogs/HelpDialog.tsx +79 -0
- package/src/components/header/FileMenu.tsx +33 -7
- package/src/components/header/Header.tsx +4 -6
- package/src/components/header/TreeSettingsMenu.tsx +2 -32
- package/src/components/header/ZoomControls.tsx +30 -66
- package/src/components/header/settingsMenuItems.ts +59 -0
- package/src/components/import/data/seq2.ts +0 -18
- package/src/components/msa/MSACanvas.tsx +2 -1
- package/src/components/msa/MSACanvasBlock.tsx +2 -6
- package/src/components/msa/drawFeatureSpans.ts +160 -79
- package/src/components/msa/msaRaster.ts +27 -9
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
- package/src/components/msa/renderHighlights.ts +2 -3
- package/src/components/msa/renderMSAMouseover.ts +5 -1
- package/src/components/renderCtx.ts +0 -1
- package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
- package/src/components/tracks/drawTracks.ts +5 -5
- package/src/components/tree/CladeLabels.tsx +30 -54
- package/src/components/tree/cladeBrackets.ts +50 -3
- package/src/components/tree/renderTreeCanvas.ts +2 -13
- package/src/components/tree/renderTreeOverview.ts +7 -14
- package/src/element.ts +3 -7
- package/src/exportLayout.ts +134 -0
- package/src/headlessRenderEnv.ts +24 -11
- package/src/hierarchy.ts +5 -88
- package/src/index.ts +5 -5
- package/src/measureTextCanvas.ts +8 -6
- package/src/model/treeModel.ts +0 -7
- package/src/model.ts +170 -122
- package/src/renderToSvg.tsx +55 -194
- package/src/types.ts +0 -1
- package/src/version.ts +1 -1
- package/src/viewerSnapshot.ts +147 -0
- package/dist/components/header/ZoomMenu.d.ts +0 -6
- package/dist/components/header/ZoomMenu.js +0 -37
- package/dist/components/header/ZoomMenu.js.map +0 -1
- package/src/components/header/ZoomMenu.tsx +0 -48
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import 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
|
-
/**
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
setShown({ source, generation:
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
)
|
package/src/components/Track.tsx
CHANGED
|
@@ -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
|
-
<
|
|
188
|
+
<TrackBlocks model={model} track={track} />
|
|
188
189
|
{anchor ? (
|
|
189
190
|
<BaseTooltip clientPoint={anchor.clientPoint}>
|
|
190
191
|
<TrackTooltipContent
|