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