@apollo-annotation/jbrowse-plugin-apollo 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/index.esm.js +6964 -4598
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +6610 -4261
  4. package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
  5. package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
  6. package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
  7. package/dist/jbrowse-plugin-apollo.umd.development.js +11563 -7493
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +23 -2
  13. package/src/ApolloInternetAccount/components/AuthTypeSelector.tsx +1 -0
  14. package/src/ApolloInternetAccount/components/LoginButtons.tsx +1 -1
  15. package/src/ApolloInternetAccount/components/LoginIcons.tsx +1 -1
  16. package/src/ApolloInternetAccount/configSchema.ts +1 -1
  17. package/src/ApolloInternetAccount/model.ts +11 -10
  18. package/src/ApolloJobModel.ts +1 -1
  19. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +8 -6
  20. package/src/ApolloRefNameAliasAdapter/index.ts +2 -2
  21. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +4 -4
  22. package/src/ApolloSequenceAdapter/index.ts +1 -1
  23. package/src/ApolloTextSearchAdapter/ApolloTextSearchAdapter.ts +8 -7
  24. package/src/ApolloTextSearchAdapter/index.ts +1 -1
  25. package/src/BackendDrivers/BackendDriver.ts +7 -7
  26. package/src/BackendDrivers/CollaborationServerDriver.ts +14 -10
  27. package/src/BackendDrivers/DesktopFileDriver.ts +11 -10
  28. package/src/BackendDrivers/InMemoryFileDriver.ts +10 -6
  29. package/src/ChangeManager.ts +15 -11
  30. package/src/FeatureDetailsWidget/ApolloFeatureDetailsWidget.tsx +8 -7
  31. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +35 -14
  32. package/src/FeatureDetailsWidget/AttributeKey.tsx +50 -0
  33. package/src/FeatureDetailsWidget/AttributeKeySelector.tsx +104 -0
  34. package/src/FeatureDetailsWidget/Attributes.tsx +215 -367
  35. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -5
  36. package/src/FeatureDetailsWidget/DefaultAttributeEditor.tsx +104 -0
  37. package/src/FeatureDetailsWidget/DefaultAttributeViewer.tsx +22 -0
  38. package/src/FeatureDetailsWidget/FeatureDetailsNavigation.tsx +4 -4
  39. package/src/FeatureDetailsWidget/NumberTextField.tsx +1 -1
  40. package/src/FeatureDetailsWidget/Sequence.tsx +2 -2
  41. package/src/FeatureDetailsWidget/StringTextField.tsx +1 -1
  42. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +15 -23
  43. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +950 -196
  44. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +8 -4
  45. package/src/FeatureDetailsWidget/model.ts +8 -3
  46. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +7 -7
  47. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +59 -72
  48. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +253 -60
  49. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +52 -6
  50. package/src/LinearApolloDisplay/glyphs/Glyph.ts +16 -8
  51. package/src/LinearApolloDisplay/stateModel/base.ts +81 -10
  52. package/src/LinearApolloDisplay/stateModel/index.ts +4 -3
  53. package/src/LinearApolloDisplay/stateModel/layouts.ts +8 -39
  54. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +63 -46
  55. package/src/LinearApolloDisplay/stateModel/rendering.ts +60 -31
  56. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +226 -0
  57. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +32 -0
  58. package/src/LinearApolloSixFrameDisplay/components/index.ts +2 -0
  59. package/src/LinearApolloSixFrameDisplay/configSchema.ts +7 -0
  60. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +940 -0
  61. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +63 -0
  62. package/src/LinearApolloSixFrameDisplay/glyphs/index.ts +1 -0
  63. package/src/LinearApolloSixFrameDisplay/index.ts +2 -0
  64. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +302 -0
  65. package/src/LinearApolloSixFrameDisplay/stateModel/index.ts +27 -0
  66. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +252 -0
  67. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +368 -0
  68. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +201 -0
  69. package/src/LinearApolloSixFrameDisplay/types.ts +1 -0
  70. package/src/OntologyManager/OntologyStore/fulltext.test.ts +1 -1
  71. package/src/OntologyManager/OntologyStore/fulltext.ts +8 -3
  72. package/src/OntologyManager/OntologyStore/index.test.ts +3 -1
  73. package/src/OntologyManager/OntologyStore/index.ts +19 -14
  74. package/src/OntologyManager/OntologyStore/indexeddb-schema.ts +6 -5
  75. package/src/OntologyManager/OntologyStore/indexeddb-storage.ts +11 -5
  76. package/src/OntologyManager/index.ts +12 -7
  77. package/src/OntologyManager/util.ts +3 -2
  78. package/src/TabularEditor/HybridGrid/ChangeHandling.ts +2 -2
  79. package/src/TabularEditor/HybridGrid/Feature.tsx +13 -7
  80. package/src/TabularEditor/HybridGrid/FeatureAttributes.tsx +1 -1
  81. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +3 -2
  82. package/src/TabularEditor/HybridGrid/ToolBar.tsx +1 -1
  83. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +114 -22
  84. package/src/TabularEditor/TabularEditorPane.tsx +1 -1
  85. package/src/TabularEditor/model.ts +2 -2
  86. package/src/TabularEditor/types.ts +5 -2
  87. package/src/components/AddAssembly.tsx +182 -179
  88. package/src/components/AddAssemblyAliases.tsx +114 -0
  89. package/src/components/AddChildFeature.tsx +8 -10
  90. package/src/components/AddFeature.tsx +216 -44
  91. package/src/components/AddRefSeqAliases.tsx +14 -12
  92. package/src/components/CopyFeature.tsx +10 -11
  93. package/src/components/CreateApolloAnnotation.tsx +342 -158
  94. package/src/components/DeleteAssembly.tsx +9 -8
  95. package/src/components/DeleteFeature.tsx +362 -14
  96. package/src/components/Dialog.tsx +1 -1
  97. package/src/components/DownloadGFF3.tsx +31 -11
  98. package/src/components/FilterFeatures.tsx +6 -4
  99. package/src/components/FilterTranscripts.tsx +86 -0
  100. package/src/components/ImportFeatures.tsx +7 -6
  101. package/src/components/LogOut.tsx +5 -4
  102. package/src/components/ManageChecks.tsx +9 -8
  103. package/src/components/ManageUsers.tsx +11 -10
  104. package/src/components/MergeExons.tsx +193 -0
  105. package/src/components/MergeTranscripts.tsx +185 -0
  106. package/src/components/OntologyTermAutocomplete.tsx +5 -5
  107. package/src/components/OntologyTermMultiSelect.tsx +6 -6
  108. package/src/components/OpenLocalFile.tsx +4 -3
  109. package/src/components/SplitExon.tsx +134 -0
  110. package/src/components/ViewChangeLog.tsx +7 -6
  111. package/src/components/ViewCheckResults.tsx +8 -7
  112. package/src/components/index.ts +3 -0
  113. package/src/config.ts +5 -0
  114. package/src/extensions/annotationFromJBrowseFeature.test.ts +1 -0
  115. package/src/extensions/annotationFromJBrowseFeature.ts +13 -10
  116. package/src/extensions/annotationFromPileup.ts +104 -94
  117. package/src/index.ts +33 -50
  118. package/src/makeDisplayComponent.tsx +90 -37
  119. package/src/session/ClientDataStore.ts +21 -17
  120. package/src/session/session.ts +46 -39
  121. package/src/types.ts +4 -4
  122. package/src/util/annotationFeatureUtils.ts +66 -1
  123. package/src/util/copyToClipboard.ts +21 -0
  124. package/src/util/glyphUtils.ts +49 -0
  125. package/src/util/index.ts +5 -3
  126. package/src/util/loadAssemblyIntoClient.ts +10 -3
  127. package/src/util/mouseEventsUtils.ts +113 -0
  128. package/src/ApolloSixFrameRenderer/ApolloSixFrameRenderer.tsx +0 -13
  129. package/src/ApolloSixFrameRenderer/components/ApolloRendering.tsx +0 -707
  130. package/src/ApolloSixFrameRenderer/configSchema.ts +0 -7
  131. package/src/ApolloSixFrameRenderer/index.ts +0 -3
  132. package/src/SixFrameFeatureDisplay/components/TrackLines.tsx +0 -19
  133. package/src/SixFrameFeatureDisplay/components/index.ts +0 -1
  134. package/src/SixFrameFeatureDisplay/configSchema.ts +0 -21
  135. package/src/SixFrameFeatureDisplay/index.ts +0 -2
  136. package/src/SixFrameFeatureDisplay/stateModel.ts +0 -443
