@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 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 (71) hide show
  1. package/dist/index.esm.js +2371 -1642
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2384 -1641
  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 +4387 -2952
  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 +33 -13
  15. package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
  16. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
  17. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +33 -31
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +60 -72
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +441 -180
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
  23. package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
  24. package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
  25. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
  26. package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
  27. package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
  28. package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
  29. package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
  30. package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
  31. package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
  32. package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
  33. package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
  34. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +95 -38
  35. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +221 -201
  36. package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
  37. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
  38. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
  39. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
  40. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
  41. package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
  42. package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
  43. package/src/components/AddAssembly.tsx +1 -1
  44. package/src/components/AddAssemblyAliases.tsx +1 -1
  45. package/src/components/AddChildFeature.tsx +5 -2
  46. package/src/components/AddFeature.tsx +9 -3
  47. package/src/components/AddRefSeqAliases.tsx +9 -9
  48. package/src/components/CopyFeature.tsx +3 -1
  49. package/src/components/CreateApolloAnnotation.tsx +1 -0
  50. package/src/components/DeleteAssembly.tsx +1 -1
  51. package/src/components/EditZoomThresholdDialog.tsx +69 -0
  52. package/src/components/FilterFeatures.tsx +7 -7
  53. package/src/components/FilterTranscripts.tsx +6 -6
  54. package/src/components/ImportFeatures.tsx +1 -1
  55. package/src/components/ManageChecks.tsx +1 -1
  56. package/src/components/MergeTranscripts.tsx +12 -15
  57. package/src/components/OntologyTermMultiSelect.tsx +11 -11
  58. package/src/components/OpenLocalFile.tsx +11 -7
  59. package/src/components/ViewCheckResults.tsx +1 -1
  60. package/src/components/index.ts +1 -0
  61. package/src/config.ts +6 -0
  62. package/src/index.ts +42 -105
  63. package/src/makeDisplayComponent.tsx +0 -1
  64. package/src/menus/index.ts +1 -0
  65. package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
  66. package/src/menus/topLevelMenuAdmin.ts +154 -0
  67. package/src/session/session.ts +162 -116
  68. package/src/util/annotationFeatureUtils.ts +15 -21
  69. package/src/util/displayUtils.ts +149 -0
  70. package/src/util/glyphUtils.ts +152 -0
  71. package/src/util/mouseEventsUtils.ts +32 -0
@@ -13,16 +13,20 @@ import { alpha } from '@mui/material'
13
13
  import equal from 'fast-deep-equal/es6'
14
14
  import { getSnapshot } from 'mobx-state-tree'
15
15
 
16
- import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
16
+ import { MergeExons, SplitExon } from '../../components'
17
17
  import { FilterTranscripts } from '../../components/FilterTranscripts'
18
- import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
19
- import { type LinearApolloSixFrameDisplay } from '../stateModel'
20
18
  import {
21
- type LinearApolloSixFrameDisplayMouseEvents,
22
19
  type MousePosition,
23
- type MousePositionWithFeatureAndGlyph,
24
- isMousePositionWithFeatureAndGlyph,
25
- } 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'
26
30
  import { type LinearApolloSixFrameDisplayRendering } from '../stateModel/rendering'
27
31
  import { type CanvasMouseEvent } from '../types'
28
32
 
@@ -133,6 +137,7 @@ function draw(
133
137
  theme,
134
138
  highestRow,
135
139
  filteredTranscripts,
140
+ selectedFeature,
136
141
  showFeatureLabels,
137
142
  } = stateModel
138
143
  const { bpPerPx, displayedRegions, offsetPx } = lgv
