@apollo-annotation/jbrowse-plugin-apollo 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +5466 -4490
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +5283 -4318
- 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 +6806 -4088
- 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/ApolloInternetAccount/addMenuItems.ts +5 -2
- package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -0
- package/src/ApolloInternetAccount/components/LoginButtons.tsx +1 -1
- package/src/ApolloInternetAccount/components/LoginIcons.tsx +1 -1
- package/src/ApolloInternetAccount/configSchema.ts +1 -1
- package/src/ApolloInternetAccount/model.ts +11 -10
- package/src/ApolloJobModel.ts +1 -1
- package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +8 -6
- package/src/ApolloRefNameAliasAdapter/index.ts +2 -2
- package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +17 -4
- package/src/ApolloSequenceAdapter/index.ts +1 -1
- package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +8 -7
- package/src/ApolloTextSearchAdapter/index.ts +1 -1
- package/src/BackendDrivers/BackendDriver.ts +7 -7
- package/src/BackendDrivers/CollaborationServerDriver.ts +14 -10
- package/src/BackendDrivers/DesktopFileDriver.ts +11 -10
- package/src/BackendDrivers/InMemoryFileDriver.ts +10 -6
- package/src/ChangeManager.ts +5 -5
- package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +92 -20
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +170 -27
- package/src/FeatureDetailsWidget/AttributeKey.tsx +50 -0
- package/src/FeatureDetailsWidget/AttributeKeySelector.tsx +104 -0
- package/src/FeatureDetailsWidget/Attributes.tsx +213 -320
- package/src/FeatureDetailsWidget/BasicInformation.tsx +8 -9
- package/src/FeatureDetailsWidget/DefaultAttributeEditor.tsx +104 -0
- package/src/FeatureDetailsWidget/DefaultAttributeViewer.tsx +22 -0
- package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +10 -8
- package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -1
- package/src/FeatureDetailsWidget/Sequence.tsx +18 -35
- package/src/FeatureDetailsWidget/StringTextField.tsx +1 -1
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +140 -95
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +600 -0
- package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +54 -0
- package/src/FeatureDetailsWidget/model.ts +8 -3
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +19 -12
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +19 -41
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +44 -22
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +6 -5
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -7
- package/src/LinearApolloDisplay/stateModel/base.ts +52 -10
- package/src/LinearApolloDisplay/stateModel/index.ts +4 -3
- package/src/LinearApolloDisplay/stateModel/layouts.ts +8 -34
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +13 -12
- package/src/LinearApolloDisplay/stateModel/rendering.ts +63 -31
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +221 -0
- package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +40 -0
- package/src/LinearApolloSixFrameDisplay/components/index.ts +2 -0
- package/src/LinearApolloSixFrameDisplay/configSchema.ts +7 -0
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +821 -0
- package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +63 -0
- package/src/LinearApolloSixFrameDisplay/glyphs/index.ts +1 -0
- package/src/LinearApolloSixFrameDisplay/index.ts +2 -0
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +261 -0
- package/src/LinearApolloSixFrameDisplay/stateModel/index.ts +27 -0
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +236 -0
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +349 -0
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +199 -0
- package/src/LinearApolloSixFrameDisplay/types.ts +1 -0
- package/src/OntologyManager/OntologyStore/fulltext-stopwords.ts +10 -1
- package/src/OntologyManager/OntologyStore/fulltext.test.ts +1 -1
- package/src/OntologyManager/OntologyStore/fulltext.ts +8 -3
- package/src/OntologyManager/OntologyStore/index.test.ts +4 -1
- package/src/OntologyManager/OntologyStore/index.ts +19 -14
- package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +6 -5
- package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +11 -5
- package/src/OntologyManager/index.ts +10 -6
- package/src/OntologyManager/util.ts +3 -2
- package/src/TabularEditor/HybridGrid/ChangeHandling.ts +2 -2
- package/src/TabularEditor/HybridGrid/Feature.tsx +9 -8
- package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +1 -1
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +3 -2
- package/src/TabularEditor/HybridGrid/NumberCell.tsx +8 -1
- package/src/TabularEditor/HybridGrid/ToolBar.tsx +15 -13
- package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +9 -33
- package/src/TabularEditor/TabularEditorPane.tsx +1 -1
- package/src/TabularEditor/model.ts +2 -2
- package/src/TabularEditor/types.ts +5 -2
- package/src/components/AddAssembly.tsx +611 -291
- package/src/components/AddChildFeature.tsx +6 -5
- package/src/components/AddFeature.tsx +211 -38
- package/src/components/AddRefSeqAliases.tsx +14 -12
- package/src/components/CopyFeature.tsx +8 -7
- package/src/components/CreateApolloAnnotation.tsx +154 -46
- package/src/components/DeleteAssembly.tsx +9 -8
- package/src/components/DeleteFeature.tsx +5 -4
- package/src/components/Dialog.tsx +1 -1
- package/src/components/DownloadGFF3.tsx +11 -10
- package/src/components/FilterFeatures.tsx +6 -4
- package/src/components/ImportFeatures.tsx +7 -6
- package/src/components/LogOut.tsx +5 -4
- package/src/components/ManageChecks.tsx +9 -8
- package/src/components/ManageUsers.tsx +11 -10
- package/src/components/OntologyTermAutocomplete.tsx +5 -5
- package/src/components/OntologyTermMultiSelect.tsx +9 -6
- package/src/components/OpenLocalFile.tsx +4 -3
- package/src/components/ViewChangeLog.tsx +7 -6
- package/src/components/ViewCheckResults.tsx +8 -7
- package/src/components/index.ts +0 -1
- package/src/extensions/annotationFromJBrowseFeature.test.ts +1 -0
- package/src/extensions/annotationFromJBrowseFeature.ts +14 -12
- package/src/extensions/annotationFromPileup.ts +6 -6
- package/src/index.ts +33 -50
- package/src/makeDisplayComponent.tsx +93 -41
- package/src/session/ClientDataStore.ts +21 -17
- package/src/session/session.ts +20 -26
- package/src/types.ts +4 -4
- package/src/util/annotationFeatureUtils.ts +53 -0
- package/src/util/index.ts +4 -3
- package/src/util/loadAssemblyIntoClient.ts +10 -3
- package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +0 -13
- package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +0 -707
- package/src/ApolloSixFrameRenderer/configSchema.ts +0 -7
- package/src/ApolloSixFrameRenderer/index.ts +0 -3
- package/src/FeatureDetailsWidget/TranscriptBasic.tsx +0 -200
- package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +0 -19
- package/src/SixFrameFeatureDisplay/components/index.ts +0 -1
- package/src/SixFrameFeatureDisplay/configSchema.ts +0 -21
- package/src/SixFrameFeatureDisplay/index.ts +0 -2
- package/src/SixFrameFeatureDisplay/stateModel.ts +0 -439
- 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 {
|
|
7
|
+
type AnnotationFeature,
|
|
8
|
+
type TranscriptPart,
|
|
9
|
+
} from '@apollo-annotation/mst'
|
|
10
|
+
import {
|
|
11
|
+
LocationEndChange,
|
|
12
|
+
LocationStartChange,
|
|
13
|
+
} from '@apollo-annotation/shared'
|
|
14
|
+
import styled from '@emotion/styled'
|
|
15
|
+
import {
|
|
16
|
+
type AbstractSessionModel,
|
|
17
|
+
defaultCodonTable,
|
|
18
|
+
revcom,
|
|
19
|
+
} from '@jbrowse/core/util'
|
|
20
|
+
import AddIcon from '@mui/icons-material/Add'
|
|
21
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|
22
|
+
import ContentCutIcon from '@mui/icons-material/ContentCut'
|
|
23
|
+
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|
24
|
+
import RemoveIcon from '@mui/icons-material/Remove'
|
|
25
|
+
import {
|
|
26
|
+
Accordion,
|
|
27
|
+
AccordionDetails,
|
|
28
|
+
Grid2,
|
|
29
|
+
Tooltip,
|
|
30
|
+
Typography,
|
|
31
|
+
} from '@mui/material'
|
|
32
|
+
import { observer } from 'mobx-react'
|
|
33
|
+
import React, { useRef } from 'react'
|
|
34
|
+
|
|
35
|
+
import { type ApolloSessionModel } from '../session'
|
|
36
|
+
|
|
37
|
+
import { StyledAccordionSummary } from './ApolloTranscriptDetailsWidget'
|
|
38
|
+
import { NumberTextField } from './NumberTextField'
|
|
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 { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
+
import styled from '@emotion/styled'
|
|
3
|
+
import { Table, TableBody, TableCell, TableRow } from '@mui/material'
|
|
4
|
+
import { observer } from 'mobx-react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
import { getFeatureId, getFeatureName, getStrand } from '../util'
|
|
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
|
+
)
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
|
3
3
|
import {
|
|
4
|
-
AnnotationFeature,
|
|
4
|
+
type AnnotationFeature,
|
|
5
5
|
AnnotationFeatureModel,
|
|
6
6
|
} from '@apollo-annotation/mst'
|
|
7
7
|
import { getSession } from '@jbrowse/core/util'
|
|
8
8
|
import { ElementId } from '@jbrowse/core/util/types/mst'
|
|
9
9
|
import { autorun } from 'mobx'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type Instance,
|
|
12
|
+
type SnapshotIn,
|
|
13
|
+
addDisposer,
|
|
14
|
+
types,
|
|
15
|
+
} from 'mobx-state-tree'
|
|
11
16
|
|
|
12
|
-
import { ApolloSessionModel } from '../session'
|
|
17
|
+
import { type ApolloSessionModel } from '../session'
|
|
13
18
|
|
|
14
19
|
export const ApolloFeatureDetailsWidgetModel = types
|
|
15
20
|
.model('ApolloFeatureDetailsWidget', {
|