@apollo-annotation/jbrowse-plugin-apollo 0.3.4 → 0.3.5
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/index.esm.js +1513 -1074
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +1510 -1065
- package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.development.js +4681 -2097
- package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
- package/package.json +4 -4
- package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +13 -0
- package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +88 -17
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +144 -22
- package/src/FeatureDetailsWidget/Attributes.tsx +93 -43
- package/src/FeatureDetailsWidget/BasicInformation.tsx +2 -4
- package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +6 -4
- package/src/FeatureDetailsWidget/Sequence.tsx +16 -33
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +137 -92
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +600 -0
- package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +54 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +13 -6
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +6 -27
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +35 -13
- package/src/LinearApolloDisplay/stateModel/layouts.ts +7 -2
- package/src/LinearApolloDisplay/stateModel/rendering.ts +4 -0
- package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +10 -1
- package/src/OntologyManager/OntologyStore/index.test.ts +1 -0
- package/src/OntologyManager/index.ts +2 -0
- package/src/SixFrameFeatureDisplay/stateModel.ts +5 -1
- package/src/TabularEditor/HybridGrid/Feature.tsx +0 -1
- package/src/TabularEditor/HybridGrid/NumberCell.tsx +8 -1
- package/src/TabularEditor/HybridGrid/ToolBar.tsx +14 -12
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +3 -27
- package/src/components/AddAssembly.tsx +608 -291
- package/src/components/CreateApolloAnnotation.tsx +144 -37
- package/src/components/OntologyTermMultiSelect.tsx +3 -0
- package/src/components/index.ts +0 -1
- package/src/extensions/annotationFromJBrowseFeature.ts +3 -2
- package/src/makeDisplayComponent.tsx +3 -4
- package/src/util/annotationFeatureUtils.ts +53 -0
- package/src/util/index.ts +1 -0
- package/src/FeatureDetailsWidget/TranscriptBasic.tsx +0 -200
- package/src/components/ModifyFeatureAttribute.tsx +0 -460
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/* eslint-disable unicorn/no-nested-ternary */
|
|
2
|
+
/* eslint-disable unicorn/prefer-at */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
6
|
+
import React, { useRef } from 'react'
|
|
7
|
+
|
|
8
|
+
import { observer } from 'mobx-react'
|
|
9
|
+
|
|
10
|
+
import styled from '@emotion/styled'
|
|
11
|
+
import {
|
|
12
|
+
Accordion,
|
|
13
|
+
AccordionDetails,
|
|
14
|
+
Grid2,
|
|
15
|
+
Tooltip,
|
|
16
|
+
Typography,
|
|
17
|
+
} from '@mui/material'
|
|
18
|
+
|
|
19
|
+
import AddIcon from '@mui/icons-material/Add'
|
|
20
|
+
import RemoveIcon from '@mui/icons-material/Remove'
|
|
21
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|
22
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|
23
|
+
import ContentCutIcon from '@mui/icons-material/ContentCut'
|
|
24
|
+
|
|
25
|
+
import { AnnotationFeature, TranscriptPart } from '@apollo-annotation/mst'
|
|
26
|
+
|
|
27
|
+
import { StyledAccordionSummary } from './ApolloTranscriptDetailsWidget'
|
|
28
|
+
import { NumberTextField } from './NumberTextField'
|
|
29
|
+
import { ApolloSessionModel } from '../session'
|
|
30
|
+
import {
|
|
31
|
+
AbstractSessionModel,
|
|
32
|
+
defaultCodonTable,
|
|
33
|
+
revcom,
|
|
34
|
+
} from '@jbrowse/core/util'
|
|
35
|
+
import {
|
|
36
|
+
LocationEndChange,
|
|
37
|
+
LocationStartChange,
|
|
38
|
+
} from '@apollo-annotation/shared'
|
|
39
|
+
|
|
40
|
+
const StyledTextField = styled(NumberTextField)(() => ({
|
|
41
|
+
'&.MuiFormControl-root': {
|
|
42
|
+
marginTop: 0,
|
|
43
|
+
marginBottom: 0,
|
|
44
|
+
width: '100%',
|
|
45
|
+
},
|
|
46
|
+
'& .MuiInputBase-input': {
|
|
47
|
+
fontSize: 12,
|
|
48
|
+
height: 20,
|
|
49
|
+
padding: 1,
|
|
50
|
+
paddingLeft: 10,
|
|
51
|
+
},
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const SequenceContainer = styled('div')({
|
|
55
|
+
display: 'flex',
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
textAlign: 'left',
|
|
59
|
+
width: '100%',
|
|
60
|
+
overflowWrap: 'break-word',
|
|
61
|
+
wordWrap: 'break-word',
|
|
62
|
+
wordBreak: 'break-all',
|
|
63
|
+
'& span': {
|
|
64
|
+
fontSize: 12,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const Strand = (props: { strand: 1 | -1 | undefined }) => {
|
|
69
|
+
const { strand } = props
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
{strand === 1 ? (
|
|
74
|
+
<AddIcon />
|
|
75
|
+
) : strand === -1 ? (
|
|
76
|
+
<RemoveIcon />
|
|
77
|
+
) : (
|
|
78
|
+
<Typography component={'span'}>N/A</Typography>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const TranscriptWidgetEditLocation = observer(
|
|
85
|
+
function TranscriptWidgetEditLocation({
|
|
86
|
+
assembly,
|
|
87
|
+
feature,
|
|
88
|
+
refName,
|
|
89
|
+
session,
|
|
90
|
+
}: {
|
|
91
|
+
feature: AnnotationFeature
|
|
92
|
+
refName: string
|
|
93
|
+
session: ApolloSessionModel
|
|
94
|
+
assembly: string
|
|
95
|
+
}) {
|
|
96
|
+
const { notify } = session as unknown as AbstractSessionModel
|
|
97
|
+
const currentAssembly = session.apolloDataStore.assemblies.get(assembly)
|
|
98
|
+
const refData = currentAssembly?.getByRefName(refName)
|
|
99
|
+
const { changeManager } = session.apolloDataStore
|
|
100
|
+
const seqRef = useRef<HTMLDivElement>(null)
|
|
101
|
+
|
|
102
|
+
// Separate function to handle CDS location change
|
|
103
|
+
// because start of CDS and exon might be same
|
|
104
|
+
function handleCDSLocationChange(
|
|
105
|
+
oldLocation: number,
|
|
106
|
+
newLocation: number,
|
|
107
|
+
feature: AnnotationFeature,
|
|
108
|
+
isMin: boolean,
|
|
109
|
+
) {
|
|
110
|
+
if (!feature.children) {
|
|
111
|
+
throw new Error('Transcript should have child features')
|
|
112
|
+
}
|
|
113
|
+
for (const [, child] of feature.children) {
|
|
114
|
+
if (child.type !== 'CDS') {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (isMin && oldLocation === child.min) {
|
|
118
|
+
const change = new LocationStartChange({
|
|
119
|
+
typeName: 'LocationStartChange',
|
|
120
|
+
changedIds: [child._id],
|
|
121
|
+
featureId: feature._id,
|
|
122
|
+
oldStart: child.min,
|
|
123
|
+
newStart: newLocation,
|
|
124
|
+
assembly,
|
|
125
|
+
})
|
|
126
|
+
changeManager.submit(change).catch(() => {
|
|
127
|
+
notify('Error updating feature start position', 'error')
|
|
128
|
+
})
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
if (!isMin && oldLocation === child.max) {
|
|
132
|
+
const change = new LocationEndChange({
|
|
133
|
+
typeName: 'LocationEndChange',
|
|
134
|
+
changedIds: [child._id],
|
|
135
|
+
featureId: feature._id,
|
|
136
|
+
oldEnd: child.max,
|
|
137
|
+
newEnd: newLocation,
|
|
138
|
+
assembly,
|
|
139
|
+
})
|
|
140
|
+
changeManager.submit(change).catch(() => {
|
|
141
|
+
notify('Error updating feature start position', 'error')
|
|
142
|
+
})
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleExonLocationChange(
|
|
149
|
+
oldLocation: number,
|
|
150
|
+
newLocation: number,
|
|
151
|
+
feature: AnnotationFeature,
|
|
152
|
+
isMin: boolean,
|
|
153
|
+
) {
|
|
154
|
+
if (!feature.children) {
|
|
155
|
+
throw new Error('Transcript should have child features')
|
|
156
|
+
}
|
|
157
|
+
for (const [, child] of feature.children) {
|
|
158
|
+
if (child.type !== 'exon') {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (isMin && oldLocation === child.min) {
|
|
162
|
+
const change = new LocationStartChange({
|
|
163
|
+
typeName: 'LocationStartChange',
|
|
164
|
+
changedIds: [child._id],
|
|
165
|
+
featureId: feature._id,
|
|
166
|
+
oldStart: child.min,
|
|
167
|
+
newStart: newLocation,
|
|
168
|
+
assembly,
|
|
169
|
+
})
|
|
170
|
+
changeManager.submit(change).catch(() => {
|
|
171
|
+
notify('Error updating feature start position', 'error')
|
|
172
|
+
})
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (!isMin && oldLocation === child.max) {
|
|
176
|
+
const change = new LocationEndChange({
|
|
177
|
+
typeName: 'LocationEndChange',
|
|
178
|
+
changedIds: [child._id],
|
|
179
|
+
featureId: feature._id,
|
|
180
|
+
oldEnd: child.max,
|
|
181
|
+
newEnd: newLocation,
|
|
182
|
+
assembly,
|
|
183
|
+
})
|
|
184
|
+
changeManager.submit(change).catch(() => {
|
|
185
|
+
notify('Error updating feature start position', 'error')
|
|
186
|
+
})
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!refData) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { cdsLocations, transcriptExonParts, strand } = feature
|
|
197
|
+
const [firstCDSLocation] = cdsLocations
|
|
198
|
+
|
|
199
|
+
const exonParts = transcriptExonParts
|
|
200
|
+
.filter((part) => part.type === 'exon')
|
|
201
|
+
.sort(({ min: a }, { min: b }) => a - b)
|
|
202
|
+
|
|
203
|
+
const exonMin: number = exonParts[0]?.min
|
|
204
|
+
const exonMax: number = exonParts[exonParts.length - 1]?.max
|
|
205
|
+
|
|
206
|
+
let cdsMin = exonMin
|
|
207
|
+
let cdsMax = exonMax
|
|
208
|
+
const cdsPresent = firstCDSLocation.length > 0
|
|
209
|
+
|
|
210
|
+
if (cdsPresent) {
|
|
211
|
+
cdsMin = firstCDSLocation[0].min
|
|
212
|
+
cdsMax = firstCDSLocation[firstCDSLocation.length - 1].max
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const getFivePrimeSpliceSite = (
|
|
216
|
+
loc: TranscriptPart,
|
|
217
|
+
prevLocIdx: number,
|
|
218
|
+
) => {
|
|
219
|
+
let spliceSite = ''
|
|
220
|
+
if (prevLocIdx > 0) {
|
|
221
|
+
const prevLoc = transcriptExonParts[prevLocIdx - 1]
|
|
222
|
+
if (strand === 1) {
|
|
223
|
+
if (prevLoc.type === 'intron') {
|
|
224
|
+
spliceSite = refData.getSequence(loc.min - 2, loc.min)
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
if (prevLoc.type === 'intron') {
|
|
228
|
+
spliceSite = revcom(refData.getSequence(loc.max, loc.max + 2))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
spliceSite,
|
|
235
|
+
color: spliceSite === 'AG' ? 'green' : 'red',
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const getThreePrimeSpliceSite = (
|
|
241
|
+
loc: TranscriptPart,
|
|
242
|
+
nextLocIdx: number,
|
|
243
|
+
) => {
|
|
244
|
+
let spliceSite = ''
|
|
245
|
+
if (nextLocIdx < transcriptExonParts.length - 1) {
|
|
246
|
+
const nextLoc = transcriptExonParts[nextLocIdx + 1]
|
|
247
|
+
if (strand === 1) {
|
|
248
|
+
if (nextLoc.type === 'intron') {
|
|
249
|
+
spliceSite = refData.getSequence(loc.max, loc.max + 2)
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
if (nextLoc.type === 'intron') {
|
|
253
|
+
spliceSite = revcom(refData.getSequence(loc.min - 2, loc.min))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
spliceSite,
|
|
260
|
+
color: spliceSite === 'GT' ? 'green' : 'red',
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const getTranslationSequence = () => {
|
|
266
|
+
let wholeSequence = ''
|
|
267
|
+
const [firstLocation] = cdsLocations
|
|
268
|
+
for (const loc of firstLocation) {
|
|
269
|
+
let sequence = refData.getSequence(loc.min, loc.max)
|
|
270
|
+
if (strand === -1) {
|
|
271
|
+
sequence = revcom(sequence)
|
|
272
|
+
}
|
|
273
|
+
wholeSequence += sequence
|
|
274
|
+
}
|
|
275
|
+
const elements = []
|
|
276
|
+
for (
|
|
277
|
+
let codonGenomicPos = 0;
|
|
278
|
+
codonGenomicPos < wholeSequence.length;
|
|
279
|
+
codonGenomicPos += 3
|
|
280
|
+
) {
|
|
281
|
+
const codonSeq = wholeSequence
|
|
282
|
+
.slice(codonGenomicPos, codonGenomicPos + 3)
|
|
283
|
+
.toUpperCase()
|
|
284
|
+
const protein =
|
|
285
|
+
defaultCodonTable[codonSeq as keyof typeof defaultCodonTable] || '&'
|
|
286
|
+
// highlight start codon and stop codons
|
|
287
|
+
if (codonSeq === 'ATG') {
|
|
288
|
+
elements.push(
|
|
289
|
+
<Typography
|
|
290
|
+
component={'span'}
|
|
291
|
+
style={{
|
|
292
|
+
backgroundColor: 'yellow',
|
|
293
|
+
cursor: 'pointer',
|
|
294
|
+
border: '1px solid black',
|
|
295
|
+
}}
|
|
296
|
+
key={codonGenomicPos}
|
|
297
|
+
onClick={() => {
|
|
298
|
+
// NOTE: codonGenomicPos is important here for calculating the genomic location
|
|
299
|
+
// of the start codon. We are using the codonGenomicPos as the key in the typography
|
|
300
|
+
// elements to maintain the genomic postion of the codon start
|
|
301
|
+
const startCodonGenomicLocation =
|
|
302
|
+
getStartCodonGenomicLocation(codonGenomicPos)
|
|
303
|
+
if (startCodonGenomicLocation !== cdsMin) {
|
|
304
|
+
handleCDSLocationChange(
|
|
305
|
+
cdsMin,
|
|
306
|
+
startCodonGenomicLocation,
|
|
307
|
+
feature,
|
|
308
|
+
true,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
{protein}
|
|
314
|
+
</Typography>,
|
|
315
|
+
)
|
|
316
|
+
} else if (['TAA', 'TAG', 'TGA'].includes(codonSeq)) {
|
|
317
|
+
elements.push(
|
|
318
|
+
<Typography
|
|
319
|
+
style={{ backgroundColor: 'red', color: 'white' }}
|
|
320
|
+
component={'span'}
|
|
321
|
+
// Pass the codonGenomicPos as the key to maintain the genomic position of the codon
|
|
322
|
+
key={codonGenomicPos}
|
|
323
|
+
>
|
|
324
|
+
{protein}
|
|
325
|
+
</Typography>,
|
|
326
|
+
)
|
|
327
|
+
} else {
|
|
328
|
+
elements.push(
|
|
329
|
+
// Pass the codonGenomicPos as the key to maintain the genomic position of the codon
|
|
330
|
+
<Typography component={'span'} key={codonGenomicPos}>
|
|
331
|
+
{protein}
|
|
332
|
+
</Typography>,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return elements
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Codon position is the index of the start codon in the CDS genomic sequence
|
|
340
|
+
// Calculate the genomic location of the start codon based on the codon position in the CDS
|
|
341
|
+
const getStartCodonGenomicLocation = (codonGenomicPosition: number) => {
|
|
342
|
+
const [firstLocation] = cdsLocations
|
|
343
|
+
let cdsLen = 0
|
|
344
|
+
for (const loc of firstLocation) {
|
|
345
|
+
const locLength = loc.max - loc.min
|
|
346
|
+
// Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
|
|
347
|
+
// and codonGenomicPosition is 25
|
|
348
|
+
// (((10 - 0) + (30 - 20)) + 10) > 25
|
|
349
|
+
// 40 + (25-20) = 45 is the genomic location of the start codon
|
|
350
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
351
|
+
return loc.min + (codonGenomicPosition - cdsLen)
|
|
352
|
+
}
|
|
353
|
+
cdsLen += locLength
|
|
354
|
+
}
|
|
355
|
+
return cdsMin
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const getStopCodonGenomicLocation = (codonGenomicPosition: number) => {
|
|
359
|
+
const [firstLocation] = cdsLocations
|
|
360
|
+
let cdsLen = 0
|
|
361
|
+
for (const loc of firstLocation) {
|
|
362
|
+
const locLength = loc.max - loc.min
|
|
363
|
+
// Check if the codonPosition is within the current location
|
|
364
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
365
|
+
return loc.min + (codonGenomicPosition - cdsLen)
|
|
366
|
+
}
|
|
367
|
+
cdsLen += locLength
|
|
368
|
+
}
|
|
369
|
+
return cdsMax
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const trimTranslationSequence = () => {
|
|
373
|
+
const sequenceElements = getTranslationSequence()
|
|
374
|
+
const translationSequence = sequenceElements
|
|
375
|
+
.map((el) => el.props.children)
|
|
376
|
+
.join('')
|
|
377
|
+
|
|
378
|
+
if (
|
|
379
|
+
translationSequence.startsWith('M') &&
|
|
380
|
+
translationSequence.endsWith('*')
|
|
381
|
+
) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// NOTE: We are maintaining the genomic location of the codon start as the "key"
|
|
386
|
+
// in typography elements. See getTranslationSequence function
|
|
387
|
+
const translSeqCodonStartGenomicPosArr = []
|
|
388
|
+
for (const el of sequenceElements) {
|
|
389
|
+
translSeqCodonStartGenomicPosArr.push({
|
|
390
|
+
codonGenomicPos: el.key,
|
|
391
|
+
sequenceLetter: el.props.children,
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (translSeqCodonStartGenomicPosArr.length === 0) {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Trim any sequence before first start codon and after last stop codon
|
|
400
|
+
const startCodonIndex = translationSequence.indexOf('M')
|
|
401
|
+
const stopCodonIndex = translationSequence.lastIndexOf('*') + 1
|
|
402
|
+
const startCodonPos =
|
|
403
|
+
translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
|
|
404
|
+
const stopCodonPos =
|
|
405
|
+
translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos
|
|
406
|
+
|
|
407
|
+
if (!startCodonPos || !stopCodonPos) {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const startCodonGenomicLoc = getStartCodonGenomicLocation(
|
|
412
|
+
startCodonPos as unknown as number,
|
|
413
|
+
)
|
|
414
|
+
const stopCodonGenomicLoc = getStopCodonGenomicLocation(
|
|
415
|
+
stopCodonPos as unknown as number,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (startCodonGenomicLoc !== cdsMin) {
|
|
419
|
+
handleCDSLocationChange(cdsMin, startCodonGenomicLoc, feature, true)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (stopCodonGenomicLoc !== cdsMax) {
|
|
423
|
+
// TODO: getting error when trying to change the CDS start and end location at the same time
|
|
424
|
+
// Need to fix this
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
handleCDSLocationChange(cdsMax, stopCodonGenomicLoc, feature, false)
|
|
427
|
+
}, 1000)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const copyToClipboard = () => {
|
|
432
|
+
const seqDiv = seqRef.current
|
|
433
|
+
if (!seqDiv) {
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
const textBlob = new Blob([seqDiv.outerText], { type: 'text/plain' })
|
|
437
|
+
const htmlBlob = new Blob([seqDiv.outerHTML], { type: 'text/html' })
|
|
438
|
+
const clipboardItem = new ClipboardItem({
|
|
439
|
+
[textBlob.type]: textBlob,
|
|
440
|
+
[htmlBlob.type]: htmlBlob,
|
|
441
|
+
})
|
|
442
|
+
void navigator.clipboard.write([clipboardItem])
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div>
|
|
447
|
+
{cdsPresent && (
|
|
448
|
+
<div>
|
|
449
|
+
<Accordion defaultExpanded>
|
|
450
|
+
<StyledAccordionSummary
|
|
451
|
+
expandIcon={<ExpandMoreIcon style={{ color: 'white' }} />}
|
|
452
|
+
aria-controls="panel1-content"
|
|
453
|
+
id="panel1-header"
|
|
454
|
+
>
|
|
455
|
+
<Typography component="span" fontWeight={'bold'}>
|
|
456
|
+
Translation
|
|
457
|
+
</Typography>
|
|
458
|
+
</StyledAccordionSummary>
|
|
459
|
+
<AccordionDetails>
|
|
460
|
+
<SequenceContainer>
|
|
461
|
+
<Typography component={'span'} ref={seqRef}>
|
|
462
|
+
{getTranslationSequence()}
|
|
463
|
+
</Typography>
|
|
464
|
+
</SequenceContainer>
|
|
465
|
+
<div
|
|
466
|
+
style={{
|
|
467
|
+
marginTop: 10,
|
|
468
|
+
display: 'flex',
|
|
469
|
+
flexDirection: 'row',
|
|
470
|
+
alignItems: 'center',
|
|
471
|
+
gap: 10,
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
<Tooltip title="Copy">
|
|
475
|
+
<ContentCopyIcon
|
|
476
|
+
style={{ fontSize: 15, cursor: 'pointer' }}
|
|
477
|
+
onClick={copyToClipboard}
|
|
478
|
+
/>
|
|
479
|
+
</Tooltip>
|
|
480
|
+
<Tooltip title="Trim">
|
|
481
|
+
<ContentCutIcon
|
|
482
|
+
style={{ fontSize: 15, cursor: 'pointer' }}
|
|
483
|
+
onClick={trimTranslationSequence}
|
|
484
|
+
/>
|
|
485
|
+
</Tooltip>
|
|
486
|
+
</div>
|
|
487
|
+
</AccordionDetails>
|
|
488
|
+
</Accordion>
|
|
489
|
+
<Grid2
|
|
490
|
+
container
|
|
491
|
+
justifyContent="center"
|
|
492
|
+
alignItems="center"
|
|
493
|
+
style={{ textAlign: 'center', marginTop: 10 }}
|
|
494
|
+
>
|
|
495
|
+
<Grid2 size={1} />
|
|
496
|
+
<Grid2 size={4}>
|
|
497
|
+
<StyledTextField
|
|
498
|
+
margin="dense"
|
|
499
|
+
variant="outlined"
|
|
500
|
+
value={cdsMin}
|
|
501
|
+
onChangeCommitted={(newLocation: number) => {
|
|
502
|
+
handleCDSLocationChange(cdsMin, newLocation, feature, true)
|
|
503
|
+
}}
|
|
504
|
+
/>
|
|
505
|
+
</Grid2>
|
|
506
|
+
<Grid2 size={2}>
|
|
507
|
+
<Typography component={'span'}>CDS</Typography>
|
|
508
|
+
</Grid2>
|
|
509
|
+
<Grid2 size={4}>
|
|
510
|
+
<StyledTextField
|
|
511
|
+
margin="dense"
|
|
512
|
+
variant="outlined"
|
|
513
|
+
value={cdsMax}
|
|
514
|
+
onChangeCommitted={(newLocation: number) => {
|
|
515
|
+
handleCDSLocationChange(cdsMax, newLocation, feature, false)
|
|
516
|
+
}}
|
|
517
|
+
/>
|
|
518
|
+
</Grid2>
|
|
519
|
+
<Grid2 size={1} />
|
|
520
|
+
</Grid2>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
<div style={{ marginTop: 5 }}>
|
|
524
|
+
{transcriptExonParts.map((loc, index) => {
|
|
525
|
+
return (
|
|
526
|
+
<div key={index}>
|
|
527
|
+
{loc.type === 'exon' && (
|
|
528
|
+
<Grid2
|
|
529
|
+
container
|
|
530
|
+
justifyContent="center"
|
|
531
|
+
alignItems="center"
|
|
532
|
+
style={{ textAlign: 'center' }}
|
|
533
|
+
>
|
|
534
|
+
<Grid2 size={1}>
|
|
535
|
+
{index !== 0 &&
|
|
536
|
+
getFivePrimeSpliceSite(loc, index).map((site, idx) => (
|
|
537
|
+
<Typography
|
|
538
|
+
key={idx}
|
|
539
|
+
component={'span'}
|
|
540
|
+
color={site.color}
|
|
541
|
+
>
|
|
542
|
+
{site.spliceSite}
|
|
543
|
+
</Typography>
|
|
544
|
+
))}
|
|
545
|
+
</Grid2>
|
|
546
|
+
<Grid2 size={4} style={{ padding: 0 }}>
|
|
547
|
+
<StyledTextField
|
|
548
|
+
margin="dense"
|
|
549
|
+
variant="outlined"
|
|
550
|
+
value={loc.min}
|
|
551
|
+
onChangeCommitted={(newLocation: number) => {
|
|
552
|
+
handleExonLocationChange(
|
|
553
|
+
loc.min,
|
|
554
|
+
newLocation,
|
|
555
|
+
feature,
|
|
556
|
+
true,
|
|
557
|
+
)
|
|
558
|
+
}}
|
|
559
|
+
/>
|
|
560
|
+
</Grid2>
|
|
561
|
+
<Grid2 size={2}>
|
|
562
|
+
<Strand strand={feature.strand} />
|
|
563
|
+
</Grid2>
|
|
564
|
+
<Grid2 size={4} style={{ padding: 0 }}>
|
|
565
|
+
<StyledTextField
|
|
566
|
+
margin="dense"
|
|
567
|
+
variant="outlined"
|
|
568
|
+
value={loc.max}
|
|
569
|
+
onChangeCommitted={(newLocation: number) => {
|
|
570
|
+
handleExonLocationChange(
|
|
571
|
+
loc.max,
|
|
572
|
+
newLocation,
|
|
573
|
+
feature,
|
|
574
|
+
false,
|
|
575
|
+
)
|
|
576
|
+
}}
|
|
577
|
+
/>
|
|
578
|
+
</Grid2>
|
|
579
|
+
<Grid2 size={1}>
|
|
580
|
+
{index !== transcriptExonParts.length - 1 &&
|
|
581
|
+
getThreePrimeSpliceSite(loc, index).map((site, idx) => (
|
|
582
|
+
<Typography
|
|
583
|
+
key={idx}
|
|
584
|
+
component={'span'}
|
|
585
|
+
color={site.color}
|
|
586
|
+
>
|
|
587
|
+
{site.spliceSite}
|
|
588
|
+
</Typography>
|
|
589
|
+
))}
|
|
590
|
+
</Grid2>
|
|
591
|
+
</Grid2>
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
)
|
|
595
|
+
})}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
)
|
|
599
|
+
},
|
|
600
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { AnnotationFeature } from '@apollo-annotation/mst'
|
|
4
|
+
import { observer } from 'mobx-react'
|
|
5
|
+
import { getFeatureId, getFeatureName, getStrand } from '../util'
|
|
6
|
+
import { Table, TableBody, TableCell, TableRow } from '@mui/material'
|
|
7
|
+
import styled from '@emotion/styled'
|
|
8
|
+
|
|
9
|
+
const HeaderTableCell = styled(TableCell)(() => ({
|
|
10
|
+
fontWeight: 'bold',
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
export const TranscriptWidgetSummary = observer(
|
|
14
|
+
function TranscriptWidgetSummary(props: {
|
|
15
|
+
feature: AnnotationFeature
|
|
16
|
+
refName: string
|
|
17
|
+
}) {
|
|
18
|
+
const { feature } = props
|
|
19
|
+
const name = getFeatureName(feature)
|
|
20
|
+
const id = getFeatureId(feature)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Table
|
|
24
|
+
size="small"
|
|
25
|
+
sx={{ fontSize: '0.75rem', '& .MuiTableCell-root': { padding: '4px' } }}
|
|
26
|
+
>
|
|
27
|
+
<TableBody>
|
|
28
|
+
{name !== '' && (
|
|
29
|
+
<TableRow>
|
|
30
|
+
<HeaderTableCell>Name</HeaderTableCell>
|
|
31
|
+
<TableCell>{getFeatureName(feature)}</TableCell>
|
|
32
|
+
</TableRow>
|
|
33
|
+
)}
|
|
34
|
+
{id !== '' && (
|
|
35
|
+
<TableRow>
|
|
36
|
+
<HeaderTableCell>ID</HeaderTableCell>
|
|
37
|
+
<TableCell>{getFeatureId(feature)}</TableCell>
|
|
38
|
+
</TableRow>
|
|
39
|
+
)}
|
|
40
|
+
<TableRow>
|
|
41
|
+
<HeaderTableCell>Location</HeaderTableCell>
|
|
42
|
+
<TableCell>
|
|
43
|
+
{props.refName}:{feature.min}..{feature.max}
|
|
44
|
+
</TableCell>
|
|
45
|
+
</TableRow>
|
|
46
|
+
<TableRow>
|
|
47
|
+
<HeaderTableCell>Strand</HeaderTableCell>
|
|
48
|
+
<TableCell>{getStrand(feature.strand)}</TableCell>
|
|
49
|
+
</TableRow>
|
|
50
|
+
</TableBody>
|
|
51
|
+
</Table>
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
)
|
|
@@ -61,6 +61,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
|
|
|
61
61
|
const { model } = props
|
|
62
62
|
const {
|
|
63
63
|
loading,
|
|
64
|
+
apolloDragging,
|
|
64
65
|
apolloRowHeight,
|
|
65
66
|
contextMenuItems: getContextMenuItems,
|
|
66
67
|
cursor,
|
|
@@ -218,18 +219,24 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
|
|
|
218
219
|
if (!feature) {
|
|
219
220
|
return null
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
let row = 0
|
|
223
|
+
const featureLayout = model.getFeatureLayoutPosition(feature)
|
|
224
|
+
if (featureLayout) {
|
|
225
|
+
row = featureLayout.layoutRow + featureLayout.featureRow
|
|
226
|
+
}
|
|
226
227
|
const top = row * apolloRowHeight
|
|
227
228
|
const height = apolloRowHeight
|
|
228
229
|
return (
|
|
229
230
|
<Tooltip key={checkResult._id} title={checkResult.message}>
|
|
230
231
|
<Avatar
|
|
231
232
|
className={classes.avatar}
|
|
232
|
-
style={{
|
|
233
|
+
style={{
|
|
234
|
+
top,
|
|
235
|
+
left,
|
|
236
|
+
height,
|
|
237
|
+
width: height,
|
|
238
|
+
pointerEvents: apolloDragging ? 'none' : 'auto',
|
|
239
|
+
}}
|
|
233
240
|
>
|
|
234
241
|
<ErrorIcon />
|
|
235
242
|
</Avatar>
|