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

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 (86) hide show
  1. package/dist/index.esm.js +11212 -10483
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +11251 -10509
  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 +7726 -9014
  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 +18 -18
  12. package/src/ApolloInternetAccount/model.ts +123 -70
  13. package/src/ApolloRefNameAliasAdapter/ApolloRefNameAliasAdapter.ts +4 -4
  14. package/src/ApolloSequenceAdapter/ApolloSequenceAdapter.ts +9 -7
  15. package/src/BackendDrivers/CollaborationServerDriver.ts +72 -20
  16. package/src/BackendDrivers/DesktopFileDriver.ts +2 -2
  17. package/src/ChangeManager.ts +36 -14
  18. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  19. package/src/FeatureDetailsWidget/BasicInformation.tsx +6 -4
  20. package/src/FeatureDetailsWidget/NumberTextField.tsx +5 -2
  21. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  22. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +72 -234
  23. package/src/LinearApolloDisplay/components/CheckResultWarnings.tsx +92 -0
  24. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +23 -131
  25. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  26. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +279 -217
  27. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  28. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  29. package/src/LinearApolloDisplay/glyphs/util.ts +19 -0
  30. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  31. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  32. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  33. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  34. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  35. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  36. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  37. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceOverlay.ts +181 -0
  38. package/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts +218 -0
  39. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  40. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  41. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  42. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +157 -0
  43. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +101 -38
  44. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +334 -262
  45. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  46. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  47. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  48. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  49. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  50. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  51. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  52. package/src/components/AddAssembly.tsx +34 -38
  53. package/src/components/AddAssemblyAliases.tsx +1 -1
  54. package/src/components/AddChildFeature.tsx +5 -2
  55. package/src/components/AddFeature.tsx +30 -21
  56. package/src/components/AddRefSeqAliases.tsx +64 -50
  57. package/src/components/CopyFeature.tsx +4 -2
  58. package/src/components/CreateApolloAnnotation.tsx +22 -9
  59. package/src/components/DeleteAssembly.tsx +3 -10
  60. package/src/components/DownloadGFF3.tsx +2 -2
  61. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  62. package/src/components/FilterFeatures.tsx +7 -7
  63. package/src/components/FilterTranscripts.tsx +6 -6
  64. package/src/components/ImportFeatures.tsx +1 -1
  65. package/src/components/ManageChecks.tsx +3 -10
  66. package/src/components/ManageUsers.tsx +23 -22
  67. package/src/components/MergeTranscripts.tsx +12 -15
  68. package/src/components/OntologyTermAutocomplete.tsx +1 -8
  69. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  70. package/src/components/OpenLocalFile.tsx +11 -7
  71. package/src/components/ViewChangeLog.tsx +25 -50
  72. package/src/components/ViewCheckResults.tsx +2 -8
  73. package/src/components/index.ts +1 -0
  74. package/src/config.ts +6 -0
  75. package/src/index.ts +53 -115
  76. package/src/makeDisplayComponent.tsx +9 -14
  77. package/src/menus/index.ts +1 -0
  78. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  79. package/src/menus/topLevelMenuAdmin.ts +154 -0
  80. package/src/session/ClientDataStore.ts +32 -14
  81. package/src/session/session.ts +159 -121
  82. package/src/util/annotationFeatureUtils.ts +15 -21
  83. package/src/util/displayUtils.ts +149 -0
  84. package/src/util/glyphUtils.ts +329 -0
  85. package/src/util/loadAssemblyIntoClient.ts +3 -2
  86. package/src/util/mouseEventsUtils.ts +32 -0
@@ -2,27 +2,41 @@ import {
2
2
  type AnnotationFeature,
3
3
  type TranscriptPartCoding,
4
4
  } from '@apollo-annotation/mst'
5
+ import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
5
6
  import { type MenuItem } from '@jbrowse/core/ui'
6
7
  import {
7
8
  type AbstractSessionModel,
9
+ getContainingView,
8
10
  getFrame,
9
11
  intersection2,
10
12
  measureText,
11
13
  } from '@jbrowse/core/util'
