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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
2
|
+
|
|
3
|
+
export function findableRowNames(model: MsaViewModel) {
|
|
4
|
+
const collapsed = new Set(model.collapsed)
|
|
5
|
+
return model.leaves
|
|
6
|
+
.filter(({ data: { id, name } }) => !(collapsed.has(id) && name === id))
|
|
7
|
+
.map(leaf => leaf.data.name)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// not MUI's createFilterOptions: JBrowse hosts do not re-export it, so
|
|
11
|
+
// jbrowse-plugin-msaview would fail to load
|
|
12
|
+
export function rowNameSuggestions(names: string[], input: string, limit = 50) {
|
|
13
|
+
const query = input.toLowerCase()
|
|
14
|
+
return names
|
|
15
|
+
.filter(name => name.toLowerCase().includes(query))
|
|
16
|
+
.slice(0, limit)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The visible row and column a "Go to" entry names: a row by its name, the
|
|
21
|
+
* column the ruler numbers N, or residue N of a row written `name:N`. A name
|
|
22
|
+
* is tried whole first, so a row called `chr1:100` stays reachable.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveGoTo(model: MsaViewModel, text: string) {
|
|
25
|
+
const query = text.trim()
|
|
26
|
+
const row = model.rowNamesSet.get(query)
|
|
27
|
+
if (row !== undefined) {
|
|
28
|
+
return { row }
|
|
29
|
+
}
|
|
30
|
+
const residue = /^(.+):(\d+)$/.exec(query)
|
|
31
|
+
if (residue) {
|
|
32
|
+
const [, name = '', pos] = residue
|
|
33
|
+
const rowIndex = model.rowNamesSet.get(name)
|
|
34
|
+
const col =
|
|
35
|
+
rowIndex === undefined ? undefined : columnOfResidue(model, name, +pos!)
|
|
36
|
+
return col === undefined ? undefined : { row: rowIndex, col }
|
|
37
|
+
}
|
|
38
|
+
if (/^\d+$/.test(query)) {
|
|
39
|
+
const n = +query
|
|
40
|
+
const { relativeTo, numColumns } = model
|
|
41
|
+
const col = relativeTo
|
|
42
|
+
? columnOfResidue(model, relativeTo, n)
|
|
43
|
+
: n >= 1 && n <= numColumns
|
|
44
|
+
? n - 1
|
|
45
|
+
: undefined
|
|
46
|
+
return col === undefined ? undefined : { col }
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// seqPosToVisibleCol maps a residue past the row's end to the column after
|
|
52
|
+
// it; here that residue names nothing
|
|
53
|
+
function columnOfResidue(model: MsaViewModel, name: string, pos: number) {
|
|
54
|
+
const globalCol = model.seqPosIndex(name)?.[pos - 1]
|
|
55
|
+
return globalCol === undefined
|
|
56
|
+
? undefined
|
|
57
|
+
: model.globalColToVisibleCol(globalCol)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** centers the view on the entry's target and lights it; false if none */
|
|
61
|
+
export function goTo(model: MsaViewModel, text: string) {
|
|
62
|
+
const target = resolveGoTo(model, text)
|
|
63
|
+
if (target) {
|
|
64
|
+
const { row, col } = target
|
|
65
|
+
const { rowHeight, colWidth, msaAreaHeight, msaAreaWidth } = model
|
|
66
|
+
if (row !== undefined) {
|
|
67
|
+
model.setScrollY(-(row + 0.5) * rowHeight + msaAreaHeight / 2)
|
|
68
|
+
}
|
|
69
|
+
if (col !== undefined) {
|
|
70
|
+
model.setScrollX(-(col + 0.5) * colWidth + msaAreaWidth / 2)
|
|
71
|
+
}
|
|
72
|
+
model.setMousePos(col, row)
|
|
73
|
+
}
|
|
74
|
+
return target !== undefined
|
|
75
|
+
}
|
|
@@ -54,13 +54,23 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
54
54
|
// col*colWidth + scrollX, row*rowHeight + scrollY
|
|
55
55
|
data-testid="msa_canvas"
|
|
56
56
|
tabIndex={0}
|
|
57
|
-
aria-label="Alignment. Arrow keys scroll, shift pages, + and - zoom, Home and End jump to the first and last column."
|
|
57
|
+
aria-label="Alignment. Arrow keys scroll, shift pages, + and - zoom, Home and End jump to the first and last column. Shift-drag selects cells, and Escape clears the selection."
|
|
58
58
|
onKeyDown={event => {
|
|
59
59
|
if (onMsaKey(model, event)) {
|
|
60
60
|
event.preventDefault()
|
|
61
61
|
}
|
|
62
62
|
}}
|
|
63
|
-
onMouseDown={
|
|
63
|
+
onMouseDown={event => {
|
|
64
|
+
// a shifted press selects cells (useMsaBlockMouse), so it takes the
|
|
65
|
+
// focus Escape clears from, and leaves out the pan and the page's own
|
|
66
|
+
// text selection
|
|
67
|
+
if (event.shiftKey) {
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
ref.current?.focus({ preventScroll: true })
|
|
70
|
+
} else {
|
|
71
|
+
onMouseDown(event)
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
64
74
|
onMouseUp={onMouseUp}
|
|
65
75
|
style={{
|
|
66
76
|
position: 'relative',
|
|
@@ -71,7 +71,11 @@ const MSACanvasBlock = observer(function ({
|
|
|
71
71
|
onMouseMove(event, ref.current)
|
|
72
72
|
}
|
|
73
73
|
}}
|
|
74
|
-
onMouseDown={
|
|
74
|
+
onMouseDown={event => {
|
|
75
|
+
if (ref.current) {
|
|
76
|
+
onMouseDown(event, ref.current)
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
75
79
|
onClick={event => {
|
|
76
80
|
if (ref.current) {
|
|
77
81
|
onClick(event, ref.current)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { setFontSize } from '../../setFontSize.ts'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
referenceColor,
|
|
4
|
+
selectionBorder,
|
|
5
|
+
selectionFill,
|
|
6
|
+
} from '../overlayColors.ts'
|
|
3
7
|
|
|
4
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
5
9
|
import type { RenderCtx } from '../renderCtx.ts'
|
|
@@ -172,11 +176,48 @@ export function renderHighlights({
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The selected block, a bordered band per run of consecutive selected rows,
|
|
181
|
+
* culled to the rows this block covers
|
|
182
|
+
*/
|
|
183
|
+
export function renderSelection({
|
|
184
|
+
ctx,
|
|
185
|
+
model,
|
|
186
|
+
offsetX,
|
|
187
|
+
offsetY,
|
|
188
|
+
height,
|
|
189
|
+
}: {
|
|
190
|
+
ctx: RenderCtx
|
|
191
|
+
model: MsaViewModel
|
|
192
|
+
offsetX: number
|
|
193
|
+
offsetY: number
|
|
194
|
+
height: number
|
|
195
|
+
}) {
|
|
196
|
+
const { resolvedSelection, colWidth, rowHeight } = model
|
|
197
|
+
if (!resolvedSelection) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
const { startCol, endCol, rowRuns } = resolvedSelection
|
|
201
|
+
const x = startCol * colWidth - offsetX
|
|
202
|
+
const w = (endCol - startCol + 1) * colWidth
|
|
203
|
+
ctx.lineWidth = 1
|
|
204
|
+
for (const [first, last] of rowRuns) {
|
|
205
|
+
const y = first * rowHeight - offsetY
|
|
206
|
+
const h = (last - first + 1) * rowHeight
|
|
207
|
+
if (y < height && y + h > 0) {
|
|
208
|
+
ctx.fillStyle = selectionFill
|
|
209
|
+
ctx.fillRect(x, y, w, h)
|
|
210
|
+
ctx.strokeStyle = selectionBorder
|
|
211
|
+
ctx.strokeRect(x, y, w, h)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/**
|
|
176
217
|
* The overlay parts that come from the snapshot: the reference row's tint, a
|
|
177
|
-
* bordered band per run of highlighted columns,
|
|
178
|
-
* live overlay draws these under its hover and click
|
|
179
|
-
* draws only these.
|
|
218
|
+
* bordered band per run of highlighted columns, the `highlights` layer and the
|
|
219
|
+
* selected block. The live overlay draws these under its hover and click
|
|
220
|
+
* bands; the SVG export draws only these.
|
|
180
221
|
*/
|
|
181
222
|
export function renderPersistentHighlights({
|
|
182
223
|
ctx,
|
|
@@ -214,4 +255,5 @@ export function renderPersistentHighlights({
|
|
|
214
255
|
}
|
|
215
256
|
|
|
216
257
|
renderHighlights({ ctx, model, theme, offsetX, offsetY, width, height })
|
|
258
|
+
renderSelection({ ctx, model, offsetX, offsetY, height })
|
|
217
259
|
}
|
|
@@ -4,12 +4,12 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
4
4
|
// only the active scheme's column table leaves the other per-column table
|
|
5
5
|
// uncomputed.
|
|
6
6
|
export function tileColorFn(model: MsaViewModel) {
|
|
7
|
-
const {
|
|
8
|
-
if (
|
|
7
|
+
const { dynamicColorSchemeName } = model
|
|
8
|
+
if (dynamicColorSchemeName === 'clustalx_protein_dynamic') {
|
|
9
9
|
const { colClustalX } = model
|
|
10
10
|
return (col: number, letter: string) => colClustalX[col]![letter]
|
|
11
11
|
}
|
|
12
|
-
if (
|
|
12
|
+
if (dynamicColorSchemeName === 'percent_identity_dynamic') {
|
|
13
13
|
const { colConsensus } = model
|
|
14
14
|
return (col: number, letter: string) => {
|
|
15
15
|
const consensus = colConsensus[col]!
|
|
@@ -20,14 +20,15 @@ export function tileColorFn(model: MsaViewModel) {
|
|
|
20
20
|
return (_col: number, letter: string) => colorScheme[letter]
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// The
|
|
24
|
-
//
|
|
23
|
+
// The table the active scheme colors from: a dynamic scheme's per-column table,
|
|
24
|
+
// or the letter table of a static scheme or a `customColorScheme` map. A cache
|
|
25
|
+
// of rendered colors keys on its identity plus the scheme name.
|
|
25
26
|
export function tileColorTable(model: MsaViewModel) {
|
|
26
|
-
const {
|
|
27
|
-
if (
|
|
27
|
+
const { dynamicColorSchemeName } = model
|
|
28
|
+
if (dynamicColorSchemeName === 'clustalx_protein_dynamic') {
|
|
28
29
|
return model.colClustalX
|
|
29
30
|
}
|
|
30
|
-
if (
|
|
31
|
+
if (dynamicColorSchemeName === 'percent_identity_dynamic') {
|
|
31
32
|
return model.colConsensus
|
|
32
33
|
}
|
|
33
34
|
return model.colorScheme
|
|
@@ -1,34 +1,12 @@
|
|
|
1
1
|
import { useRef } from 'react'
|
|
2
2
|
|
|
3
|
+
import { dragSelection, maxClickTravel } from '../selectionDrag.ts'
|
|
3
4
|
import { useHoverAnchor } from '../useHoverAnchor.ts'
|
|
4
5
|
|
|
5
6
|
import type { MsaViewModel } from '../../model.ts'
|
|
7
|
+
import type { ClientPoint } from '../selectionDrag.ts'
|
|
6
8
|
import type React from 'react'
|
|
7
9
|
|
|
8
|
-
const maxClickTravel = 3
|
|
9
|
-
|
|
10
|
-
function eventToColRow({
|
|
11
|
-
event,
|
|
12
|
-
el,
|
|
13
|
-
offsetX,
|
|
14
|
-
offsetY,
|
|
15
|
-
colWidth,
|
|
16
|
-
rowHeight,
|
|
17
|
-
}: {
|
|
18
|
-
event: React.MouseEvent
|
|
19
|
-
el: HTMLElement
|
|
20
|
-
offsetX: number
|
|
21
|
-
offsetY: number
|
|
22
|
-
colWidth: number
|
|
23
|
-
rowHeight: number
|
|
24
|
-
}) {
|
|
25
|
-
const { left, top } = el.getBoundingClientRect()
|
|
26
|
-
return {
|
|
27
|
-
col: Math.floor((event.clientX - left + offsetX) / colWidth),
|
|
28
|
-
row: Math.floor((event.clientY - top + offsetY) / rowHeight),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
10
|
/**
|
|
33
11
|
* Translates pointer events over one MSA block into model column/row hover and
|
|
34
12
|
* click state, and reports where a tooltip should be anchored.
|
|
@@ -51,9 +29,23 @@ export function useMsaBlockMouse({
|
|
|
51
29
|
const { anchor, hoverAt, clearAnchor } = useHoverAnchor<true>()
|
|
52
30
|
const downAt = useRef<{ x: number; y: number }>(undefined)
|
|
53
31
|
|
|
32
|
+
// The block's rect places its cells on the page. A drag keeps the mapping
|
|
33
|
+
// it started with, so each point subtracts the scroll since then.
|
|
34
|
+
function cellMapper(el: HTMLElement) {
|
|
35
|
+
const { left, top } = el.getBoundingClientRect()
|
|
36
|
+
const { scrollX, scrollY } = model
|
|
37
|
+
return ({ clientX, clientY }: ClientPoint) => ({
|
|
38
|
+
col: Math.floor(
|
|
39
|
+
(clientX - left + offsetX - (model.scrollX - scrollX)) / model.colWidth,
|
|
40
|
+
),
|
|
41
|
+
row: Math.floor(
|
|
42
|
+
(clientY - top + offsetY - (model.scrollY - scrollY)) / model.rowHeight,
|
|
43
|
+
),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
54
47
|
function colRow(event: React.MouseEvent, el: HTMLElement) {
|
|
55
|
-
|
|
56
|
-
return eventToColRow({ event, el, offsetX, offsetY, colWidth, rowHeight })
|
|
48
|
+
return cellMapper(el)(event)
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
// a block can extend past the alignment (the last block is a full tile wide,
|
|
@@ -73,8 +65,13 @@ export function useMsaBlockMouse({
|
|
|
73
65
|
hoverAt(event, hasTooltip || undefined)
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
|
|
68
|
+
// shift+drag selects a block; MSACanvas leaves a shifted press out of its
|
|
69
|
+
// pan
|
|
70
|
+
function onMouseDown(event: React.MouseEvent, el: HTMLElement) {
|
|
77
71
|
downAt.current = { x: event.clientX, y: event.clientY }
|
|
72
|
+
if (event.shiftKey && event.button === 0) {
|
|
73
|
+
dragSelection(model, event, cellMapper(el))
|
|
74
|
+
}
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
function onClick(event: React.MouseEvent, el: HTMLElement) {
|
|
@@ -16,3 +16,7 @@ export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
|
|
|
16
16
|
// match or the crosshair appears to change color as it crosses the tracks.
|
|
17
17
|
export const hoverColor = 'rgba(0,0,0,0.15)'
|
|
18
18
|
export const clickColor = 'rgba(128,128,0,0.2)'
|
|
19
|
+
|
|
20
|
+
// the selected block, a blue that reads apart from the orange highlights
|
|
21
|
+
export const selectionFill = 'rgba(25,118,210,0.22)'
|
|
22
|
+
export const selectionBorder = 'rgba(25,118,210,0.95)'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
2
|
+
|
|
3
|
+
import type { MsaViewModel } from '../model.ts'
|
|
4
|
+
|
|
5
|
+
// a press that travels further than this, in px, is a drag and not a click
|
|
6
|
+
export const maxClickTravel = 3
|
|
7
|
+
|
|
8
|
+
export interface ClientPoint {
|
|
9
|
+
clientX: number
|
|
10
|
+
clientY: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Follows a press through the window until its release, selecting the block
|
|
15
|
+
* between the cell it started on and the cell under the pointer. The selection
|
|
16
|
+
* starts once the pointer travels past `maxClickTravel`, so a press that stays
|
|
17
|
+
* put reaches the click handlers unchanged. `cellAt` maps a client point to a
|
|
18
|
+
* visible column and row index, or a column alone to select across every row.
|
|
19
|
+
*/
|
|
20
|
+
export function dragSelection(
|
|
21
|
+
model: MsaViewModel,
|
|
22
|
+
start: ClientPoint,
|
|
23
|
+
cellAt: (point: ClientPoint) => { col: number; row?: number },
|
|
24
|
+
) {
|
|
25
|
+
const anchor = cellAt(start)
|
|
26
|
+
const { clientX, clientY } = start
|
|
27
|
+
let dragging = false
|
|
28
|
+
function onMove(event: MouseEvent) {
|
|
29
|
+
dragging ||=
|
|
30
|
+
Math.hypot(event.clientX - clientX, event.clientY - clientY) >
|
|
31
|
+
maxClickTravel
|
|
32
|
+
if (dragging && isAlive(model)) {
|
|
33
|
+
model.selectBlock(anchor, cellAt(event))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function onUp() {
|
|
37
|
+
window.removeEventListener('mousemove', onMove)
|
|
38
|
+
window.removeEventListener('mouseup', onUp)
|
|
39
|
+
}
|
|
40
|
+
window.addEventListener('mousemove', onMove)
|
|
41
|
+
window.addEventListener('mouseup', onUp)
|
|
42
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dragSelection } from '../selectionDrag.ts'
|
|
1
2
|
import { useHoverAnchor } from '../useHoverAnchor.ts'
|
|
2
3
|
|
|
3
4
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -6,7 +7,8 @@ import type React from 'react'
|
|
|
6
7
|
/**
|
|
7
8
|
* Translates pointer events over a track's row into the hovered column and a
|
|
8
9
|
* tooltip anchor. The track shares the alignment's columns, so hovering it sets
|
|
9
|
-
* the model's mouse column too and the alignment's hover band follows along.
|
|
10
|
+
* the model's mouse column too and the alignment's hover band follows along. A
|
|
11
|
+
* drag along the row selects the columns it covers across every row.
|
|
10
12
|
*
|
|
11
13
|
* `tooltip` off skips the anchor, so a track whose tooltip is turned off still
|
|
12
14
|
* drives the hover band without re-rendering on every mouse move.
|
|
@@ -22,21 +24,41 @@ export function useTrackHover({
|
|
|
22
24
|
|
|
23
25
|
// the blocks are laid out in column space and scrolled by a transform on
|
|
24
26
|
// their container, so the pointer's column comes off the untransformed row
|
|
27
|
+
function colAt(clientX: number, left: number) {
|
|
28
|
+
return Math.floor((clientX - left - model.scrollX) / model.colWidth)
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
function onMouseMove(event: React.MouseEvent, el: HTMLElement) {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const col = Math.floor((event.clientX - left - scrollX) / colWidth)
|
|
29
|
-
const hit = col >= 0 && col < numColumns
|
|
32
|
+
const col = colAt(event.clientX, el.getBoundingClientRect().left)
|
|
33
|
+
const hit = col >= 0 && col < model.numColumns
|
|
30
34
|
model.setMousePos(hit ? col : undefined, undefined)
|
|
31
35
|
if (tooltip) {
|
|
32
36
|
hoverAt(event, hit ? col : undefined)
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
// a press on a track's canvas, which leaves out the resize divider under it.
|
|
41
|
+
// The row takes the focus, so Escape reaches onMsaKey from there too
|
|
42
|
+
function onMouseDown(event: React.MouseEvent, el: HTMLElement) {
|
|
43
|
+
if (event.button === 0 && event.target instanceof HTMLCanvasElement) {
|
|
44
|
+
event.preventDefault()
|
|
45
|
+
el.focus({ preventScroll: true })
|
|
46
|
+
const { left } = el.getBoundingClientRect()
|
|
47
|
+
dragSelection(model, event, ({ clientX }) => ({
|
|
48
|
+
col: colAt(clientX, left),
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
function onMouseLeave() {
|
|
37
54
|
clearAnchor()
|
|
38
55
|
model.setMousePos(undefined, undefined)
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
anchor: tooltip ? anchor : undefined,
|
|
60
|
+
onMouseMove,
|
|
61
|
+
onMouseDown,
|
|
62
|
+
onMouseLeave,
|
|
63
|
+
}
|
|
42
64
|
}
|
|
@@ -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 from '../../MetadataTable.tsx'
|
|
9
8
|
import SequenceTextArea from '../../SequenceTextArea.tsx'
|
|
10
9
|
|
|
11
10
|
import type { MsaViewModel } from '../../../model.ts'
|
|
@@ -34,20 +33,19 @@ const TreeNodeInfoDialog = observer(function ({
|
|
|
34
33
|
maxWidth="xl"
|
|
35
34
|
>
|
|
36
35
|
<DialogContent>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
</
|
|
40
|
-
|
|
41
|
-
{res
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)}
|
|
46
|
-
</BaseCard>
|
|
36
|
+
<Typography variant="h6">Attributes</Typography>
|
|
37
|
+
<MetadataTable record={{ nodeName, ...info }} />
|
|
38
|
+
<Typography variant="h6">Sequence</Typography>
|
|
39
|
+
{res ? (
|
|
40
|
+
<SequenceTextArea str={[res]} />
|
|
41
|
+
) : (
|
|
42
|
+
<Typography>Sequence not found</Typography>
|
|
43
|
+
)}
|
|
47
44
|
{metadata ? (
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
45
|
+
<>
|
|
46
|
+
<Typography variant="h6">Extra metadata</Typography>
|
|
47
|
+
<MetadataTable record={metadata} />
|
|
48
|
+
</>
|
|
51
49
|
) : null}
|
|
52
50
|
</DialogContent>
|
|
53
51
|
</Dialog>
|
package/src/constants.ts
CHANGED
|
@@ -106,12 +106,12 @@ export const rowTintAlpha = 0.25
|
|
|
106
106
|
export const cladeHighlightColor = '#fff3c4'
|
|
107
107
|
export const cladeHighlightAlpha = 0.6
|
|
108
108
|
|
|
109
|
-
// The largest inline document
|
|
110
|
-
// locally-opened file stays in the
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
export const maxInlineSnapshotBytes =
|
|
109
|
+
// The largest inline document a snapshot carries by default, which keeps a
|
|
110
|
+
// jbrowse session small. A larger pasted or locally-opened file stays in the
|
|
111
|
+
// live model and out of the snapshot, and `unshareableData` warns using the
|
|
112
|
+
// same number. `MSAModelF({ maxInlineSnapshotBytes })` sets another; the demo
|
|
113
|
+
// app's links hold far more.
|
|
114
|
+
export const maxInlineSnapshotBytes = 50_000
|
|
115
115
|
|
|
116
116
|
// Height of the band the `rowPanels` headers draw in, above the strips in the
|
|
117
117
|
// top area beside the tree ruler and the minimap. A panel wider than the band
|
package/src/element.ts
CHANGED
|
@@ -7,15 +7,26 @@ import type { MsaViewModel } from './model.ts'
|
|
|
7
7
|
import type { MountedViewer } from './mount.tsx'
|
|
8
8
|
import type {
|
|
9
9
|
Cell,
|
|
10
|
+
ColorScheme,
|
|
10
11
|
ColumnTrackSpec,
|
|
12
|
+
Feature,
|
|
11
13
|
Highlight,
|
|
14
|
+
MsaSelection,
|
|
12
15
|
ResidueMapping,
|
|
13
16
|
Viewport,
|
|
14
17
|
} from './types.ts'
|
|
15
18
|
|
|
16
19
|
type ElementData = Pick<
|
|
17
20
|
MSAViewerProps,
|
|
18
|
-
|
|
21
|
+
| 'msa'
|
|
22
|
+
| 'tree'
|
|
23
|
+
| 'gff'
|
|
24
|
+
| 'features'
|
|
25
|
+
| 'colorScheme'
|
|
26
|
+
| 'highlights'
|
|
27
|
+
| 'columnTracks'
|
|
28
|
+
| 'residueMappings'
|
|
29
|
+
| 'selection'
|
|
19
30
|
>
|
|
20
31
|
|
|
21
32
|
interface NightingaleManager extends HTMLElement {
|
|
@@ -78,9 +89,13 @@ function residueRange(model: MsaViewModel, row: string, viewport: Viewport) {
|
|
|
78
89
|
/**
|
|
79
90
|
* Registers a custom element that renders MSAViewer, for pages with no React.
|
|
80
91
|
* Attributes: msa-url, tree-url, gff-url, color-scheme, height, hide-header,
|
|
81
|
-
* theme, tree-area-width and reference-row. Properties: msa, tree and gff text,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
92
|
+
* theme, tree-area-width and reference-row. Properties: msa, tree and gff text,
|
|
93
|
+
* features, colorScheme, highlights, columnTracks, residueMappings and
|
|
94
|
+
* selection. The
|
|
95
|
+
* colorScheme property takes a `{map}` the color-scheme attribute cannot hold,
|
|
96
|
+
* and wins over the attribute while set. Events: cell-hover, cell-click,
|
|
97
|
+
* viewport-change and selection-change, each with the MSAViewer callback's
|
|
98
|
+
* value as detail.
|
|
84
99
|
*
|
|
85
100
|
* Inside a <nightingale-manager>, the element registers with it and speaks its
|
|
86
101
|
* protocol: it follows the display-start, display-end and highlight attributes
|
|
@@ -122,6 +137,18 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
122
137
|
set gff(value: string | undefined) {
|
|
123
138
|
this.#setData({ gff: value })
|
|
124
139
|
}
|
|
140
|
+
get features() {
|
|
141
|
+
return this.#data.features
|
|
142
|
+
}
|
|
143
|
+
set features(value: Feature[] | undefined) {
|
|
144
|
+
this.#setData({ features: value })
|
|
145
|
+
}
|
|
146
|
+
get colorScheme() {
|
|
147
|
+
return this.#data.colorScheme
|
|
148
|
+
}
|
|
149
|
+
set colorScheme(value: ColorScheme | undefined) {
|
|
150
|
+
this.#setData({ colorScheme: value })
|
|
151
|
+
}
|
|
125
152
|
get highlights() {
|
|
126
153
|
return this.#data.highlights
|
|
127
154
|
}
|
|
@@ -140,6 +167,13 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
140
167
|
set residueMappings(value: ResidueMapping[] | undefined) {
|
|
141
168
|
this.#setData({ residueMappings: value })
|
|
142
169
|
}
|
|
170
|
+
// the viewer's selection, which the reader's drags change
|
|
171
|
+
get selection() {
|
|
172
|
+
return this.#model ? this.#model.selection : this.#data.selection
|
|
173
|
+
}
|
|
174
|
+
set selection(value: MsaSelection | undefined) {
|
|
175
|
+
this.#setData({ selection: value })
|
|
176
|
+
}
|
|
143
177
|
|
|
144
178
|
get #referenceRow() {
|
|
145
179
|
return this.getAttribute('reference-row') ?? undefined
|
|
@@ -157,7 +191,10 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
157
191
|
msaFilehandle: location(this.getAttribute('msa-url')),
|
|
158
192
|
treeFilehandle: location(this.getAttribute('tree-url')),
|
|
159
193
|
gffFilehandle: location(this.getAttribute('gff-url')),
|
|
160
|
-
colorScheme:
|
|
194
|
+
colorScheme:
|
|
195
|
+
this.#data.colorScheme ??
|
|
196
|
+
this.getAttribute('color-scheme') ??
|
|
197
|
+
undefined,
|
|
161
198
|
height: numberAttribute(this.getAttribute('height')),
|
|
162
199
|
treeAreaWidth: numberAttribute(this.getAttribute('tree-area-width')),
|
|
163
200
|
hideHeader: this.hasAttribute('hide-header'),
|
|
@@ -173,6 +210,9 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
173
210
|
this.#emit('viewport-change', viewport)
|
|
174
211
|
this.#reportRange(viewport)
|
|
175
212
|
},
|
|
213
|
+
onSelectionChange: selection => {
|
|
214
|
+
this.#emit('selection-change', selection)
|
|
215
|
+
},
|
|
176
216
|
onModel: model => {
|
|
177
217
|
this.#model = model
|
|
178
218
|
this.#followRange()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Annotation, Feature } from './types.ts'
|
|
2
|
+
|
|
3
|
+
const placement = new Set(['row', 'start', 'end'])
|
|
4
|
+
|
|
5
|
+
// The colors and labels are maps keyed by annotation, and a getter read
|
|
6
|
+
// outside a reaction recomputes on every read. Caching on the frozen feature
|
|
7
|
+
// gives it one annotation however often the layer converts.
|
|
8
|
+
const converted = new WeakMap<Feature, Annotation>()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The `features` layer as the annotations a GFF would parse to. Every field
|
|
12
|
+
* but the placement is also an attribute, as a string like a GFF value, so an
|
|
13
|
+
* encoding reads `type` or a host's own field the way it reads a GFF column 9
|
|
14
|
+
* key.
|
|
15
|
+
*/
|
|
16
|
+
export function featuresToAnnotations(features: Feature[]): Annotation[] {
|
|
17
|
+
return features.map(feature => {
|
|
18
|
+
const cached = converted.get(feature)
|
|
19
|
+
if (cached) {
|
|
20
|
+
return cached
|
|
21
|
+
}
|
|
22
|
+
const annotation = featureAnnotation(feature)
|
|
23
|
+
converted.set(feature, annotation)
|
|
24
|
+
return annotation
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function featureAnnotation(feature: Feature): Annotation {
|
|
29
|
+
const { row, start, end, name, description, type, strand, color } = feature
|
|
30
|
+
const accession = name ?? `${row}:${start}-${end}`
|
|
31
|
+
const attributes = Object.fromEntries(
|
|
32
|
+
Object.entries(feature)
|
|
33
|
+
.filter(([key, value]) => !placement.has(key) && value !== undefined)
|
|
34
|
+
.map(([key, value]) => [key, String(value)]),
|
|
35
|
+
)
|
|
36
|
+
return {
|
|
37
|
+
id: row,
|
|
38
|
+
accession,
|
|
39
|
+
name: name ?? accession,
|
|
40
|
+
description: description ?? name ?? accession,
|
|
41
|
+
featureType: type,
|
|
42
|
+
start,
|
|
43
|
+
end,
|
|
44
|
+
strand: strand === '+' ? 1 : strand === '-' ? -1 : undefined,
|
|
45
|
+
color,
|
|
46
|
+
attributes,
|
|
47
|
+
}
|
|
48
|
+
}
|