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