jbrowse-plugin-mafviewer 1.4.5 → 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.
Files changed (169) hide show
  1. package/dist/BigMafAdapter/BigMafAdapter.js +4 -5
  2. package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
  3. package/dist/BigMafAdapter/configSchema.d.ts +2 -2
  4. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +38 -108
  5. package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
  6. package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +0 -3
  7. package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
  8. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.d.ts +9 -0
  9. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js +34 -0
  10. package/dist/LinearMafDisplay/components/MsaHighlightOverlay.js.map +1 -0
  11. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js +1 -1
  12. package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -1
  13. package/dist/LinearMafDisplay/components/useDragSelection.d.ts +25 -0
  14. package/dist/LinearMafDisplay/components/useDragSelection.js +103 -0
  15. package/dist/LinearMafDisplay/components/useDragSelection.js.map +1 -0
  16. package/dist/LinearMafDisplay/configSchema.d.ts +3 -30
  17. package/dist/LinearMafDisplay/stateModel.d.ts +1043 -121
  18. package/dist/LinearMafDisplay/stateModel.js +85 -41
  19. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  20. package/dist/LinearMafDisplay/types.d.ts +2 -2
  21. package/dist/LinearMafDisplay/util.d.ts +5 -0
  22. package/dist/LinearMafDisplay/util.js +25 -4
  23. package/dist/LinearMafDisplay/util.js.map +1 -1
  24. package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +41 -5
  25. package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -1
  26. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  27. package/dist/LinearMafRenderer/components/LinearMafRendering.d.ts +14 -5
  28. package/dist/LinearMafRenderer/components/LinearMafRendering.js +21 -19
  29. package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -1
  30. package/dist/LinearMafRenderer/configSchema.d.ts +1 -6
  31. package/dist/LinearMafRenderer/configSchema.js +1 -6
  32. package/dist/LinearMafRenderer/configSchema.js.map +1 -1
  33. package/dist/LinearMafRenderer/rendering/insertions.d.ts +1 -1
  34. package/dist/LinearMafRenderer/rendering/insertions.js +2 -2
  35. package/dist/LinearMafRenderer/rendering/mismatches.d.ts +1 -1
  36. package/dist/LinearMafRenderer/rendering/mismatches.js +3 -3
  37. package/dist/LinearMafRenderer/rendering/types.d.ts +1 -1
  38. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js +1 -1
  39. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
  40. package/dist/MafAddTrackWorkflow/index.js +1 -1
  41. package/dist/MafAddTrackWorkflow/index.js.map +1 -1
  42. package/dist/MafGetSequences/MafGetSequences.d.ts +1 -0
  43. package/dist/MafGetSequences/MafGetSequences.js +2 -1
  44. package/dist/MafGetSequences/MafGetSequences.js.map +1 -1
  45. package/dist/MafSequenceWidget/LabelsCanvas.d.ts +8 -0
  46. package/dist/MafSequenceWidget/LabelsCanvas.js +37 -0
  47. package/dist/MafSequenceWidget/LabelsCanvas.js.map +1 -0
  48. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.d.ts +6 -0
  49. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js +52 -0
  50. package/dist/MafSequenceWidget/MafSequenceHoverHighlight.js.map +1 -0
  51. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.d.ts +2 -0
  52. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js +12 -0
  53. package/dist/MafSequenceWidget/MafSequenceHoverHighlightExtension.js.map +1 -0
  54. package/dist/MafSequenceWidget/MafSequenceWidget.d.ts +6 -0
  55. package/dist/MafSequenceWidget/MafSequenceWidget.js +189 -0
  56. package/dist/MafSequenceWidget/MafSequenceWidget.js.map +1 -0
  57. package/dist/MafSequenceWidget/SequenceCanvas.d.ts +12 -0
  58. package/dist/MafSequenceWidget/SequenceCanvas.js +86 -0
  59. package/dist/MafSequenceWidget/SequenceCanvas.js.map +1 -0
  60. package/dist/MafSequenceWidget/SequenceDisplay.d.ts +12 -0
  61. package/dist/MafSequenceWidget/SequenceDisplay.js +117 -0
  62. package/dist/MafSequenceWidget/SequenceDisplay.js.map +1 -0
  63. package/dist/MafSequenceWidget/SequenceTooltip.d.ts +11 -0
  64. package/dist/MafSequenceWidget/SequenceTooltip.js +39 -0
  65. package/dist/MafSequenceWidget/SequenceTooltip.js.map +1 -0
  66. package/dist/MafSequenceWidget/baseColors.d.ts +3 -0
  67. package/dist/MafSequenceWidget/baseColors.js +64 -0
  68. package/dist/MafSequenceWidget/baseColors.js.map +1 -0
  69. package/dist/MafSequenceWidget/colToGenomePos.d.ts +13 -0
  70. package/dist/MafSequenceWidget/colToGenomePos.js +32 -0
  71. package/dist/MafSequenceWidget/colToGenomePos.js.map +1 -0
  72. package/dist/MafSequenceWidget/colToGenomePos.test.d.ts +1 -0
  73. package/dist/MafSequenceWidget/colToGenomePos.test.js +136 -0
  74. package/dist/MafSequenceWidget/colToGenomePos.test.js.map +1 -0
  75. package/dist/MafSequenceWidget/configSchema.d.ts +1 -0
  76. package/dist/MafSequenceWidget/configSchema.js +3 -0
  77. package/dist/MafSequenceWidget/configSchema.js.map +1 -0
  78. package/dist/MafSequenceWidget/constants.d.ts +4 -0
  79. package/dist/MafSequenceWidget/constants.js +5 -0
  80. package/dist/MafSequenceWidget/constants.js.map +1 -0
  81. package/dist/MafSequenceWidget/index.d.ts +2 -0
  82. package/dist/MafSequenceWidget/index.js +16 -0
  83. package/dist/MafSequenceWidget/index.js.map +1 -0
  84. package/dist/MafSequenceWidget/stateModelFactory.d.ts +67 -0
  85. package/dist/MafSequenceWidget/stateModelFactory.js +21 -0
  86. package/dist/MafSequenceWidget/stateModelFactory.js.map +1 -0
  87. package/dist/MafTabixAdapter/MafTabixAdapter.js +4 -35
  88. package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
  89. package/dist/MafTabixAdapter/configSchema.d.ts +4 -4
  90. package/dist/MafTrack/configSchema.d.ts +16 -11
  91. package/dist/index.js +2 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +12 -24
  94. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  95. package/dist/util/clipboard.d.ts +2 -0
  96. package/dist/util/clipboard.js +28 -0
  97. package/dist/util/clipboard.js.map +1 -0
  98. package/dist/util/fastaUtils.d.ts +2 -1
  99. package/dist/util/fastaUtils.js +72 -2
  100. package/dist/util/fastaUtils.js.map +1 -1
  101. package/dist/util/fastaUtils.test.js +190 -0
  102. package/dist/util/fastaUtils.test.js.map +1 -1
  103. package/dist/util/parseAssemblyName.d.ts +32 -0
  104. package/dist/util/parseAssemblyName.js +87 -0
  105. package/dist/util/parseAssemblyName.js.map +1 -0
  106. package/dist/util/parseAssemblyName.test.d.ts +1 -0
  107. package/dist/util/parseAssemblyName.test.js +269 -0
  108. package/dist/util/parseAssemblyName.test.js.map +1 -0
  109. package/package.json +7 -7
  110. package/src/BigMafAdapter/BigMafAdapter.ts +5 -5
  111. package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +62 -144
  112. package/src/LinearMafDisplay/components/MAFTooltip.tsx +0 -3
  113. package/src/LinearMafDisplay/components/MsaHighlightOverlay.tsx +62 -0
  114. package/src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx +1 -1
  115. package/src/LinearMafDisplay/components/useDragSelection.ts +159 -0
  116. package/src/LinearMafDisplay/stateModel.ts +135 -48
  117. package/src/LinearMafDisplay/types.ts +2 -2
  118. package/src/LinearMafDisplay/util.ts +31 -5
  119. package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -1
  120. package/src/LinearMafRenderer/components/LinearMafRendering.tsx +38 -24
  121. package/src/LinearMafRenderer/configSchema.ts +1 -6
  122. package/src/LinearMafRenderer/rendering/insertions.ts +2 -2
  123. package/src/LinearMafRenderer/rendering/mismatches.ts +3 -3
  124. package/src/LinearMafRenderer/rendering/types.ts +1 -1
  125. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +1 -1
  126. package/src/MafAddTrackWorkflow/index.ts +1 -1
  127. package/src/MafGetSequences/MafGetSequences.ts +10 -2
  128. package/src/MafSequenceWidget/LabelsCanvas.tsx +58 -0
  129. package/src/MafSequenceWidget/MafSequenceHoverHighlight.tsx +83 -0
  130. package/src/MafSequenceWidget/MafSequenceHoverHighlightExtension.tsx +24 -0
  131. package/src/MafSequenceWidget/MafSequenceWidget.tsx +294 -0
  132. package/src/MafSequenceWidget/SequenceCanvas.tsx +136 -0
  133. package/src/MafSequenceWidget/SequenceDisplay.tsx +188 -0
  134. package/src/MafSequenceWidget/SequenceTooltip.tsx +70 -0
  135. package/src/MafSequenceWidget/baseColors.ts +76 -0
  136. package/src/MafSequenceWidget/colToGenomePos.test.ts +166 -0
  137. package/src/MafSequenceWidget/colToGenomePos.ts +40 -0
  138. package/src/MafSequenceWidget/configSchema.ts +3 -0
  139. package/src/MafSequenceWidget/constants.ts +4 -0
  140. package/src/MafSequenceWidget/index.ts +24 -0
  141. package/src/MafSequenceWidget/stateModelFactory.ts +43 -0
  142. package/src/MafTabixAdapter/MafTabixAdapter.ts +12 -51
  143. package/src/index.ts +2 -0
  144. package/src/util/__snapshots__/fastaUtils.test.ts.snap +35 -0
  145. package/src/util/clipboard.ts +35 -0
  146. package/src/util/fastaUtils.test.ts +199 -0
  147. package/src/util/fastaUtils.ts +94 -1
  148. package/src/util/parseAssemblyName.test.ts +350 -0
  149. package/src/util/parseAssemblyName.ts +106 -0
  150. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.d.ts +0 -11
  151. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js +0 -97
  152. package/dist/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.js.map +0 -1
  153. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.d.ts +0 -14
  154. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js +0 -69
  155. package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js.map +0 -1
  156. package/dist/LinearMafDisplay/components/util.d.ts +0 -1
  157. package/dist/LinearMafDisplay/components/util.js +0 -8
  158. package/dist/LinearMafDisplay/components/util.js.map +0 -1
  159. package/dist/util/fetchSequences.d.ts +0 -18
  160. package/dist/util/fetchSequences.js +0 -39
  161. package/dist/util/fetchSequences.js.map +0 -1
  162. package/dist/util/useSequences.d.ts +0 -21
  163. package/dist/util/useSequences.js +0 -64
  164. package/dist/util/useSequences.js.map +0 -1
  165. package/src/LinearMafDisplay/components/GetSequenceDialog/GetSequenceDialog.tsx +0 -175
  166. package/src/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.tsx +0 -105
  167. package/src/LinearMafDisplay/components/util.ts +0 -7
  168. package/src/util/fetchSequences.ts +0 -57
  169. 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
+ }