@@ -0,0 +1,940 @@
1
+ import {
2
+ type AnnotationFeature,
3
+ type TranscriptPartCoding,
4
+ } from '@apollo-annotation/mst'
5
+ import { type MenuItem } from '@jbrowse/core/ui'
6
+ import {
7
+ type AbstractSessionModel,
8
+ getFrame,
9
+ intersection2,
10
+ measureText,
11
+ } from '@jbrowse/core/util'
12
+ import { alpha } from '@mui/material'
13
+ import equal from 'fast-deep-equal/es6'
14
+ import { getSnapshot } from 'mobx-state-tree'
15
+
16
+ import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
17
+ import { FilterTranscripts } from '../../components/FilterTranscripts'
18
+ import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
19
+ import { type LinearApolloSixFrameDisplay } from '../stateModel'
20
+ import {
21
+ type LinearApolloSixFrameDisplayMouseEvents,
22
+ type MousePosition,
23
+ type MousePositionWithFeatureAndGlyph,
24
+ isMousePositionWithFeatureAndGlyph,
25
+ } from '../stateModel/mouseEvents'
26
+ import { type LinearApolloSixFrameDisplayRendering } from '../stateModel/rendering'
27
+ import { type CanvasMouseEvent } from '../types'
28
+
29
+ import { type Glyph } from './Glyph'
30
+
31
+ let forwardFillLight: CanvasPattern | null = null
32
+ let backwardFillLight: CanvasPattern | null = null
33
+ let forwardFillDark: CanvasPattern | null = null
34
+ let backwardFillDark: CanvasPattern | null = null
35
+ const canvas = globalThis.document.createElement('canvas')
36
+ // @ts-expect-error getContext is undefined in the web worker
37
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
38
+ if (canvas?.getContext) {
39
+ for (const direction of ['forward', 'backward']) {
40
+ for (const themeMode of ['light', 'dark']) {
41
+ const canvas = document.createElement('canvas')
42
+ const canvasSize = 10
43
+ canvas.width = canvas.height = canvasSize
44
+ const ctx = canvas.getContext('2d')
45
+ if (ctx) {
46
+ const stripeColor1 =
47
+ themeMode === 'light' ? 'rgba(0,0,0,0)' : 'rgba(0,0,0,0.75)'
48
+ const stripeColor2 =
49
+ themeMode === 'light' ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.50)'
50
+ const gradient =
51
+ direction === 'forward'
52
+ ? ctx.createLinearGradient(0, canvasSize, canvasSize, 0)
53
+ : ctx.createLinearGradient(0, 0, canvasSize, canvasSize)
54
+ gradient.addColorStop(0, stripeColor1)
55
+ gradient.addColorStop(0.25, stripeColor1)
56
+ gradient.addColorStop(0.25, stripeColor2)
57
+ gradient.addColorStop(0.5, stripeColor2)
58
+ gradient.addColorStop(0.5, stripeColor1)
59
+ gradient.addColorStop(0.75, stripeColor1)
60
+ gradient.addColorStop(0.75, stripeColor2)
61
+ gradient.addColorStop(1, stripeColor2)
62
+ ctx.fillStyle = gradient
63
+ ctx.fillRect(0, 0, 10, 10)
64
+ if (direction === 'forward') {
65
+ if (themeMode === 'light') {
66
+ forwardFillLight = ctx.createPattern(canvas, 'repeat')
67
+ } else {
68
+ forwardFillDark = ctx.createPattern(canvas, 'repeat')
69
+ }
70
+ } else {
71
+ if (themeMode === 'light') {
72
+ backwardFillLight = ctx.createPattern(canvas, 'repeat')
73
+ } else {
74
+ backwardFillDark = ctx.createPattern(canvas, 'repeat')
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ function deepSetHas<T>(set: Set<T>, item: T): boolean {
83
+ for (const elem of set) {
84
+ if (equal(elem, item)) {
85
+ return true
86
+ }
87
+ }
88
+ return false
89
+ }
90
+
91
+ interface Label {
92
+ x: number
93
+ y: number
94
+ h: number
95
+ text: string | undefined
96
+ color: string
97
+ isSelected: boolean
98
+ }
99
+
100
+ function drawTextLabels(
101
+ ctx: CanvasRenderingContext2D,
102
+ labelArray: Label[],
103
+ font = '10px sans-serif',
104
+ ) {
105
+ for (let i = labelArray.length - 1; i >= 0; --i) {
106
+ const label = labelArray[i]
107
+ ctx.fillStyle = label.color
108
+ const labelRowX = Math.max(label.x + 1, 0)
109
+ const labelRowY = label.y + label.h
110
+ const textWidth = measureText(label.text, 10)
111
+ if (label.isSelected) {
112
+ ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
113
+ ctx.font = 'bold '.concat(font)
114
+ }
115
+ if (label.text) {
116
+ ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth)
117
+ ctx.font = font
118
+ }
119
+ }
120
+ }
121
+
122
+ function draw(
123
+ ctx: CanvasRenderingContext2D,
124
+ topLevelFeature: AnnotationFeature,
125
+ _row: number,
126
+ stateModel: LinearApolloSixFrameDisplayRendering,
127
+ displayedRegionIndex: number,
128
+ ): void {
129
+ const {
130
+ apolloRowHeight,
131
+ lgv,
132
+ session,
133
+ theme,
134
+ highestRow,
135
+ filteredTranscripts,
136
+ showFeatureLabels,
137
+ } = stateModel
138
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
139
+ const displayedRegion = displayedRegions[displayedRegionIndex]
140
+ const { refName, reversed } = displayedRegion
141
+ const rowHeight = apolloRowHeight
142
+ const exonHeight = rowHeight
143
+ const cdsHeight = rowHeight
144
+ const topLevelFeatureHeight = rowHeight
145
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
146
+ const textColor = theme?.palette.text.primary ?? 'black'
147
+ const { attributes, children, min, strand } = topLevelFeature
148
+ if (!children) {
149
+ return
150
+ }
151
+ const { apolloSelectedFeature } = session
152
+ const { apolloDataStore } = session
153
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
154
+ if (!featureTypeOntology) {
155
+ throw new Error('featureTypeOntology is undefined')
156
+ }
157
+ const labelArray: Label[] = []
158
+
159
+ // Draw background for gene
160
+ const topLevelFeatureMinX =
161
+ (lgv.bpToPx({
162
+ refName,
163
+ coord: min,
164
+ regionNumber: displayedRegionIndex,
165
+ })?.offsetPx ?? 0) - offsetPx
166
+ const topLevelFeatureWidthPx = topLevelFeature.length / bpPerPx
167
+ const topLevelFeatureStartPx = reversed
168
+ ? topLevelFeatureMinX - topLevelFeatureWidthPx
169
+ : topLevelFeatureMinX
170
+ const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer
171
+ const topLevelFeatureTop = topLevelRow * rowHeight
172
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
173
+ ctx.fillRect(
174
+ topLevelFeatureStartPx,
175
+ topLevelFeatureTop,
176
+ topLevelFeatureWidthPx,
177
+ topLevelFeatureHeight,
178
+ )
179
+
180
+ ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
181
+ ? alpha('rgb(0,0,0)', 0.7)
182
+ : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
183
+ ctx.fillRect(
184
+ topLevelFeatureStartPx + 1,
185
+ topLevelFeatureTop + 1,
186
+ topLevelFeatureWidthPx - 2,
187
+ topLevelFeatureHeight - 2,
188
+ )
189
+
190
+ const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
191
+ const label: Label = {
192
+ x: topLevelFeatureStartPx,
193
+ y: topLevelFeatureTop,
194
+ h: topLevelFeatureHeight,
195
+ text: attributes.get('gff_id')?.toString(),
196
+ color: textColor,
197
+ isSelected,
198
+ }
199
+ if (isSelected) {
200
+ labelArray.unshift(label)
201
+ } else {
202
+ labelArray.push(label)
203
+ }
204
+
205
+ const forwardFill =
206
+ theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
207
+ const backwardFill =
208
+ theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
209
+ const reversal = reversed ? -1 : 1
210
+ let topFill: CanvasPattern | null = null,
211
+ bottomFill: CanvasPattern | null = null
212
+ if (strand) {
213
+ ;[topFill, bottomFill] =
214
+ strand * reversal === 1
215
+ ? [forwardFill, backwardFill]
216
+ : [backwardFill, forwardFill]
217
+ }
218
+
219
+ if (topFill && bottomFill) {
220
+ ctx.fillStyle = topFill
221
+ ctx.fillRect(
222
+ topLevelFeatureStartPx + 1,
223
+ topLevelFeatureTop + 1,
224
+ topLevelFeatureWidthPx - 2,
225
+ (topLevelFeatureHeight - 2) / 2,
226
+ )
227
+ ctx.fillStyle = bottomFill
228
+ ctx.fillRect(
229
+ topLevelFeatureStartPx + 1,
230
+ topLevelFeatureTop + (topLevelFeatureHeight - 2) / 2,
231
+ topLevelFeatureWidthPx - 2,
232
+ (topLevelFeatureHeight - 2) / 2,
233
+ )
234
+ }
235
+
236
+ const renderedCDS = new Set<TranscriptPartCoding>()
237
+ // Draw exon and CDS for each mRNA
238
+ for (const [, child] of children) {
239
+ if (
240
+ !(
241
+ featureTypeOntology.isTypeOf(child.type, 'transcript') ||
242
+ featureTypeOntology.isTypeOf(child.type, 'pseudogenic_transcript')
243
+ )
244
+ ) {
245
+ continue
246
+ }
247
+ const { children: childrenOfmRNA, cdsLocations } = child
248
+ if (!childrenOfmRNA) {
249
+ continue
250
+ }
251
+ const childID: string | undefined = child.attributes
252
+ .get('gff_id')
253
+ ?.toString()
254
+ if (childID && filteredTranscripts.includes(childID)) {
255
+ continue
256
+ }
257
+ for (const [, exon] of childrenOfmRNA) {
258
+ if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
259
+ continue
260
+ }
261
+ const minX =
262
+ (lgv.bpToPx({
263
+ refName,
264
+ coord: exon.min,
265
+ regionNumber: displayedRegionIndex,
266
+ })?.offsetPx ?? 0) - offsetPx
267
+ const widthPx = exon.length / bpPerPx
268
+ const startPx = reversed ? minX - widthPx : minX
269
+
270
+ const exonTop =
271
+ topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2
272
+ const isSelected = isSelectedFeature(exon, apolloSelectedFeature)
273
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
274
+ ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
275
+ if (widthPx > 2) {
276
+ ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
277
+ ctx.fillStyle = isSelected ? 'rgb(0,0,0)' : alpha('#f5f500', 0.6)
278
+ ctx.fillRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
279
+ if (topFill && bottomFill) {
280
+ ctx.fillStyle = topFill
281
+ ctx.fillRect(
282
+ startPx + 1,
283
+ exonTop + 1,
284
+ widthPx - 2,
285
+ (exonHeight - 2) / 2,
286
+ )
287
+ ctx.fillStyle = bottomFill
288
+ ctx.fillRect(
289
+ startPx + 1,
290
+ exonTop + 1 + (exonHeight - 2) / 2,
291
+ widthPx - 2,
292
+ (exonHeight - 2) / 2,
293
+ )
294
+ }
295
+
296
+ const label: Label = {
297
+ x: startPx,
298
+ y: exonTop,
299
+ h: exonHeight,
300
+ text: exon.attributes.get('gff_id')?.toString(),
301
+ color: textColor,
302
+ isSelected,
303
+ }
304
+ if (isSelected) {
305
+ labelArray.unshift(label)
306
+ } else {
307
+ labelArray.push(label)
308
+ }
309
+ }
310
+ }
311
+
312
+ const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent)
313
+ let cdsStartPx = 0
314
+ let cdsTop = 0
315
+ for (const cdsRow of cdsLocations) {
316
+ let prevCDSTop = 0
317
+ let prevCDSEndPx = 0
318
+ let counter = 1
319
+ for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
320
+ if (
321
+ (apolloSelectedFeature &&
322
+ isSelected &&
323
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
324
+ !deepSetHas(renderedCDS, cds)
325
+ ) {
326
+ const cdsWidthPx = (cds.max - cds.min) / bpPerPx
327
+ const minX =
328
+ (lgv.bpToPx({
329
+ refName,
330
+ coord: cds.min,
331
+ regionNumber: displayedRegionIndex,
332
+ })?.offsetPx ?? 0) - offsetPx
333
+ cdsStartPx = reversed ? minX - cdsWidthPx : minX
334
+ ctx.fillStyle = theme?.palette.text.primary ?? 'black'
335
+ const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase)
336
+ const frameAdjust =
337
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
338
+ cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
339
+ ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
340
+ if (cdsWidthPx > 2) {
341
+ ctx.clearRect(
342
+ cdsStartPx + 1,
343
+ cdsTop + 1,
344
+ cdsWidthPx - 2,
345
+ cdsHeight - 2,
346
+ )
347
+
348
+ const frameColor = theme?.palette.framesCDS.at(frame)?.main
349
+ const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
350
+ ctx.fillStyle = cdsColorCode
351
+ ctx.fillStyle =
352
+ apolloSelectedFeature &&
353
+ isSelected &&
354
+ featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
355
+ ? 'rgb(0,0,0)'
356
+ : cdsColorCode
357
+ ctx.fillRect(
358
+ cdsStartPx + 1,
359
+ cdsTop + 1,
360
+ cdsWidthPx - 2,
361
+ cdsHeight - 2,
362
+ )
363
+
364
+ // Draw lines to connect CDS features with shared mRNA parent
365
+ if (counter > 1) {
366
+ // Mid-point for intron line "hat"
367
+ const midPoint: [number, number] = [
368
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
369
+ Math.max(
370
+ frame < 0
371
+ ? rowHeight * featureLabelSpacer * highestRow + 1
372
+ : 1, // Avoid render ceiling
373
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
374
+ ),
375
+ ]
376
+ ctx.strokeStyle = 'rgb(0, 128, 128)'
377
+ ctx.beginPath()
378
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
379
+ ctx.lineTo(...midPoint)
380
+ ctx.stroke()
381
+ ctx.moveTo(...midPoint)
382
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
383
+ ctx.stroke()
384
+ }
385
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
386
+ prevCDSTop = cdsTop + rowHeight / 2
387
+ counter += 1
388
+
389
+ if (topFill && bottomFill) {
390
+ ctx.fillStyle = topFill
391
+ ctx.fillRect(
392
+ cdsStartPx + 1,
393
+ cdsTop + 1,
394
+ cdsWidthPx - 2,
395
+ (cdsHeight - 2) / 2,
396
+ )
397
+ ctx.fillStyle = bottomFill
398
+ ctx.fillRect(
399
+ cdsStartPx + 1,
400
+ cdsTop + (cdsHeight - 2) / 2,
401
+ cdsWidthPx - 2,
402
+ (cdsHeight - 2) / 2,
403
+ )
404
+ }
405
+ }
406
+ renderedCDS.add(cds)
407
+ }
408
+ }
409
+ }
410
+ const label: Label = {
411
+ x: cdsStartPx,
412
+ y: cdsTop,
413
+ h: cdsHeight,
414
+ text: child.attributes.get('gff_id')?.toString(),
415
+ color: textColor,
416
+ isSelected,
417
+ }
418
+ if (isSelected) {
419
+ labelArray.unshift(label)
420
+ } else {
421
+ labelArray.push(label)
422
+ }
423
+ }
424
+ if (showFeatureLabels) {
425
+ drawTextLabels(ctx, labelArray)
426
+ }
427
+ }
428
+
429
+ function drawDragPreview(
430
+ stateModel: LinearApolloSixFrameDisplay,
431
+ overlayCtx: CanvasRenderingContext2D,
432
+ ) {
433
+ const { apolloDragging, apolloRowHeight, lgv, theme } = stateModel
434
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
435
+ if (!apolloDragging) {
436
+ return
437
+ }
438
+ const { current, edge, feature, start } = apolloDragging
439
+
440
+ const row = Math.floor(start.y / apolloRowHeight)
441
+ const region = displayedRegions[start.regionNumber]
442
+ const rowCount = 1
443
+ const featureEdgeBp = region.reversed
444
+ ? region.end - feature[edge]
445
+ : feature[edge] - region.start
446
+ const featureEdgePx = featureEdgeBp / bpPerPx - offsetPx
447
+ const rectX = Math.min(current.x, featureEdgePx)
448
+ const rectY = row * apolloRowHeight
449
+ const rectWidth = Math.abs(current.x - featureEdgePx)
450
+ const rectHeight = apolloRowHeight * rowCount
451
+ overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
452
+ overlayCtx.setLineDash([6])
453
+ overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
454
+ overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
455
+ overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
456
+ }
457
+
458
+ function drawHover(
459
+ stateModel: LinearApolloSixFrameDisplay,
460
+ ctx: CanvasRenderingContext2D,
461
+ ) {
462
+ const {
463
+ apolloHover,
464
+ apolloRowHeight,
465
+ filteredTranscripts,
466
+ lgv,
467
+ highestRow,
468
+ session,
469
+ showFeatureLabels,
470
+ } = stateModel
471
+ if (!apolloHover) {
472
+ return
473
+ }
474
+ const { apolloDataStore } = session
475
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
476
+ if (!featureTypeOntology) {
477
+ throw new Error('featureTypeOntology is undefined')
478
+ }
479
+ const { feature } = apolloHover
480
+ if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
481
+ return
482
+ }
483
+ const featureID: string | undefined = feature.attributes
484
+ .get('gff_id')
485
+ ?.toString()
486
+ if (featureID && filteredTranscripts.includes(featureID)) {
487
+ return
488
+ }
489
+ const position = stateModel.getFeatureLayoutPosition(feature)
490
+ if (!position) {
491
+ return
492
+ }
493
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
494
+ const { layoutIndex } = position
495
+ const displayedRegion = displayedRegions[layoutIndex]
496
+ const { refName, reversed } = displayedRegion
497
+ const rowHeight = apolloRowHeight
498
+ const cdsHeight = rowHeight
499
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
500
+ const { cdsLocations, strand } = feature
501
+ for (const cdsRow of cdsLocations) {
502
+ let prevCDSTop = 0
503
+ let prevCDSEndPx = 0
504
+ let counter = 1
505
+ for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
506
+ const cdsWidthPx = (cds.max - cds.min) / bpPerPx
507
+ if (cdsWidthPx > 2) {
508
+ const minX =
509
+ (lgv.bpToPx({
510
+ refName,
511
+ coord: cds.min,
512
+ regionNumber: layoutIndex,
513
+ })?.offsetPx ?? 0) - offsetPx
514
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
515
+ const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
516
+ const frameAdjust =
517
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
518
+ const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
519
+ ctx.fillStyle = 'rgba(255,0,0,0.6)'
520
+ ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
521
+
522
+ if (counter > 1) {
523
+ // Mid-point for intron line "hat"
524
+ const midPoint: [number, number] = [
525
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
526
+ Math.max(
527
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
528
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
529
+ ),
530
+ ]
531
+ ctx.strokeStyle = 'rgb(0, 0, 0)'
532
+ ctx.lineWidth = 2
533
+ ctx.beginPath()
534
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
535
+ ctx.lineTo(...midPoint)
536
+ ctx.stroke()
537
+ ctx.moveTo(...midPoint)
538
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
539
+ ctx.stroke()
540
+ }
541
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
542
+ prevCDSTop = cdsTop + rowHeight / 2
543
+ counter += 1
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ function onMouseDown(
550
+ stateModel: LinearApolloSixFrameDisplay,
551
+ currentMousePosition: MousePositionWithFeatureAndGlyph,
552
+ event: CanvasMouseEvent,
553
+ ) {
554
+ const { featureAndGlyphUnderMouse } = currentMousePosition
555
+ // swallow the mouseDown if we are on the edge of the feature so that we
556
+ // don't start dragging the view if we try to drag the feature edge
557
+ const { cds, feature } = featureAndGlyphUnderMouse
558
+ const draggableFeature = getDraggableFeatureInfo(
559
+ currentMousePosition,
560
+ cds,
561
+ feature,
562
+ stateModel,
563
+ )
564
+ if (draggableFeature) {
565
+ event.stopPropagation()
566
+ stateModel.startDrag(
567
+ currentMousePosition,
568
+ draggableFeature.feature,
569
+ draggableFeature.edge,
570
+ true,
571
+ )
572
+ }
573
+ }
574
+
575
+ function onMouseMove(
576
+ stateModel: LinearApolloSixFrameDisplay,
577
+ mousePosition: MousePosition,
578
+ ) {
579
+ if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
580
+ const { featureAndGlyphUnderMouse } = mousePosition
581
+ stateModel.setApolloHover(featureAndGlyphUnderMouse)
582
+ const { cds, feature } = featureAndGlyphUnderMouse
583
+ const draggableFeature = getDraggableFeatureInfo(
584
+ mousePosition,
585
+ cds,
586
+ feature,
587
+ stateModel,
588
+ )
589
+ if (draggableFeature) {
590
+ stateModel.setCursor('col-resize')
591
+ return
592
+ }
593
+ }
594
+ stateModel.setCursor()
595
+ }
596
+
597
+ function onMouseUp(
598
+ stateModel: LinearApolloSixFrameDisplay,
599
+ mousePosition: MousePosition,
600
+ ) {
601
+ if (stateModel.apolloDragging) {
602
+ return
603
+ }
604
+ const { featureAndGlyphUnderMouse } = mousePosition
605
+ const { session } = stateModel
606
+ const { apolloDataStore } = session
607
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
608
+ if (!featureAndGlyphUnderMouse) {
609
+ return
610
+ }
611
+ const { feature } = featureAndGlyphUnderMouse
612
+ stateModel.setSelectedFeature(feature)
613
+ if (!featureTypeOntology) {
614
+ throw new Error('featureTypeOntology is undefined')
615
+ }
616
+
617
+ let containsCDSOrExon = false
618
+ for (const [, child] of feature.children ?? []) {
619
+ if (
620
+ featureTypeOntology.isTypeOf(child.type, 'CDS') ||
621
+ featureTypeOntology.isTypeOf(child.type, 'exon')
622
+ ) {
623
+ containsCDSOrExon = true
624
+ break
625
+ }
626
+ }
627
+ if (
628
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
629
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
630
+ containsCDSOrExon
631
+ ) {
632
+ stateModel.showFeatureDetailsWidget(feature, [
633
+ 'ApolloTranscriptDetails',
634
+ 'apolloTranscriptDetails',
635
+ ])
636
+ } else {
637
+ stateModel.showFeatureDetailsWidget(feature)
638
+ }
639
+ }
640
+
641
+ export function isSelectedFeature(
642
+ feature: AnnotationFeature,
643
+ selectedFeature: AnnotationFeature | undefined,
644
+ ) {
645
+ return Boolean(selectedFeature && feature._id === selectedFeature._id)
646
+ }
647
+
648
+ function getDraggableFeatureInfo(
649
+ mousePosition: MousePosition,
650
+ cds: TranscriptPartCoding | null,
651
+ feature: AnnotationFeature,
652
+ stateModel: LinearApolloSixFrameDisplay,
653
+ ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
654
+ const { filteredTranscripts, session } = stateModel
655
+ const { apolloDataStore } = session
656
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
657
+ if (!featureTypeOntology) {
658
+ throw new Error('featureTypeOntology is undefined')
659
+ }
660
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript')
661
+ if (cds === null) {
662
+ return
663
+ }
664
+ const featureID: string | undefined = feature.attributes
665
+ .get('gff_id')
666
+ ?.toString()
667
+ if (featureID && filteredTranscripts.includes(featureID)) {
668
+ return
669
+ }
670
+ const { bp, refName, regionNumber, x } = mousePosition
671
+ const { lgv } = stateModel
672
+ if (isTranscript) {
673
+ const transcript = feature
674
+ if (!transcript.children) {
675
+ return
676
+ }
677
+ const exonChildren: AnnotationFeature[] = []
678
+ const cdsChildren: AnnotationFeature[] = []
679
+ for (const child of transcript.children.values()) {
680
+ const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon')
681
+ const childIsCDS = featureTypeOntology.isTypeOf(child.type, 'CDS')
682
+ if (childIsExon) {
683
+ exonChildren.push(child)
684
+ } else if (childIsCDS) {
685
+ cdsChildren.push(child)
686
+ }
687
+ }
688
+ const overlappingExon = exonChildren.find((child) => {
689
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
690
+ return start !== undefined && end !== undefined
691
+ })
692
+ if (overlappingExon) {
693
+ // We are on an exon, are we on the edge of it?
694
+ const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
695
+ if (minMax) {
696
+ const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
697
+ if (overlappingEdge) {
698
+ return overlappingEdge
699
+ }
700
+ }
701
+ }
702
+ // End of special cases, let's see if we're on the edge of this CDS or exon
703
+ const minMax = getMinAndMaxPx(cds, refName, regionNumber, lgv)
704
+ if (minMax) {
705
+ const overlappingCDS = cdsChildren.find((child) => {
706
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
707
+ return start !== undefined && end !== undefined
708
+ })
709
+ if (overlappingCDS) {
710
+ const overlappingEdge = getOverlappingEdge(overlappingCDS, x, minMax)
711
+ if (overlappingEdge) {
712
+ return overlappingEdge
713
+ }
714
+ }
715
+ }
716
+ }
717
+ return
718
+ }
719
+
720
+ function drawTooltip(
721
+ display: LinearApolloSixFrameDisplayMouseEvents,
722
+ context: CanvasRenderingContext2D,
723
+ ): void {
724
+ const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } =
725
+ display
726
+ if (!apolloHover) {
727
+ return
728
+ }
729
+ const { cds, feature } = apolloHover
730
+ if (!cds) {
731
+ return
732
+ }
733
+ const position = display.getFeatureLayoutPosition(feature)
734
+ if (!position) {
735
+ return
736
+ }
737
+ const featureID: string | undefined = feature.attributes
738
+ .get('gff_id')
739
+ ?.toString()
740
+ if (featureID && filteredTranscripts.includes(featureID)) {
741
+ return
742
+ }
743
+ const { layoutIndex } = position
744
+ const { bpPerPx, displayedRegions, offsetPx } = lgv
745
+ const displayedRegion = displayedRegions[layoutIndex]
746
+ const { refName, reversed } = displayedRegion
747
+ const rowHeight = apolloRowHeight
748
+ const cdsHeight = Math.round(0.7 * rowHeight)
749
+ let location = 'Loc: '
750
+
751
+ const { strand } = feature
752
+ const { max, min, phase } = cds
753
+ location += `${min + 1}–${max}`
754
+
755
+ let startPx =
756
+ (lgv.bpToPx({
757
+ refName,
758
+ coord: reversed ? max : min,
759
+ regionNumber: layoutIndex,
760
+ })?.offsetPx ?? 0) - offsetPx
761
+ const frame = getFrame(min, max, strand ?? 1, phase)
762
+ const frameAdjust = frame < 0 ? -1 * frame + 5 : frame
763
+ const cdsTop = (frameAdjust - 1) * rowHeight + (rowHeight - cdsHeight) / 2
764
+ const cdsWidthPx = (max - min) / bpPerPx
765
+
766
+ const featureType = `Type: ${cds.type}`
767
+ const { attributes } = feature
768
+ const featureName = attributes.get('gff_name')?.find((name) => name !== '')
769
+ const textWidth = [
770
+ context.measureText(featureType).width,
771
+ context.measureText(location).width,
772
+ ]
773
+ if (featureName) {
774
+ textWidth.push(
775
+ context.measureText(`Parent Type: ${feature.type}`).width,
776
+ context.measureText(`Parent Name: ${featureName}`).width,
777
+ )
778
+ }
779
+ const maxWidth = Math.max(...textWidth)
780
+
781
+ startPx = startPx + cdsWidthPx + 5
782
+ context.fillStyle = alpha(theme?.palette.text.primary ?? 'rgb(1, 1, 1)', 0.7)
783
+ context.fillRect(
784
+ startPx,
785
+ cdsTop,
786
+ maxWidth + 4,
787
+ textWidth.length === 4 ? 55 : 35,
788
+ )
789
+ context.beginPath()
790
+ context.moveTo(startPx, cdsTop)
791
+ context.lineTo(startPx - 5, cdsTop + 5)
792
+ context.lineTo(startPx, cdsTop + 10)
793
+ context.fill()
794
+ context.fillStyle = theme?.palette.background.default ?? 'rgba(255, 255, 255)'
795
+ let textTop = cdsTop + 12
796
+ context.fillText(featureType, startPx + 2, textTop)
797
+ if (featureName) {
798
+ textTop = textTop + 12
799
+ context.fillText(`Parent Type: ${feature.type}`, startPx + 2, textTop)
800
+ textTop = textTop + 12
801
+ context.fillText(`Parent Name: ${featureName}`, startPx + 2, textTop)
802
+ }
803
+ textTop = textTop + 12
804
+ context.fillText(location, startPx + 2, textTop)
805
+ }
806
+
807
+ function getContextMenuItems(
808
+ display: LinearApolloSixFrameDisplayMouseEvents,
809
+ ): MenuItem[] {
810
+ const {
811
+ apolloHover,
812
+ apolloInternetAccount: internetAccount,
813
+ changeManager,
814
+ filteredTranscripts,
815
+ regions,
816
+ selectedFeature,
817
+ session,
818
+ } = display
819
+ const menuItems: MenuItem[] = []
820
+ if (!apolloHover) {
821
+ return menuItems
822
+ }
823
+ const { feature: sourceFeature } = apolloHover
824
+ const role = internetAccount ? internetAccount.role : 'admin'
825
+ const admin = role === 'admin'
826
+ const readOnly = !(role && ['admin', 'user'].includes(role))
827
+ const [region] = regions
828
+ const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
829
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
830
+ menuItems.push(
831
+ {
832
+ label: 'Add child feature',
833
+ disabled: readOnly,
834
+ onClick: () => {
835
+ ;(session as unknown as AbstractSessionModel).queueDialog(
836
+ (doneCallback) => [
837
+ AddChildFeature,
838
+ {
839
+ session,
840
+ handleClose: () => {
841
+ doneCallback()
842
+ },
843
+ changeManager,
844
+ sourceFeature,
845
+ sourceAssemblyId,
846
+ internetAccount,
847
+ },
848
+ ],
849
+ )
850
+ },
851
+ },
852
+ {
853
+ label: 'Copy features and annotations',
854
+ disabled: readOnly,
855
+ onClick: () => {
856
+ ;(session as unknown as AbstractSessionModel).queueDialog(
857
+ (doneCallback) => [
858
+ CopyFeature,
859
+ {
860
+ session,
861
+ handleClose: () => {
862
+ doneCallback()
863
+ },
864
+ changeManager,
865
+ sourceFeature,
866
+ sourceAssemblyId: currentAssemblyId,
867
+ },
868
+ ],
869
+ )
870
+ },
871
+ },
872
+ {
873
+ label: 'Delete feature',
874
+ disabled: !admin,
875
+ onClick: () => {
876
+ ;(session as unknown as AbstractSessionModel).queueDialog(
877
+ (doneCallback) => [
878
+ DeleteFeature,
879
+ {
880
+ session,
881
+ handleClose: () => {
882
+ doneCallback()
883
+ },
884
+ changeManager,
885
+ sourceFeature,
886
+ sourceAssemblyId: currentAssemblyId,
887
+ selectedFeature,
888
+ setSelectedFeature: (feature?: AnnotationFeature) => {
889
+ display.setSelectedFeature(feature)
890
+ },
891
+ },
892
+ ],
893
+ )
894
+ },
895
+ },
896
+ )
897
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
898
+ if (!featureTypeOntology) {
899
+ throw new Error('featureTypeOntology is undefined')
900
+ }
901
+ if (featureTypeOntology.isTypeOf(sourceFeature.type, 'gene')) {
902
+ menuItems.push({
903
+ label: 'Filter alternate transcripts',
904
+ onClick: () => {
905
+ ;(session as unknown as AbstractSessionModel).queueDialog(
906
+ (doneCallback) => [
907
+ FilterTranscripts,
908
+ {
909
+ handleClose: () => {
910
+ doneCallback()
911
+ },
912
+ sourceFeature,
913
+ filteredTranscripts: getSnapshot(filteredTranscripts),
914
+ onUpdate: (forms: string[]) => {
915
+ display.updateFilteredTranscripts(forms)
916
+ },
917
+ },
918
+ ],
919
+ )
920
+ },
921
+ })
922
+ }
923
+ return menuItems
924
+ }
925
+
926
+ function onMouseLeave(): void {
927
+ return
928
+ }
929
+
930
+ export const geneGlyph: Glyph = {
931
+ draw,
932
+ drawDragPreview,
933
+ drawHover,
934
+ drawTooltip,
935
+ getContextMenuItems,
936
+ onMouseDown,
937
+ onMouseLeave,
938
+ onMouseMove,
939
+ onMouseUp,
940
+ }