@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
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
|
-
import {
|
|
2
|
+
import { readConfObject } from '@jbrowse/core/configuration'
|
|
3
|
+
import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
|
|
4
|
+
import { type MenuItem } from '@jbrowse/core/ui'
|
|
5
|
+
import {
|
|
6
|
+
type AbstractSessionModel,
|
|
7
|
+
getContainingView,
|
|
8
|
+
getFrame,
|
|
9
|
+
intersection2,
|
|
10
|
+
isSessionModelWithWidgets,
|
|
11
|
+
} from '@jbrowse/core/util'
|
|
12
|
+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
13
|
+
import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded'
|
|
14
|
+
import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded'
|
|
3
15
|
import { alpha } from '@mui/material'
|
|
4
16
|
|
|
5
17
|
import { type OntologyRecord } from '../../OntologyManager'
|
|
6
|
-
import {
|
|
18
|
+
import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
|
|
19
|
+
import { type ApolloSessionModel } from '../../session'
|
|
7
20
|
import {
|
|
8
21
|
type MousePosition,
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
type MousePositionWithFeature,
|
|
23
|
+
containsSelectedFeature,
|
|
24
|
+
getMinAndMaxPx,
|
|
25
|
+
getOverlappingEdge,
|
|
26
|
+
isMousePositionWithFeature,
|
|
27
|
+
navToFeatureCenter,
|
|
28
|
+
} from '../../util'
|
|
29
|
+
import { getRelatedFeatures } from '../../util/annotationFeatureUtils'
|
|
30
|
+
import { type LinearApolloDisplay } from '../stateModel'
|
|
31
|
+
import { type LinearApolloDisplayMouseEvents } from '../stateModel/mouseEvents'
|
|
12
32
|
import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
|
|
13
33
|
import { type CanvasMouseEvent } from '../types'
|
|
14
34
|
|
|
@@ -19,7 +39,10 @@ let forwardFillLight: CanvasPattern | null = null
|
|
|
19
39
|
let backwardFillLight: CanvasPattern | null = null
|
|
20
40
|
let forwardFillDark: CanvasPattern | null = null
|
|
21
41
|
let backwardFillDark: CanvasPattern | null = null
|
|
22
|
-
|
|
42
|
+
const canvas = globalThis.document.createElement('canvas')
|
|
43
|
+
// @ts-expect-error getContext is undefined in the web worker
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
45
|
+
if (canvas?.getContext) {
|
|
23
46
|
for (const direction of ['forward', 'backward']) {
|
|
24
47
|
for (const themeMode of ['light', 'dark']) {
|
|
25
48
|
const canvas = document.createElement('canvas')
|
|
@@ -63,52 +86,102 @@ if ('document' in globalThis) {
|
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
function
|
|
89
|
+
function drawBackground(
|
|
67
90
|
ctx: CanvasRenderingContext2D,
|
|
68
91
|
feature: AnnotationFeature,
|
|
69
|
-
row: number,
|
|
70
92
|
stateModel: LinearApolloDisplayRendering,
|
|
71
93
|
displayedRegionIndex: number,
|
|
72
|
-
|
|
73
|
-
|
|
94
|
+
row: number,
|
|
95
|
+
color: string,
|
|
96
|
+
) {
|
|
97
|
+
const { apolloRowHeight, lgv, session } = stateModel
|
|
74
98
|
const { bpPerPx, displayedRegions, offsetPx } = lgv
|
|
75
99
|
const displayedRegion = displayedRegions[displayedRegionIndex]
|
|
76
100
|
const { refName, reversed } = displayedRegion
|
|
77
|
-
const rowHeight = apolloRowHeight
|
|
78
|
-
const cdsHeight = Math.round(0.9 * rowHeight)
|
|
79
|
-
const { children, min, strand } = feature
|
|
80
|
-
if (!children) {
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
const { apolloSelectedFeature } = session
|
|
84
101
|
const { apolloDataStore } = session
|
|
85
102
|
const { featureTypeOntology } = apolloDataStore.ontologyManager
|
|
86
103
|
if (!featureTypeOntology) {
|
|
87
104
|
throw new Error('featureTypeOntology is undefined')
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
// Draw background for gene
|
|
91
107
|
const topLevelFeatureMinX =
|
|
92
108
|
(lgv.bpToPx({
|
|
93
109
|
refName,
|
|
94
|
-
coord: min,
|
|
110
|
+
coord: feature.min,
|
|
95
111
|
regionNumber: displayedRegionIndex,
|
|
96
112
|
})?.offsetPx ?? 0) - offsetPx
|
|
97
113
|
const topLevelFeatureWidthPx = feature.length / bpPerPx
|
|
98
114
|
const topLevelFeatureStartPx = reversed
|
|
99
115
|
? topLevelFeatureMinX - topLevelFeatureWidthPx
|
|
100
116
|
: topLevelFeatureMinX
|
|
101
|
-
const topLevelFeatureTop = row *
|
|
117
|
+
const topLevelFeatureTop = row * apolloRowHeight
|
|
102
118
|
const topLevelFeatureHeight =
|
|
103
|
-
getRowCount(feature, featureTypeOntology) *
|
|
119
|
+
getRowCount(feature, featureTypeOntology) * apolloRowHeight
|
|
104
120
|
|
|
105
|
-
ctx.fillStyle =
|
|
121
|
+
ctx.fillStyle = color
|
|
106
122
|
ctx.fillRect(
|
|
107
123
|
topLevelFeatureStartPx,
|
|
108
124
|
topLevelFeatureTop,
|
|
109
125
|
topLevelFeatureWidthPx,
|
|
110
126
|
topLevelFeatureHeight,
|
|
111
127
|
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function backgroundColorForFeature(
|
|
131
|
+
session: ApolloSessionModel,
|
|
132
|
+
featureType: string,
|
|
133
|
+
): string {
|
|
134
|
+
const color = readConfObject(
|
|
135
|
+
session.getPluginConfiguration(),
|
|
136
|
+
'backgroundColorForFeature',
|
|
137
|
+
{ featureType },
|
|
138
|
+
) as string
|
|
139
|
+
return color
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function draw(
|
|
143
|
+
ctx: CanvasRenderingContext2D,
|
|
144
|
+
feature: AnnotationFeature,
|
|
145
|
+
row: number,
|
|
146
|
+
stateModel: LinearApolloDisplayRendering,
|
|
147
|
+
displayedRegionIndex: number,
|
|
148
|
+
): void {
|
|
149
|
+
const { apolloRowHeight, lgv, selectedFeature, session, theme } = stateModel
|
|
150
|
+
const { bpPerPx, displayedRegions, offsetPx } = lgv
|
|
151
|
+
const displayedRegion = displayedRegions[displayedRegionIndex]
|
|
152
|
+
const { refName, reversed } = displayedRegion
|
|
153
|
+
const rowHeight = apolloRowHeight
|
|
154
|
+
const cdsHeight = Math.round(0.9 * rowHeight)
|
|
155
|
+
const { children, strand } = feature
|
|
156
|
+
if (!children) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const { apolloDataStore } = session
|
|
160
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager
|
|
161
|
+
if (!featureTypeOntology) {
|
|
162
|
+
throw new Error('featureTypeOntology is undefined')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Draw background for gene
|
|
166
|
+
drawBackground(
|
|
167
|
+
ctx,
|
|
168
|
+
feature,
|
|
169
|
+
stateModel,
|
|
170
|
+
displayedRegionIndex,
|
|
171
|
+
row,
|
|
172
|
+
alpha(theme.palette.background.paper, 0.6),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (featureTypeOntology.isTypeOf(feature.type, 'pseudogene')) {
|
|
176
|
+
drawBackground(
|
|
177
|
+
ctx,
|
|
178
|
+
feature,
|
|
179
|
+
stateModel,
|
|
180
|
+
displayedRegionIndex,
|
|
181
|
+
row,
|
|
182
|
+
backgroundColorForFeature(session, 'pseudogenic_transcript'),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
112
185
|
|
|
113
186
|
// Draw lines on different rows for each transcript
|
|
114
187
|
let currentRow = 0
|
|
@@ -126,6 +199,29 @@ function draw(
|
|
|
126
199
|
}
|
|
127
200
|
|
|
128
201
|
const cdsCount = getCDSCount(transcript, featureTypeOntology)
|
|
202
|
+
if (cdsCount === 0) {
|
|
203
|
+
drawBackground(
|
|
204
|
+
ctx,
|
|
205
|
+
transcript,
|
|
206
|
+
stateModel,
|
|
207
|
+
displayedRegionIndex,
|
|
208
|
+
currentRow,
|
|
209
|
+
backgroundColorForFeature(session, 'nonCodingTranscript'),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
if (
|
|
213
|
+
featureTypeOntology.isTypeOf(transcript.type, 'pseudogenic_transcript')
|
|
214
|
+
) {
|
|
215
|
+
drawBackground(
|
|
216
|
+
ctx,
|
|
217
|
+
transcript,
|
|
218
|
+
stateModel,
|
|
219
|
+
displayedRegionIndex,
|
|
220
|
+
currentRow,
|
|
221
|
+
backgroundColorForFeature(session, 'pseudogenic_transcript'),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
129
225
|
for (const [, childFeature] of transcriptChildren) {
|
|
130
226
|
if (!featureTypeOntology.isTypeOf(childFeature.type, 'CDS')) {
|
|
131
227
|
continue
|
|
@@ -155,9 +251,9 @@ function draw(
|
|
|
155
251
|
}
|
|
156
252
|
|
|
157
253
|
const forwardFill =
|
|
158
|
-
theme
|
|
254
|
+
theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
|
|
159
255
|
const backwardFill =
|
|
160
|
-
theme
|
|
256
|
+
theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
|
|
161
257
|
// Draw exon and CDS for each transcript
|
|
162
258
|
currentRow = 0
|
|
163
259
|
for (const [, child] of children) {
|
|
@@ -174,7 +270,7 @@ function draw(
|
|
|
174
270
|
const cdsCount = getCDSCount(child, featureTypeOntology)
|
|
175
271
|
if (cdsCount != 0) {
|
|
176
272
|
for (const cdsRow of child.cdsLocations) {
|
|
177
|
-
const {
|
|
273
|
+
const { children: transcriptChildren } = child
|
|
178
274
|
if (!transcriptChildren) {
|
|
179
275
|
continue
|
|
180
276
|
}
|
|
@@ -203,7 +299,7 @@ function draw(
|
|
|
203
299
|
regionNumber: displayedRegionIndex,
|
|
204
300
|
})?.offsetPx ?? 0) - offsetPx
|
|
205
301
|
const cdsStartPx = reversed ? minX - cdsWidthPx : minX
|
|
206
|
-
ctx.fillStyle = theme
|
|
302
|
+
ctx.fillStyle = theme.palette.text.primary
|
|
207
303
|
const cdsTop =
|
|
208
304
|
(row + currentRow) * rowHeight + (rowHeight - cdsHeight) / 2
|
|
209
305
|
ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
|
|
@@ -220,12 +316,8 @@ function draw(
|
|
|
220
316
|
child.strand ?? 1,
|
|
221
317
|
cds.phase,
|
|
222
318
|
)
|
|
223
|
-
const frameColor = theme
|
|
224
|
-
|
|
225
|
-
ctx.fillStyle =
|
|
226
|
-
apolloSelectedFeature && _id === apolloSelectedFeature._id
|
|
227
|
-
? 'rgb(0,0,0)'
|
|
228
|
-
: cdsColorCode
|
|
319
|
+
const frameColor = theme.palette.framesCDS.at(frame)?.main
|
|
320
|
+
ctx.fillStyle = frameColor ?? 'black'
|
|
229
321
|
ctx.fillRect(
|
|
230
322
|
cdsStartPx + 1,
|
|
231
323
|
cdsTop + 1,
|
|
@@ -281,6 +373,9 @@ function draw(
|
|
|
281
373
|
currentRow += 1
|
|
282
374
|
}
|
|
283
375
|
}
|
|
376
|
+
if (selectedFeature && containsSelectedFeature(feature, selectedFeature)) {
|
|
377
|
+
drawHighlight(stateModel, ctx, selectedFeature, true)
|
|
378
|
+
}
|
|
284
379
|
}
|
|
285
380
|
|
|
286
381
|
function drawExon(
|
|
@@ -294,11 +389,10 @@ function drawExon(
|
|
|
294
389
|
forwardFill: CanvasPattern | null,
|
|
295
390
|
backwardFill: CanvasPattern | null,
|
|
296
391
|
) {
|
|
297
|
-
const { apolloRowHeight, lgv,
|
|
392
|
+
const { apolloRowHeight, lgv, theme } = stateModel
|
|
298
393
|
const { bpPerPx, displayedRegions, offsetPx } = lgv
|
|
299
394
|
const displayedRegion = displayedRegions[displayedRegionIndex]
|
|
300
395
|
const { refName, reversed } = displayedRegion
|
|
301
|
-
const { apolloSelectedFeature } = session
|
|
302
396
|
|
|
303
397
|
const minX =
|
|
304
398
|
(lgv.bpToPx({
|
|
@@ -312,14 +406,11 @@ function drawExon(
|
|
|
312
406
|
const top = (row + currentRow) * apolloRowHeight
|
|
313
407
|
const exonHeight = Math.round(0.6 * apolloRowHeight)
|
|
314
408
|
const exonTop = top + (apolloRowHeight - exonHeight) / 2
|
|
315
|
-
ctx.fillStyle = theme
|
|
409
|
+
ctx.fillStyle = theme.palette.text.primary
|
|
316
410
|
ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
|
|
317
411
|
if (widthPx > 2) {
|
|
318
412
|
ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
|
|
319
|
-
ctx.fillStyle =
|
|
320
|
-
apolloSelectedFeature && exon._id === apolloSelectedFeature._id
|
|
321
|
-
? 'rgb(0,0,0)'
|
|
322
|
-
: 'rgb(211,211,211)'
|
|
413
|
+
ctx.fillStyle = 'rgb(211,211,211)'
|
|
323
414
|
ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
|
|
324
415
|
if (forwardFill && backwardFill && strand) {
|
|
325
416
|
const reversal = reversed ? -1 : 1
|
|
@@ -340,6 +431,21 @@ function drawExon(
|
|
|
340
431
|
}
|
|
341
432
|
}
|
|
342
433
|
|
|
434
|
+
function* range(start: number, stop: number, step = 1): Generator<number> {
|
|
435
|
+
if (start === stop) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
if (start < stop) {
|
|
439
|
+
for (let i = start; i < stop; i += step) {
|
|
440
|
+
yield i
|
|
441
|
+
}
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
for (let i = start; i > stop; i -= step) {
|
|
445
|
+
yield i
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
343
449
|
function drawLine(
|
|
344
450
|
ctx: CanvasRenderingContext2D,
|
|
345
451
|
stateModel: LinearApolloDisplayRendering,
|
|
@@ -358,14 +464,33 @@ function drawLine(
|
|
|
358
464
|
coord: transcript.min,
|
|
359
465
|
regionNumber: displayedRegionIndex,
|
|
360
466
|
})?.offsetPx ?? 0) - offsetPx
|
|
361
|
-
const widthPx = transcript.length / bpPerPx
|
|
467
|
+
const widthPx = Math.round(transcript.length / bpPerPx)
|
|
362
468
|
const startPx = reversed ? minX - widthPx : minX
|
|
363
469
|
const height =
|
|
364
470
|
Math.round((currentRow + 1 / 2) * apolloRowHeight) + row * apolloRowHeight
|
|
365
|
-
ctx.strokeStyle = theme
|
|
471
|
+
ctx.strokeStyle = theme.palette.text.primary
|
|
472
|
+
const { strand = 1 } = transcript
|
|
366
473
|
ctx.beginPath()
|
|
367
|
-
|
|
368
|
-
|
|
474
|
+
// Draw the transcript line, and extend it out a bit on the 3` end
|
|
475
|
+
const lineStart = startPx - (strand === -1 ? 5 : 0)
|
|
476
|
+
const lineEnd = startPx + widthPx + (strand === -1 ? 0 : 5)
|
|
477
|
+
ctx.moveTo(lineStart, height)
|
|
478
|
+
ctx.lineTo(lineEnd, height)
|
|
479
|
+
// Now to draw arrows every 20 pixels along the line
|
|
480
|
+
// Make the arrow range a bit shorter to avoid an arrow hanging off the 5` end
|
|
481
|
+
const arrowsStart = lineStart + (strand === -1 ? 0 : 3)
|
|
482
|
+
const arrowsEnd = lineEnd - (strand === -1 ? 3 : 0)
|
|
483
|
+
// Offset determines if the arrows face left or right
|
|
484
|
+
const offset = strand === -1 ? 3 : -3
|
|
485
|
+
const arrowRange =
|
|
486
|
+
strand === -1
|
|
487
|
+
? range(arrowsStart, arrowsEnd, 20)
|
|
488
|
+
: range(arrowsEnd, arrowsStart, 20)
|
|
489
|
+
for (const arrowLocation of arrowRange) {
|
|
490
|
+
ctx.moveTo(arrowLocation + offset, height + offset)
|
|
491
|
+
ctx.lineTo(arrowLocation, height)
|
|
492
|
+
ctx.lineTo(arrowLocation + offset, height - offset)
|
|
493
|
+
}
|
|
369
494
|
ctx.stroke()
|
|
370
495
|
}
|
|
371
496
|
|
|
@@ -391,24 +516,22 @@ function drawDragPreview(
|
|
|
391
516
|
const rectY = row * apolloRowHeight
|
|
392
517
|
const rectWidth = Math.abs(current.x - featureEdgePx)
|
|
393
518
|
const rectHeight = apolloRowHeight * rowCount
|
|
394
|
-
overlayCtx.strokeStyle = theme
|
|
519
|
+
overlayCtx.strokeStyle = theme.palette.info.main
|
|
395
520
|
overlayCtx.setLineDash([6])
|
|
396
521
|
overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
|
|
397
|
-
overlayCtx.fillStyle = alpha(theme
|
|
522
|
+
overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
|
|
398
523
|
overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
|
|
399
524
|
}
|
|
400
525
|
|
|
401
|
-
function
|
|
402
|
-
stateModel:
|
|
526
|
+
function drawHighlight(
|
|
527
|
+
stateModel: LinearApolloDisplayRendering,
|
|
403
528
|
ctx: CanvasRenderingContext2D,
|
|
529
|
+
feature: AnnotationFeature,
|
|
530
|
+
selected = false,
|
|
404
531
|
) {
|
|
405
|
-
const {
|
|
532
|
+
const { apolloRowHeight, lgv, session, theme } = stateModel
|
|
406
533
|
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
407
534
|
|
|
408
|
-
if (!apolloHover) {
|
|
409
|
-
return
|
|
410
|
-
}
|
|
411
|
-
const { feature } = apolloHover
|
|
412
535
|
const position = stateModel.getFeatureLayoutPosition(feature)
|
|
413
536
|
if (!position) {
|
|
414
537
|
return
|
|
@@ -427,7 +550,9 @@ function drawHover(
|
|
|
427
550
|
const row = layoutRow + featureRow
|
|
428
551
|
const top = row * apolloRowHeight
|
|
429
552
|
const widthPx = length / bpPerPx
|
|
430
|
-
ctx.fillStyle =
|
|
553
|
+
ctx.fillStyle = selected
|
|
554
|
+
? theme.palette.action.disabled
|
|
555
|
+
: theme.palette.action.focus
|
|
431
556
|
|
|
432
557
|
if (!featureTypeOntology) {
|
|
433
558
|
throw new Error('featureTypeOntology is undefined')
|
|
@@ -440,6 +565,18 @@ function drawHover(
|
|
|
440
565
|
)
|
|
441
566
|
}
|
|
442
567
|
|
|
568
|
+
function drawHover(
|
|
569
|
+
stateModel: LinearApolloDisplay,
|
|
570
|
+
ctx: CanvasRenderingContext2D,
|
|
571
|
+
) {
|
|
572
|
+
const { hoveredFeature } = stateModel
|
|
573
|
+
|
|
574
|
+
if (!hoveredFeature) {
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
drawHighlight(stateModel, ctx, hoveredFeature.feature)
|
|
578
|
+
}
|
|
579
|
+
|
|
443
580
|
function getFeatureFromLayout(
|
|
444
581
|
feature: AnnotationFeature,
|
|
445
582
|
bp: number,
|
|
@@ -599,15 +736,53 @@ function getRowForFeature(
|
|
|
599
736
|
return
|
|
600
737
|
}
|
|
601
738
|
|
|
739
|
+
function selectFeatureAndOpenWidget(
|
|
740
|
+
stateModel: LinearApolloDisplayMouseEvents,
|
|
741
|
+
feature: AnnotationFeature,
|
|
742
|
+
) {
|
|
743
|
+
if (stateModel.apolloDragging) {
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
stateModel.setSelectedFeature(feature)
|
|
747
|
+
const { session } = stateModel
|
|
748
|
+
const { apolloDataStore } = session
|
|
749
|
+
const { featureTypeOntology } = apolloDataStore.ontologyManager
|
|
750
|
+
if (!featureTypeOntology) {
|
|
751
|
+
throw new Error('featureTypeOntology is undefined')
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let containsCDSOrExon = false
|
|
755
|
+
for (const [, child] of feature.children ?? []) {
|
|
756
|
+
if (
|
|
757
|
+
featureTypeOntology.isTypeOf(child.type, 'CDS') ||
|
|
758
|
+
featureTypeOntology.isTypeOf(child.type, 'exon')
|
|
759
|
+
) {
|
|
760
|
+
containsCDSOrExon = true
|
|
761
|
+
break
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (
|
|
765
|
+
(featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
766
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
|
|
767
|
+
containsCDSOrExon
|
|
768
|
+
) {
|
|
769
|
+
stateModel.showFeatureDetailsWidget(feature, [
|
|
770
|
+
'ApolloTranscriptDetails',
|
|
771
|
+
'apolloTranscriptDetails',
|
|
772
|
+
])
|
|
773
|
+
} else {
|
|
774
|
+
stateModel.showFeatureDetailsWidget(feature)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
602
778
|
function onMouseDown(
|
|
603
779
|
stateModel: LinearApolloDisplay,
|
|
604
|
-
currentMousePosition:
|
|
780
|
+
currentMousePosition: MousePositionWithFeature,
|
|
605
781
|
event: CanvasMouseEvent,
|
|
606
782
|
) {
|
|
607
|
-
const {
|
|
783
|
+
const { feature } = currentMousePosition
|
|
608
784
|
// swallow the mouseDown if we are on the edge of the feature so that we
|
|
609
785
|
// don't start dragging the view if we try to drag the feature edge
|
|
610
|
-
const { feature } = featureAndGlyphUnderMouse
|
|
611
786
|
const draggableFeature = getDraggableFeatureInfo(
|
|
612
787
|
currentMousePosition,
|
|
613
788
|
feature,
|
|
@@ -619,6 +794,7 @@ function onMouseDown(
|
|
|
619
794
|
currentMousePosition,
|
|
620
795
|
draggableFeature.feature,
|
|
621
796
|
draggableFeature.edge,
|
|
797
|
+
true,
|
|
622
798
|
)
|
|
623
799
|
}
|
|
624
800
|
}
|
|
@@ -627,10 +803,9 @@ function onMouseMove(
|
|
|
627
803
|
stateModel: LinearApolloDisplay,
|
|
628
804
|
mousePosition: MousePosition,
|
|
629
805
|
) {
|
|
630
|
-
if (
|
|
631
|
-
const {
|
|
632
|
-
stateModel.
|
|
633
|
-
const { feature } = featureAndGlyphUnderMouse
|
|
806
|
+
if (isMousePositionWithFeature(mousePosition)) {
|
|
807
|
+
const { feature, bp } = mousePosition
|
|
808
|
+
stateModel.setHoveredFeature({ feature, bp })
|
|
634
809
|
const draggableFeature = getDraggableFeatureInfo(
|
|
635
810
|
mousePosition,
|
|
636
811
|
feature,
|
|
@@ -651,10 +826,11 @@ function onMouseUp(
|
|
|
651
826
|
if (stateModel.apolloDragging) {
|
|
652
827
|
return
|
|
653
828
|
}
|
|
654
|
-
const {
|
|
655
|
-
if (
|
|
656
|
-
|
|
829
|
+
const { feature } = mousePosition
|
|
830
|
+
if (!feature) {
|
|
831
|
+
return
|
|
657
832
|
}
|
|
833
|
+
selectFeatureAndOpenWidget(stateModel, feature)
|
|
658
834
|
}
|
|
659
835
|
|
|
660
836
|
function getDraggableFeatureInfo(
|
|
@@ -674,31 +850,18 @@ function getDraggableFeatureInfo(
|
|
|
674
850
|
const isTranscript =
|
|
675
851
|
featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
676
852
|
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
|
|
677
|
-
const
|
|
853
|
+
const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS')
|
|
678
854
|
if (isGene || isTranscript) {
|
|
855
|
+
// For gene glyphs, the sizes of genes and transcripts are determined by
|
|
856
|
+
// their child exons, so we don't make them draggable
|
|
679
857
|
return
|
|
680
858
|
}
|
|
859
|
+
// So now the type of feature is either CDS or exon. If an exon and CDS edge
|
|
860
|
+
// are in the same place, we want to prioritize dragging the exon. If the
|
|
861
|
+
// feature we're on is a CDS, let's find any exon it may overlap.
|
|
681
862
|
const { bp, refName, regionNumber, x } = mousePosition
|
|
682
863
|
const { lgv } = stateModel
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber })
|
|
686
|
-
const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber })
|
|
687
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
688
|
-
return
|
|
689
|
-
}
|
|
690
|
-
const minPx = minPxInfo.offsetPx - offsetPx
|
|
691
|
-
const maxPx = maxPxInfo.offsetPx - offsetPx
|
|
692
|
-
if (Math.abs(maxPx - minPx) < 8) {
|
|
693
|
-
return
|
|
694
|
-
}
|
|
695
|
-
if (Math.abs(minPx - x) < 4) {
|
|
696
|
-
return { feature, edge: 'min' }
|
|
697
|
-
}
|
|
698
|
-
if (Math.abs(maxPx - x) < 4) {
|
|
699
|
-
return { feature, edge: 'max' }
|
|
700
|
-
}
|
|
701
|
-
if (isCds) {
|
|
864
|
+
if (isCDS) {
|
|
702
865
|
const transcript = feature.parent
|
|
703
866
|
if (!transcript?.children) {
|
|
704
867
|
return
|
|
@@ -710,46 +873,336 @@ function getDraggableFeatureInfo(
|
|
|
710
873
|
exonChildren.push(child)
|
|
711
874
|
}
|
|
712
875
|
}
|
|
713
|
-
|
|
714
876
|
const overlappingExon = exonChildren.find((child) => {
|
|
715
877
|
const [start, end] = intersection2(bp - 1, bp, child.min, child.max)
|
|
716
878
|
return start !== undefined && end !== undefined
|
|
717
879
|
})
|
|
880
|
+
if (overlappingExon) {
|
|
881
|
+
// We are on an exon, are we on the edge of it?
|
|
882
|
+
const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
|
|
883
|
+
if (minMax) {
|
|
884
|
+
const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
|
|
885
|
+
if (overlappingEdge) {
|
|
886
|
+
return overlappingEdge
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// End of special cases, let's see if we're on the edge of this CDS or exon
|
|
892
|
+
const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv)
|
|
893
|
+
if (minMax) {
|
|
894
|
+
const overlappingEdge = getOverlappingEdge(feature, x, minMax)
|
|
895
|
+
if (overlappingEdge) {
|
|
896
|
+
return overlappingEdge
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return
|
|
900
|
+
}
|
|
718
901
|
|
|
719
|
-
|
|
720
|
-
|
|
902
|
+
function isTranscriptFeature(
|
|
903
|
+
feature: AnnotationFeature,
|
|
904
|
+
session: ApolloSessionModel,
|
|
905
|
+
): boolean {
|
|
906
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
907
|
+
if (!featureTypeOntology) {
|
|
908
|
+
throw new Error('featureTypeOntology is undefined')
|
|
909
|
+
}
|
|
910
|
+
return (
|
|
911
|
+
featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
|
|
912
|
+
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isExonFeature(
|
|
917
|
+
feature: AnnotationFeature,
|
|
918
|
+
session: ApolloSessionModel,
|
|
919
|
+
): boolean {
|
|
920
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
921
|
+
if (!featureTypeOntology) {
|
|
922
|
+
throw new Error('featureTypeOntology is undefined')
|
|
923
|
+
}
|
|
924
|
+
return featureTypeOntology.isTypeOf(feature.type, 'exon')
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function isCDSFeature(
|
|
928
|
+
feature: AnnotationFeature,
|
|
929
|
+
session: ApolloSessionModel,
|
|
930
|
+
): boolean {
|
|
931
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
932
|
+
if (!featureTypeOntology) {
|
|
933
|
+
throw new Error('featureTypeOntology is undefined')
|
|
934
|
+
}
|
|
935
|
+
return featureTypeOntology.isTypeOf(feature.type, 'CDS')
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
interface AdjacentExons {
|
|
939
|
+
upstream: AnnotationFeature | undefined
|
|
940
|
+
downstream: AnnotationFeature | undefined
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function getAdjacentExons(
|
|
944
|
+
currentExon: AnnotationFeature,
|
|
945
|
+
display: LinearApolloDisplayMouseEvents,
|
|
946
|
+
mousePosition: MousePositionWithFeature,
|
|
947
|
+
session: ApolloSessionModel,
|
|
948
|
+
): AdjacentExons {
|
|
949
|
+
const lgv = getContainingView(
|
|
950
|
+
display as BaseDisplayModel,
|
|
951
|
+
) as unknown as LinearGenomeViewModel
|
|
952
|
+
|
|
953
|
+
// Genomic coords of current view
|
|
954
|
+
const viewGenomicLeft = mousePosition.bp - lgv.bpPerPx * mousePosition.x
|
|
955
|
+
const viewGenomicRight = viewGenomicLeft + lgv.coarseTotalBp
|
|
956
|
+
if (!currentExon.parent) {
|
|
957
|
+
return { upstream: undefined, downstream: undefined }
|
|
958
|
+
}
|
|
959
|
+
const transcript = currentExon.parent
|
|
960
|
+
if (!transcript.children) {
|
|
961
|
+
throw new Error(`Error getting children of ${transcript._id}`)
|
|
962
|
+
}
|
|
963
|
+
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
|
|
964
|
+
if (!featureTypeOntology) {
|
|
965
|
+
throw new Error('featureTypeOntology is undefined')
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let exons = []
|
|
969
|
+
for (const [, child] of transcript.children) {
|
|
970
|
+
if (featureTypeOntology.isTypeOf(child.type, 'exon')) {
|
|
971
|
+
exons.push(child)
|
|
721
972
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (minPxInfo === undefined || maxPxInfo === undefined) {
|
|
733
|
-
return
|
|
973
|
+
}
|
|
974
|
+
const adjacentExons: AdjacentExons = {
|
|
975
|
+
upstream: undefined,
|
|
976
|
+
downstream: undefined,
|
|
977
|
+
}
|
|
978
|
+
exons = exons.sort((a, b) => (a.min < b.min ? -1 : 1))
|
|
979
|
+
for (const exon of exons) {
|
|
980
|
+
if (exon.min > viewGenomicRight) {
|
|
981
|
+
adjacentExons.downstream = exon
|
|
982
|
+
break
|
|
734
983
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
984
|
+
}
|
|
985
|
+
exons = exons.sort((a, b) => (a.min > b.min ? -1 : 1))
|
|
986
|
+
for (const exon of exons) {
|
|
987
|
+
if (exon.max < viewGenomicLeft) {
|
|
988
|
+
adjacentExons.upstream = exon
|
|
989
|
+
break
|
|
739
990
|
}
|
|
740
|
-
|
|
741
|
-
|
|
991
|
+
}
|
|
992
|
+
if (transcript.strand === -1) {
|
|
993
|
+
const newUpstream = adjacentExons.downstream
|
|
994
|
+
adjacentExons.downstream = adjacentExons.upstream
|
|
995
|
+
adjacentExons.upstream = newUpstream
|
|
996
|
+
}
|
|
997
|
+
return adjacentExons
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function getStreamIcon(
|
|
1001
|
+
strand: 1 | -1 | undefined,
|
|
1002
|
+
isUpstream: boolean,
|
|
1003
|
+
isFlipped: boolean | undefined,
|
|
1004
|
+
) {
|
|
1005
|
+
// This is the icon you would use for strand=1, downstream, straight
|
|
1006
|
+
// (non-flipped) view
|
|
1007
|
+
let icon = SkipNextRoundedIcon
|
|
1008
|
+
|
|
1009
|
+
if (strand === -1) {
|
|
1010
|
+
icon = SkipPreviousRoundedIcon
|
|
1011
|
+
}
|
|
1012
|
+
if (isUpstream) {
|
|
1013
|
+
icon =
|
|
1014
|
+
icon === SkipPreviousRoundedIcon
|
|
1015
|
+
? SkipNextRoundedIcon
|
|
1016
|
+
: SkipPreviousRoundedIcon
|
|
1017
|
+
}
|
|
1018
|
+
if (isFlipped) {
|
|
1019
|
+
icon =
|
|
1020
|
+
icon === SkipPreviousRoundedIcon
|
|
1021
|
+
? SkipNextRoundedIcon
|
|
1022
|
+
: SkipPreviousRoundedIcon
|
|
1023
|
+
}
|
|
1024
|
+
return icon
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function getContextMenuItems(
|
|
1028
|
+
display: LinearApolloDisplayMouseEvents,
|
|
1029
|
+
mousePosition: MousePositionWithFeature,
|
|
1030
|
+
): MenuItem[] {
|
|
1031
|
+
const {
|
|
1032
|
+
apolloInternetAccount: internetAccount,
|
|
1033
|
+
hoveredFeature,
|
|
1034
|
+
changeManager,
|
|
1035
|
+
regions,
|
|
1036
|
+
selectedFeature,
|
|
1037
|
+
session,
|
|
1038
|
+
} = display
|
|
1039
|
+
const [region] = regions
|
|
1040
|
+
const currentAssemblyId = display.getAssemblyId(region.assemblyName)
|
|
1041
|
+
const menuItems: MenuItem[] = []
|
|
1042
|
+
const role = internetAccount ? internetAccount.role : 'admin'
|
|
1043
|
+
const admin = role === 'admin'
|
|
1044
|
+
if (!hoveredFeature) {
|
|
1045
|
+
return menuItems
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (isMousePositionWithFeature(mousePosition)) {
|
|
1049
|
+
const { bp, feature } = mousePosition
|
|
1050
|
+
let featuresUnderClick = getRelatedFeatures(feature, bp)
|
|
1051
|
+
if (isCDSFeature(feature, session)) {
|
|
1052
|
+
featuresUnderClick = getRelatedFeatures(feature, bp, true)
|
|
742
1053
|
}
|
|
743
|
-
|
|
744
|
-
|
|
1054
|
+
|
|
1055
|
+
for (const feature of featuresUnderClick) {
|
|
1056
|
+
const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
|
|
1057
|
+
display,
|
|
1058
|
+
feature,
|
|
1059
|
+
)
|
|
1060
|
+
if (isExonFeature(feature, session)) {
|
|
1061
|
+
const adjacentExons = getAdjacentExons(
|
|
1062
|
+
feature,
|
|
1063
|
+
display,
|
|
1064
|
+
mousePosition,
|
|
1065
|
+
session,
|
|
1066
|
+
)
|
|
1067
|
+
const lgv = getContainingView(
|
|
1068
|
+
display as BaseDisplayModel,
|
|
1069
|
+
) as unknown as LinearGenomeViewModel
|
|
1070
|
+
if (adjacentExons.upstream) {
|
|
1071
|
+
const exon = adjacentExons.upstream
|
|
1072
|
+
contextMenuItemsForFeature.push({
|
|
1073
|
+
label: 'Go to upstream exon',
|
|
1074
|
+
icon: getStreamIcon(
|
|
1075
|
+
feature.strand,
|
|
1076
|
+
true,
|
|
1077
|
+
lgv.displayedRegions.at(0)?.reversed,
|
|
1078
|
+
),
|
|
1079
|
+
onClick: () => {
|
|
1080
|
+
lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
|
|
1081
|
+
selectFeatureAndOpenWidget(display, exon)
|
|
1082
|
+
},
|
|
1083
|
+
})
|
|
1084
|
+
}
|
|
1085
|
+
if (adjacentExons.downstream) {
|
|
1086
|
+
const exon = adjacentExons.downstream
|
|
1087
|
+
contextMenuItemsForFeature.push({
|
|
1088
|
+
label: 'Go to downstream exon',
|
|
1089
|
+
icon: getStreamIcon(
|
|
1090
|
+
feature.strand,
|
|
1091
|
+
false,
|
|
1092
|
+
lgv.displayedRegions.at(0)?.reversed,
|
|
1093
|
+
),
|
|
1094
|
+
onClick: () => {
|
|
1095
|
+
lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
|
|
1096
|
+
selectFeatureAndOpenWidget(display, exon)
|
|
1097
|
+
},
|
|
1098
|
+
})
|
|
1099
|
+
}
|
|
1100
|
+
contextMenuItemsForFeature.push(
|
|
1101
|
+
{
|
|
1102
|
+
label: 'Merge exons',
|
|
1103
|
+
disabled: !admin,
|
|
1104
|
+
onClick: () => {
|
|
1105
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
1106
|
+
(doneCallback) => [
|
|
1107
|
+
MergeExons,
|
|
1108
|
+
{
|
|
1109
|
+
session,
|
|
1110
|
+
handleClose: () => {
|
|
1111
|
+
doneCallback()
|
|
1112
|
+
},
|
|
1113
|
+
changeManager,
|
|
1114
|
+
sourceFeature: feature,
|
|
1115
|
+
sourceAssemblyId: currentAssemblyId,
|
|
1116
|
+
selectedFeature,
|
|
1117
|
+
setSelectedFeature: (feature?: AnnotationFeature) => {
|
|
1118
|
+
display.setSelectedFeature(feature)
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
)
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
label: 'Split exon',
|
|
1127
|
+
disabled: !admin,
|
|
1128
|
+
onClick: () => {
|
|
1129
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
1130
|
+
(doneCallback) => [
|
|
1131
|
+
SplitExon,
|
|
1132
|
+
{
|
|
1133
|
+
session,
|
|
1134
|
+
handleClose: () => {
|
|
1135
|
+
doneCallback()
|
|
1136
|
+
},
|
|
1137
|
+
changeManager,
|
|
1138
|
+
sourceFeature: feature,
|
|
1139
|
+
sourceAssemblyId: currentAssemblyId,
|
|
1140
|
+
selectedFeature,
|
|
1141
|
+
setSelectedFeature: (feature?: AnnotationFeature) => {
|
|
1142
|
+
display.setSelectedFeature(feature)
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
],
|
|
1146
|
+
)
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
)
|
|
1150
|
+
}
|
|
1151
|
+
if (isTranscriptFeature(feature, session)) {
|
|
1152
|
+
contextMenuItemsForFeature.push({
|
|
1153
|
+
label: 'Merge transcript',
|
|
1154
|
+
onClick: () => {
|
|
1155
|
+
;(session as unknown as AbstractSessionModel).queueDialog(
|
|
1156
|
+
(doneCallback) => [
|
|
1157
|
+
MergeTranscripts,
|
|
1158
|
+
{
|
|
1159
|
+
session,
|
|
1160
|
+
handleClose: () => {
|
|
1161
|
+
doneCallback()
|
|
1162
|
+
},
|
|
1163
|
+
changeManager,
|
|
1164
|
+
sourceFeature: feature,
|
|
1165
|
+
sourceAssemblyId: currentAssemblyId,
|
|
1166
|
+
selectedFeature,
|
|
1167
|
+
setSelectedFeature: (feature?: AnnotationFeature) => {
|
|
1168
|
+
display.setSelectedFeature(feature)
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
],
|
|
1172
|
+
)
|
|
1173
|
+
},
|
|
1174
|
+
})
|
|
1175
|
+
if (isSessionModelWithWidgets(session)) {
|
|
1176
|
+
contextMenuItemsForFeature.push({
|
|
1177
|
+
label: 'Open transcript details',
|
|
1178
|
+
onClick: () => {
|
|
1179
|
+
const apolloTranscriptWidget = session.addWidget(
|
|
1180
|
+
'ApolloTranscriptDetails',
|
|
1181
|
+
'apolloTranscriptDetails',
|
|
1182
|
+
{
|
|
1183
|
+
feature,
|
|
1184
|
+
assembly: currentAssemblyId,
|
|
1185
|
+
changeManager,
|
|
1186
|
+
refName: region.refName,
|
|
1187
|
+
},
|
|
1188
|
+
)
|
|
1189
|
+
session.showWidget(apolloTranscriptWidget)
|
|
1190
|
+
},
|
|
1191
|
+
})
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
menuItems.push({
|
|
1195
|
+
label: feature.type,
|
|
1196
|
+
subMenu: contextMenuItemsForFeature,
|
|
1197
|
+
})
|
|
745
1198
|
}
|
|
746
1199
|
}
|
|
747
|
-
return
|
|
1200
|
+
return menuItems
|
|
748
1201
|
}
|
|
749
1202
|
|
|
750
1203
|
// False positive here, none of these functions use "this"
|
|
751
1204
|
/* eslint-disable @typescript-eslint/unbound-method */
|
|
752
|
-
const { drawTooltip,
|
|
1205
|
+
const { drawTooltip, getContextMenuItemsForFeature, onMouseLeave } = boxGlyph
|
|
753
1206
|
/* eslint-enable @typescript-eslint/unbound-method */
|
|
754
1207
|
|
|
755
1208
|
export const geneGlyph: Glyph = {
|
|
@@ -758,6 +1211,7 @@ export const geneGlyph: Glyph = {
|
|
|
758
1211
|
drawHover,
|
|
759
1212
|
drawTooltip,
|
|
760
1213
|
getContextMenuItems,
|
|
1214
|
+
getContextMenuItemsForFeature,
|
|
761
1215
|
getFeatureFromLayout,
|
|
762
1216
|
getRowCount,
|
|
763
1217
|
getRowForFeature,
|