14
+ import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
12
15
  import { alpha } from '@mui/material'
13
16
  import equal from 'fast-deep-equal/es6'
14
17
  import { getSnapshot } from 'mobx-state-tree'
15
18
 
16
- import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
19
+ import { MergeExons, SplitExon } from '../../components'
17
20
  import { FilterTranscripts } from '../../components/FilterTranscripts'
18
- import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
19
- import { type LinearApolloSixFrameDisplay } from '../stateModel'
20
21
  import {
21
- type LinearApolloSixFrameDisplayMouseEvents,
22
22
  type MousePosition,
23
- type MousePositionWithFeatureAndGlyph,
24
- isMousePositionWithFeatureAndGlyph,
25
- } from '../stateModel/mouseEvents'
23
+ type MousePositionWithFeature,
24
+ getAdjacentExons,
25
+ getContextMenuItemsForFeature,
26
+ getMinAndMaxPx,
27
+ getOverlappingEdge,
28
+ getRelatedFeatures,
29
+ getStreamIcon,
30
+ isCDSFeature,
31
+ isExonFeature,
32
+ isMousePositionWithFeature,
33
+ // isTranscriptFeature,
34
+ isSelectedFeature,
35
+ navToFeatureCenter,
36
+ selectFeatureAndOpenWidget,
37
+ } from '../../util'
38
+ import { type LinearApolloSixFrameDisplay } from '../stateModel'
39
+ import { type LinearApolloSixFrameDisplayMouseEvents } from '../stateModel/mouseEvents'
26
40
  import { type LinearApolloSixFrameDisplayRendering } from '../stateModel/rendering'
27
41
  import { type CanvasMouseEvent } from '../types'
28
42
 
