react-msaview 8.3.0 → 8.4.1
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 +499 -295
- 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/GoToBox.d.ts +338 -0
- package/dist/components/header/GoToBox.js +31 -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 +242 -187
- package/dist/model.js +264 -45
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +284 -336
- package/dist/types.d.ts +50 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -6
- 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 +64 -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 +313 -55
- package/src/types.ts +58 -0
- package/src/version.ts +1 -1
- package/dist/components/header/RowSearch.d.ts +0 -6
- 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
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export type TrackKind = 'text' | 'bar' | 'logo' | 'arc' | 'ruler';
|
|
|
12
12
|
* graphics draws between a shape's interior and its own ink.
|
|
13
13
|
*/
|
|
14
14
|
export type ResidueEncoding = 'fill' | 'color';
|
|
15
|
+
/**
|
|
16
|
+
* The residue colors: a scheme name from the built-in table, or a scale's
|
|
17
|
+
* `{map}` of letter to color, which leaves every letter it does not list
|
|
18
|
+
* uncolored
|
|
19
|
+
*/
|
|
20
|
+
export type ColorScheme = string | Required<Pick<ScaleSpec, 'map'>>;
|
|
15
21
|
export interface BasicTrackModel {
|
|
16
22
|
id: string;
|
|
17
23
|
name: string;
|
|
@@ -266,6 +272,25 @@ export interface Highlight {
|
|
|
266
272
|
label?: string;
|
|
267
273
|
color?: string;
|
|
268
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* A feature on one row as data, the JSON form of a GFF line: `start` and `end`
|
|
277
|
+
* are residues of `row`, 1-based and inclusive. `name` is the key the legend,
|
|
278
|
+
* the palette and the filter dialog group features by, `color` overrides the
|
|
279
|
+
* palette and any scale, and a `strand` draws the span as an arrow. Every other
|
|
280
|
+
* field is a column the `featureFill` and `featureLabel` encodings can read.
|
|
281
|
+
*/
|
|
282
|
+
export interface Feature {
|
|
283
|
+
row: string;
|
|
284
|
+
start: number;
|
|
285
|
+
end: number;
|
|
286
|
+
name?: string;
|
|
287
|
+
description?: string;
|
|
288
|
+
/** a GFF feature type; `exon`, `CDS` and the UTRs draw as numbered segments */
|
|
289
|
+
type?: string;
|
|
290
|
+
strand?: '+' | '-';
|
|
291
|
+
color?: string;
|
|
292
|
+
[field: string]: string | number | undefined;
|
|
293
|
+
}
|
|
269
294
|
/**
|
|
270
295
|
* What the viewer draws over a clade. `highlight` fills the rows behind it,
|
|
271
296
|
* across the tree and the alignment. `bracket` draws a bar and the clade's
|
|
@@ -330,6 +355,25 @@ export interface Region {
|
|
|
330
355
|
start: number;
|
|
331
356
|
end: number;
|
|
332
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* A block of the alignment the reader selected: columns `start` to `end` of the
|
|
360
|
+
* file, 1-based and inclusive like a Highlight without a row, across the rows
|
|
361
|
+
* `rows` names, or across every row where `rows` is absent.
|
|
362
|
+
*/
|
|
363
|
+
export interface MsaSelection {
|
|
364
|
+
start: number;
|
|
365
|
+
end: number;
|
|
366
|
+
rows?: string[];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* An MsaSelection resolved to what is on screen: the visible columns it covers
|
|
370
|
+
* and its rows as runs of consecutive row indices, top to bottom
|
|
371
|
+
*/
|
|
372
|
+
export interface ResolvedSelection {
|
|
373
|
+
startCol: number;
|
|
374
|
+
endCol: number;
|
|
375
|
+
rowRuns: [number, number][];
|
|
376
|
+
}
|
|
333
377
|
/**
|
|
334
378
|
* A cell of the alignment in the host's coordinates, 1-based like Highlight:
|
|
335
379
|
* `column` counts every column of the file, hidden gappy ones included, and
|
|
@@ -348,6 +392,12 @@ export interface Viewport {
|
|
|
348
392
|
startColumn: number;
|
|
349
393
|
endColumn: number;
|
|
350
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* A row table as a host passes it: fields per row name. A number or a boolean
|
|
397
|
+
* reads as its string, the way a GFF value does, and the viewer leaves out any
|
|
398
|
+
* other value. See docs/layers.md
|
|
399
|
+
*/
|
|
400
|
+
export type RowDataInput = Record<string, Record<string, string | number | boolean | null>>;
|
|
351
401
|
/**
|
|
352
402
|
* A channel of a mark the viewer always draws. `tipLabel` colors each tip label
|
|
353
403
|
* in the tree, `rowTint` washes the row across the tree gutter and the
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "8.
|
|
1
|
+
export declare const version = "8.4.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version = '8.
|
|
1
|
+
export const version = '8.4.1';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-msaview",
|
|
3
3
|
"author": "Colin",
|
|
4
|
-
"version": "8.
|
|
4
|
+
"version": "8.4.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"react-dom": ">=16.8.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@jbrowse/core": "
|
|
41
|
-
"@jbrowse/mobx-state-tree": "^6.
|
|
40
|
+
"@jbrowse/core": "^5.0.0-beta.9",
|
|
41
|
+
"@jbrowse/mobx-state-tree": "^6.6.1",
|
|
42
42
|
"@mui/icons-material": "^9.4.0",
|
|
43
43
|
"@mui/material": "^9.4.0",
|
|
44
|
-
"@types/node": "^26.
|
|
44
|
+
"@types/node": "^26.6.2",
|
|
45
45
|
"esbuild": "^0.28.2",
|
|
46
46
|
"mobx": "^7.0.3",
|
|
47
47
|
"mobx-react": "^10.0.2",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@gmod/newick": "^1.0.3",
|
|
53
|
-
"@jbrowse/svgcanvas": "8.
|
|
53
|
+
"@jbrowse/svgcanvas": "8.4.1",
|
|
54
54
|
"colord": "^2.10.0",
|
|
55
55
|
"flatbush": "^4.6.2",
|
|
56
|
-
"msa-parsers": "8.
|
|
56
|
+
"msa-parsers": "8.4.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"clean": "rimraf dist bundle",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FASTA for a block of the alignment: each row's letters from column `start`
|
|
3
|
+
* to `end`, 1-based and inclusive, under its name. Gaps stay in, and a row that
|
|
4
|
+
* ends before `end` is padded with gaps, so every record is the block's width.
|
|
5
|
+
*/
|
|
6
|
+
export function blockFasta(
|
|
7
|
+
rows: readonly (readonly [string, string])[],
|
|
8
|
+
start: number,
|
|
9
|
+
end: number,
|
|
10
|
+
) {
|
|
11
|
+
const width = Math.max(0, end - start + 1)
|
|
12
|
+
return rows
|
|
13
|
+
.map(
|
|
14
|
+
([name, seq]) =>
|
|
15
|
+
`>${name}\n${seq.slice(start - 1, end).padEnd(width, '-')}\n`,
|
|
16
|
+
)
|
|
17
|
+
.join('')
|
|
18
|
+
}
|
package/src/colorSchemes.ts
CHANGED
|
@@ -359,3 +359,23 @@ export default transform(colorSchemes, ([key, val]) => [
|
|
|
359
359
|
key,
|
|
360
360
|
transform(val, ([letter, color]) => [letter, colord(color).toHex()]),
|
|
361
361
|
])
|
|
362
|
+
|
|
363
|
+
const letterTables = new WeakMap<
|
|
364
|
+
Record<string, string>,
|
|
365
|
+
Record<string, string>
|
|
366
|
+
>()
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A letter map as the renderer looks letters up. The model upper-cases every
|
|
370
|
+
* row before coloring it, so a table keys its letters in upper case and a map
|
|
371
|
+
* may key them in either. The table is cached per map, so its identity is
|
|
372
|
+
* stable enough for the raster cache to key on.
|
|
373
|
+
*/
|
|
374
|
+
export function letterColorTable(map: Record<string, string>) {
|
|
375
|
+
let table = letterTables.get(map)
|
|
376
|
+
if (!table) {
|
|
377
|
+
table = transform(map, ([letter, color]) => [letter.toUpperCase(), color])
|
|
378
|
+
letterTables.set(map, table)
|
|
379
|
+
}
|
|
380
|
+
return table
|
|
381
|
+
}
|
|
@@ -11,7 +11,7 @@ import VerticalScrollbar from './VerticalScrollbar.tsx'
|
|
|
11
11
|
import Header from './header/Header.tsx'
|
|
12
12
|
import Minimap from './minimap/Minimap.tsx'
|
|
13
13
|
import MSAPanel from './msa/MSAPanel.tsx'
|
|
14
|
-
import { clickColor, hoverColor } from './overlayColors.ts'
|
|
14
|
+
import { clickColor, hoverColor, selectionFill } from './overlayColors.ts'
|
|
15
15
|
import RowPanelHeaders from './rowpanels/RowPanelHeaders.tsx'
|
|
16
16
|
import RowPanels from './rowpanels/RowPanels.tsx'
|
|
17
17
|
import TreeOverview from './tree/TreeOverview.tsx'
|
|
@@ -53,12 +53,19 @@ const TrackColumnIndicator = observer(function ({
|
|
|
53
53
|
resizeHandleWidth,
|
|
54
54
|
totalTrackAreaHeight,
|
|
55
55
|
msaCanvasWidth,
|
|
56
|
+
resolvedSelection,
|
|
56
57
|
} = model
|
|
57
58
|
|
|
58
|
-
// hovered column, then the pinned one, matching
|
|
59
|
+
// the selected columns, the hovered column, then the pinned one, matching
|
|
60
|
+
// the alignment's own overlay
|
|
59
61
|
const bands = [
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
+
{
|
|
63
|
+
start: resolvedSelection?.startCol,
|
|
64
|
+
end: resolvedSelection?.endCol,
|
|
65
|
+
color: selectionFill,
|
|
66
|
+
},
|
|
67
|
+
{ start: mouseCol, end: mouseCol, color: hoverColor },
|
|
68
|
+
{ start: mouseClickCol, end: mouseClickCol, color: clickColor },
|
|
62
69
|
]
|
|
63
70
|
|
|
64
71
|
return (
|
|
@@ -73,15 +80,15 @@ const TrackColumnIndicator = observer(function ({
|
|
|
73
80
|
pointerEvents: 'none',
|
|
74
81
|
}}
|
|
75
82
|
>
|
|
76
|
-
{bands.map(({
|
|
77
|
-
|
|
83
|
+
{bands.map(({ start, end, color }) =>
|
|
84
|
+
start === undefined || end === undefined ? null : (
|
|
78
85
|
<div
|
|
79
86
|
key={color}
|
|
80
87
|
style={{
|
|
81
88
|
position: 'absolute',
|
|
82
|
-
left:
|
|
89
|
+
left: start * colWidth + scrollX,
|
|
83
90
|
top: 0,
|
|
84
|
-
width: colWidth,
|
|
91
|
+
width: (end - start + 1) * colWidth,
|
|
85
92
|
height: totalTrackAreaHeight,
|
|
86
93
|
backgroundColor: color,
|
|
87
94
|
zIndex: 100,
|
|
@@ -140,7 +147,7 @@ const MSAView = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
140
147
|
<div>
|
|
141
148
|
{viewInitialized ? (
|
|
142
149
|
<>
|
|
143
|
-
<div style={{ height, overflow: '
|
|
150
|
+
<div style={{ height, overflow: 'clip' }}>
|
|
144
151
|
{hideHeader ? null : <Header model={model} />}
|
|
145
152
|
<View model={model} />
|
|
146
153
|
</div>
|
|
@@ -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>
|