@@ -143,12 +148,11 @@ function draw(
143
148
  const cdsHeight = rowHeight
144
149
  const topLevelFeatureHeight = rowHeight
145
150
  const featureLabelSpacer = showFeatureLabels ? 2 : 1
146
- const textColor = theme?.palette.text.primary ?? 'black'
151
+ const textColor = theme.palette.text.primary
147
152
  const { attributes, children, min, strand } = topLevelFeature
148
153
  if (!children) {
149
154
  return
150
155
  }
151
- const { apolloSelectedFeature } = session
152
156
  const { apolloDataStore } = session
153
157
  const { featureTypeOntology } = apolloDataStore.ontologyManager
154
158
  if (!featureTypeOntology) {
@@ -169,7 +173,7 @@ function draw(
169
173
  : topLevelFeatureMinX
170
174
  const topLevelRow = (strand == 1 ? 3 : 4) * featureLabelSpacer
171
175
  const topLevelFeatureTop = topLevelRow * rowHeight
172
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
176
+ ctx.fillStyle = theme.palette.text.primary
173
177
  ctx.fillRect(
174
178
  topLevelFeatureStartPx,
175
179
  topLevelFeatureTop,
@@ -177,9 +181,9 @@ function draw(
177
181
  topLevelFeatureHeight,
178
182
  )
179
183
 
180
- ctx.fillStyle = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
184
+ ctx.fillStyle = isSelectedFeature(topLevelFeature, selectedFeature)
181
185
  ? alpha('rgb(0,0,0)', 0.7)
182
- : alpha(theme?.palette.background.paper ?? '#ffffff', 0.7)
186
+ : alpha(theme.palette.background.paper, 0.7)
183
187
  ctx.fillRect(
184
188
  topLevelFeatureStartPx + 1,
185
189
  topLevelFeatureTop + 1,
@@ -187,7 +191,7 @@ function draw(
187
191
  topLevelFeatureHeight - 2,
188
192
  )
189
193
 
190
- const isSelected = isSelectedFeature(topLevelFeature, apolloSelectedFeature)
194
+ const isSelected = isSelectedFeature(topLevelFeature, selectedFeature)
191
195
  const label: Label = {
192
196
  x: topLevelFeatureStartPx,
193
197
  y: topLevelFeatureTop,
@@ -203,9 +207,9 @@ function draw(
203
207
  }
204
208
 
205
209
  const forwardFill =
206
- theme?.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
210
+ theme.palette.mode === 'dark' ? forwardFillDark : forwardFillLight
207
211
  const backwardFill =
208
- theme?.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
212
+ theme.palette.mode === 'dark' ? backwardFillDark : backwardFillLight
209
213
  const reversal = reversed ? -1 : 1
210
214
  let topFill: CanvasPattern | null = null,
211
215
  bottomFill: CanvasPattern | null = null
@@ -269,8 +273,8 @@ function draw(
269
273
 
270
274
  const exonTop =
271
275
  topLevelFeatureTop + (topLevelFeatureHeight - exonHeight) / 2
272
- const isSelected = isSelectedFeature(exon, apolloSelectedFeature)
273
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
276
+ const isSelected = isSelectedFeature(exon, selectedFeature)
277
+ ctx.fillStyle = theme.palette.text.primary
274
278
  ctx.fillRect(startPx, exonTop, widthPx, exonHeight)
275
279
  if (widthPx > 2) {
276
280
  ctx.clearRect(startPx + 1, exonTop + 1, widthPx - 2, exonHeight - 2)
@@ -309,7 +313,7 @@ function draw(
309
313
  }
310
314
  }
311
315
 
312
- const isSelected = isSelectedFeature(child, apolloSelectedFeature?.parent)
316
+ const isSelected = isSelectedFeature(child, selectedFeature?.parent)
313
317
  let cdsStartPx = 0
314
318
  let cdsTop = 0
315
319
  for (const cdsRow of cdsLocations) {
@@ -318,9 +322,9 @@ function draw(
318
322
  let counter = 1
319
323
  for (const cds of cdsRow.sort((a, b) => a.max - b.max)) {
320
324
  if (
321
- (apolloSelectedFeature &&
325
+ (selectedFeature &&
322
326
  isSelected &&
323
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')) ||
327
+ featureTypeOntology.isTypeOf(selectedFeature.type, 'CDS')) ||
324
328
  !deepSetHas(renderedCDS, cds)
325
329
  ) {
326
330
  const cdsWidthPx = (cds.max - cds.min) / bpPerPx
@@ -331,7 +335,7 @@ function draw(
331
335
  regionNumber: displayedRegionIndex,
332
336
  })?.offsetPx ?? 0) - offsetPx
333
337
  cdsStartPx = reversed ? minX - cdsWidthPx : minX
334
- ctx.fillStyle = theme?.palette.text.primary ?? 'black'
338
+ ctx.fillStyle = theme.palette.text.primary
335
339
  const frame = getFrame(cds.min, cds.max, child.strand ?? 1, cds.phase)
336
340
  const frameAdjust =
337
341
  (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
@@ -345,13 +349,13 @@ function draw(
345
349
  cdsHeight - 2,
346
350
  )
347
351
 
348
- const frameColor = theme?.palette.framesCDS.at(frame)?.main
352
+ const frameColor = theme.palette.framesCDS.at(frame)?.main
349
353
  const cdsColorCode = frameColor ?? 'rgb(171,71,188)'
350
354
  ctx.fillStyle = cdsColorCode
351
355
  ctx.fillStyle =
352
- apolloSelectedFeature &&
356
+ selectedFeature &&
353
357
  isSelected &&
354
- featureTypeOntology.isTypeOf(apolloSelectedFeature.type, 'CDS')
358
+ featureTypeOntology.isTypeOf(selectedFeature.type, 'CDS')
355
359
  ? 'rgb(0,0,0)'
356
360
  : cdsColorCode
357
361
  ctx.fillRect(
@@ -448,10 +452,10 @@ function drawDragPreview(
448
452
  const rectY = row * apolloRowHeight
449
453
  const rectWidth = Math.abs(current.x - featureEdgePx)
450
454
  const rectHeight = apolloRowHeight * rowCount
451
- overlayCtx.strokeStyle = theme?.palette.info.main ?? 'rgb(255,0,0)'
455
+ overlayCtx.strokeStyle = theme.palette.info.main
452
456
  overlayCtx.setLineDash([6])
453
457
  overlayCtx.strokeRect(rectX, rectY, rectWidth, rectHeight)
454
- overlayCtx.fillStyle = alpha(theme?.palette.info.main ?? 'rgb(255,0,0)', 0.2)
458
+ overlayCtx.fillStyle = alpha(theme.palette.info.main, 0.2)
455
459
  overlayCtx.fillRect(rectX, rectY, rectWidth, rectHeight)
456
460
  }
457
461
 
@@ -460,7 +464,7 @@ function drawHover(
460
464
  ctx: CanvasRenderingContext2D,
461
465
  ) {
462
466
  const {
463
- apolloHover,
467
+ hoveredFeature,
464
468
  apolloRowHeight,
465
469
  filteredTranscripts,
466
470
  lgv,
@@ -468,15 +472,15 @@ function drawHover(
468
472
  session,
469
473
  showFeatureLabels,
470
474
  } = stateModel
471
- if (!apolloHover) {
475
+ if (!hoveredFeature) {
472
476
  return
473
477
  }
478
+ const { feature } = hoveredFeature
474
479
  const { apolloDataStore } = session
475
480
  const { featureTypeOntology } = apolloDataStore.ontologyManager
476
481
  if (!featureTypeOntology) {
477
482
  throw new Error('featureTypeOntology is undefined')
478
483
  }
479
- const { feature } = apolloHover
480
484
  if (!featureTypeOntology.isTypeOf(feature.type, 'transcript')) {
481
485
  return
482
486
  }
@@ -548,16 +552,14 @@ function drawHover(
548
552
 
549
553
  function onMouseDown(
550
554
  stateModel: LinearApolloSixFrameDisplay,
551
- currentMousePosition: MousePositionWithFeatureAndGlyph,
555
+ currentMousePosition: MousePositionWithFeature,
552
556
  event: CanvasMouseEvent,
553
557
  ) {
554
- const { featureAndGlyphUnderMouse } = currentMousePosition
558
+ const { feature } = currentMousePosition
555
559
  // swallow the mouseDown if we are on the edge of the feature so that we
556
560
  // don't start dragging the view if we try to drag the feature edge
557
- const { cds, feature } = featureAndGlyphUnderMouse
558
561
  const draggableFeature = getDraggableFeatureInfo(
559
562
  currentMousePosition,
560
- cds,
561
563
  feature,
562
564
  stateModel,
563
565
  )
@@ -576,13 +578,11 @@ function onMouseMove(
576
578
  stateModel: LinearApolloSixFrameDisplay,
577
579
  mousePosition: MousePosition,
578
580
  ) {
579
- if (isMousePositionWithFeatureAndGlyph(mousePosition)) {
580
- const { featureAndGlyphUnderMouse } = mousePosition
581
- stateModel.setApolloHover(featureAndGlyphUnderMouse)
582
- const { cds, feature } = featureAndGlyphUnderMouse
581
+ if (isMousePositionWithFeature(mousePosition)) {
582
+ const { feature, bp } = mousePosition
583
+ stateModel.setHoveredFeature({ feature, bp })
583
584
  const draggableFeature = getDraggableFeatureInfo(
584
585
  mousePosition,
585
- cds,
586
586
  feature,
587
587
  stateModel,
588
588
  )
@@ -601,53 +601,43 @@ function onMouseUp(
601
601
  if (stateModel.apolloDragging) {
602
602
  return
603
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
- }
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)
610
+ if (!featureTypeOntology) {
611
+ throw new Error('featureTypeOntology is undefined')
612
+ }
616
613
 
617
- let containsCDSOrExon = false
618
- for (const [, child] of feature.children ?? []) {
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
621
+ break
622
+ }
623
+ }
619
624
  if (
620
- featureTypeOntology.isTypeOf(child.type, 'CDS') ||
621
- featureTypeOntology.isTypeOf(child.type, 'exon')
625
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
626
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
627
+ containsCDSOrExon
622
628
  ) {
623
- containsCDSOrExon = true
624
- break
629
+ stateModel.showFeatureDetailsWidget(feature, [
630
+ 'ApolloTranscriptDetails',
631
+ 'apolloTranscriptDetails',
632
+ ])
633
+ } else {
634
+ stateModel.showFeatureDetailsWidget(feature)
625
635
  }
626
636
  }
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
637
  }
647
638
 
648
639
  function getDraggableFeatureInfo(
649
640
  mousePosition: MousePosition,
650
- cds: TranscriptPartCoding | null,
651
641
  feature: AnnotationFeature,
652
642
  stateModel: LinearApolloSixFrameDisplay,
653
643
  ): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
@@ -658,9 +648,6 @@ function getDraggableFeatureInfo(
658
648
  throw new Error('featureTypeOntology is undefined')
659
649
  }
660
650
  const isTranscript = featureTypeOntology.isTypeOf(feature.type, 'transcript')
661
- if (cds === null) {
662
- return
663
- }
664
651
  const featureID: string | undefined = feature.attributes
665
652
  .get('gff_id')
666
653
  ?.toString()
@@ -700,16 +687,24 @@ function getDraggableFeatureInfo(
700
687
  }
701
688
  }
702
689
  // 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
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
+ }
713
708
  }
714
709
  }
715
710
  }
@@ -721,22 +716,33 @@ function drawTooltip(
721
716
  display: LinearApolloSixFrameDisplayMouseEvents,
722
717
  context: CanvasRenderingContext2D,
723
718
  ): void {
724
- const { apolloHover, apolloRowHeight, filteredTranscripts, lgv, theme } =
725
- display
726
- if (!apolloHover) {
719
+ const {
720
+ hoveredFeature,
721
+ apolloRowHeight,
722
+ filteredTranscripts,
723
+ lgv,
724
+ session,
725
+ showFeatureLabels,
726
+ theme,
727
+ } = display
728
+ if (!hoveredFeature) {
727
729
  return
728
730
  }
729
- const { cds, feature } = apolloHover
730
- 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) {
731
738
  return
732
739
  }
740
+ const { attributes, strand, type } = feature
733
741
  const position = display.getFeatureLayoutPosition(feature)
734
742
  if (!position) {
735
743
  return
736
744
  }
737
- const featureID: string | undefined = feature.attributes
738
- .get('gff_id')
739
- ?.toString()
745
+ const featureID: string | undefined = attributes.get('gff_id')?.toString()
740
746
  if (featureID && filteredTranscripts.includes(featureID)) {
741
747
  return
742
748
  }
@@ -746,9 +752,20 @@ function drawTooltip(
746
752
  const { refName, reversed } = displayedRegion
747
753
  const rowHeight = apolloRowHeight
748
754
  const cdsHeight = Math.round(0.7 * rowHeight)
755
+ const featureLabelSpacer = showFeatureLabels ? 2 : 1
749
756
  let location = 'Loc: '
750
-
751
- 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
+ }
752
769
  const { max, min, phase } = cds
753
770
  location += `${min + 1}–${max}`
754
771
 
@@ -759,12 +776,12 @@ function drawTooltip(
759
776
  regionNumber: layoutIndex,
760
777
  })?.offsetPx ?? 0) - offsetPx
761
778
  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
779
+ const frameAdjust = (frame < 0 ? -1 * frame + 5 : frame) * featureLabelSpacer
780
+ const cdsTop =
781
+ (frameAdjust - featureLabelSpacer) * rowHeight + (rowHeight - cdsHeight) / 2
764
782
  const cdsWidthPx = (max - min) / bpPerPx
765
783
 
766
784
  const featureType = `Type: ${cds.type}`
767
- const { attributes } = feature
768
785
  const featureName = attributes.get('gff_name')?.find((name) => name !== '')
769
786
  const textWidth = [
770
787
  context.measureText(featureType).width,
@@ -772,14 +789,14 @@ function drawTooltip(
772
789
  ]
773
790
  if (featureName) {
774
791
  textWidth.push(
775
- context.measureText(`Parent Type: ${feature.type}`).width,
792
+ context.measureText(`Parent Type: ${type}`).width,
776
793
  context.measureText(`Parent Name: ${featureName}`).width,
777
794
  )
778
795
  }
779
796
  const maxWidth = Math.max(...textWidth)
780
797
 
781
798
  startPx = startPx + cdsWidthPx + 5
782
- context.fillStyle = alpha(theme?.palette.text.primary ?? 'rgb(1, 1, 1)', 0.7)
799
+ context.fillStyle = alpha(theme.palette.text.primary, 0.7)
783
800
  context.fillRect(
784
801
  startPx,
785
802
  cdsTop,
@@ -791,12 +808,12 @@ function drawTooltip(
791
808
  context.lineTo(startPx - 5, cdsTop + 5)
792
809
  context.lineTo(startPx, cdsTop + 10)
793
810
  context.fill()
794
- context.fillStyle = theme?.palette.background.default ?? 'rgba(255, 255, 255)'
811
+ context.fillStyle = theme.palette.background.default
795
812
  let textTop = cdsTop + 12
796
813
  context.fillText(featureType, startPx + 2, textTop)
797
814
  if (featureName) {
798
815
  textTop = textTop + 12
799
- context.fillText(`Parent Type: ${feature.type}`, startPx + 2, textTop)
816
+ context.fillText(`Parent Type: ${type}`, startPx + 2, textTop)
800
817
  textTop = textTop + 12
801
818
  context.fillText(`Parent Name: ${featureName}`, startPx + 2, textTop)
802
819
  }
@@ -806,119 +823,121 @@ function drawTooltip(
806
823
 
807
824
  function getContextMenuItems(
808
825
  display: LinearApolloSixFrameDisplayMouseEvents,
826
+ mousePosition: MousePositionWithFeature,
809
827
  ): MenuItem[] {
810
828
  const {
811
- apolloHover,
812
829
  apolloInternetAccount: internetAccount,
830
+ hoveredFeature,
813
831
  changeManager,
814
832
  filteredTranscripts,
815
833
  regions,
816
834
  selectedFeature,
817
835
  session,
818
836
  } = display
837
+ const [region] = regions
838
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
819
839
  const menuItems: MenuItem[] = []
820
- if (!apolloHover) {
821
- return menuItems
822
- }
823
- const { feature: sourceFeature } = apolloHover
824
840
  const role = internetAccount ? internetAccount.role : 'admin'
825
841
  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
- )
842
+ if (!hoveredFeature) {
843
+ return menuItems
844
+ }
897
845
  const { featureTypeOntology } = session.apolloDataStore.ontologyManager
898
846
  if (!featureTypeOntology) {
899
847
  throw new Error('featureTypeOntology is undefined')
900
848
  }
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
- },
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(
864
+ {
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
+ )
917
886
  },
918
- ],
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
+ },
911
+ },
919
912
  )
920
- },
921
- })
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
+ }
922
941
  }
923
942
  return menuItems
924
943
  }
@@ -933,6 +952,7 @@ export const geneGlyph: Glyph = {
933
952
  drawHover,
934
953
  drawTooltip,
935
954
  getContextMenuItems,
955
+ getContextMenuItemsForFeature,
936
956
  onMouseDown,
937
957
  onMouseLeave,
938
958
  onMouseMove,