@@ -105,14 +119,14 @@ function drawTextLabels(
105
119
  for (let i = labelArray.length - 1; i >= 0; --i) {
106
120
  const label = labelArray[i]
107
121
  ctx.fillStyle = label.color
108
- const labelRowX = Math.max(label.x + 1, 0)
122
+ const labelRowX = label.x + 1
109
123
  const labelRowY = label.y + label.h
110
124
  const textWidth = measureText(label.text, 10)
111
125
  if (label.isSelected) {
112
- ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
113
126
  ctx.font = 'bold '.concat(font)
114
127
  }
115
128
  if (label.text) {
129
+ ctx.clearRect(labelRowX - 5, labelRowY, textWidth + 10, label.h)
116
130
  ctx.fillText(label.text, labelRowX, labelRowY + 11, textWidth)
117
131
  ctx.font = font
118
132
  }
@@ -133,6 +147,7 @@ function draw(
133
147
  theme,
134
148
  highestRow,
135
149
  filteredTranscripts,
150
+ selectedFeature,
136
151
  showFeatureLabels,
137
152
  } = stateModel
138
153
  const { bpPerPx, displayedRegions, offsetPx } = lgv
@@ -143,12 +158,11 @@ function draw(
143
158
  const cdsHeight = rowHeight
144
159
  const topLevelFeatureHeight = rowHeight
145
160
  const featureLabelSpacer = showFeatureLabels ? 2 : 1
146
- const textColor = theme?.palette.text.primary ?? 'black'
161
+ const textColor = theme.palette.text.primary
147
162
  const { attributes, children, min, strand } = topLevelFeature
148
163
  if (!children) {
149
164
  return
150
165
  }
151
- const { apolloSelectedFeature } = session
152
166
  const { apolloDataStore } = session
153
167
  const { featureTypeOntology } = apolloDataStore.ontologyManager
154
168
  if (!featureTypeOntology) {
@@ -169,7 +183,7 @@ function draw(
169
183
  : topLevelFeatureMinX
170
184
  const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer
171
185
  const topLevelFeatureTop = topLevelRow * rowHeight
172
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
186
+ ctx.fillStyle = theme.palette.text.primary
173
187
  ctx.fillRect(
174
188
  topLevelFeatureStartPx,
175
189
  topLevelFeatureTop,
@@ -177,9 +191,9 @@ function draw(
177
191
  topLevelFeatureHeight,
178
192
  )
179
193
 
180
- ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
194
+ ctx.fillStyle = isSelectedFeature(topLevelFeature, selectedFeature)
181
195
  ? alpha('rgb(0,0,0)', 0.7)
182
- : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
196
+ : alpha(theme.palette.background.paper, 0.7)
183
197
  ctx.fillRect(
184
198
  topLevelFeatureStartPx + 1,
185
199
  topLevelFeatureTop + 1,
@@ -187,7 +201,7 @@ function draw(
187
201
  topLevelFeatureHeight - 2,
188
202
  )
189
203
 
190
- const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
204
+ const isSelected = isSelectedFeature(topLevelFeature, selectedFeature)
191
205
  const label: Label = {
192
206
  x: topLevelFeatureStartPx,
193
207
  y: topLevelFeatureTop,
@@ -203,9 +217,9 @@ function draw(
203
217
  }
204
218
 
205
219
  const forwardFill =
206
- theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
220
+ theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
207
221
  const backwardFill =
208
- theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
222
+ theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
209
223
  const reversal = reversed ? -1 : 1
210
224
  let topFill: CanvasPattern | null = null,
211
225
  bottomFill: CanvasPattern | null = null
@@ -269,8 +283,8 @@ function draw(
269
283
 
270
284
  const exonTop =
271
285
  topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2
272
- const isSelected = isSelectedFeature(exon, apolloSelectedFeature)
273
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
286
+ const isSelected = isSelectedFeature(exon, selectedFeature)
287
+ ctx.fillStyle = theme.palette.text.primary
274
288
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
275
289
  if (widthPx > 2) {
276
290
  ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
@@ -309,7 +323,7 @@ function draw(
309
323
  }
310
324
  }
311
325
 
312
- const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent)
326
+ const isSelected = isSelectedFeature(child, selectedFeature?.parent)
313
327
  let cdsStartPx = 0
314
328
  let cdsTop = 0
315
329
  for (const cdsRow of cdsLocations) {
@@ -318,9 +332,9 @@ function draw(
318
332
  let counter = 1
319
333
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
320
334
  if (
321
- (apolloSelectedFeature &&
335
+ (selectedFeature &&
322
336
  isSelected &&
323
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
337
+ featureTypeOntology.isTypeOf(selectedFeature.type, 'CDS')) ||
324
338
  !deepSetHas(renderedCDS, cds)
325
339
  ) {
326
340
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
@@ -331,12 +345,36 @@ function draw(
331
345
  regionNumber: displayedRegionIndex,
332
346
  })?.offsetPx ?? 0) - offsetPx
333
347
  cdsStartPx = reversed ? minX - cdsWidthPx : minX
334
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
348
+ ctx.fillStyle = theme.palette.text.primary
335
349
  const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase)
336
350
  const frameAdjust =
337
351
  (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
338
352
  cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
339
353
  ctx.fillRect(cdsStartPx, cdsTop, cdsWidthPx, cdsHeight)
354
+
355
+ // Draw lines to connect CDS features with shared mRNA parent
356
+ if (counter > 1) {
357
+ // Mid-point for intron line "hat"
358
+ const midPoint: [number, number] = [
359
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
360
+ Math.max(
361
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
362
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
363
+ ),
364
+ ]
365
+ ctx.strokeStyle = 'rgb(0, 128, 128)'
366
+ ctx.beginPath()
367
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
368
+ ctx.lineTo(...midPoint)
369
+ ctx.stroke()
370
+ ctx.moveTo(...midPoint)
371
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
372
+ ctx.stroke()
373
+ }
374
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
375
+ prevCDSTop = cdsTop + rowHeight / 2
376
+ counter += 1
377
+
340
378
  if (cdsWidthPx > 2) {
341
379
  ctx.clearRect(
342
380
  cdsStartPx + 1,
@@ -345,13 +383,13 @@ function draw(
345
383
  cdsHeight - 2,
346
384
  )
347
385
 
348
- const frameColor = theme?.palette.framesCDS.at(frame)?.main
386
+ const frameColor = theme.palette.framesCDS.at(frame)?.main
349
387
  const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
350
388
  ctx.fillStyle = cdsColorCode
351
389
  ctx.fillStyle =
352
- apolloSelectedFeature &&
390
+ selectedFeature &&
353
391
  isSelected &&
354
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
392
+ featureTypeOntology.isTypeOf(selectedFeature.type, 'CDS')
355
393
  ? 'rgb(0,0,0)'
356
394
  : cdsColorCode
357
395
  ctx.fillRect(
@@ -361,31 +399,6 @@ function draw(
361
399
  cdsHeight - 2,
362
400
  )
363
401
 
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
402
  if (topFill && bottomFill) {
390
403
  ctx.fillStyle = topFill
391
404
  ctx.fillRect(
@@ -448,10 +461,10 @@ function drawDragPreview(
448
461
  const rectY = row * apolloRowHeight
449
462
  const rectWidth = Math.abs(current.x - featureEdgePx)
450
463
  const rectHeight = apolloRowHeight * rowCount
451
- overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
464
+ overlayCtx.strokeStyle = theme.palette.info.main
452
465
  overlayCtx.setLineDash([6])
453
466
  overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
454
- overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
467
+ overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
455
468
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
456
469
  }
457
470
 
@@ -460,7 +473,7 @@ function drawHover(
460
473
  ctx: CanvasRenderingContext2D,
461
474
  ) {
462
475
  const {
463
- apolloHover,
476
+ hoveredFeature,
464
477
  apolloRowHeight,
465
478
  filteredTranscripts,
466
479
  lgv,
@@ -468,15 +481,15 @@ function drawHover(
468
481
  session,
469
482
  showFeatureLabels,
470
483
  } = stateModel
471
- if (!apolloHover) {
484
+ if (!hoveredFeature) {
472
485
  return
473
486
  }
487
+ const { feature } = hoveredFeature
474
488
  const { apolloDataStore } = session
475
489
  const { featureTypeOntology } = apolloDataStore.ontologyManager
476
490
  if (!featureTypeOntology) {
477
491
  throw new Error('featureTypeOntology is undefined')
478
492
  }
479
- const { feature } = apolloHover
480
493
  if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
481
494
  return
482
495
  }
@@ -504,43 +517,42 @@ function drawHover(
504
517
  let counter = 1
505
518
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
506
519
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
520
+ const minX =
521
+ (lgv.bpToPx({
522
+ refName,
523
+ coord: cds.min,
524
+ regionNumber: layoutIndex,
525
+ })?.offsetPx ?? 0) - offsetPx
526
+ const cdsStartPx = reversed ? minX - cdsWidthPx : minX
527
+ const frame = getFrame(cds.min, cds.max, strand ?? 1, cds.phase)
528
+ const frameAdjust =
529
+ (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
530
+ const cdsTop = (frameAdjust - featureLabelSpacer) * rowHeight
531
+ if (counter > 1) {
532
+ // Mid-point for intron line "hat"
533
+ const midPoint: [number, number] = [
534
+ (cdsStartPx - prevCDSEndPx) / 2 + prevCDSEndPx,
535
+ Math.max(
536
+ frame < 0 ? rowHeight * featureLabelSpacer * highestRow + 1 : 1, // Avoid render ceiling
537
+ Math.min(prevCDSTop, cdsTop) - rowHeight / 2,
538
+ ),
539
+ ]
540
+ ctx.strokeStyle = 'rgb(0, 0, 0)'
541
+ ctx.lineWidth = 2
542
+ ctx.beginPath()
543
+ ctx.moveTo(prevCDSEndPx, prevCDSTop)
544
+ ctx.lineTo(...midPoint)
545
+ ctx.stroke()
546
+ ctx.moveTo(...midPoint)
547
+ ctx.lineTo(cdsStartPx, cdsTop + rowHeight / 2)
548
+ ctx.stroke()
549
+ }
550
+ prevCDSEndPx = cdsStartPx + cdsWidthPx
551
+ prevCDSTop = cdsTop + rowHeight / 2
552
+ counter += 1
507
553
  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
554
  ctx.fillStyle = 'rgba(255,0,0,0.6)'
520
555
  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
556
  }
545
557
  }
546
558
  }
@@ -548,16 +560,14 @@ function drawHover(
548
560
 
549
561
  function onMouseDown(
550
562
  stateModel: LinearApolloSixFrameDisplay,
551
- currentMousePosition: MousePositionWithFeatureAndGlyph,
563
+ currentMousePosition: MousePositionWithFeature,
552
564
  event: CanvasMouseEvent,
553
565
  ) {
554
- const { featureAndGlyphUnderMouse } = currentMousePosition
566
+ const { feature } = currentMousePosition
555
567
  // swallow the mouseDown if we are on the edge of the feature so that we
556
568
  // don't start dragging the view if we try to drag the feature edge
557
- const { cds, feature } = featureAndGlyphUnderMouse
558
569
  const draggableFeature = getDraggableFeatureInfo(
559
570
  currentMousePosition,
560
- cds,
561
571
  feature,
562
572
  stateModel,
563
573
  )
@@ -576,13 +586,11 @@ function onMouseMove(
576
586
  stateModel: LinearApolloSixFrameDisplay,
577
587
  mousePosition: MousePosition,
578
588
  ) {
579
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
580
- const { featureAndGlyphUnderMouse } = mousePosition
581
- stateModel.setApolloHover(featureAndGlyphUnderMouse)
582
- const { cds, feature } = featureAndGlyphUnderMouse
589
+ if (isMousePositionWithFeature(mousePosition)) {
590
+ const { feature, bp } = mousePosition
591
+ stateModel.setHoveredFeature({ feature, bp })
583
592
  const draggableFeature = getDraggableFeatureInfo(
584
593
  mousePosition,
585
- cds,
586
594
  feature,
587
595
  stateModel,
588
596
  )
@@ -601,53 +609,43 @@ function onMouseUp(
601
609
  if (stateModel.apolloDragging) {
602
610
  return
603
611
  }
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
- }
612
+ if (isMousePositionWithFeature(mousePosition)) {
613
+ const { feature } = mousePosition
614
+ const { session } = stateModel
615
+ const { apolloDataStore } = session
616
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
617
+ stateModel.setSelectedFeature(feature)
618
+ if (!featureTypeOntology) {
619
+ throw new Error('featureTypeOntology is undefined')
620
+ }
616
621
 
617
- let containsCDSOrExon = false
618
- for (const [, child] of feature.children ?? []) {
622
+ let containsCDSOrExon = false
623
+ for (const [, child] of feature.children ?? []) {
624
+ if (
625
+ featureTypeOntology.isTypeOf(child.type, 'CDS') ||
626
+ featureTypeOntology.isTypeOf(child.type, 'exon')
627
+ ) {
628
+ containsCDSOrExon = true
629
+ break
630
+ }
631
+ }
619
632
  if (
620
- featureTypeOntology.isTypeOf(child.type, 'CDS') ||
621
- featureTypeOntology.isTypeOf(child.type, 'exon')
633
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
634
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
635
+ containsCDSOrExon
622
636
  ) {
623
- containsCDSOrExon = true
624
- break
637
+ stateModel.showFeatureDetailsWidget(feature, [
638
+ 'ApolloTranscriptDetails',
639
+ 'apolloTranscriptDetails',
640
+ ])
641
+ } else {
642
+ stateModel.showFeatureDetailsWidget(feature)
625
643
  }
626
644
  }
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
645
  }
647
646
 
648
647
  function getDraggableFeatureInfo(
649
648
  mousePosition: MousePosition,
650
- cds: TranscriptPartCoding | null,
651
649
  feature: AnnotationFeature,
652
650
  stateModel: LinearApolloSixFrameDisplay,
653
651
  ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
@@ -658,9 +656,6 @@ function getDraggableFeatureInfo(
658
656
  throw new Error('featureTypeOntology is undefined')
659
657
  }
660
658
  const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript')
661
- if (cds === null) {
662
- return
663
- }
664
659
  const featureID: string | undefined = feature.attributes
665
660
  .get('gff_id')
666
661
  ?.toString()
@@ -700,16 +695,24 @@ function getDraggableFeatureInfo(
700
695
  }
701
696
  }
702
697
  // 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
698
+ for (const loc of transcript.cdsLocations) {
699
+ for (const cds of loc) {
700
+ const minMax = getMinAndMaxPx(cds, refName, regionNumber, lgv)
701
+ if (minMax) {
702
+ const overlappingCDS = cdsChildren.find((child) => {
703
+ const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
704
+ return start !== undefined && end !== undefined
705
+ })
706
+ if (overlappingCDS) {
707
+ const overlappingEdge = getOverlappingEdge(
708
+ overlappingCDS,
709
+ x,
710
+ minMax,
711
+ )
712
+ if (overlappingEdge) {
713
+ return overlappingEdge
714
+ }
715
+ }
713
716
  }
714
717
  }
715
718
  }
@@ -721,22 +724,33 @@ function drawTooltip(
721
724
  display: LinearApolloSixFrameDisplayMouseEvents,
722
725
  context: CanvasRenderingContext2D,
723
726
  ): void {
724
- const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } =
725
- display
726
- if (!apolloHover) {
727
+ const {
728
+ hoveredFeature,
729
+ apolloRowHeight,
730
+ filteredTranscripts,
731
+ lgv,
732
+ session,
733
+ showFeatureLabels,
734
+ theme,
735
+ } = display
736
+ if (!hoveredFeature) {
727
737
  return
728
738
  }
729
- const { cds, feature } = apolloHover
730
- if (!cds) {
739
+ const { feature, bp } = hoveredFeature
740
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
741
+ if (!featureTypeOntology) {
742
+ throw new Error('featureTypeOntology is undefined')
743
+ }
744
+ const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript')
745
+ if (!isTranscript) {
731
746
  return
732
747
  }
748
+ const { attributes, strand, type } = feature
733
749
  const position = display.getFeatureLayoutPosition(feature)
734
750
  if (!position) {
735
751
  return
736
752
  }
737
- const featureID: string | undefined = feature.attributes
738
- .get('gff_id')
739
- ?.toString()
753
+ const featureID: string | undefined = attributes.get('gff_id')?.toString()
740
754
  if (featureID && filteredTranscripts.includes(featureID)) {
741
755
  return
742
756
  }
@@ -746,9 +760,20 @@ function drawTooltip(
746
760
  const { refName, reversed } = displayedRegion
747
761
  const rowHeight = apolloRowHeight
748
762
  const cdsHeight = Math.round(0.7 * rowHeight)
763
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
749
764
  let location = 'Loc: '
750
-
751
- const { strand } = feature
765
+ let cds: TranscriptPartCoding | undefined = undefined
766
+ for (const loc of feature.cdsLocations) {
767
+ for (const cdsLoc of loc) {
768
+ if (bp >= cdsLoc.min && bp <= cdsLoc.max) {
769
+ cds = cdsLoc
770
+ break
771
+ }
772
+ }
773
+ }
774
+ if (!cds) {
775
+ return
776
+ }
752
777
  const { max, min, phase } = cds
753
778
  location += `${min + 1}–${max}`
754
779
 
@@ -759,12 +784,12 @@ function drawTooltip(
759
784
  regionNumber: layoutIndex,
760
785
  })?.offsetPx ?? 0) - offsetPx
761
786
  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
787
+ const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
788
+ const cdsTop =
789
+ (frameAdjust - featureLabelSpacer) * rowHeight + (rowHeight - cdsHeight) / 2
764
790
  const cdsWidthPx = (max - min) / bpPerPx
765
791
 
766
792
  const featureType = `Type: ${cds.type}`
767
- const { attributes } = feature
768
793
  const featureName = attributes.get('gff_name')?.find((name) => name !== '')
769
794
  const textWidth = [
770
795
  context.measureText(featureType).width,
@@ -772,14 +797,14 @@ function drawTooltip(
772
797
  ]
773
798
  if (featureName) {
774
799
  textWidth.push(
775
- context.measureText(`Parent Type: ${feature.type}`).width,
800
+ context.measureText(`Parent Type: ${type}`).width,
776
801
  context.measureText(`Parent Name: ${featureName}`).width,
777
802
  )
778
803
  }
779
804
  const maxWidth = Math.max(...textWidth)
780
805
 
781
806
  startPx = startPx + cdsWidthPx + 5
782
- context.fillStyle = alpha(theme?.palette.text.primary ?? 'rgb(1, 1, 1)', 0.7)
807
+ context.fillStyle = alpha(theme.palette.text.primary, 0.7)
783
808
  context.fillRect(
784
809
  startPx,
785
810
  cdsTop,
@@ -791,12 +816,12 @@ function drawTooltip(
791
816
  context.lineTo(startPx - 5, cdsTop + 5)
792
817
  context.lineTo(startPx, cdsTop + 10)
793
818
  context.fill()
794
- context.fillStyle = theme?.palette.background.default ?? 'rgba(255, 255, 255)'
819
+ context.fillStyle = theme.palette.background.default
795
820
  let textTop = cdsTop + 12
796
821
  context.fillText(featureType, startPx + 2, textTop)
797
822
  if (featureName) {
798
823
  textTop = textTop + 12
799
- context.fillText(`Parent Type: ${feature.type}`, startPx + 2, textTop)
824
+ context.fillText(`Parent Type: ${type}`, startPx + 2, textTop)
800
825
  textTop = textTop + 12
801
826
  context.fillText(`Parent Name: ${featureName}`, startPx + 2, textTop)
802
827
  }
@@ -806,119 +831,165 @@ function drawTooltip(
806
831
 
807
832
  function getContextMenuItems(
808
833
  display: LinearApolloSixFrameDisplayMouseEvents,
834
+ mousePosition: MousePositionWithFeature,
809
835
  ): MenuItem[] {
810
836
  const {
811
- apolloHover,
812
837
  apolloInternetAccount: internetAccount,
838
+ hoveredFeature,
813
839
  changeManager,
814
840
  filteredTranscripts,
815
841
  regions,
816
842
  selectedFeature,
817
843
  session,
818
844
  } = display
845
+ const [region] = regions
846
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
819
847
  const menuItems: MenuItem[] = []
820
- if (!apolloHover) {
821
- return menuItems
822
- }
823
- const { feature: sourceFeature } = apolloHover
824
848
  const role = internetAccount ? internetAccount.role : 'admin'
825
849
  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
- )
850
+ if (!hoveredFeature) {
851
+ return menuItems
852
+ }
897
853
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
898
854
  if (!featureTypeOntology) {
899
855
  throw new Error('featureTypeOntology is undefined')
900
856
  }
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
- },
857
+ if (isMousePositionWithFeature(mousePosition)) {
858
+ const { bp, feature } = mousePosition
859
+ let featuresUnderClick = getRelatedFeatures(feature, bp)
860
+ if (isCDSFeature(feature, session)) {
861
+ featuresUnderClick = getRelatedFeatures(feature, bp, true)
862
+ }
863
+
864
+ for (const feature of featuresUnderClick) {
865
+ const featureID: string | undefined = feature.attributes
866
+ .get('gff_id')
867
+ ?.toString()
868
+ if (featureID && filteredTranscripts.includes(featureID)) {
869
+ continue
870
+ }
871
+ const contextMenuItemsForFeature = getContextMenuItemsForFeature(
872
+ display,
873
+ feature,
874
+ )
875
+ if (isExonFeature(feature, session)) {
876
+ const adjacentExons = getAdjacentExons(
877
+ feature,
878
+ display,
879
+ mousePosition,
880
+ session,
881
+ )
882
+ const lgv = getContainingView(
883
+ display as BaseDisplayModel,
884
+ ) as unknown as LinearGenomeViewModel
885
+ if (adjacentExons.upstream) {
886
+ const exon = adjacentExons.upstream
887
+ contextMenuItemsForFeature.push({
888
+ label: 'Go to upstream exon',
889
+ icon: getStreamIcon(
890
+ feature.strand,
891
+ true,
892
+ lgv.displayedRegions.at(0)?.reversed,
893
+ ),
894
+ onClick: () => {
895
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
896
+ selectFeatureAndOpenWidget(display, exon)
897
+ },
898
+ })
899
+ }
900
+ if (adjacentExons.downstream) {
901
+ const exon = adjacentExons.downstream
902
+ contextMenuItemsForFeature.push({
903
+ label: 'Go to downstream exon',
904
+ icon: getStreamIcon(
905
+ feature.strand,
906
+ false,
907
+ lgv.displayedRegions.at(0)?.reversed,
908
+ ),
909
+ onClick: () => {
910
+ lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
911
+ selectFeatureAndOpenWidget(display, exon)
912
+ },
913
+ })
914
+ }
915
+ contextMenuItemsForFeature.push(
916
+ {
917
+ label: 'Merge exons',
918
+ disabled: !admin,
919
+ onClick: () => {
920
+ ;(session as unknown as AbstractSessionModel).queueDialog(
921
+ (doneCallback) => [
922
+ MergeExons,
923
+ {
924
+ session,
925
+ handleClose: () => {
926
+ doneCallback()
927
+ },
928
+ changeManager,
929
+ sourceFeature: feature,
930
+ sourceAssemblyId: currentAssemblyId,
931
+ selectedFeature,
932
+ setSelectedFeature: (feature?: AnnotationFeature) => {
933
+ display.setSelectedFeature(feature)
934
+ },
935
+ },
936
+ ],
937
+ )
938
+ },
939
+ },
940
+ {
941
+ label: 'Split exon',
942
+ disabled: !admin,
943
+ onClick: () => {
944
+ ;(session as unknown as AbstractSessionModel).queueDialog(
945
+ (doneCallback) => [
946
+ SplitExon,
947
+ {
948
+ session,
949
+ handleClose: () => {
950
+ doneCallback()
951
+ },
952
+ changeManager,
953
+ sourceFeature: feature,
954
+ sourceAssemblyId: currentAssemblyId,
955
+ selectedFeature,
956
+ setSelectedFeature: (feature?: AnnotationFeature) => {
957
+ display.setSelectedFeature(feature)
958
+ },
959
+ },
960
+ ],
961
+ )
917
962
  },
918
- ],
963
+ },
919
964
  )
920
- },
921
- })
965
+ }
966
+ if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
967
+ contextMenuItemsForFeature.push({
968
+ label: 'Filter alternate transcripts',
969
+ onClick: () => {
970
+ ;(session as unknown as AbstractSessionModel).queueDialog(
971
+ (doneCallback) => [
972
+ FilterTranscripts,
973
+ {
974
+ handleClose: () => {
975
+ doneCallback()
976
+ },
977
+ sourceFeature: feature,
978
+ filteredTranscripts: getSnapshot(filteredTranscripts),
979
+ onUpdate: (forms: string[]) => {
980
+ display.updateFilteredTranscripts(forms)
981
+ },
982
+ },
983
+ ],
984
+ )
985
+ },
986
+ })
987
+ }
988
+ menuItems.push({
989
+ label: feature.type,
990
+ subMenu: contextMenuItemsForFeature,
991
+ })
992
+ }
922
993
  }
923
994
  return menuItems
924
995
  }
@@ -933,6 +1004,7 @@ export const geneGlyph: Glyph = {
933
1004
  drawHover,
934
1005
  drawTooltip,
935
1006
  getContextMenuItems,
1007
+ getContextMenuItemsForFeature,
936
1008
  onMouseDown,
937
1009
  onMouseLeave,
938
1010
  onMouseMove,