jbrowse-plugin-mafviewer 1.4.3 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/BigMafAdapter/BigMafAdapter.js +4 -5
- package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
- package/dist/BigMafAdapter/configSchema.d.ts +2 -2
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +39 -109
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
- package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +0 -3
- package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
- package/dist/LinearMafDisplay/components/MsaHighlightOverlay.d.ts +9 -0
- package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js +34 -0
- package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js.map +1 -0
- package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js +2 -2
- package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.d.ts +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.js +2 -3
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js +81 -11
- package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/Tree.js +30 -9
- package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.d.ts +0 -1
- package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -1
- package/dist/LinearMafDisplay/components/useDragSelection.d.ts +25 -0
- package/dist/LinearMafDisplay/components/useDragSelection.js +103 -0
- package/dist/LinearMafDisplay/components/useDragSelection.js.map +1 -0
- package/dist/LinearMafDisplay/configSchema.d.ts +3 -30
- package/dist/LinearMafDisplay/renderSvg.js +1 -1
- package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
- package/dist/LinearMafDisplay/stateModel.d.ts +1090 -102
- package/dist/LinearMafDisplay/stateModel.js +156 -17
- package/dist/LinearMafDisplay/stateModel.js.map +1 -1
- package/dist/LinearMafDisplay/types.d.ts +2 -2
- package/dist/LinearMafDisplay/util.d.ts +6 -0
- package/dist/LinearMafDisplay/util.js +28 -6
- package/dist/LinearMafDisplay/util.js.map +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +43 -10
- package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
- package/dist/LinearMafRenderer/components/LinearMafRendering.d.ts +14 -5
- package/dist/LinearMafRenderer/components/LinearMafRendering.js +40 -20
- package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -1
- package/dist/LinearMafRenderer/configSchema.d.ts +1 -6
- package/dist/LinearMafRenderer/configSchema.js +1 -6
- package/dist/LinearMafRenderer/configSchema.js.map +1 -1
- package/dist/LinearMafRenderer/makeImageData.js +6 -7
- package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/features.d.ts +0 -1
- package/dist/LinearMafRenderer/rendering/features.js +1 -14
- package/dist/LinearMafRenderer/rendering/features.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/insertions.d.ts +1 -1
- package/dist/LinearMafRenderer/rendering/insertions.js +10 -8
- package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/matches.d.ts +1 -1
- package/dist/LinearMafRenderer/rendering/matches.js +3 -15
- package/dist/LinearMafRenderer/rendering/matches.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/mismatches.d.ts +1 -1
- package/dist/LinearMafRenderer/rendering/mismatches.js +3 -3
- package/dist/LinearMafRenderer/rendering/spatialIndex.js +8 -2
- package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/text.js +1 -3
- package/dist/LinearMafRenderer/rendering/text.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/types.d.ts +6 -5
- package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js +1 -1
- package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
- package/dist/MafAddTrackWorkflow/index.js +1 -1
- package/dist/MafAddTrackWorkflow/index.js.map +1 -1
- package/dist/MafGetSequences/MafGetSequences.d.ts +1 -0
- package/dist/MafGetSequences/MafGetSequences.js +2 -1
- package/dist/MafGetSequences/MafGetSequences.js.map +1 -1
- package/dist/MafSequenceWidget/LabelsCanvas.d.ts +8 -0
- package/dist/MafSequenceWidget/LabelsCanvas.js +37 -0
- package/dist/MafSequenceWidget/LabelsCanvas.js.map +1 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlight.d.ts +6 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js +52 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js.map +1 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.d.ts +2 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js +12 -0
- package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js.map +1 -0
- package/dist/MafSequenceWidget/MafSequenceWidget.d.ts +6 -0
- package/dist/MafSequenceWidget/MafSequenceWidget.js +189 -0
- package/dist/MafSequenceWidget/MafSequenceWidget.js.map +1 -0
- package/dist/MafSequenceWidget/SequenceCanvas.d.ts +12 -0
- package/dist/MafSequenceWidget/SequenceCanvas.js +86 -0
- package/dist/MafSequenceWidget/SequenceCanvas.js.map +1 -0
- package/dist/MafSequenceWidget/SequenceDisplay.d.ts +12 -0
- package/dist/MafSequenceWidget/SequenceDisplay.js +117 -0
- package/dist/MafSequenceWidget/SequenceDisplay.js.map +1 -0
- package/dist/MafSequenceWidget/SequenceTooltip.d.ts +11 -0
- package/dist/MafSequenceWidget/SequenceTooltip.js +39 -0
- package/dist/MafSequenceWidget/SequenceTooltip.js.map +1 -0
- package/dist/MafSequenceWidget/baseColors.d.ts +3 -0
- package/dist/MafSequenceWidget/baseColors.js +64 -0
- package/dist/MafSequenceWidget/baseColors.js.map +1 -0
- package/dist/MafSequenceWidget/colToGenomePos.d.ts +13 -0
- package/dist/MafSequenceWidget/colToGenomePos.js +32 -0
- package/dist/MafSequenceWidget/colToGenomePos.js.map +1 -0
- package/dist/MafSequenceWidget/colToGenomePos.test.d.ts +1 -0
- package/dist/MafSequenceWidget/colToGenomePos.test.js +136 -0
- package/dist/MafSequenceWidget/colToGenomePos.test.js.map +1 -0
- package/dist/MafSequenceWidget/configSchema.d.ts +1 -0
- package/dist/MafSequenceWidget/configSchema.js +3 -0
- package/dist/MafSequenceWidget/configSchema.js.map +1 -0
- package/dist/MafSequenceWidget/constants.d.ts +4 -0
- package/dist/MafSequenceWidget/constants.js +5 -0
- package/dist/MafSequenceWidget/constants.js.map +1 -0
- package/dist/MafSequenceWidget/index.d.ts +2 -0
- package/dist/MafSequenceWidget/index.js +16 -0
- package/dist/MafSequenceWidget/index.js.map +1 -0
- package/dist/MafSequenceWidget/stateModelFactory.d.ts +67 -0
- package/dist/MafSequenceWidget/stateModelFactory.js +21 -0
- package/dist/MafSequenceWidget/stateModelFactory.js.map +1 -0
- package/dist/MafTabixAdapter/MafTabixAdapter.js +4 -35
- package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
- package/dist/MafTabixAdapter/configSchema.d.ts +4 -4
- package/dist/MafTrack/configSchema.d.ts +16 -11
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +12 -24
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
- package/dist/util/clipboard.d.ts +2 -0
- package/dist/util/clipboard.js +28 -0
- package/dist/util/clipboard.js.map +1 -0
- package/dist/util/fastaUtils.d.ts +2 -1
- package/dist/util/fastaUtils.js +93 -50
- package/dist/util/fastaUtils.js.map +1 -1
- package/dist/util/fastaUtils.test.js +190 -0
- package/dist/util/fastaUtils.test.js.map +1 -1
- package/dist/util/parseAssemblyName.d.ts +32 -0
- package/dist/util/parseAssemblyName.js +87 -0
- package/dist/util/parseAssemblyName.js.map +1 -0
- package/dist/util/parseAssemblyName.test.d.ts +1 -0
- package/dist/util/parseAssemblyName.test.js +269 -0
- package/dist/util/parseAssemblyName.test.js.map +1 -0
- package/package.json +12 -12
- package/src/BigMafAdapter/BigMafAdapter.ts +5 -5
- package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +63 -145
- package/src/LinearMafDisplay/components/MAFTooltip.tsx +0 -3
- package/src/LinearMafDisplay/components/MsaHighlightOverlay.tsx +62 -0
- package/src/LinearMafDisplay/components/Sidebar/ColorLegend.tsx +2 -6
- package/src/LinearMafDisplay/components/Sidebar/RectBg.tsx +8 -3
- package/src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx +117 -15
- package/src/LinearMafDisplay/components/Sidebar/Tree.tsx +53 -8
- package/src/LinearMafDisplay/components/Sidebar/YScaleBars.tsx +0 -1
- package/src/LinearMafDisplay/components/useDragSelection.ts +159 -0
- package/src/LinearMafDisplay/renderSvg.tsx +1 -1
- package/src/LinearMafDisplay/stateModel.ts +215 -20
- package/src/LinearMafDisplay/types.ts +2 -2
- package/src/LinearMafDisplay/util.ts +35 -7
- package/src/LinearMafRenderer/LinearMafRenderer.ts +3 -5
- package/src/LinearMafRenderer/components/LinearMafRendering.tsx +67 -33
- package/src/LinearMafRenderer/configSchema.ts +1 -6
- package/src/LinearMafRenderer/makeImageData.ts +5 -14
- package/src/LinearMafRenderer/rendering/features.ts +2 -36
- package/src/LinearMafRenderer/rendering/insertions.ts +13 -8
- package/src/LinearMafRenderer/rendering/matches.ts +2 -27
- package/src/LinearMafRenderer/rendering/mismatches.ts +3 -3
- package/src/LinearMafRenderer/rendering/spatialIndex.ts +9 -2
- package/src/LinearMafRenderer/rendering/text.ts +1 -2
- package/src/LinearMafRenderer/rendering/types.ts +8 -5
- package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +1 -1
- package/src/MafAddTrackWorkflow/index.ts +1 -1
- package/src/MafGetSequences/MafGetSequences.ts +10 -2
- package/src/MafSequenceWidget/LabelsCanvas.tsx +58 -0
- package/src/MafSequenceWidget/MafSequenceHoverHighlight.tsx +83 -0
- package/src/MafSequenceWidget/MafSequenceHoverHighlightExtension.tsx +24 -0
- package/src/MafSequenceWidget/MafSequenceWidget.tsx +294 -0
- package/src/MafSequenceWidget/SequenceCanvas.tsx +136 -0
- package/src/MafSequenceWidget/SequenceDisplay.tsx +188 -0
- package/src/MafSequenceWidget/SequenceTooltip.tsx +70 -0
- package/src/MafSequenceWidget/baseColors.ts +76 -0
- package/src/MafSequenceWidget/colToGenomePos.test.ts +166 -0
- package/src/MafSequenceWidget/colToGenomePos.ts +40 -0
- package/src/MafSequenceWidget/configSchema.ts +3 -0
- package/src/MafSequenceWidget/constants.ts +4 -0
- package/src/MafSequenceWidget/index.ts +24 -0
- package/src/MafSequenceWidget/stateModelFactory.ts +43 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +12 -51
- package/src/index.ts +2 -0
- package/src/util/__snapshots__/fastaUtils.test.ts.snap +35 -0
- package/src/util/clipboard.ts +35 -0
- package/src/util/fastaUtils.test.ts +199 -0
- package/src/util/fastaUtils.ts +118 -51
- package/src/util/parseAssemblyName.test.ts +350 -0
- package/src/util/parseAssemblyName.ts +106 -0
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.d.ts +0 -11
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js +0 -97
- package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js.map +0 -1
- package/dist/LinearMafDisplay/components/util.d.ts +0 -1
- package/dist/LinearMafDisplay/components/util.js +0 -8
- package/dist/LinearMafDisplay/components/util.js.map +0 -1
- package/dist/LinearMafRenderer/components/util.d.ts +0 -1
- package/dist/LinearMafRenderer/components/util.js +0 -13
- package/dist/LinearMafRenderer/components/util.js.map +0 -1
- package/dist/util/fetchSequences.d.ts +0 -18
- package/dist/util/fetchSequences.js +0 -39
- package/dist/util/fetchSequences.js.map +0 -1
- package/dist/util/useSequences.d.ts +0 -21
- package/dist/util/useSequences.js +0 -64
- package/dist/util/useSequences.js.map +0 -1
- package/src/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.tsx +0 -175
- package/src/LinearMafDisplay/components/util.ts +0 -7
- package/src/LinearMafRenderer/components/util.ts +0 -13
- package/src/util/fetchSequences.ts +0 -57
- package/src/util/useSequences.ts +0 -90
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CascadingMenuButton,
|
|
5
|
+
ErrorMessage,
|
|
6
|
+
LoadingEllipses,
|
|
7
|
+
} from '@jbrowse/core/ui'
|
|
8
|
+
import { getSession, useLocalStorage } from '@jbrowse/core/util'
|
|
9
|
+
import { Button, Paper } from '@mui/material'
|
|
10
|
+
import { observer } from 'mobx-react'
|
|
11
|
+
import { makeStyles } from 'tss-react/mui'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ContentCopy as CopyIcon,
|
|
15
|
+
Difference as DifferenceIcon,
|
|
16
|
+
Download as DownloadIcon,
|
|
17
|
+
FormatColorFill as ColorBackgroundIcon,
|
|
18
|
+
KeyboardArrowDown,
|
|
19
|
+
Label as LabelIcon,
|
|
20
|
+
PlaylistAdd as InsertionsIcon,
|
|
21
|
+
Subject as AllLettersIcon,
|
|
22
|
+
TableRows as TableRowsIcon,
|
|
23
|
+
} from '@mui/icons-material'
|
|
24
|
+
|
|
25
|
+
import SequenceDisplay from './SequenceDisplay'
|
|
26
|
+
import { copyToClipboard, downloadAsFile } from '../util/clipboard'
|
|
27
|
+
|
|
28
|
+
import type { MafSequenceWidgetModel } from './stateModelFactory'
|
|
29
|
+
import type { MenuItem } from '@jbrowse/core/ui'
|
|
30
|
+
|
|
31
|
+
const useStyles = makeStyles()(theme => ({
|
|
32
|
+
root: {
|
|
33
|
+
padding: theme.spacing(2),
|
|
34
|
+
},
|
|
35
|
+
controls: {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
flexWrap: 'wrap',
|
|
39
|
+
gap: theme.spacing(1),
|
|
40
|
+
marginBottom: theme.spacing(2),
|
|
41
|
+
},
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const MafSequenceWidget = observer(function MafSequenceWidget({
|
|
45
|
+
model,
|
|
46
|
+
}: {
|
|
47
|
+
model: MafSequenceWidgetModel
|
|
48
|
+
}) {
|
|
49
|
+
const { classes } = useStyles()
|
|
50
|
+
const session = getSession(model)
|
|
51
|
+
const { adapterConfig, samples, regions } = model
|
|
52
|
+
|
|
53
|
+
const [showAllLetters, setShowAllLetters] = useLocalStorage(
|
|
54
|
+
'mafSequenceWidget-showAllLetters',
|
|
55
|
+
true,
|
|
56
|
+
)
|
|
57
|
+
const [includeInsertions, setIncludeInsertions] = useLocalStorage(
|
|
58
|
+
'mafSequenceWidget-includeInsertions',
|
|
59
|
+
false,
|
|
60
|
+
)
|
|
61
|
+
const [singleLineFormat, setSingleLineFormat] = useLocalStorage(
|
|
62
|
+
'mafSequenceWidget-singleLineFormat',
|
|
63
|
+
false,
|
|
64
|
+
)
|
|
65
|
+
const [colorBackground, setColorBackground] = useLocalStorage(
|
|
66
|
+
'mafSequenceWidget-colorBackground',
|
|
67
|
+
true,
|
|
68
|
+
)
|
|
69
|
+
const [showSampleNames, setShowSampleNames] = useLocalStorage(
|
|
70
|
+
'mafSequenceWidget-showSampleNames',
|
|
71
|
+
true,
|
|
72
|
+
)
|
|
73
|
+
const [rawSequences, setRawSequences] = useState<string[]>([])
|
|
74
|
+
const [formattedSequence, setFormattedSequence] = useState<string>('')
|
|
75
|
+
const [loading, setLoading] = useState(true)
|
|
76
|
+
const [error, setError] = useState<unknown>()
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!adapterConfig || !samples || !regions) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
83
|
+
;(async () => {
|
|
84
|
+
try {
|
|
85
|
+
setLoading(true)
|
|
86
|
+
setError(undefined)
|
|
87
|
+
|
|
88
|
+
const { rpcManager } = session
|
|
89
|
+
|
|
90
|
+
const fastaSequence = (await rpcManager.call(
|
|
91
|
+
'MafSequenceWidget',
|
|
92
|
+
'MafGetSequences',
|
|
93
|
+
{
|
|
94
|
+
sessionId: 'MafSequenceWidget',
|
|
95
|
+
adapterConfig,
|
|
96
|
+
samples,
|
|
97
|
+
showAllLetters,
|
|
98
|
+
includeInsertions,
|
|
99
|
+
regions,
|
|
100
|
+
},
|
|
101
|
+
)) as string[]
|
|
102
|
+
|
|
103
|
+
setRawSequences(fastaSequence)
|
|
104
|
+
|
|
105
|
+
let formatted: string
|
|
106
|
+
if (singleLineFormat) {
|
|
107
|
+
const maxLabelLength = Math.max(
|
|
108
|
+
...samples.map(s => (s.label ?? s.id).length),
|
|
109
|
+
)
|
|
110
|
+
formatted = fastaSequence
|
|
111
|
+
.map((r, idx) => {
|
|
112
|
+
const sample = samples[idx]!
|
|
113
|
+
const label = sample.label ?? sample.id
|
|
114
|
+
const padding = ' '.repeat(maxLabelLength - label.length + 2)
|
|
115
|
+
return `>${label}${padding}${r}`
|
|
116
|
+
})
|
|
117
|
+
.join('\n')
|
|
118
|
+
} else {
|
|
119
|
+
formatted = fastaSequence
|
|
120
|
+
.map((r, idx) => {
|
|
121
|
+
const sample = samples[idx]!
|
|
122
|
+
return `>${sample.label ?? sample.id}\n${r}`
|
|
123
|
+
})
|
|
124
|
+
.join('\n')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setFormattedSequence(formatted)
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error(e)
|
|
130
|
+
setError(e)
|
|
131
|
+
} finally {
|
|
132
|
+
setLoading(false)
|
|
133
|
+
}
|
|
134
|
+
})()
|
|
135
|
+
}, [
|
|
136
|
+
adapterConfig,
|
|
137
|
+
samples,
|
|
138
|
+
regions,
|
|
139
|
+
showAllLetters,
|
|
140
|
+
includeInsertions,
|
|
141
|
+
singleLineFormat,
|
|
142
|
+
session,
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
const sequenceTooLarge = formattedSequence
|
|
146
|
+
? formattedSequence.length > 5_000_000
|
|
147
|
+
: false
|
|
148
|
+
|
|
149
|
+
if (!adapterConfig || !samples || !regions) {
|
|
150
|
+
return (
|
|
151
|
+
<Paper className={classes.root}>
|
|
152
|
+
<div>No sequence data available</div>
|
|
153
|
+
</Paper>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Paper className={classes.root}>
|
|
159
|
+
<div className={classes.controls}>
|
|
160
|
+
<CascadingMenuButton
|
|
161
|
+
menuItems={
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
label: 'Show all letters',
|
|
165
|
+
icon: AllLettersIcon,
|
|
166
|
+
type: 'radio',
|
|
167
|
+
checked: showAllLetters,
|
|
168
|
+
onClick: () => {
|
|
169
|
+
setShowAllLetters(true)
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: 'Show only differences',
|
|
174
|
+
icon: DifferenceIcon,
|
|
175
|
+
type: 'radio',
|
|
176
|
+
checked: !showAllLetters,
|
|
177
|
+
onClick: () => {
|
|
178
|
+
setShowAllLetters(false)
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
label: 'Include insertions',
|
|
183
|
+
icon: InsertionsIcon,
|
|
184
|
+
type: 'checkbox',
|
|
185
|
+
checked: includeInsertions,
|
|
186
|
+
onClick: () => {
|
|
187
|
+
setIncludeInsertions(!includeInsertions)
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
label: 'Single line format',
|
|
192
|
+
icon: TableRowsIcon,
|
|
193
|
+
type: 'checkbox',
|
|
194
|
+
checked: singleLineFormat,
|
|
195
|
+
onClick: () => {
|
|
196
|
+
setSingleLineFormat(!singleLineFormat)
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
label: 'Color background',
|
|
201
|
+
icon: ColorBackgroundIcon,
|
|
202
|
+
type: 'checkbox',
|
|
203
|
+
checked: colorBackground,
|
|
204
|
+
onClick: () => {
|
|
205
|
+
setColorBackground(!colorBackground)
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
label: 'Show sample names',
|
|
210
|
+
icon: LabelIcon,
|
|
211
|
+
type: 'checkbox',
|
|
212
|
+
checked: showSampleNames,
|
|
213
|
+
onClick: () => {
|
|
214
|
+
setShowSampleNames(!showSampleNames)
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{ type: 'divider' },
|
|
218
|
+
{
|
|
219
|
+
label: 'Copy to clipboard',
|
|
220
|
+
icon: CopyIcon,
|
|
221
|
+
disabled: loading || !formattedSequence,
|
|
222
|
+
onClick: () => {
|
|
223
|
+
copyToClipboard(
|
|
224
|
+
formattedSequence,
|
|
225
|
+
() => {
|
|
226
|
+
session.notify('Sequence copied to clipboard', 'info')
|
|
227
|
+
},
|
|
228
|
+
e => {
|
|
229
|
+
session.notifyError(`${e}`, e)
|
|
230
|
+
},
|
|
231
|
+
).catch((e: unknown) => {
|
|
232
|
+
console.error(e)
|
|
233
|
+
})
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: 'Download as FASTA',
|
|
238
|
+
icon: DownloadIcon,
|
|
239
|
+
disabled: loading || !formattedSequence,
|
|
240
|
+
onClick: () => {
|
|
241
|
+
downloadAsFile(
|
|
242
|
+
formattedSequence,
|
|
243
|
+
'sequence.fasta',
|
|
244
|
+
() => {
|
|
245
|
+
session.notify('Sequence downloaded', 'info')
|
|
246
|
+
},
|
|
247
|
+
e => {
|
|
248
|
+
session.notifyError(`${e}`, e)
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
] as MenuItem[]
|
|
254
|
+
}
|
|
255
|
+
ButtonComponent={props => (
|
|
256
|
+
<Button
|
|
257
|
+
{...props}
|
|
258
|
+
variant="contained"
|
|
259
|
+
size="small"
|
|
260
|
+
endIcon={<KeyboardArrowDown />}
|
|
261
|
+
>
|
|
262
|
+
Actions
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{error ? (
|
|
269
|
+
<ErrorMessage error={error} />
|
|
270
|
+
) : (
|
|
271
|
+
<>
|
|
272
|
+
{loading ? (
|
|
273
|
+
<LoadingEllipses />
|
|
274
|
+
) : sequenceTooLarge ? (
|
|
275
|
+
<div>
|
|
276
|
+
Reference sequence too large to display, use the Download button
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
<SequenceDisplay
|
|
280
|
+
model={model}
|
|
281
|
+
sequences={rawSequences}
|
|
282
|
+
singleLineFormat={singleLineFormat}
|
|
283
|
+
includeInsertions={includeInsertions}
|
|
284
|
+
colorBackground={colorBackground}
|
|
285
|
+
showSampleNames={showSampleNames}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
</>
|
|
289
|
+
)}
|
|
290
|
+
</Paper>
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
export default MafSequenceWidget
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { alpha, useTheme } from '@mui/material'
|
|
4
|
+
|
|
5
|
+
import { getBaseColor, getContrastText } from './baseColors'
|
|
6
|
+
import { CHAR_WIDTH, FONT, ROW_HEIGHT } from './constants'
|
|
7
|
+
|
|
8
|
+
import type { Sample } from '../LinearMafDisplay/types'
|
|
9
|
+
|
|
10
|
+
interface SequenceCanvasProps {
|
|
11
|
+
samples: Sample[]
|
|
12
|
+
sequences: string[]
|
|
13
|
+
colorBackground: boolean
|
|
14
|
+
hoveredCol?: number
|
|
15
|
+
onHover: (
|
|
16
|
+
col: number | undefined,
|
|
17
|
+
row: number | undefined,
|
|
18
|
+
clientX: number,
|
|
19
|
+
clientY: number,
|
|
20
|
+
) => void
|
|
21
|
+
onLeave: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function SequenceCanvas({
|
|
25
|
+
samples,
|
|
26
|
+
sequences,
|
|
27
|
+
colorBackground,
|
|
28
|
+
hoveredCol,
|
|
29
|
+
onHover,
|
|
30
|
+
onLeave,
|
|
31
|
+
}: SequenceCanvasProps) {
|
|
32
|
+
const theme = useTheme()
|
|
33
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
34
|
+
|
|
35
|
+
const seqLength = sequences[0]?.length || 0
|
|
36
|
+
const canvasWidth = seqLength * CHAR_WIDTH
|
|
37
|
+
const canvasHeight = samples.length * ROW_HEIGHT
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const canvas = canvasRef.current
|
|
41
|
+
if (!canvas) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ctx = canvas.getContext('2d')
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dpr = window.devicePixelRatio || 1
|
|
51
|
+
canvas.width = canvasWidth * dpr
|
|
52
|
+
canvas.height = canvasHeight * dpr
|
|
53
|
+
canvas.style.width = `${canvasWidth}px`
|
|
54
|
+
canvas.style.height = `${canvasHeight}px`
|
|
55
|
+
ctx.scale(dpr, dpr)
|
|
56
|
+
|
|
57
|
+
ctx.fillStyle = theme.palette.background.paper
|
|
58
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
|
|
59
|
+
|
|
60
|
+
ctx.font = FONT
|
|
61
|
+
ctx.textBaseline = 'top'
|
|
62
|
+
|
|
63
|
+
for (let rowIdx = 0; rowIdx < samples.length; rowIdx++) {
|
|
64
|
+
const seq = sequences[rowIdx] || ''
|
|
65
|
+
const y = rowIdx * ROW_HEIGHT
|
|
66
|
+
|
|
67
|
+
for (let colIdx = 0; colIdx < seq.length; colIdx++) {
|
|
68
|
+
const char = seq[colIdx]!
|
|
69
|
+
const x = colIdx * CHAR_WIDTH
|
|
70
|
+
|
|
71
|
+
if (colorBackground && char !== '-' && char !== '.') {
|
|
72
|
+
ctx.fillStyle = getBaseColor(char, theme)
|
|
73
|
+
ctx.fillRect(x, y, CHAR_WIDTH, ROW_HEIGHT)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (colIdx === hoveredCol) {
|
|
77
|
+
const highlight = (theme.palette as any).highlight as
|
|
78
|
+
| { main: string }
|
|
79
|
+
| undefined
|
|
80
|
+
const highlightColor = highlight?.main ?? '#FFB11D'
|
|
81
|
+
ctx.fillStyle = alpha(highlightColor, 0.5)
|
|
82
|
+
ctx.fillRect(x, y, CHAR_WIDTH, ROW_HEIGHT)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (char === '-') {
|
|
86
|
+
ctx.fillStyle = theme.palette.grey[400]
|
|
87
|
+
} else if (char === '.') {
|
|
88
|
+
ctx.fillStyle = theme.palette.grey[500]
|
|
89
|
+
} else if (colorBackground) {
|
|
90
|
+
ctx.fillStyle = getContrastText(char, theme)
|
|
91
|
+
} else {
|
|
92
|
+
ctx.fillStyle = getBaseColor(char, theme)
|
|
93
|
+
}
|
|
94
|
+
ctx.fillText(char, x + 2, y + 2)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [
|
|
98
|
+
samples,
|
|
99
|
+
sequences,
|
|
100
|
+
canvasWidth,
|
|
101
|
+
canvasHeight,
|
|
102
|
+
hoveredCol,
|
|
103
|
+
colorBackground,
|
|
104
|
+
theme,
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
const handleMouseMove = useCallback(
|
|
108
|
+
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
109
|
+
const canvas = canvasRef.current
|
|
110
|
+
if (!canvas) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rect = canvas.getBoundingClientRect()
|
|
115
|
+
const x = e.clientX - rect.left
|
|
116
|
+
const y = e.clientY - rect.top
|
|
117
|
+
const col = Math.floor(x / CHAR_WIDTH)
|
|
118
|
+
const row = Math.floor(y / ROW_HEIGHT)
|
|
119
|
+
|
|
120
|
+
const validCol = col >= 0 && col < seqLength ? col : undefined
|
|
121
|
+
const validRow = row >= 0 && row < samples.length ? row : undefined
|
|
122
|
+
|
|
123
|
+
onHover(validCol, validRow, e.clientX, e.clientY)
|
|
124
|
+
},
|
|
125
|
+
[seqLength, samples.length, onHover],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<canvas
|
|
130
|
+
ref={canvasRef}
|
|
131
|
+
style={{ display: 'block' }}
|
|
132
|
+
onMouseMove={handleMouseMove}
|
|
133
|
+
onMouseLeave={onLeave}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
import { makeStyles } from 'tss-react/mui'
|
|
5
|
+
|
|
6
|
+
import LabelsCanvas from './LabelsCanvas'
|
|
7
|
+
import SequenceCanvas from './SequenceCanvas'
|
|
8
|
+
import SequenceTooltip from './SequenceTooltip'
|
|
9
|
+
import { buildColToGenomePos, findRefSampleIndex } from './colToGenomePos'
|
|
10
|
+
|
|
11
|
+
import type { MafSequenceWidgetModel } from './stateModelFactory'
|
|
12
|
+
|
|
13
|
+
const useStyles = makeStyles()(theme => ({
|
|
14
|
+
container: {
|
|
15
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
16
|
+
borderRadius: theme.shape.borderRadius,
|
|
17
|
+
maxHeight: 400,
|
|
18
|
+
backgroundColor: theme.palette.background.paper,
|
|
19
|
+
display: 'flex',
|
|
20
|
+
overflow: 'hidden',
|
|
21
|
+
position: 'relative',
|
|
22
|
+
},
|
|
23
|
+
labelsContainer: {
|
|
24
|
+
flexShrink: 0,
|
|
25
|
+
borderRight: `1px solid ${theme.palette.divider}`,
|
|
26
|
+
backgroundColor: theme.palette.background.paper,
|
|
27
|
+
overflowY: 'auto',
|
|
28
|
+
scrollbarWidth: 'none',
|
|
29
|
+
'&::-webkit-scrollbar': {
|
|
30
|
+
display: 'none',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
sequenceContainer: {
|
|
34
|
+
flex: 1,
|
|
35
|
+
overflow: 'auto',
|
|
36
|
+
},
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
interface SequenceDisplayProps {
|
|
40
|
+
model: MafSequenceWidgetModel
|
|
41
|
+
sequences: string[]
|
|
42
|
+
singleLineFormat: boolean
|
|
43
|
+
includeInsertions: boolean
|
|
44
|
+
colorBackground: boolean
|
|
45
|
+
showSampleNames: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SequenceDisplay = observer(function SequenceDisplay({
|
|
49
|
+
model,
|
|
50
|
+
sequences,
|
|
51
|
+
colorBackground,
|
|
52
|
+
showSampleNames,
|
|
53
|
+
}: SequenceDisplayProps) {
|
|
54
|
+
const { classes } = useStyles()
|
|
55
|
+
const labelsContainerRef = useRef<HTMLDivElement>(null)
|
|
56
|
+
const seqContainerRef = useRef<HTMLDivElement>(null)
|
|
57
|
+
const { samples, regions } = model
|
|
58
|
+
|
|
59
|
+
const [hoveredCol, setHoveredCol] = React.useState<number | undefined>()
|
|
60
|
+
const [hoveredRow, setHoveredRow] = React.useState<number | undefined>()
|
|
61
|
+
const [tooltipPos, setTooltipPos] = React.useState<
|
|
62
|
+
{ x: number; y: number } | undefined
|
|
63
|
+
>()
|
|
64
|
+
|
|
65
|
+
const maxLabelLength = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
samples ? Math.max(...samples.map(s => (s.label ?? s.id).length)) : 0,
|
|
68
|
+
[samples],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const colToGenomePos = useMemo(() => {
|
|
72
|
+
if (!regions) {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
const region = regions[0]
|
|
76
|
+
if (!region) {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const refIdx = findRefSampleIndex(samples, region.assemblyName)
|
|
81
|
+
const refSequence = sequences[refIdx] || ''
|
|
82
|
+
return buildColToGenomePos(refSequence, region.start)
|
|
83
|
+
}, [sequences, regions, samples])
|
|
84
|
+
|
|
85
|
+
// Sync vertical scroll between labels and sequences
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const labelsContainer = labelsContainerRef.current
|
|
88
|
+
const seqContainer = seqContainerRef.current
|
|
89
|
+
if (!labelsContainer || !seqContainer) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const handleSeqScroll = () => {
|
|
94
|
+
labelsContainer.scrollTop = seqContainer.scrollTop
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
seqContainer.addEventListener('scroll', handleSeqScroll)
|
|
98
|
+
return () => {
|
|
99
|
+
seqContainer.removeEventListener('scroll', handleSeqScroll)
|
|
100
|
+
}
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
const handleHover = useCallback(
|
|
104
|
+
(
|
|
105
|
+
col: number | undefined,
|
|
106
|
+
row: number | undefined,
|
|
107
|
+
clientX: number,
|
|
108
|
+
clientY: number,
|
|
109
|
+
) => {
|
|
110
|
+
if (!regions) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setHoveredCol(col)
|
|
115
|
+
setHoveredRow(row)
|
|
116
|
+
setTooltipPos({ x: clientX, y: clientY })
|
|
117
|
+
|
|
118
|
+
if (col !== undefined) {
|
|
119
|
+
const genomicPos = colToGenomePos[col]
|
|
120
|
+
const region = regions[0]
|
|
121
|
+
if (genomicPos !== undefined && region) {
|
|
122
|
+
model.setHoverHighlight({
|
|
123
|
+
refName: region.refName,
|
|
124
|
+
start: genomicPos,
|
|
125
|
+
end: genomicPos + 1,
|
|
126
|
+
assemblyName: region.assemblyName,
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
model.setHoverHighlight(undefined)
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
model.setHoverHighlight(undefined)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
[colToGenomePos, model, regions],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const handleLeave = useCallback(() => {
|
|
139
|
+
setHoveredCol(undefined)
|
|
140
|
+
setHoveredRow(undefined)
|
|
141
|
+
setTooltipPos(undefined)
|
|
142
|
+
model.setHoverHighlight(undefined)
|
|
143
|
+
}, [model])
|
|
144
|
+
|
|
145
|
+
if (!samples || !regions || sequences.length === 0) {
|
|
146
|
+
return <div>No sequence data</div>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hoveredSample =
|
|
150
|
+
hoveredRow !== undefined ? samples[hoveredRow] : undefined
|
|
151
|
+
const hoveredChar =
|
|
152
|
+
hoveredRow !== undefined && hoveredCol !== undefined
|
|
153
|
+
? sequences[hoveredRow]?.[hoveredCol]
|
|
154
|
+
: undefined
|
|
155
|
+
const genomicPos =
|
|
156
|
+
hoveredCol !== undefined ? colToGenomePos[hoveredCol] : undefined
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className={classes.container}>
|
|
160
|
+
{showSampleNames && (
|
|
161
|
+
<div ref={labelsContainerRef} className={classes.labelsContainer}>
|
|
162
|
+
<LabelsCanvas samples={samples} maxLabelLength={maxLabelLength} />
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
<div ref={seqContainerRef} className={classes.sequenceContainer}>
|
|
166
|
+
<SequenceCanvas
|
|
167
|
+
samples={samples}
|
|
168
|
+
sequences={sequences}
|
|
169
|
+
colorBackground={colorBackground}
|
|
170
|
+
hoveredCol={hoveredCol}
|
|
171
|
+
onHover={handleHover}
|
|
172
|
+
onLeave={handleLeave}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
{tooltipPos && hoveredSample && (
|
|
176
|
+
<SequenceTooltip
|
|
177
|
+
x={tooltipPos.x}
|
|
178
|
+
y={tooltipPos.y}
|
|
179
|
+
sample={hoveredSample}
|
|
180
|
+
base={hoveredChar}
|
|
181
|
+
genomicPos={genomicPos}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
export default SequenceDisplay
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { makeStyles } from 'tss-react/mui'
|
|
4
|
+
|
|
5
|
+
import type { Sample } from '../LinearMafDisplay/types'
|
|
6
|
+
|
|
7
|
+
const useStyles = makeStyles()(theme => ({
|
|
8
|
+
tooltip: {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
pointerEvents: 'none',
|
|
11
|
+
zIndex: 1000,
|
|
12
|
+
backgroundColor: theme.palette.grey[800],
|
|
13
|
+
color: theme.palette.common.white,
|
|
14
|
+
padding: '4px 8px',
|
|
15
|
+
borderRadius: 4,
|
|
16
|
+
fontSize: 12,
|
|
17
|
+
whiteSpace: 'nowrap',
|
|
18
|
+
},
|
|
19
|
+
insertion: {
|
|
20
|
+
color: theme.palette.warning.light,
|
|
21
|
+
fontStyle: 'italic',
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
interface SequenceTooltipProps {
|
|
26
|
+
x: number
|
|
27
|
+
y: number
|
|
28
|
+
sample: Sample
|
|
29
|
+
base?: string
|
|
30
|
+
genomicPos?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default function SequenceTooltip({
|
|
34
|
+
x,
|
|
35
|
+
y,
|
|
36
|
+
sample,
|
|
37
|
+
base,
|
|
38
|
+
genomicPos,
|
|
39
|
+
}: SequenceTooltipProps) {
|
|
40
|
+
const { classes } = useStyles()
|
|
41
|
+
|
|
42
|
+
// An insertion is when we have a base but no genomic position
|
|
43
|
+
// (the reference has a gap at this column)
|
|
44
|
+
const isInsertion = base !== undefined && genomicPos === undefined
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={classes.tooltip}
|
|
49
|
+
style={{
|
|
50
|
+
left: x + 12,
|
|
51
|
+
top: y + 12,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<div>
|
|
55
|
+
<strong>{sample.label}</strong>
|
|
56
|
+
</div>
|
|
57
|
+
{base && (
|
|
58
|
+
<div>
|
|
59
|
+
Base: {base}
|
|
60
|
+
{genomicPos !== undefined
|
|
61
|
+
? ` | Pos: ${(genomicPos + 1).toLocaleString('en-US')}`
|
|
62
|
+
: null}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
{isInsertion && (
|
|
66
|
+
<div className={classes.insertion}>Insertion (not in reference)</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|