jbrowse-plugin-mafviewer 1.2.4 → 1.3.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/dist/LinearMafDisplay/components/Crosshairs.d.ts +10 -0
- package/dist/LinearMafDisplay/components/Crosshairs.js +18 -0
- package/dist/LinearMafDisplay/components/Crosshairs.js.map +1 -0
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.d.ts +11 -0
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js +97 -0
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js.map +1 -0
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +147 -29
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
- package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +12 -0
- package/dist/LinearMafDisplay/components/MAFTooltip.js +29 -0
- package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -0
- package/dist/LinearMafDisplay/components/SetRowHeightDialog/SetRowHeightDialog.js.map +1 -0
- package/dist/LinearMafDisplay/components/{ColorLegend.d.ts → Sidebar/ColorLegend.d.ts} +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -0
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -0
- package/dist/LinearMafDisplay/components/{SvgWrapper.d.ts → Sidebar/SvgWrapper.d.ts} +1 -1
- package/dist/LinearMafDisplay/components/{SvgWrapper.js → Sidebar/SvgWrapper.js} +3 -1
- package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -0
- package/dist/LinearMafDisplay/components/{Tree.d.ts → Sidebar/Tree.d.ts} +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -0
- package/dist/LinearMafDisplay/components/{YScaleBars.d.ts → Sidebar/YScaleBars.d.ts} +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -0
- package/dist/LinearMafDisplay/renderSvg.js +1 -1
- package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
- package/dist/LinearMafDisplay/stateModel.d.ts +8 -6
- package/dist/LinearMafDisplay/stateModel.js +21 -2
- package/dist/LinearMafDisplay/stateModel.js.map +1 -1
- package/dist/LinearMafDisplay/types.d.ts +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +1 -0
- package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
- package/dist/LinearMafRenderer/makeImageData.d.ts +1 -0
- package/dist/LinearMafRenderer/makeImageData.js +7 -4
- package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
- package/dist/{MafRPC/index.d.ts → MafGetSamples/MafGetSamples.d.ts} +1 -3
- package/dist/{MafRPC/index.js → MafGetSamples/MafGetSamples.js} +2 -7
- package/dist/MafGetSamples/MafGetSamples.js.map +1 -0
- package/dist/MafGetSamples/index.d.ts +2 -0
- package/dist/MafGetSamples/index.js +7 -0
- package/dist/MafGetSamples/index.js.map +1 -0
- package/dist/MafGetSequences/MafGetSequences.d.ts +16 -0
- package/dist/MafGetSequences/MafGetSequences.js +20 -0
- package/dist/MafGetSequences/MafGetSequences.js.map +1 -0
- package/dist/MafGetSequences/index.d.ts +2 -0
- package/dist/MafGetSequences/index.js +7 -0
- package/dist/MafGetSequences/index.js.map +1 -0
- package/dist/MafTabixAdapter/MafTabixAdapter.js +3 -4
- package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +9 -7
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
- package/dist/util/extractSubsequence.d.ts +12 -0
- package/dist/util/extractSubsequence.js +60 -0
- package/dist/util/extractSubsequence.js.map +1 -0
- package/dist/util/extractSubsequence.test.d.ts +1 -0
- package/dist/util/extractSubsequence.test.js +42 -0
- package/dist/util/extractSubsequence.test.js.map +1 -0
- package/dist/util/fastaUtils.d.ts +16 -0
- package/dist/util/fastaUtils.js +84 -0
- package/dist/util/fastaUtils.js.map +1 -0
- package/dist/util/fastaUtils.test.d.ts +1 -0
- package/dist/util/fastaUtils.test.js +95 -0
- package/dist/util/fastaUtils.test.js.map +1 -0
- package/dist/util/fetchSequences.d.ts +18 -0
- package/dist/util/fetchSequences.js +39 -0
- package/dist/util/fetchSequences.js.map +1 -0
- package/dist/util/useSequences.d.ts +21 -0
- package/dist/util/useSequences.js +64 -0
- package/dist/util/useSequences.js.map +1 -0
- package/package.json +5 -5
- package/src/LinearMafDisplay/components/Crosshairs.tsx +50 -0
- package/src/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.tsx +175 -0
- package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +211 -45
- package/src/LinearMafDisplay/components/MAFTooltip.tsx +59 -0
- package/src/LinearMafDisplay/components/{ColorLegend.tsx → Sidebar/ColorLegend.tsx} +1 -1
- package/src/LinearMafDisplay/components/{SvgWrapper.tsx → Sidebar/SvgWrapper.tsx} +5 -2
- package/src/LinearMafDisplay/components/{Tree.tsx → Sidebar/Tree.tsx} +1 -1
- package/src/LinearMafDisplay/components/{YScaleBars.tsx → Sidebar/YScaleBars.tsx} +1 -1
- package/src/LinearMafDisplay/renderSvg.tsx +1 -1
- package/src/LinearMafDisplay/stateModel.ts +23 -1
- package/src/LinearMafDisplay/types.ts +1 -1
- package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -0
- package/src/LinearMafRenderer/makeImageData.ts +15 -5
- package/src/{MafRPC/index.ts → MafGetSamples/MafGetSamples.ts} +1 -8
- package/src/MafGetSamples/index.ts +9 -0
- package/src/MafGetSequences/MafGetSequences.ts +47 -0
- package/src/MafGetSequences/index.ts +9 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +4 -5
- package/src/index.ts +4 -2
- package/src/util/__snapshots__/fastaUtils.test.ts.snap +22 -0
- package/src/util/extractSubsequence.test.ts +54 -0
- package/src/util/extractSubsequence.ts +74 -0
- package/src/util/fastaUtils.test.ts +99 -0
- package/src/util/fastaUtils.ts +102 -0
- package/src/util/fetchSequences.ts +57 -0
- package/src/util/useSequences.ts +90 -0
- package/dist/LinearMafDisplay/components/ColorLegend.js.map +0 -1
- package/dist/LinearMafDisplay/components/RectBg.js.map +0 -1
- package/dist/LinearMafDisplay/components/SetRowHeightDialog.js.map +0 -1
- package/dist/LinearMafDisplay/components/SvgWrapper.js.map +0 -1
- package/dist/LinearMafDisplay/components/Tree.js.map +0 -1
- package/dist/LinearMafDisplay/components/YScaleBars.js.map +0 -1
- package/dist/MafRPC/index.js.map +0 -1
- /package/dist/LinearMafDisplay/components/{SetRowHeightDialog.d.ts → SetRowHeightDialog/SetRowHeightDialog.d.ts} +0 -0
- /package/dist/LinearMafDisplay/components/{SetRowHeightDialog.js → SetRowHeightDialog/SetRowHeightDialog.js} +0 -0
- /package/dist/LinearMafDisplay/components/{ColorLegend.js → Sidebar/ColorLegend.js} +0 -0
- /package/dist/LinearMafDisplay/components/{RectBg.d.ts → Sidebar/RectBg.d.ts} +0 -0
- /package/dist/LinearMafDisplay/components/{RectBg.js → Sidebar/RectBg.js} +0 -0
- /package/dist/LinearMafDisplay/components/{Tree.js → Sidebar/Tree.js} +0 -0
- /package/dist/LinearMafDisplay/components/{YScaleBars.js → Sidebar/YScaleBars.js} +0 -0
- /package/src/LinearMafDisplay/components/{SetRowHeightDialog.tsx → SetRowHeightDialog/SetRowHeightDialog.tsx} +0 -0
- /package/src/LinearMafDisplay/components/{RectBg.tsx → Sidebar/RectBg.tsx} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Dialog, ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui'
|
|
4
|
+
import { getSession } from '@jbrowse/core/util'
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
DialogActions,
|
|
8
|
+
DialogContent,
|
|
9
|
+
TextField,
|
|
10
|
+
ToggleButton,
|
|
11
|
+
ToggleButtonGroup,
|
|
12
|
+
} from '@mui/material'
|
|
13
|
+
import { observer } from 'mobx-react'
|
|
14
|
+
import { makeStyles } from 'tss-react/mui'
|
|
15
|
+
|
|
16
|
+
import { useSequences } from '../../../util/useSequences'
|
|
17
|
+
|
|
18
|
+
import type { LinearMafDisplayModel } from '../../stateModel'
|
|
19
|
+
|
|
20
|
+
const useStyles = makeStyles()({
|
|
21
|
+
dialogContent: {
|
|
22
|
+
width: '80em',
|
|
23
|
+
},
|
|
24
|
+
textAreaInput: {
|
|
25
|
+
fontFamily: 'monospace',
|
|
26
|
+
whiteSpace: 'pre',
|
|
27
|
+
overflowX: 'auto',
|
|
28
|
+
},
|
|
29
|
+
ml: {
|
|
30
|
+
marginLeft: 10,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const GetSequenceDialog = observer(function ({
|
|
35
|
+
onClose,
|
|
36
|
+
model,
|
|
37
|
+
selectionCoords,
|
|
38
|
+
}: {
|
|
39
|
+
onClose: () => void
|
|
40
|
+
model: LinearMafDisplayModel
|
|
41
|
+
selectionCoords?: {
|
|
42
|
+
dragStartX: number
|
|
43
|
+
dragEndX: number
|
|
44
|
+
}
|
|
45
|
+
}) {
|
|
46
|
+
const [showAllLetters, setShowAllLetters] = useState(true)
|
|
47
|
+
const { classes } = useStyles()
|
|
48
|
+
const { sequence, loading, error } = useSequences({
|
|
49
|
+
model,
|
|
50
|
+
selectionCoords,
|
|
51
|
+
showAllLetters,
|
|
52
|
+
})
|
|
53
|
+
const sequenceTooLarge = sequence ? sequence.length > 1_000_000 : false
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Dialog open onClose={onClose} title="Subsequence Data" maxWidth="xl">
|
|
57
|
+
<DialogContent>
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
display: 'flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
marginBottom: '16px',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<ToggleButtonGroup
|
|
66
|
+
value={showAllLetters}
|
|
67
|
+
exclusive
|
|
68
|
+
size="small"
|
|
69
|
+
onChange={(_event, newDisplayMode) => {
|
|
70
|
+
if (newDisplayMode !== null) {
|
|
71
|
+
setShowAllLetters(newDisplayMode)
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<ToggleButton value={true}>Show All Letters</ToggleButton>
|
|
76
|
+
<ToggleButton value={false}>Show Only Differences</ToggleButton>
|
|
77
|
+
</ToggleButtonGroup>
|
|
78
|
+
<div style={{ flexGrow: 1 }} />
|
|
79
|
+
<Button
|
|
80
|
+
variant="contained"
|
|
81
|
+
color="primary"
|
|
82
|
+
disabled={loading || !sequence}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
85
|
+
;(async () => {
|
|
86
|
+
try {
|
|
87
|
+
await navigator.clipboard.writeText(sequence)
|
|
88
|
+
getSession(model).notify(
|
|
89
|
+
'Sequence copied to clipboard',
|
|
90
|
+
'info',
|
|
91
|
+
)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(e)
|
|
94
|
+
getSession(model).notifyError(`${e}`, e)
|
|
95
|
+
}
|
|
96
|
+
})()
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
Copy to Clipboard
|
|
100
|
+
</Button>
|
|
101
|
+
<Button
|
|
102
|
+
variant="contained"
|
|
103
|
+
color="secondary"
|
|
104
|
+
disabled={loading || !sequence}
|
|
105
|
+
onClick={() => {
|
|
106
|
+
try {
|
|
107
|
+
const url = URL.createObjectURL(
|
|
108
|
+
new Blob([sequence], { type: 'text/plain' }),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Create a temporary anchor element
|
|
112
|
+
const a = document.createElement('a')
|
|
113
|
+
a.href = url
|
|
114
|
+
a.download = 'sequence.fasta'
|
|
115
|
+
|
|
116
|
+
// Trigger the download
|
|
117
|
+
document.body.append(a)
|
|
118
|
+
a.click()
|
|
119
|
+
|
|
120
|
+
// Clean up
|
|
121
|
+
a.remove()
|
|
122
|
+
URL.revokeObjectURL(url)
|
|
123
|
+
getSession(model).notify('Sequence downloaded', 'info')
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(e)
|
|
126
|
+
getSession(model).notifyError(`${e}`, e)
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
Download
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{error ? (
|
|
135
|
+
<ErrorMessage error={error} />
|
|
136
|
+
) : (
|
|
137
|
+
<>
|
|
138
|
+
{loading ? <LoadingEllipses /> : null}
|
|
139
|
+
<TextField
|
|
140
|
+
variant="outlined"
|
|
141
|
+
multiline
|
|
142
|
+
minRows={5}
|
|
143
|
+
maxRows={10}
|
|
144
|
+
disabled={sequenceTooLarge}
|
|
145
|
+
className={classes.dialogContent}
|
|
146
|
+
fullWidth
|
|
147
|
+
value={
|
|
148
|
+
loading
|
|
149
|
+
? 'Loading...'
|
|
150
|
+
: sequenceTooLarge
|
|
151
|
+
? 'Reference sequence too large to display, use the download FASTA button'
|
|
152
|
+
: sequence
|
|
153
|
+
}
|
|
154
|
+
slotProps={{
|
|
155
|
+
input: {
|
|
156
|
+
readOnly: true,
|
|
157
|
+
classes: {
|
|
158
|
+
input: classes.textAreaInput,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
</DialogContent>
|
|
166
|
+
<DialogActions>
|
|
167
|
+
<Button color="primary" variant="outlined" onClick={onClose}>
|
|
168
|
+
Close
|
|
169
|
+
</Button>
|
|
170
|
+
</DialogActions>
|
|
171
|
+
</Dialog>
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
export default GetSequenceDialog
|
|
@@ -1,30 +1,26 @@
|
|
|
1
|
-
import React, { useRef, useState } from 'react'
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import BaseTooltip from '@jbrowse/core/ui/BaseTooltip'
|
|
3
|
+
import { Menu } from '@jbrowse/core/ui'
|
|
5
4
|
import { getContainingView, getEnv } from '@jbrowse/core/util'
|
|
5
|
+
import { useTheme } from '@mui/material'
|
|
6
6
|
import { observer } from 'mobx-react'
|
|
7
|
-
import { makeStyles } from 'tss-react/mui'
|
|
8
7
|
|
|
9
|
-
import
|
|
8
|
+
import Crosshairs from './Crosshairs'
|
|
9
|
+
import SequenceDialog from './GetSequenceDialog/GetSequenceDialog'
|
|
10
|
+
import MAFTooltip from './MAFTooltip'
|
|
11
|
+
import YScaleBars from './Sidebar/YScaleBars'
|
|
10
12
|
|
|
11
13
|
import type { LinearMafDisplayModel } from '../stateModel'
|
|
12
14
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
13
15
|
|
|
14
|
-
const useStyles = makeStyles()({
|
|
15
|
-
cursor: {
|
|
16
|
-
pointerEvents: 'none',
|
|
17
|
-
},
|
|
18
|
-
})
|
|
19
|
-
|
|
20
16
|
const LinearMafDisplay = observer(function (props: {
|
|
21
17
|
model: LinearMafDisplayModel
|
|
22
18
|
}) {
|
|
23
19
|
const { model } = props
|
|
24
|
-
const { classes } = useStyles()
|
|
25
20
|
const { pluginManager } = getEnv(model)
|
|
26
21
|
const { rowHeight, height, scrollTop, samples: sources } = model
|
|
27
22
|
const ref = useRef<HTMLDivElement>(null)
|
|
23
|
+
const theme = useTheme()
|
|
28
24
|
|
|
29
25
|
const LinearGenomePlugin = pluginManager.getPlugin(
|
|
30
26
|
'LinearGenomeViewPlugin',
|
|
@@ -33,57 +29,227 @@ const LinearMafDisplay = observer(function (props: {
|
|
|
33
29
|
|
|
34
30
|
const [mouseY, setMouseY] = useState<number>()
|
|
35
31
|
const [mouseX, setMouseX] = useState<number>()
|
|
32
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
33
|
+
const [dragStartX, setDragStartX] = useState<number>()
|
|
34
|
+
const [dragEndX, setDragEndX] = useState<number>()
|
|
35
|
+
const [showSelectionBox, setShowSelectionBox] = useState(false)
|
|
36
|
+
const [contextCoord, setContextCoord] = useState<{
|
|
37
|
+
coord: [number, number]
|
|
38
|
+
dragStartX: number
|
|
39
|
+
dragEndX: number
|
|
40
|
+
}>()
|
|
41
|
+
const [showSequenceDialog, setShowSequenceDialog] = useState(false)
|
|
42
|
+
const [selectionCoords, setSelectionCoords] = useState<
|
|
43
|
+
| {
|
|
44
|
+
dragStartX: number
|
|
45
|
+
dragEndX: number
|
|
46
|
+
}
|
|
47
|
+
| undefined
|
|
48
|
+
>()
|
|
36
49
|
const { width } = getContainingView(model) as LinearGenomeViewModel
|
|
37
50
|
|
|
51
|
+
const handleMouseDown = (event: React.MouseEvent) => {
|
|
52
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
53
|
+
const left = rect?.left || 0
|
|
54
|
+
const clientX = event.clientX - left
|
|
55
|
+
|
|
56
|
+
// Clear the previous selection box when starting a new drag
|
|
57
|
+
setShowSelectionBox(false)
|
|
58
|
+
setIsDragging(true)
|
|
59
|
+
setDragStartX(clientX)
|
|
60
|
+
setDragEndX(clientX)
|
|
61
|
+
event.stopPropagation()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleMouseMove = (event: React.MouseEvent) => {
|
|
65
|
+
const rect = ref.current?.getBoundingClientRect()
|
|
66
|
+
const top = rect?.top || 0
|
|
67
|
+
const left = rect?.left || 0
|
|
68
|
+
const clientX = event.clientX - left
|
|
69
|
+
const clientY = event.clientY - top
|
|
70
|
+
|
|
71
|
+
setMouseY(clientY)
|
|
72
|
+
setMouseX(clientX)
|
|
73
|
+
|
|
74
|
+
if (isDragging) {
|
|
75
|
+
setDragEndX(clientX)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleMouseUp = (event: React.MouseEvent) => {
|
|
80
|
+
if (isDragging && dragStartX !== undefined && dragEndX !== undefined) {
|
|
81
|
+
// Calculate the drag distance
|
|
82
|
+
const dragDistanceX = Math.abs(dragEndX - dragStartX)
|
|
83
|
+
|
|
84
|
+
// Only show context menu if the drag distance is at least 2 pixels in either direction
|
|
85
|
+
if (dragDistanceX >= 2) {
|
|
86
|
+
setContextCoord({
|
|
87
|
+
coord: [event.clientX, event.clientY],
|
|
88
|
+
dragEndX: event.clientX,
|
|
89
|
+
dragStartX: dragStartX,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Set showSelectionBox to true to keep the selection visible
|
|
93
|
+
setShowSelectionBox(true)
|
|
94
|
+
} else {
|
|
95
|
+
// For very small drags (less than 2px), don't show selection box or context menu
|
|
96
|
+
clearSelectionBox()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Only set isDragging to false, but keep the coordinates
|
|
101
|
+
setIsDragging(false)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Function to clear the selection box
|
|
105
|
+
const clearSelectionBox = () => {
|
|
106
|
+
setShowSelectionBox(false)
|
|
107
|
+
setDragStartX(undefined)
|
|
108
|
+
setDragEndX(undefined)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add keydown event handler to clear selection box when Escape key is pressed
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
114
|
+
if (event.key === 'Escape' && showSelectionBox) {
|
|
115
|
+
clearSelectionBox()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Add click handler to clear selection box when clicking outside of it
|
|
120
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
121
|
+
if (
|
|
122
|
+
ref.current &&
|
|
123
|
+
!ref.current.contains(event.target as Node) &&
|
|
124
|
+
showSelectionBox
|
|
125
|
+
) {
|
|
126
|
+
clearSelectionBox()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
131
|
+
document.addEventListener('click', handleClickOutside)
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
135
|
+
document.removeEventListener('click', handleClickOutside)
|
|
136
|
+
}
|
|
137
|
+
}, [showSelectionBox, clearSelectionBox])
|
|
138
|
+
|
|
38
139
|
return (
|
|
39
140
|
<div
|
|
40
141
|
ref={ref}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
142
|
+
onMouseDown={handleMouseDown}
|
|
143
|
+
onMouseMove={handleMouseMove}
|
|
144
|
+
onMouseUp={handleMouseUp}
|
|
145
|
+
onDoubleClick={() => {
|
|
146
|
+
// Clear selection box on double click
|
|
147
|
+
if (showSelectionBox) {
|
|
148
|
+
clearSelectionBox()
|
|
149
|
+
}
|
|
47
150
|
}}
|
|
48
151
|
onMouseLeave={() => {
|
|
49
152
|
setMouseY(undefined)
|
|
50
153
|
setMouseX(undefined)
|
|
154
|
+
setIsDragging(false)
|
|
51
155
|
}}
|
|
52
156
|
>
|
|
53
157
|
<BaseLinearDisplayComponent {...props} />
|
|
54
158
|
<YScaleBars model={model} />
|
|
55
|
-
{mouseY && sources ? (
|
|
159
|
+
{mouseY && mouseX && sources && !contextCoord && !showSequenceDialog ? (
|
|
56
160
|
<div style={{ position: 'relative' }}>
|
|
57
|
-
<
|
|
58
|
-
className={classes.cursor}
|
|
161
|
+
<Crosshairs
|
|
59
162
|
width={width}
|
|
60
163
|
height={height}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<line x1={mouseX} x2={mouseX} y1={0} y2={height} stroke="black" />
|
|
74
|
-
</svg>
|
|
75
|
-
<BaseTooltip>
|
|
76
|
-
<SanitizedHTML
|
|
77
|
-
html={Object.entries(
|
|
78
|
-
sources[Math.floor(mouseY / rowHeight)] || {},
|
|
79
|
-
)
|
|
80
|
-
.filter(([key]) => key !== 'color' && key !== 'id')
|
|
81
|
-
.map(([key, value]) => `${key}:${value}`)
|
|
82
|
-
.join('\n')}
|
|
83
|
-
/>
|
|
84
|
-
</BaseTooltip>
|
|
164
|
+
scrollTop={scrollTop}
|
|
165
|
+
mouseX={mouseX}
|
|
166
|
+
mouseY={mouseY}
|
|
167
|
+
/>
|
|
168
|
+
<MAFTooltip
|
|
169
|
+
model={model}
|
|
170
|
+
mouseX={mouseX}
|
|
171
|
+
mouseY={mouseY}
|
|
172
|
+
origMouseX={dragStartX}
|
|
173
|
+
rowHeight={rowHeight}
|
|
174
|
+
sources={sources}
|
|
175
|
+
/>
|
|
85
176
|
</div>
|
|
86
177
|
) : null}
|
|
178
|
+
{(isDragging || showSelectionBox) &&
|
|
179
|
+
dragStartX !== undefined &&
|
|
180
|
+
dragEndX !== undefined ? (
|
|
181
|
+
<div
|
|
182
|
+
style={{
|
|
183
|
+
position: 'absolute',
|
|
184
|
+
left: Math.min(dragStartX, dragEndX),
|
|
185
|
+
top: 0,
|
|
186
|
+
width: Math.abs(dragEndX - dragStartX),
|
|
187
|
+
height,
|
|
188
|
+
backgroundColor: 'rgba(0, 0, 255, 0.2)',
|
|
189
|
+
border: '1px solid rgba(0, 0, 255, 0.5)',
|
|
190
|
+
pointerEvents: 'none',
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
) : null}
|
|
194
|
+
<Menu
|
|
195
|
+
open={Boolean(contextCoord)}
|
|
196
|
+
onMenuItemClick={(_, callback) => {
|
|
197
|
+
callback()
|
|
198
|
+
setContextCoord(undefined)
|
|
199
|
+
}}
|
|
200
|
+
onClose={() => {
|
|
201
|
+
setContextCoord(undefined)
|
|
202
|
+
}}
|
|
203
|
+
slotProps={{
|
|
204
|
+
transition: {
|
|
205
|
+
onExit: () => {
|
|
206
|
+
setContextCoord(undefined)
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}}
|
|
210
|
+
anchorReference="anchorPosition"
|
|
211
|
+
anchorPosition={
|
|
212
|
+
contextCoord
|
|
213
|
+
? { top: contextCoord.coord[1], left: contextCoord.coord[0] }
|
|
214
|
+
: undefined
|
|
215
|
+
}
|
|
216
|
+
style={{
|
|
217
|
+
zIndex: theme.zIndex.tooltip,
|
|
218
|
+
}}
|
|
219
|
+
menuItems={[
|
|
220
|
+
{
|
|
221
|
+
label: 'View subsequence',
|
|
222
|
+
onClick: () => {
|
|
223
|
+
if (!contextCoord) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Store the selection coordinates for the SequenceDialog to use
|
|
228
|
+
setSelectionCoords({
|
|
229
|
+
dragStartX: contextCoord.dragStartX,
|
|
230
|
+
dragEndX: contextCoord.dragEndX,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Show the dialog
|
|
234
|
+
setShowSequenceDialog(true)
|
|
235
|
+
|
|
236
|
+
// Close the context menu
|
|
237
|
+
setContextCoord(undefined)
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
]}
|
|
241
|
+
/>
|
|
242
|
+
|
|
243
|
+
{showSequenceDialog ? (
|
|
244
|
+
<SequenceDialog
|
|
245
|
+
model={model}
|
|
246
|
+
selectionCoords={selectionCoords}
|
|
247
|
+
onClose={() => {
|
|
248
|
+
setShowSequenceDialog(false)
|
|
249
|
+
setSelectionCoords(undefined)
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
) : null}
|
|
87
253
|
</div>
|
|
88
254
|
)
|
|
89
255
|
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { SanitizedHTML } from '@jbrowse/core/ui'
|
|
4
|
+
import BaseTooltip from '@jbrowse/core/ui/BaseTooltip'
|
|
5
|
+
import {
|
|
6
|
+
getBpDisplayStr,
|
|
7
|
+
getContainingView,
|
|
8
|
+
toLocale,
|
|
9
|
+
} from '@jbrowse/core/util'
|
|
10
|
+
import { observer } from 'mobx-react'
|
|
11
|
+
|
|
12
|
+
import type { LinearMafDisplayModel } from '../stateModel'
|
|
13
|
+
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
14
|
+
|
|
15
|
+
interface MAFTooltipProps {
|
|
16
|
+
mouseY: number
|
|
17
|
+
mouseX: number
|
|
18
|
+
rowHeight: number
|
|
19
|
+
sources: Record<string, any>[]
|
|
20
|
+
model: LinearMafDisplayModel
|
|
21
|
+
origMouseX?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAFTooltip = observer(function ({
|
|
25
|
+
model,
|
|
26
|
+
mouseY,
|
|
27
|
+
mouseX,
|
|
28
|
+
origMouseX,
|
|
29
|
+
rowHeight,
|
|
30
|
+
sources,
|
|
31
|
+
}: MAFTooltipProps) {
|
|
32
|
+
const view = getContainingView(model) as LinearGenomeViewModel
|
|
33
|
+
const ret = Object.entries(sources[Math.floor(mouseY / rowHeight)] || {})
|
|
34
|
+
.filter(([key]) => key !== 'color' && key !== 'id')
|
|
35
|
+
.map(([key, value]) => `${key}:${value}`)
|
|
36
|
+
.join('\n')
|
|
37
|
+
const p1 = origMouseX ? view.pxToBp(origMouseX) : undefined
|
|
38
|
+
const p2 = view.pxToBp(mouseX)
|
|
39
|
+
return ret ? (
|
|
40
|
+
<BaseTooltip>
|
|
41
|
+
<SanitizedHTML
|
|
42
|
+
html={[
|
|
43
|
+
ret,
|
|
44
|
+
...(p1
|
|
45
|
+
? [
|
|
46
|
+
`Start: ${p1.refName}:${toLocale(p1.coord)}`,
|
|
47
|
+
`End: ${p2.refName}:${toLocale(p2.coord)}`,
|
|
48
|
+
`Length: ${getBpDisplayStr(Math.abs(p1.coord - p2.coord))}`,
|
|
49
|
+
]
|
|
50
|
+
: [`${p2.refName}:${toLocale(p2.coord)}`]),
|
|
51
|
+
]
|
|
52
|
+
.filter(f => !!f)
|
|
53
|
+
.join('<br/>')}
|
|
54
|
+
/>
|
|
55
|
+
</BaseTooltip>
|
|
56
|
+
) : null
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default MAFTooltip
|
|
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react'
|
|
|
5
5
|
import RectBg from './RectBg'
|
|
6
6
|
import Tree from './Tree'
|
|
7
7
|
|
|
8
|
-
import type { LinearMafDisplayModel } from '
|
|
8
|
+
import type { LinearMafDisplayModel } from '../../stateModel'
|
|
9
9
|
|
|
10
10
|
const ColorLegend = observer(function ({
|
|
11
11
|
model,
|
|
@@ -3,7 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { getContainingView } from '@jbrowse/core/util'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import type { LinearMafDisplayModel } from '
|
|
6
|
+
import type { LinearMafDisplayModel } from '../../stateModel'
|
|
7
|
+
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
7
8
|
|
|
8
9
|
const SvgWrapper = observer(function ({
|
|
9
10
|
children,
|
|
@@ -18,15 +19,17 @@ const SvgWrapper = observer(function ({
|
|
|
18
19
|
return <>{children}</>
|
|
19
20
|
} else {
|
|
20
21
|
const { totalHeight } = model
|
|
22
|
+
const { width } = getContainingView(model) as LinearGenomeViewModel
|
|
21
23
|
return (
|
|
22
24
|
<svg
|
|
23
25
|
style={{
|
|
24
26
|
position: 'absolute',
|
|
27
|
+
userSelect: 'none',
|
|
25
28
|
top: 0,
|
|
26
29
|
left: 0,
|
|
27
30
|
pointerEvents: 'none',
|
|
28
31
|
height: totalHeight,
|
|
29
|
-
width
|
|
32
|
+
width,
|
|
30
33
|
}}
|
|
31
34
|
>
|
|
32
35
|
{children}
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
-
import type { LinearMafDisplayModel } from '
|
|
5
|
+
import type { LinearMafDisplayModel } from '../../stateModel'
|
|
6
6
|
|
|
7
7
|
const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
|
|
8
8
|
const {
|
|
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react'
|
|
|
5
5
|
import ColorLegend from './ColorLegend'
|
|
6
6
|
import SvgWrapper from './SvgWrapper'
|
|
7
7
|
|
|
8
|
-
import type { LinearMafDisplayModel } from '
|
|
8
|
+
import type { LinearMafDisplayModel } from '../../stateModel'
|
|
9
9
|
|
|
10
10
|
export const YScaleBars = observer(function (props: {
|
|
11
11
|
model: LinearMafDisplayModel
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { getContainingView } from '@jbrowse/core/util'
|
|
4
4
|
|
|
5
|
-
import YScaleBars from './components/YScaleBars'
|
|
5
|
+
import YScaleBars from './components/Sidebar/YScaleBars'
|
|
6
6
|
|
|
7
7
|
import type { LinearMafDisplayModel } from './stateModel'
|
|
8
8
|
import type {
|
|
@@ -22,7 +22,9 @@ import type { ExportSvgDisplayOptions } from '@jbrowse/plugin-linear-genome-view
|
|
|
22
22
|
import type { HierarchyNode } from 'd3-hierarchy'
|
|
23
23
|
import type { Instance } from 'mobx-state-tree'
|
|
24
24
|
|
|
25
|
-
const SetRowHeightDialog = lazy(
|
|
25
|
+
const SetRowHeightDialog = lazy(
|
|
26
|
+
() => import('./components/SetRowHeightDialog/SetRowHeightDialog'),
|
|
27
|
+
)
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* #stateModel LinearMafDisplay
|
|
@@ -76,6 +78,10 @@ export default function stateModelFactory(
|
|
|
76
78
|
* #property
|
|
77
79
|
*/
|
|
78
80
|
treeAreaWidth: 80,
|
|
81
|
+
/**
|
|
82
|
+
* #property
|
|
83
|
+
*/
|
|
84
|
+
showAsUpperCase: true,
|
|
79
85
|
}),
|
|
80
86
|
)
|
|
81
87
|
.volatile(() => ({
|
|
@@ -128,6 +134,12 @@ export default function stateModelFactory(
|
|
|
128
134
|
self.volatileTree = tree
|
|
129
135
|
}
|
|
130
136
|
},
|
|
137
|
+
/**
|
|
138
|
+
* #action
|
|
139
|
+
*/
|
|
140
|
+
setShowAsUpperCase(arg: boolean) {
|
|
141
|
+
self.showAsUpperCase = arg
|
|
142
|
+
},
|
|
131
143
|
}))
|
|
132
144
|
.views(self => ({
|
|
133
145
|
/**
|
|
@@ -248,6 +260,7 @@ export default function stateModelFactory(
|
|
|
248
260
|
rowHeight,
|
|
249
261
|
rowProportion,
|
|
250
262
|
mismatchRendering,
|
|
263
|
+
showAsUpperCase,
|
|
251
264
|
} = self
|
|
252
265
|
const s = superRenderProps()
|
|
253
266
|
return {
|
|
@@ -260,6 +273,7 @@ export default function stateModelFactory(
|
|
|
260
273
|
rowProportion,
|
|
261
274
|
showAllLetters,
|
|
262
275
|
mismatchRendering,
|
|
276
|
+
showAsUpperCase,
|
|
263
277
|
}
|
|
264
278
|
},
|
|
265
279
|
/**
|
|
@@ -300,6 +314,14 @@ export default function stateModelFactory(
|
|
|
300
314
|
},
|
|
301
315
|
],
|
|
302
316
|
},
|
|
317
|
+
{
|
|
318
|
+
label: 'Use upper-case',
|
|
319
|
+
type: 'checkbox',
|
|
320
|
+
checked: self.showAsUpperCase,
|
|
321
|
+
onClick: () => {
|
|
322
|
+
self.setShowAsUpperCase(!self.showAsUpperCase)
|
|
323
|
+
},
|
|
324
|
+
},
|
|
303
325
|
{
|
|
304
326
|
label: 'Show all letters',
|
|
305
327
|
type: 'checkbox',
|