@apollo-annotation/jbrowse-plugin-apollo 0.3.6 → 0.3.8
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 +4603 -2045
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +4611 -2039
- 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 +9387 -4016
- 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 +15 -15
- package/src/ApolloInternetAccount/model.ts +48 -13
- package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
- package/src/ChangeManager.ts +42 -18
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
- package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -81
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +946 -190
- package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +61 -73
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +55 -211
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +562 -108
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +78 -14
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +15 -9
- package/src/LinearApolloDisplay/stateModel/base.ts +63 -43
- package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +79 -292
- package/src/LinearApolloDisplay/stateModel/rendering.ts +45 -344
- package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
- package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
- package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
- package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +102 -40
- package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +382 -243
- package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +83 -4
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +23 -11
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +118 -123
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +53 -63
- package/src/OntologyManager/index.ts +4 -1
- package/src/TabularEditor/HybridGrid/Feature.tsx +20 -14
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
- package/src/components/AddAssembly.tsx +1 -1
- package/src/components/AddAssemblyAliases.tsx +114 -0
- package/src/components/AddChildFeature.tsx +7 -7
- package/src/components/AddFeature.tsx +20 -15
- package/src/components/AddRefSeqAliases.tsx +9 -9
- package/src/components/CopyFeature.tsx +4 -4
- package/src/components/CreateApolloAnnotation.tsx +335 -151
- package/src/components/DeleteAssembly.tsx +1 -1
- package/src/components/DeleteFeature.tsx +358 -11
- package/src/components/DownloadGFF3.tsx +20 -1
- package/src/components/EditZoomThresholdDialog.tsx +69 -0
- package/src/components/FilterFeatures.tsx +7 -7
- package/src/components/FilterTranscripts.tsx +86 -0
- package/src/components/ImportFeatures.tsx +1 -1
- package/src/components/ManageChecks.tsx +1 -1
- package/src/components/MergeExons.tsx +193 -0
- package/src/components/MergeTranscripts.tsx +182 -0
- package/src/components/OntologyTermMultiSelect.tsx +11 -11
- package/src/components/OpenLocalFile.tsx +11 -7
- package/src/components/SplitExon.tsx +134 -0
- package/src/components/ViewCheckResults.tsx +1 -1
- package/src/components/index.ts +4 -0
- package/src/config.ts +11 -0
- package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
- package/src/extensions/annotationFromPileup.ts +99 -89
- package/src/index.ts +42 -105
- package/src/makeDisplayComponent.tsx +0 -1
- package/src/menus/index.ts +1 -0
- package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +60 -33
- package/src/menus/topLevelMenuAdmin.ts +154 -0
- package/src/session/session.ts +163 -104
- package/src/util/annotationFeatureUtils.ts +59 -0
- package/src/util/copyToClipboard.ts +21 -0
- package/src/util/displayUtils.ts +149 -0
- package/src/util/glyphUtils.ts +201 -0
- package/src/util/index.ts +2 -0
- package/src/util/mouseEventsUtils.ts +145 -0
|
@@ -25,14 +25,16 @@ import RemoveIcon from '@mui/icons-material/Remove'
|
|
|
25
25
|
import {
|
|
26
26
|
Accordion,
|
|
27
27
|
AccordionDetails,
|
|
28
|
-
|
|
28
|
+
Grid,
|
|
29
29
|
Tooltip,
|
|
30
30
|
Typography,
|
|
31
31
|
} from '@mui/material'
|
|
32
32
|
import { observer } from 'mobx-react'
|
|
33
33
|
import React, { useRef } from 'react'
|
|
34
34
|
|
|
35
|
+
import { type OntologyRecord } from '../OntologyManager'
|
|
35
36
|
import { type ApolloSessionModel } from '../session'
|
|
37
|
+
import { copyToClipboard } from '../util/copyToClipboard'
|
|
36
38
|
|
|
37
39
|
import { StyledAccordionSummary } from './ApolloTranscriptDetailsWidget'
|
|
38
40
|
import { NumberTextField } from './NumberTextField'
|
|
@@ -81,6 +83,19 @@ const Strand = (props: { strand: 1 | -1 | undefined }) => {
|
|
|
81
83
|
)
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
const minMaxExonTranscriptLocation = (
|
|
87
|
+
transcript: AnnotationFeature,
|
|
88
|
+
featureTypeOntology: OntologyRecord,
|
|
89
|
+
) => {
|
|
90
|
+
const { transcriptExonParts } = transcript
|
|
91
|
+
const exonParts = transcriptExonParts
|
|
92
|
+
.filter((part) => featureTypeOntology.isTypeOf(part.type, 'exon'))
|
|
93
|
+
.sort(({ min: a }, { min: b }) => a - b)
|
|
94
|
+
const exonMin: number = exonParts[0]?.min
|
|
95
|
+
const exonMax: number = exonParts[exonParts.length - 1]?.max
|
|
96
|
+
return [exonMin, exonMax]
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
export const TranscriptWidgetEditLocation = observer(
|
|
85
100
|
function TranscriptWidgetEditLocation({
|
|
86
101
|
assembly,
|
|
@@ -99,8 +114,41 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
99
114
|
const { changeManager } = session.apolloDataStore
|
|
100
115
|
const seqRef = useRef<HTMLDivElement>(null)
|
|
101
116
|
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
if (!refData) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { apolloDataStore } = session
|
|
122
|
+
const { featureTypeOntology } =
|
|
123
|
+
apolloDataStore.ontologyManager as unknown as {
|
|
124
|
+
featureTypeOntology: OntologyRecord
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
!featureTypeOntology.isTypeOf(feature.type, 'transcript') &&
|
|
129
|
+
!featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
|
|
130
|
+
) {
|
|
131
|
+
throw new Error('Feature is not a transcript or equivalent')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { cdsLocations, transcriptExonParts, strand } = feature
|
|
135
|
+
const [firstCDSLocation] = cdsLocations
|
|
136
|
+
const [exonMin, exonMax] = minMaxExonTranscriptLocation(
|
|
137
|
+
feature,
|
|
138
|
+
featureTypeOntology,
|
|
139
|
+
)
|
|
140
|
+
let cdsMin = exonMin
|
|
141
|
+
let cdsMax = exonMax
|
|
142
|
+
const cdsPresent = firstCDSLocation.length > 0
|
|
143
|
+
|
|
144
|
+
if (cdsPresent) {
|
|
145
|
+
const sortedCDSLocations = firstCDSLocation.toSorted(
|
|
146
|
+
({ min: a }, { min: b }) => a - b,
|
|
147
|
+
)
|
|
148
|
+
cdsMin = sortedCDSLocations[0].min
|
|
149
|
+
cdsMax = sortedCDSLocations[sortedCDSLocations.length - 1].max
|
|
150
|
+
}
|
|
151
|
+
|
|
104
152
|
function handleCDSLocationChange(
|
|
105
153
|
oldLocation: number,
|
|
106
154
|
newLocation: number,
|
|
@@ -110,39 +158,245 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
110
158
|
if (!feature.children) {
|
|
111
159
|
throw new Error('Transcript should have child features')
|
|
112
160
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
|
|
162
|
+
const overlappingExon = getOverlappingExonForCDS(
|
|
163
|
+
feature,
|
|
164
|
+
featureTypeOntology,
|
|
165
|
+
oldLocation,
|
|
166
|
+
isMin,
|
|
167
|
+
)
|
|
168
|
+
if (!overlappingExon) {
|
|
169
|
+
notify('No matching exon found', 'error')
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const oldExonLocation = isMin ? overlappingExon.min : overlappingExon.max
|
|
173
|
+
const { prevExon, nextExon } = getNeighboringExonParts(
|
|
174
|
+
feature,
|
|
175
|
+
featureTypeOntology,
|
|
176
|
+
oldExonLocation,
|
|
177
|
+
isMin,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
// Start location should be less than end location
|
|
181
|
+
if (isMin && newLocation >= overlappingExon.max) {
|
|
182
|
+
notify(
|
|
183
|
+
'Start location should be less than overlapping exon end location',
|
|
184
|
+
'error',
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// End location should be greater than start location
|
|
190
|
+
if (!isMin && newLocation <= overlappingExon.min) {
|
|
191
|
+
notify(
|
|
192
|
+
'End location should be greater than overlapping exon start location',
|
|
193
|
+
'error',
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
198
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
199
|
+
notify(
|
|
200
|
+
'Start location should be greater than previous exon end location',
|
|
201
|
+
'error',
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
// Changed location should be less than start location of next exon
|
|
206
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
207
|
+
notify(
|
|
208
|
+
'End location should be less than next exon start location',
|
|
209
|
+
'error',
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const cdsFeature = getMatchingCDSFeature(
|
|
215
|
+
feature,
|
|
216
|
+
featureTypeOntology,
|
|
217
|
+
oldLocation,
|
|
218
|
+
isMin,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if (!cdsFeature) {
|
|
222
|
+
notify('No matching CDS feature found', 'error')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isMin && newLocation <= cdsFeature.min) {
|
|
227
|
+
notify(
|
|
228
|
+
'End location should be greater than CDS start location',
|
|
229
|
+
'error',
|
|
230
|
+
)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
if (isMin && newLocation >= cdsFeature.max) {
|
|
234
|
+
notify('Start location should be less than CDS end location', 'error')
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const overlappingExonFeature = getExonFeature(
|
|
239
|
+
feature,
|
|
240
|
+
overlappingExon.min,
|
|
241
|
+
overlappingExon.max,
|
|
242
|
+
featureTypeOntology,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if (!overlappingExonFeature) {
|
|
246
|
+
notify('No matching exon feature found', 'error')
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (isMin && newLocation !== cdsFeature.min) {
|
|
251
|
+
const startChange: LocationStartChange = new LocationStartChange({
|
|
252
|
+
typeName: 'LocationStartChange',
|
|
253
|
+
changedIds: [],
|
|
254
|
+
changes: [],
|
|
255
|
+
assembly,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
if (newLocation < overlappingExon.min) {
|
|
259
|
+
if (prevExon) {
|
|
260
|
+
// update exon start location
|
|
261
|
+
appendStartLocationChange(
|
|
262
|
+
overlappingExonFeature,
|
|
263
|
+
startChange,
|
|
264
|
+
newLocation,
|
|
265
|
+
)
|
|
266
|
+
// update CDS start location
|
|
267
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation)
|
|
268
|
+
} else {
|
|
269
|
+
const transcriptStart = feature.min
|
|
270
|
+
const gene = feature.parent
|
|
271
|
+
if (newLocation < transcriptStart) {
|
|
272
|
+
if (gene && newLocation < gene.min) {
|
|
273
|
+
// update gene start location
|
|
274
|
+
appendStartLocationChange(gene, startChange, newLocation)
|
|
275
|
+
}
|
|
276
|
+
// update transcript start location
|
|
277
|
+
appendStartLocationChange(feature, startChange, newLocation)
|
|
278
|
+
// update exon start location
|
|
279
|
+
appendStartLocationChange(
|
|
280
|
+
overlappingExonFeature,
|
|
281
|
+
startChange,
|
|
282
|
+
newLocation,
|
|
283
|
+
)
|
|
284
|
+
// update CDS start location
|
|
285
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
// update CDS start location
|
|
290
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation)
|
|
116
291
|
}
|
|
117
|
-
|
|
118
|
-
|
|
292
|
+
|
|
293
|
+
void changeManager.submit(startChange).catch(() => {
|
|
294
|
+
notify('Error updating feature CDS start position', 'error')
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!isMin && newLocation !== cdsFeature.max) {
|
|
299
|
+
const endChange: LocationEndChange = new LocationEndChange({
|
|
300
|
+
typeName: 'LocationEndChange',
|
|
301
|
+
changedIds: [],
|
|
302
|
+
changes: [],
|
|
303
|
+
assembly,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
if (newLocation > overlappingExon.max) {
|
|
307
|
+
if (nextExon) {
|
|
308
|
+
// update exon end location
|
|
309
|
+
appendEndLocationChange(
|
|
310
|
+
overlappingExonFeature,
|
|
311
|
+
endChange,
|
|
312
|
+
newLocation,
|
|
313
|
+
)
|
|
314
|
+
// update CDS end location
|
|
315
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation)
|
|
316
|
+
} else {
|
|
317
|
+
const transcriptEnd = feature.max
|
|
318
|
+
const gene = feature.parent
|
|
319
|
+
if (newLocation > transcriptEnd) {
|
|
320
|
+
if (gene && newLocation > gene.max) {
|
|
321
|
+
// update gene end location
|
|
322
|
+
appendEndLocationChange(gene, endChange, newLocation)
|
|
323
|
+
}
|
|
324
|
+
// update transcript end location
|
|
325
|
+
appendEndLocationChange(feature, endChange, newLocation)
|
|
326
|
+
// update exon end location
|
|
327
|
+
appendEndLocationChange(
|
|
328
|
+
overlappingExonFeature,
|
|
329
|
+
endChange,
|
|
330
|
+
newLocation,
|
|
331
|
+
)
|
|
332
|
+
// update CDS end location
|
|
333
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
// update CDS end location
|
|
338
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
void changeManager.submit(endChange).catch(() => {
|
|
342
|
+
notify('Error updating feature CDS end position', 'error')
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const updateCDSLocation = (
|
|
348
|
+
oldLocation: number,
|
|
349
|
+
newLocation: number,
|
|
350
|
+
feature: AnnotationFeature,
|
|
351
|
+
isMin: boolean,
|
|
352
|
+
onComplete?: () => void,
|
|
353
|
+
) => {
|
|
354
|
+
if (!feature.children) {
|
|
355
|
+
throw new Error('Transcript should have child features')
|
|
356
|
+
}
|
|
357
|
+
if (oldLocation === newLocation) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cdsFeature = getMatchingCDSFeature(
|
|
362
|
+
feature,
|
|
363
|
+
featureTypeOntology,
|
|
364
|
+
oldLocation,
|
|
365
|
+
isMin,
|
|
366
|
+
)
|
|
367
|
+
if (!cdsFeature) {
|
|
368
|
+
notify('No matching CDS feature found', 'error')
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const change = isMin
|
|
373
|
+
? new LocationStartChange({
|
|
119
374
|
typeName: 'LocationStartChange',
|
|
120
|
-
changedIds: [
|
|
121
|
-
featureId:
|
|
122
|
-
oldStart:
|
|
375
|
+
changedIds: [cdsFeature._id],
|
|
376
|
+
featureId: cdsFeature._id,
|
|
377
|
+
oldStart: cdsFeature.min,
|
|
123
378
|
newStart: newLocation,
|
|
124
379
|
assembly,
|
|
125
380
|
})
|
|
126
|
-
|
|
127
|
-
notify('Error updating feature start position', 'error')
|
|
128
|
-
})
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
if (!isMin && oldLocation === child.max) {
|
|
132
|
-
const change = new LocationEndChange({
|
|
381
|
+
: new LocationEndChange({
|
|
133
382
|
typeName: 'LocationEndChange',
|
|
134
|
-
changedIds: [
|
|
135
|
-
featureId:
|
|
136
|
-
oldEnd:
|
|
383
|
+
changedIds: [cdsFeature._id],
|
|
384
|
+
featureId: cdsFeature._id,
|
|
385
|
+
oldEnd: cdsFeature.max,
|
|
137
386
|
newEnd: newLocation,
|
|
138
387
|
assembly,
|
|
139
388
|
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
389
|
+
|
|
390
|
+
void changeManager
|
|
391
|
+
.submit(change)
|
|
392
|
+
.then(() => {
|
|
393
|
+
if (onComplete) {
|
|
394
|
+
onComplete()
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
.catch(() => {
|
|
398
|
+
notify('Error updating feature CDS position', 'error')
|
|
399
|
+
})
|
|
146
400
|
}
|
|
147
401
|
|
|
148
402
|
function handleExonLocationChange(
|
|
@@ -154,62 +408,400 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
154
408
|
if (!feature.children) {
|
|
155
409
|
throw new Error('Transcript should have child features')
|
|
156
410
|
}
|
|
157
|
-
|
|
158
|
-
|
|
411
|
+
const { matchingExon, prevExon, nextExon } = getNeighboringExonParts(
|
|
412
|
+
feature,
|
|
413
|
+
featureTypeOntology,
|
|
414
|
+
oldLocation,
|
|
415
|
+
isMin,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (!matchingExon) {
|
|
419
|
+
notify('No matching exon found', 'error')
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Start location should be less than end location
|
|
424
|
+
if (isMin && newLocation >= matchingExon.max) {
|
|
425
|
+
notify(`Start location should be less than end location`, 'error')
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
// End location should be greater than start location
|
|
429
|
+
if (!isMin && newLocation <= matchingExon.min) {
|
|
430
|
+
notify(`End location should be greater than start location`, 'error')
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
// Changed location should be greater than end location of previous exon - give 2bp buffer
|
|
434
|
+
if (prevExon && prevExon.max + 2 > newLocation) {
|
|
435
|
+
notify(`Error while changing start location`, 'error')
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
// Changed location should be less than start location of next exon - give 2bp buffer
|
|
439
|
+
if (nextExon && nextExon.min - 2 < newLocation) {
|
|
440
|
+
notify(`Error while changing end location`, 'error')
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const exonFeature = getExonFeature(
|
|
445
|
+
feature,
|
|
446
|
+
matchingExon.min,
|
|
447
|
+
matchingExon.max,
|
|
448
|
+
featureTypeOntology,
|
|
449
|
+
)
|
|
450
|
+
if (!exonFeature) {
|
|
451
|
+
notify('No matching exon feature found', 'error')
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const cdsFeature = getFirstCDSFeature(feature, featureTypeOntology)
|
|
456
|
+
|
|
457
|
+
// START LOCATION CHANGE
|
|
458
|
+
if (isMin && newLocation !== matchingExon.min) {
|
|
459
|
+
const startChange = new LocationStartChange({
|
|
460
|
+
typeName: 'LocationStartChange',
|
|
461
|
+
changedIds: [],
|
|
462
|
+
changes: [],
|
|
463
|
+
assembly,
|
|
464
|
+
})
|
|
465
|
+
if (prevExon) {
|
|
466
|
+
// update exon start location
|
|
467
|
+
appendStartLocationChange(exonFeature, startChange, newLocation)
|
|
468
|
+
} else {
|
|
469
|
+
const transcriptStart = feature.min
|
|
470
|
+
const gene = feature.parent
|
|
471
|
+
if (newLocation < transcriptStart) {
|
|
472
|
+
if (gene && newLocation < gene.min) {
|
|
473
|
+
// update gene start location
|
|
474
|
+
appendStartLocationChange(gene, startChange, newLocation)
|
|
475
|
+
}
|
|
476
|
+
// update transcript start location
|
|
477
|
+
appendStartLocationChange(feature, startChange, newLocation)
|
|
478
|
+
// update exon start location
|
|
479
|
+
appendStartLocationChange(exonFeature, startChange, newLocation)
|
|
480
|
+
} else if (newLocation > transcriptStart) {
|
|
481
|
+
// update exon start location
|
|
482
|
+
appendStartLocationChange(exonFeature, startChange, newLocation)
|
|
483
|
+
// update transcript start location
|
|
484
|
+
appendStartLocationChange(feature, startChange, newLocation)
|
|
485
|
+
|
|
486
|
+
if (gene) {
|
|
487
|
+
const [geneMinWithNewLoc] = geneMinMaxWithNewLocation(
|
|
488
|
+
gene,
|
|
489
|
+
feature,
|
|
490
|
+
newLocation,
|
|
491
|
+
featureTypeOntology,
|
|
492
|
+
isMin,
|
|
493
|
+
)
|
|
494
|
+
if (gene.min != geneMinWithNewLoc) {
|
|
495
|
+
// update gene start location
|
|
496
|
+
appendStartLocationChange(gene, startChange, geneMinWithNewLoc)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// When we change the start location of the exon overlapping with start location of the CDS
|
|
503
|
+
// and the new start location is greater than the CDS start location then we need to update the CDS start location
|
|
504
|
+
if (
|
|
505
|
+
cdsFeature &&
|
|
506
|
+
cdsFeature.min >= matchingExon.min &&
|
|
507
|
+
cdsFeature.min <= matchingExon.max &&
|
|
508
|
+
newLocation > cdsFeature.min
|
|
509
|
+
) {
|
|
510
|
+
// update CDS start location
|
|
511
|
+
appendStartLocationChange(cdsFeature, startChange, newLocation)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
void changeManager.submit(startChange).catch(() => {
|
|
515
|
+
notify('Error updating feature exon start position', 'error')
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// END LOCATION CHANGE
|
|
520
|
+
if (!isMin && newLocation !== matchingExon.max) {
|
|
521
|
+
const endChange = new LocationEndChange({
|
|
522
|
+
typeName: 'LocationEndChange',
|
|
523
|
+
changedIds: [],
|
|
524
|
+
changes: [],
|
|
525
|
+
assembly,
|
|
526
|
+
})
|
|
527
|
+
if (nextExon) {
|
|
528
|
+
// update exon end location
|
|
529
|
+
appendEndLocationChange(exonFeature, endChange, newLocation)
|
|
530
|
+
} else {
|
|
531
|
+
const transcriptEnd = feature.max
|
|
532
|
+
const gene = feature.parent
|
|
533
|
+
if (newLocation > transcriptEnd) {
|
|
534
|
+
if (gene && newLocation > gene.max) {
|
|
535
|
+
// update gene end location
|
|
536
|
+
appendEndLocationChange(gene, endChange, newLocation)
|
|
537
|
+
}
|
|
538
|
+
// update transcript end location
|
|
539
|
+
appendEndLocationChange(feature, endChange, newLocation)
|
|
540
|
+
// update exon end location
|
|
541
|
+
appendEndLocationChange(exonFeature, endChange, newLocation)
|
|
542
|
+
} else if (newLocation < transcriptEnd) {
|
|
543
|
+
// update exon end location
|
|
544
|
+
appendEndLocationChange(exonFeature, endChange, newLocation)
|
|
545
|
+
// update transcript end location
|
|
546
|
+
appendEndLocationChange(feature, endChange, newLocation)
|
|
547
|
+
|
|
548
|
+
if (gene) {
|
|
549
|
+
const [, geneMaxWithNewLoc] = geneMinMaxWithNewLocation(
|
|
550
|
+
gene,
|
|
551
|
+
feature,
|
|
552
|
+
newLocation,
|
|
553
|
+
featureTypeOntology,
|
|
554
|
+
isMin,
|
|
555
|
+
)
|
|
556
|
+
if (gene.max != geneMaxWithNewLoc) {
|
|
557
|
+
// update gene end location
|
|
558
|
+
appendEndLocationChange(gene, endChange, geneMaxWithNewLoc)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// When we change the end location of the exon overlapping with end location of the CDS
|
|
565
|
+
// and the new end location is less than the CDS end location then we need to update the CDS end location
|
|
566
|
+
if (
|
|
567
|
+
cdsFeature &&
|
|
568
|
+
cdsFeature.max >= matchingExon.min &&
|
|
569
|
+
cdsFeature.max <= matchingExon.max &&
|
|
570
|
+
newLocation < cdsFeature.max
|
|
571
|
+
) {
|
|
572
|
+
// update CDS end location
|
|
573
|
+
appendEndLocationChange(cdsFeature, endChange, newLocation)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
void changeManager.submit(endChange).catch(() => {
|
|
577
|
+
notify('Error updating feature exon end position', 'error')
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const appendEndLocationChange = (
|
|
583
|
+
feature: AnnotationFeature,
|
|
584
|
+
change: LocationEndChange,
|
|
585
|
+
newLocation: number,
|
|
586
|
+
) => {
|
|
587
|
+
change.changedIds.push(feature._id)
|
|
588
|
+
change.changes.push({
|
|
589
|
+
featureId: feature._id,
|
|
590
|
+
oldEnd: feature.max,
|
|
591
|
+
newEnd: newLocation,
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const appendStartLocationChange = (
|
|
596
|
+
feature: AnnotationFeature,
|
|
597
|
+
change: LocationStartChange,
|
|
598
|
+
newLocation: number,
|
|
599
|
+
) => {
|
|
600
|
+
change.changedIds.push(feature._id)
|
|
601
|
+
change.changes.push({
|
|
602
|
+
featureId: feature._id,
|
|
603
|
+
oldStart: feature.min,
|
|
604
|
+
newStart: newLocation,
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const getMatchingCDSFeature = (
|
|
609
|
+
feature: AnnotationFeature,
|
|
610
|
+
featureTypeOntology: OntologyRecord,
|
|
611
|
+
oldCDSLocation: number,
|
|
612
|
+
isMin: boolean,
|
|
613
|
+
) => {
|
|
614
|
+
let cdsFeature
|
|
615
|
+
for (const [, child] of feature.children ?? []) {
|
|
616
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
159
617
|
continue
|
|
160
618
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
619
|
+
|
|
620
|
+
if (isMin && oldCDSLocation === child.min) {
|
|
621
|
+
cdsFeature = child
|
|
622
|
+
break
|
|
174
623
|
}
|
|
175
|
-
if (!isMin &&
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
624
|
+
if (!isMin && oldCDSLocation === child.max) {
|
|
625
|
+
cdsFeature = child
|
|
626
|
+
break
|
|
188
627
|
}
|
|
189
628
|
}
|
|
629
|
+
return cdsFeature
|
|
190
630
|
}
|
|
191
631
|
|
|
192
|
-
|
|
193
|
-
|
|
632
|
+
const getFirstCDSFeature = (
|
|
633
|
+
feature: AnnotationFeature,
|
|
634
|
+
featureTypeOntology: OntologyRecord,
|
|
635
|
+
) => {
|
|
636
|
+
let cdsFeature
|
|
637
|
+
for (const [, child] of feature.children ?? []) {
|
|
638
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'CDS')) {
|
|
639
|
+
continue
|
|
640
|
+
}
|
|
641
|
+
cdsFeature = child
|
|
642
|
+
break
|
|
643
|
+
}
|
|
644
|
+
return cdsFeature
|
|
194
645
|
}
|
|
195
646
|
|
|
196
|
-
const
|
|
197
|
-
|
|
647
|
+
const getExonFeature = (
|
|
648
|
+
feature: AnnotationFeature,
|
|
649
|
+
exonMin: number,
|
|
650
|
+
exonMax: number,
|
|
651
|
+
featureTypeOntology: OntologyRecord,
|
|
652
|
+
) => {
|
|
653
|
+
let exonFeature
|
|
654
|
+
for (const [, child] of feature.children ?? []) {
|
|
655
|
+
if (!featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
656
|
+
continue
|
|
657
|
+
}
|
|
658
|
+
if (exonMin === child.min && exonMax === child.max) {
|
|
659
|
+
exonFeature = child
|
|
660
|
+
break
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return exonFeature
|
|
664
|
+
}
|
|
198
665
|
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
666
|
+
const geneMinMaxWithNewLocation = (
|
|
667
|
+
gene: AnnotationFeature,
|
|
668
|
+
transcript: AnnotationFeature,
|
|
669
|
+
newLocation: number,
|
|
670
|
+
featureTypeOntology: OntologyRecord,
|
|
671
|
+
isMin: boolean,
|
|
672
|
+
) => {
|
|
673
|
+
const mins = []
|
|
674
|
+
const maxs = []
|
|
675
|
+
for (const [, t] of gene.children?.entries() ?? []) {
|
|
676
|
+
if (!featureTypeOntology.isTypeOf(t.type, 'transcript')) {
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
202
679
|
|
|
203
|
-
|
|
204
|
-
|
|
680
|
+
if (t._id === transcript._id) {
|
|
681
|
+
if (isMin) {
|
|
682
|
+
mins.push(newLocation)
|
|
683
|
+
maxs.push(t.max)
|
|
684
|
+
} else {
|
|
685
|
+
maxs.push(newLocation)
|
|
686
|
+
mins.push(t.min)
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
mins.push(t.min)
|
|
690
|
+
maxs.push(t.max)
|
|
691
|
+
}
|
|
692
|
+
}
|
|
205
693
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
694
|
+
const newMin = Math.min(...mins)
|
|
695
|
+
const newMax = Math.max(...maxs)
|
|
696
|
+
return [newMin, newMax]
|
|
697
|
+
}
|
|
209
698
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
699
|
+
const getOverlappingExonForCDS = (
|
|
700
|
+
transcript: AnnotationFeature,
|
|
701
|
+
featureTypeOntology: OntologyRecord,
|
|
702
|
+
oldCDSLocation: number,
|
|
703
|
+
isMin: boolean,
|
|
704
|
+
) => {
|
|
705
|
+
const { transcriptExonParts } = transcript
|
|
706
|
+
let overlappingExonPart
|
|
707
|
+
for (const [, exonPart] of transcriptExonParts.entries()) {
|
|
708
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
if (
|
|
712
|
+
!isMin &&
|
|
713
|
+
oldCDSLocation >= exonPart.min &&
|
|
714
|
+
oldCDSLocation <= exonPart.max
|
|
715
|
+
) {
|
|
716
|
+
overlappingExonPart = exonPart
|
|
717
|
+
break
|
|
718
|
+
}
|
|
719
|
+
if (
|
|
720
|
+
isMin &&
|
|
721
|
+
oldCDSLocation >= exonPart.min &&
|
|
722
|
+
oldCDSLocation <= exonPart.max
|
|
723
|
+
) {
|
|
724
|
+
overlappingExonPart = exonPart
|
|
725
|
+
break
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return overlappingExonPart
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const getNeighboringExonParts = (
|
|
732
|
+
transcript: AnnotationFeature,
|
|
733
|
+
featureTypeOntology: OntologyRecord,
|
|
734
|
+
oldExonLoc: number,
|
|
735
|
+
isMin: boolean,
|
|
736
|
+
) => {
|
|
737
|
+
const { transcriptExonParts, strand } = transcript
|
|
738
|
+
let matchingExon, matchingExonIdx, prevExon, nextExon
|
|
739
|
+
for (const [i, exonPart] of transcriptExonParts.entries()) {
|
|
740
|
+
if (!featureTypeOntology.isTypeOf(exonPart.type, 'exon')) {
|
|
741
|
+
continue
|
|
742
|
+
}
|
|
743
|
+
if (isMin && exonPart.min === oldExonLoc) {
|
|
744
|
+
matchingExon = exonPart
|
|
745
|
+
matchingExonIdx = i
|
|
746
|
+
break
|
|
747
|
+
}
|
|
748
|
+
if (!isMin && exonPart.max === oldExonLoc) {
|
|
749
|
+
matchingExon = exonPart
|
|
750
|
+
matchingExonIdx = i
|
|
751
|
+
break
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (matchingExon && matchingExonIdx !== undefined) {
|
|
756
|
+
if (strand === 1 && matchingExonIdx > 0) {
|
|
757
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
758
|
+
const prevLoc = transcriptExonParts[i]
|
|
759
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
760
|
+
prevExon = prevLoc
|
|
761
|
+
break
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (strand === -1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
767
|
+
for (
|
|
768
|
+
let i = matchingExonIdx + 1;
|
|
769
|
+
i < transcriptExonParts.length;
|
|
770
|
+
i++
|
|
771
|
+
) {
|
|
772
|
+
const prevLoc = transcriptExonParts[i]
|
|
773
|
+
if (featureTypeOntology.isTypeOf(prevLoc.type, 'exon')) {
|
|
774
|
+
prevExon = prevLoc
|
|
775
|
+
break
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (strand === 1 && matchingExonIdx < transcriptExonParts.length - 1) {
|
|
781
|
+
for (
|
|
782
|
+
let i = matchingExonIdx + 1;
|
|
783
|
+
i < transcriptExonParts.length;
|
|
784
|
+
i++
|
|
785
|
+
) {
|
|
786
|
+
const nextLoc = transcriptExonParts[i]
|
|
787
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
788
|
+
nextExon = nextLoc
|
|
789
|
+
break
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (strand === -1 && matchingExonIdx > 0) {
|
|
795
|
+
for (let i = matchingExonIdx - 1; i >= 0; i--) {
|
|
796
|
+
const nextLoc = transcriptExonParts[i]
|
|
797
|
+
if (featureTypeOntology.isTypeOf(nextLoc.type, 'exon')) {
|
|
798
|
+
nextExon = nextLoc
|
|
799
|
+
break
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { matchingExon, prevExon, nextExon }
|
|
213
805
|
}
|
|
214
806
|
|
|
215
807
|
const getFivePrimeSpliceSite = (
|
|
@@ -229,6 +821,7 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
229
821
|
}
|
|
230
822
|
}
|
|
231
823
|
}
|
|
824
|
+
spliceSite = spliceSite.toUpperCase()
|
|
232
825
|
return [
|
|
233
826
|
{
|
|
234
827
|
spliceSite,
|
|
@@ -254,6 +847,7 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
254
847
|
}
|
|
255
848
|
}
|
|
256
849
|
}
|
|
850
|
+
spliceSite = spliceSite.toUpperCase()
|
|
257
851
|
return [
|
|
258
852
|
{
|
|
259
853
|
spliceSite,
|
|
@@ -265,12 +859,17 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
265
859
|
const getTranslationSequence = () => {
|
|
266
860
|
let wholeSequence = ''
|
|
267
861
|
const [firstLocation] = cdsLocations
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
862
|
+
const sortedCDSLocations = firstLocation.toSorted(
|
|
863
|
+
({ min: a }, { min: b }) => a - b,
|
|
864
|
+
)
|
|
865
|
+
for (const loc of sortedCDSLocations) {
|
|
866
|
+
wholeSequence += refData.getSequence(loc.min, loc.max)
|
|
867
|
+
}
|
|
868
|
+
if (strand === -1) {
|
|
869
|
+
// Original: ACGCAT
|
|
870
|
+
// Complement: TGCGTA
|
|
871
|
+
// Reverse complement: ATGCGT
|
|
872
|
+
wholeSequence = revcom(wholeSequence)
|
|
274
873
|
}
|
|
275
874
|
const elements = []
|
|
276
875
|
for (
|
|
@@ -299,15 +898,23 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
299
898
|
// of the start codon. We are using the codonGenomicPos as the key in the typography
|
|
300
899
|
// elements to maintain the genomic postion of the codon start
|
|
301
900
|
const startCodonGenomicLocation =
|
|
302
|
-
|
|
303
|
-
if (startCodonGenomicLocation !== cdsMin) {
|
|
304
|
-
|
|
901
|
+
getCodonGenomicLocation(codonGenomicPos)
|
|
902
|
+
if (startCodonGenomicLocation !== cdsMin && strand === 1) {
|
|
903
|
+
updateCDSLocation(
|
|
305
904
|
cdsMin,
|
|
306
905
|
startCodonGenomicLocation,
|
|
307
906
|
feature,
|
|
308
907
|
true,
|
|
309
908
|
)
|
|
310
909
|
}
|
|
910
|
+
if (startCodonGenomicLocation !== cdsMax && strand === -1) {
|
|
911
|
+
updateCDSLocation(
|
|
912
|
+
cdsMax,
|
|
913
|
+
startCodonGenomicLocation,
|
|
914
|
+
feature,
|
|
915
|
+
false,
|
|
916
|
+
)
|
|
917
|
+
}
|
|
311
918
|
}}
|
|
312
919
|
>
|
|
313
920
|
{protein}
|
|
@@ -338,34 +945,41 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
338
945
|
|
|
339
946
|
// Codon position is the index of the start codon in the CDS genomic sequence
|
|
340
947
|
// Calculate the genomic location of the start codon based on the codon position in the CDS
|
|
341
|
-
const
|
|
948
|
+
const getCodonGenomicLocation = (codonGenomicPosition: number) => {
|
|
342
949
|
const [firstLocation] = cdsLocations
|
|
343
950
|
let cdsLen = 0
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
}
|
|
951
|
+
const sortedCDSLocations = firstLocation.toSorted(
|
|
952
|
+
({ min: a }, { min: b }) => a - b,
|
|
953
|
+
)
|
|
357
954
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
955
|
+
// Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}]
|
|
956
|
+
// and codonGenomicPosition is 25
|
|
957
|
+
// ((10 - 0) + (30 - 20) + (50 - 40)) > 25
|
|
958
|
+
// So, start codon is in (40, 50)
|
|
959
|
+
// 40 + (25-20) = 45 is the genomic location of the start codon
|
|
960
|
+
if (strand === 1) {
|
|
961
|
+
for (const loc of sortedCDSLocations) {
|
|
962
|
+
const locLength = loc.max - loc.min
|
|
963
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
964
|
+
return loc.min + (codonGenomicPosition - cdsLen)
|
|
965
|
+
}
|
|
966
|
+
cdsLen += locLength
|
|
366
967
|
}
|
|
367
|
-
|
|
968
|
+
} else if (strand === -1) {
|
|
969
|
+
for (let i = sortedCDSLocations.length - 1; i >= 0; i--) {
|
|
970
|
+
const loc = sortedCDSLocations[i]
|
|
971
|
+
const locLength = loc.max - loc.min
|
|
972
|
+
if (cdsLen + locLength > codonGenomicPosition) {
|
|
973
|
+
return loc.max - (codonGenomicPosition - cdsLen)
|
|
974
|
+
}
|
|
975
|
+
cdsLen += locLength
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (strand === 1) {
|
|
980
|
+
return cdsMin
|
|
368
981
|
}
|
|
982
|
+
|
|
369
983
|
return cdsMax
|
|
370
984
|
}
|
|
371
985
|
|
|
@@ -396,9 +1010,10 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
396
1010
|
return
|
|
397
1011
|
}
|
|
398
1012
|
|
|
399
|
-
// Trim any sequence before first start codon and after
|
|
1013
|
+
// Trim any sequence before first start codon and after stop codon
|
|
400
1014
|
const startCodonIndex = translationSequence.indexOf('M')
|
|
401
|
-
const stopCodonIndex = translationSequence.
|
|
1015
|
+
const stopCodonIndex = translationSequence.indexOf('*') + 1
|
|
1016
|
+
|
|
402
1017
|
const startCodonPos =
|
|
403
1018
|
translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos
|
|
404
1019
|
const stopCodonPos =
|
|
@@ -407,46 +1022,97 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
407
1022
|
if (!startCodonPos || !stopCodonPos) {
|
|
408
1023
|
return
|
|
409
1024
|
}
|
|
410
|
-
|
|
411
|
-
const startCodonGenomicLoc = getStartCodonGenomicLocation(
|
|
1025
|
+
const startCodonGenomicLoc = getCodonGenomicLocation(
|
|
412
1026
|
startCodonPos as unknown as number,
|
|
413
1027
|
)
|
|
414
|
-
const stopCodonGenomicLoc =
|
|
1028
|
+
const stopCodonGenomicLoc = getCodonGenomicLocation(
|
|
415
1029
|
stopCodonPos as unknown as number,
|
|
416
1030
|
)
|
|
417
1031
|
|
|
418
|
-
if (
|
|
419
|
-
|
|
1032
|
+
if (strand === 1) {
|
|
1033
|
+
if (startCodonGenomicLoc > stopCodonGenomicLoc) {
|
|
1034
|
+
notify(
|
|
1035
|
+
'Start codon genomic location should be less than stop codon genomic location',
|
|
1036
|
+
'error',
|
|
1037
|
+
)
|
|
1038
|
+
return
|
|
1039
|
+
}
|
|
1040
|
+
let promise
|
|
1041
|
+
if (startCodonGenomicLoc !== cdsMin) {
|
|
1042
|
+
promise = new Promise((resolve) => {
|
|
1043
|
+
updateCDSLocation(
|
|
1044
|
+
cdsMin,
|
|
1045
|
+
startCodonGenomicLoc,
|
|
1046
|
+
feature,
|
|
1047
|
+
true,
|
|
1048
|
+
() => {
|
|
1049
|
+
resolve(true)
|
|
1050
|
+
},
|
|
1051
|
+
)
|
|
1052
|
+
})
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (stopCodonGenomicLoc !== cdsMax) {
|
|
1056
|
+
if (promise) {
|
|
1057
|
+
void promise.then(() => {
|
|
1058
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
|
|
1059
|
+
})
|
|
1060
|
+
} else {
|
|
1061
|
+
updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
420
1064
|
}
|
|
421
1065
|
|
|
422
|
-
if (
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1066
|
+
if (strand === -1) {
|
|
1067
|
+
// reverse strand
|
|
1068
|
+
if (startCodonGenomicLoc < stopCodonGenomicLoc) {
|
|
1069
|
+
notify(
|
|
1070
|
+
'Start codon genomic location should be less than stop codon genomic location',
|
|
1071
|
+
'error',
|
|
1072
|
+
)
|
|
1073
|
+
return
|
|
1074
|
+
}
|
|
1075
|
+
let promise
|
|
1076
|
+
if (startCodonGenomicLoc !== cdsMax) {
|
|
1077
|
+
promise = new Promise((resolve) => {
|
|
1078
|
+
updateCDSLocation(
|
|
1079
|
+
cdsMax,
|
|
1080
|
+
startCodonGenomicLoc,
|
|
1081
|
+
feature,
|
|
1082
|
+
false,
|
|
1083
|
+
() => {
|
|
1084
|
+
resolve(true)
|
|
1085
|
+
},
|
|
1086
|
+
)
|
|
1087
|
+
})
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (stopCodonGenomicLoc !== cdsMin) {
|
|
1091
|
+
if (promise) {
|
|
1092
|
+
void promise.then(() => {
|
|
1093
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
|
|
1094
|
+
})
|
|
1095
|
+
} else {
|
|
1096
|
+
updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
428
1099
|
}
|
|
1100
|
+
notify('Translation sequence trimmed to start and stop codons', 'success')
|
|
429
1101
|
}
|
|
430
1102
|
|
|
431
|
-
const
|
|
1103
|
+
const onCopyClick = () => {
|
|
432
1104
|
const seqDiv = seqRef.current
|
|
433
1105
|
if (!seqDiv) {
|
|
434
1106
|
return
|
|
435
1107
|
}
|
|
436
|
-
|
|
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])
|
|
1108
|
+
void copyToClipboard(seqDiv)
|
|
443
1109
|
}
|
|
444
1110
|
|
|
445
1111
|
return (
|
|
446
1112
|
<div>
|
|
447
1113
|
{cdsPresent && (
|
|
448
1114
|
<div>
|
|
449
|
-
<Accordion
|
|
1115
|
+
<Accordion>
|
|
450
1116
|
<StyledAccordionSummary
|
|
451
1117
|
expandIcon={<ExpandMoreIcon style={{ color: 'white' }} />}
|
|
452
1118
|
aria-controls="panel1-content"
|
|
@@ -458,7 +1124,11 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
458
1124
|
</StyledAccordionSummary>
|
|
459
1125
|
<AccordionDetails>
|
|
460
1126
|
<SequenceContainer>
|
|
461
|
-
<Typography
|
|
1127
|
+
<Typography
|
|
1128
|
+
component={'span'}
|
|
1129
|
+
ref={seqRef}
|
|
1130
|
+
style={{ maxHeight: 120, overflowY: 'scroll' }}
|
|
1131
|
+
>
|
|
462
1132
|
{getTranslationSequence()}
|
|
463
1133
|
</Typography>
|
|
464
1134
|
</SequenceContainer>
|
|
@@ -474,7 +1144,7 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
474
1144
|
<Tooltip title="Copy">
|
|
475
1145
|
<ContentCopyIcon
|
|
476
1146
|
style={{ fontSize: 15, cursor: 'pointer' }}
|
|
477
|
-
onClick={
|
|
1147
|
+
onClick={onCopyClick}
|
|
478
1148
|
/>
|
|
479
1149
|
</Tooltip>
|
|
480
1150
|
<Tooltip title="Trim">
|
|
@@ -486,38 +1156,88 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
486
1156
|
</div>
|
|
487
1157
|
</AccordionDetails>
|
|
488
1158
|
</Accordion>
|
|
489
|
-
<
|
|
1159
|
+
<Grid
|
|
490
1160
|
container
|
|
491
1161
|
justifyContent="center"
|
|
492
1162
|
alignItems="center"
|
|
493
1163
|
style={{ textAlign: 'center', marginTop: 10 }}
|
|
494
1164
|
>
|
|
495
|
-
<
|
|
496
|
-
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1165
|
+
<Grid size={1} />
|
|
1166
|
+
{strand === 1 ? (
|
|
1167
|
+
<Grid size={4}>
|
|
1168
|
+
<StyledTextField
|
|
1169
|
+
margin="dense"
|
|
1170
|
+
variant="outlined"
|
|
1171
|
+
value={cdsMin + 1}
|
|
1172
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1173
|
+
handleCDSLocationChange(
|
|
1174
|
+
cdsMin,
|
|
1175
|
+
newLocation - 1,
|
|
1176
|
+
feature,
|
|
1177
|
+
true,
|
|
1178
|
+
)
|
|
1179
|
+
}}
|
|
1180
|
+
style={{ border: '1px solid black', borderRadius: 5 }}
|
|
1181
|
+
/>
|
|
1182
|
+
</Grid>
|
|
1183
|
+
) : (
|
|
1184
|
+
<Grid size={4}>
|
|
1185
|
+
<StyledTextField
|
|
1186
|
+
margin="dense"
|
|
1187
|
+
variant="outlined"
|
|
1188
|
+
value={cdsMax}
|
|
1189
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1190
|
+
handleCDSLocationChange(
|
|
1191
|
+
cdsMax,
|
|
1192
|
+
newLocation,
|
|
1193
|
+
feature,
|
|
1194
|
+
false,
|
|
1195
|
+
)
|
|
1196
|
+
}}
|
|
1197
|
+
style={{ border: '1px solid black', borderRadius: 5 }}
|
|
1198
|
+
/>
|
|
1199
|
+
</Grid>
|
|
1200
|
+
)}
|
|
1201
|
+
<Grid size={2}>
|
|
507
1202
|
<Typography component={'span'}>CDS</Typography>
|
|
508
|
-
</
|
|
509
|
-
|
|
510
|
-
<
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1203
|
+
</Grid>
|
|
1204
|
+
{strand === 1 ? (
|
|
1205
|
+
<Grid size={4}>
|
|
1206
|
+
<StyledTextField
|
|
1207
|
+
margin="dense"
|
|
1208
|
+
variant="outlined"
|
|
1209
|
+
value={cdsMax}
|
|
1210
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1211
|
+
handleCDSLocationChange(
|
|
1212
|
+
cdsMax,
|
|
1213
|
+
newLocation,
|
|
1214
|
+
feature,
|
|
1215
|
+
false,
|
|
1216
|
+
)
|
|
1217
|
+
}}
|
|
1218
|
+
style={{ border: '1px solid black', borderRadius: 5 }}
|
|
1219
|
+
/>
|
|
1220
|
+
</Grid>
|
|
1221
|
+
) : (
|
|
1222
|
+
<Grid size={4}>
|
|
1223
|
+
<StyledTextField
|
|
1224
|
+
margin="dense"
|
|
1225
|
+
variant="outlined"
|
|
1226
|
+
value={cdsMin + 1}
|
|
1227
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1228
|
+
handleCDSLocationChange(
|
|
1229
|
+
cdsMin,
|
|
1230
|
+
newLocation - 1,
|
|
1231
|
+
feature,
|
|
1232
|
+
true,
|
|
1233
|
+
)
|
|
1234
|
+
}}
|
|
1235
|
+
style={{ border: '1px solid black', borderRadius: 5 }}
|
|
1236
|
+
/>
|
|
1237
|
+
</Grid>
|
|
1238
|
+
)}
|
|
1239
|
+
<Grid size={1} />
|
|
1240
|
+
</Grid>
|
|
521
1241
|
</div>
|
|
522
1242
|
)}
|
|
523
1243
|
<div style={{ marginTop: 5 }}>
|
|
@@ -525,13 +1245,13 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
525
1245
|
return (
|
|
526
1246
|
<div key={index}>
|
|
527
1247
|
{loc.type === 'exon' && (
|
|
528
|
-
<
|
|
1248
|
+
<Grid
|
|
529
1249
|
container
|
|
530
1250
|
justifyContent="center"
|
|
531
1251
|
alignItems="center"
|
|
532
1252
|
style={{ textAlign: 'center' }}
|
|
533
1253
|
>
|
|
534
|
-
<
|
|
1254
|
+
<Grid size={1}>
|
|
535
1255
|
{index !== 0 &&
|
|
536
1256
|
getFivePrimeSpliceSite(loc, index).map((site, idx) => (
|
|
537
1257
|
<Typography
|
|
@@ -542,41 +1262,77 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
542
1262
|
{site.spliceSite}
|
|
543
1263
|
</Typography>
|
|
544
1264
|
))}
|
|
545
|
-
</
|
|
546
|
-
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1265
|
+
</Grid>
|
|
1266
|
+
{strand === 1 ? (
|
|
1267
|
+
<Grid size={4} style={{ padding: 0 }}>
|
|
1268
|
+
<StyledTextField
|
|
1269
|
+
margin="dense"
|
|
1270
|
+
variant="outlined"
|
|
1271
|
+
value={loc.min + 1}
|
|
1272
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1273
|
+
handleExonLocationChange(
|
|
1274
|
+
loc.min,
|
|
1275
|
+
newLocation - 1,
|
|
1276
|
+
feature,
|
|
1277
|
+
true,
|
|
1278
|
+
)
|
|
1279
|
+
}}
|
|
1280
|
+
/>
|
|
1281
|
+
</Grid>
|
|
1282
|
+
) : (
|
|
1283
|
+
<Grid size={4} style={{ padding: 0 }}>
|
|
1284
|
+
<StyledTextField
|
|
1285
|
+
margin="dense"
|
|
1286
|
+
variant="outlined"
|
|
1287
|
+
value={loc.max}
|
|
1288
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1289
|
+
handleExonLocationChange(
|
|
1290
|
+
loc.max,
|
|
1291
|
+
newLocation,
|
|
1292
|
+
feature,
|
|
1293
|
+
false,
|
|
1294
|
+
)
|
|
1295
|
+
}}
|
|
1296
|
+
/>
|
|
1297
|
+
</Grid>
|
|
1298
|
+
)}
|
|
1299
|
+
<Grid size={2}>
|
|
562
1300
|
<Strand strand={feature.strand} />
|
|
563
|
-
</
|
|
564
|
-
|
|
565
|
-
<
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1301
|
+
</Grid>
|
|
1302
|
+
{strand === 1 ? (
|
|
1303
|
+
<Grid size={4} style={{ padding: 0 }}>
|
|
1304
|
+
<StyledTextField
|
|
1305
|
+
margin="dense"
|
|
1306
|
+
variant="outlined"
|
|
1307
|
+
value={loc.max}
|
|
1308
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1309
|
+
handleExonLocationChange(
|
|
1310
|
+
loc.max,
|
|
1311
|
+
newLocation,
|
|
1312
|
+
feature,
|
|
1313
|
+
false,
|
|
1314
|
+
)
|
|
1315
|
+
}}
|
|
1316
|
+
/>
|
|
1317
|
+
</Grid>
|
|
1318
|
+
) : (
|
|
1319
|
+
<Grid size={4} style={{ padding: 0 }}>
|
|
1320
|
+
<StyledTextField
|
|
1321
|
+
margin="dense"
|
|
1322
|
+
variant="outlined"
|
|
1323
|
+
value={loc.min + 1}
|
|
1324
|
+
onChangeCommitted={(newLocation: number) => {
|
|
1325
|
+
handleExonLocationChange(
|
|
1326
|
+
loc.min,
|
|
1327
|
+
newLocation - 1,
|
|
1328
|
+
feature,
|
|
1329
|
+
true,
|
|
1330
|
+
)
|
|
1331
|
+
}}
|
|
1332
|
+
/>
|
|
1333
|
+
</Grid>
|
|
1334
|
+
)}
|
|
1335
|
+
<Grid size={1}>
|
|
580
1336
|
{index !== transcriptExonParts.length - 1 &&
|
|
581
1337
|
getThreePrimeSpliceSite(loc, index).map((site, idx) => (
|
|
582
1338
|
<Typography
|
|
@@ -587,8 +1343,8 @@ export const TranscriptWidgetEditLocation = observer(
|
|
|
587
1343
|
{site.spliceSite}
|
|
588
1344
|
</Typography>
|
|
589
1345
|
))}
|
|
590
|
-
</
|
|
591
|
-
</
|
|
1346
|
+
</Grid>
|
|
1347
|
+
</Grid>
|
|
592
1348
|
)}
|
|
593
1349
|
</div>
|
|
594
1350
|
)
|