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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.esm.js +2679 -850
  2. package/dist/index.esm.js.map +1 -1
  3. package/dist/jbrowse-plugin-apollo.cjs.development.js +2676 -847
  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 +5194 -1258
  8. package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
  9. package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
  10. package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
  11. package/package.json +4 -4
  12. package/src/ApolloInternetAccount/addMenuItems.ts +18 -0
  13. package/src/ChangeManager.ts +10 -6
  14. package/src/FeatureDetailsWidget/Attributes.tsx +8 -3
  15. package/src/FeatureDetailsWidget/TranscriptSequence.tsx +12 -20
  16. package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +929 -175
  17. package/src/FeatureDetailsWidget/TranscriptWidgetSummary.tsx +4 -0
  18. package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +1 -1
  19. package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +48 -60
  20. package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +244 -51
  21. package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +46 -1
  22. package/src/LinearApolloDisplay/glyphs/Glyph.ts +9 -1
  23. package/src/LinearApolloDisplay/stateModel/base.ts +29 -0
  24. package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +51 -35
  25. package/src/LinearApolloDisplay/stateModel/rendering.ts +2 -1
  26. package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +7 -2
  27. package/src/LinearApolloSixFrameDisplay/components/TrackLines.tsx +12 -20
  28. package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +243 -124
  29. package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -1
  30. package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +19 -3
  31. package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +53 -34
  32. package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +4 -2
  33. package/src/OntologyManager/index.ts +4 -1
  34. package/src/TabularEditor/HybridGrid/Feature.tsx +4 -0
  35. package/src/TabularEditor/HybridGrid/featureContextMenuItems.ts +108 -16
  36. package/src/components/AddAssemblyAliases.tsx +114 -0
  37. package/src/components/AddChildFeature.tsx +3 -6
  38. package/src/components/AddFeature.tsx +14 -15
  39. package/src/components/CopyFeature.tsx +2 -4
  40. package/src/components/CreateApolloAnnotation.tsx +334 -151
  41. package/src/components/DeleteFeature.tsx +358 -11
  42. package/src/components/DownloadGFF3.tsx +20 -1
  43. package/src/components/FilterTranscripts.tsx +86 -0
  44. package/src/components/MergeExons.tsx +193 -0
  45. package/src/components/MergeTranscripts.tsx +185 -0
  46. package/src/components/SplitExon.tsx +134 -0
  47. package/src/components/index.ts +3 -0
  48. package/src/config.ts +5 -0
  49. package/src/extensions/annotationFromJBrowseFeature.ts +2 -0
  50. package/src/extensions/annotationFromPileup.ts +99 -89
  51. package/src/session/session.ts +26 -13
  52. package/src/util/annotationFeatureUtils.ts +65 -0
  53. package/src/util/copyToClipboard.ts +21 -0
  54. package/src/util/glyphUtils.ts +49 -0
  55. package/src/util/index.ts +2 -0
  56. package/src/util/mouseEventsUtils.ts +113 -0
@@ -37,6 +37,10 @@ export const TranscriptWidgetSummary = observer(
37
37
  <TableCell>{getFeatureId(feature)}</TableCell>
38
38
  </TableRow>
39
39
  )}
40
+ <TableRow>
41
+ <HeaderTableCell>Type</HeaderTableCell>
42
+ <TableCell>{feature.type}</TableCell>
43
+ </TableRow>
40
44
  <TableRow>
41
45
  <HeaderTableCell>Location</HeaderTableCell>
42
46
  <TableCell>
@@ -139,7 +139,7 @@ export const LinearApolloDisplay = observer(function LinearApolloDisplay(
139
139
  } else {
140
140
  const coord: [number, number] = [event.clientX, event.clientY]
141
141
  setContextCoord(coord)
142
- setContextMenuItems(getContextMenuItems(coord))
142
+ setContextMenuItems(getContextMenuItems(event))
143
143
  }
144
144
  }}
145
145
  >
@@ -1,10 +1,6 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
2
  import { type MenuItem } from '@jbrowse/core/ui'
3
- import {
4
- type AbstractSessionModel,
5
- type SessionWithWidgets,
6
- isSessionModelWithWidgets,
7
- } from '@jbrowse/core/util'
3
+ import { type AbstractSessionModel } from '@jbrowse/core/util'
8
4
  import { type Theme, alpha } from '@mui/material'
9
5
 
10
6
  import { AddChildFeature, CopyFeature, DeleteFeature } from '../../components'
@@ -257,9 +253,39 @@ export function drawBox(
257
253
 
258
254
  function getContextMenuItems(
259
255
  display: LinearApolloDisplayMouseEvents,
256
+ ): MenuItem[] {
257
+ const { apolloHover } = display
258
+ if (!apolloHover) {
259
+ return []
260
+ }
261
+ const { feature: sourceFeature } = apolloHover
262
+ return getContextMenuItemsForFeature(display, sourceFeature)
263
+ }
264
+
265
+ function makeFeatureLabel(feature: AnnotationFeature) {
266
+ let name: string | undefined
267
+ if (feature.attributes.get('gff_name')) {
268
+ name = feature.attributes.get('gff_name')?.join(',')
269
+ } else if (feature.attributes.get('gff_id')) {
270
+ name = feature.attributes.get('gff_id')?.join(',')
271
+ } else {
272
+ name = feature._id
273
+ }
274
+ const coords = `(${(feature.min + 1).toLocaleString('en')}..${feature.max.toLocaleString('en')})`
275
+ const maxLen = 60
276
+ if (name && name.length + coords.length > maxLen + 5) {
277
+ const trim = maxLen - coords.length
278
+ name = trim > 0 ? name.slice(0, trim) : ''
279
+ name = `${name}[...]`
280
+ }
281
+ return `${name} ${coords}`
282
+ }
283
+
284
+ function getContextMenuItemsForFeature(
285
+ display: LinearApolloDisplayMouseEvents,
286
+ sourceFeature: AnnotationFeature,
260
287
  ): MenuItem[] {
261
288
  const {
262
- apolloHover,
263
289
  apolloInternetAccount: internetAccount,
264
290
  changeManager,
265
291
  regions,
@@ -267,17 +293,23 @@ function getContextMenuItems(
267
293
  session,
268
294
  } = display
269
295
  const menuItems: MenuItem[] = []
270
- if (!apolloHover) {
271
- return menuItems
272
- }
273
- const { feature: sourceFeature } = apolloHover
274
296
  const role = internetAccount ? internetAccount.role : 'admin'
275
297
  const admin = role === 'admin'
276
298
  const readOnly = !(role && ['admin', 'user'].includes(role))
277
299
  const [region] = regions
278
300
  const sourceAssemblyId = display.getAssemblyId(region.assemblyName)
279
301
  const currentAssemblyId = display.getAssemblyId(region.assemblyName)
302
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
303
+ if (!featureTypeOntology) {
304
+ throw new Error('featureTypeOntology is undefined')
305
+ }
306
+
307
+ // Add only relevant options
280
308
  menuItems.push(
309
+ {
310
+ label: makeFeatureLabel(sourceFeature),
311
+ type: 'subHeader',
312
+ },
281
313
  {
282
314
  label: 'Add child feature',
283
315
  disabled: readOnly,
@@ -343,55 +375,7 @@ function getContextMenuItems(
343
375
  )
344
376
  },
345
377
  },
346
- {
347
- label: 'Edit feature details',
348
- onClick: () => {
349
- const apolloFeatureWidget = (
350
- session as unknown as SessionWithWidgets
351
- ).addWidget(
352
- 'ApolloFeatureDetailsWidget',
353
- 'apolloFeatureDetailsWidget',
354
- {
355
- feature: sourceFeature,
356
- assembly: currentAssemblyId,
357
- refName: region.refName,
358
- },
359
- )
360
- ;(session as unknown as SessionWithWidgets).showWidget(
361
- apolloFeatureWidget,
362
- )
363
- },
364
- },
365
378
  )
366
- const { featureTypeOntology } = session.apolloDataStore.ontologyManager
367
- if (!featureTypeOntology) {
368
- throw new Error('featureTypeOntology is undefined')
369
- }
370
- if (
371
- (featureTypeOntology.isTypeOf(sourceFeature.type, 'transcript') ||
372
- featureTypeOntology.isTypeOf(
373
- sourceFeature.type,
374
- 'pseudogenic_transcript',
375
- )) &&
376
- isSessionModelWithWidgets(session)
377
- ) {
378
- menuItems.push({
379
- label: 'Edit transcript details',
380
- onClick: () => {
381
- const apolloTranscriptWidget = session.addWidget(
382
- 'ApolloTranscriptDetails',
383
- 'apolloTranscriptDetails',
384
- {
385
- feature: sourceFeature,
386
- assembly: currentAssemblyId,
387
- changeManager,
388
- refName: region.refName,
389
- },
390
- )
391
- session.showWidget(apolloTranscriptWidget)
392
- },
393
- })
394
- }
395
379
  return menuItems
396
380
  }
397
381
 
@@ -459,9 +443,12 @@ function onMouseUp(
459
443
  return
460
444
  }
461
445
  const { featureAndGlyphUnderMouse } = mousePosition
462
- if (featureAndGlyphUnderMouse?.feature) {
463
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
446
+ if (!featureAndGlyphUnderMouse) {
447
+ return
464
448
  }
449
+ const { feature } = featureAndGlyphUnderMouse
450
+ stateModel.setSelectedFeature(feature)
451
+ stateModel.showFeatureDetailsWidget(feature)
465
452
  }
466
453
 
467
454
  /** @returns undefined if mouse not on the edge of this feature, otherwise 'start' or 'end' depending on which edge */
@@ -496,6 +483,7 @@ export const boxGlyph: Glyph = {
496
483
  drawDragPreview,
497
484
  drawHover,
498
485
  drawTooltip,
486
+ getContextMenuItemsForFeature,
499
487
  getContextMenuItems,
500
488
  getFeatureFromLayout,
501
489
  getRowCount,
@@ -1,10 +1,21 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
- import { getFrame, intersection2 } from '@jbrowse/core/util'
2
+ import { type MenuItem } from '@jbrowse/core/ui'
3
+ import {
4
+ type AbstractSessionModel,
5
+ getFrame,
6
+ intersection2,
7
+ isSessionModelWithWidgets,
8
+ } from '@jbrowse/core/util'
3
9
  import { alpha } from '@mui/material'
4
10
 
5
11
  import { type OntologyRecord } from '../../OntologyManager'
12
+ import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
13
+ import { type ApolloSessionModel } from '../../session'
14
+ import { getMinAndMaxPx, getOverlappingEdge } from '../../util'
15
+ import { getFeaturesUnderClick } from '../../util/annotationFeatureUtils'
6
16
  import { type LinearApolloDisplay } from '../stateModel'
7
17
  import {
18
+ type LinearApolloDisplayMouseEvents,
8
19
  type MousePosition,
9
20
  type MousePositionWithFeatureAndGlyph,
10
21
  isMousePositionWithFeatureAndGlyph,
@@ -19,7 +30,10 @@ let forwardFillLight: CanvasPattern | null = null
19
30
  let backwardFillLight: CanvasPattern | null = null
20
31
  let forwardFillDark: CanvasPattern | null = null
21
32
  let backwardFillDark: CanvasPattern | null = null
22
- if ('document' in globalThis) {
33
+ const canvas = globalThis.document.createElement('canvas')
34
+ // @ts-expect-error getContext is undefined in the web worker
35
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
36
+ if (canvas?.getContext) {
23
37
  for (const direction of ['forward', 'backward']) {
24
38
  for (const themeMode of ['light', 'dark']) {
25
39
  const canvas = document.createElement('canvas')
@@ -619,6 +633,7 @@ function onMouseDown(
619
633
  currentMousePosition,
620
634
  draggableFeature.feature,
621
635
  draggableFeature.edge,
636
+ true,
622
637
  )
623
638
  }
624
639
  }
@@ -652,8 +667,39 @@ function onMouseUp(
652
667
  return
653
668
  }
654
669
  const { featureAndGlyphUnderMouse } = mousePosition
655
- if (featureAndGlyphUnderMouse?.feature) {
656
- stateModel.setSelectedFeature(featureAndGlyphUnderMouse.feature)
670
+ if (!featureAndGlyphUnderMouse) {
671
+ return
672
+ }
673
+ const { feature } = featureAndGlyphUnderMouse
674
+ stateModel.setSelectedFeature(feature)
675
+ const { session } = stateModel
676
+ const { apolloDataStore } = session
677
+ const { featureTypeOntology } = apolloDataStore.ontologyManager
678
+ if (!featureTypeOntology) {
679
+ throw new Error('featureTypeOntology is undefined')
680
+ }
681
+
682
+ let containsCDSOrExon = false
683
+ for (const [, child] of feature.children ?? []) {
684
+ if (
685
+ featureTypeOntology.isTypeOf(child.type, 'CDS') ||
686
+ featureTypeOntology.isTypeOf(child.type, 'exon')
687
+ ) {
688
+ containsCDSOrExon = true
689
+ break
690
+ }
691
+ }
692
+ if (
693
+ (featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
694
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
695
+ containsCDSOrExon
696
+ ) {
697
+ stateModel.showFeatureDetailsWidget(feature, [
698
+ 'ApolloTranscriptDetails',
699
+ 'apolloTranscriptDetails',
700
+ ])
701
+ } else {
702
+ stateModel.showFeatureDetailsWidget(feature)
657
703
  }
658
704
  }
659
705
 
@@ -674,31 +720,18 @@ function getDraggableFeatureInfo(
674
720
  const isTranscript =
675
721
  featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
676
722
  featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
677
- const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS')
723
+ const isCDS = featureTypeOntology.isTypeOf(feature.type, 'CDS')
678
724
  if (isGene || isTranscript) {
725
+ // For gene glyphs, the sizes of genes and transcripts are determined by
726
+ // their child exons, so we don't make them draggable
679
727
  return
680
728
  }
729
+ // So now the type of feature is either CDS or exon. If an exon and CDS edge
730
+ // are in the same place, we want to prioritize dragging the exon. If the
731
+ // feature we're on is a CDS, let's find any exon it may overlap.
681
732
  const { bp, refName, regionNumber, x } = mousePosition
682
733
  const { lgv } = stateModel
683
- const { offsetPx } = lgv
684
-
685
- const minPxInfo = lgv.bpToPx({ refName, coord: feature.min, regionNumber })
686
- const maxPxInfo = lgv.bpToPx({ refName, coord: feature.max, regionNumber })
687
- if (minPxInfo === undefined || maxPxInfo === undefined) {
688
- return
689
- }
690
- const minPx = minPxInfo.offsetPx - offsetPx
691
- const maxPx = maxPxInfo.offsetPx - offsetPx
692
- if (Math.abs(maxPx - minPx) < 8) {
693
- return
694
- }
695
- if (Math.abs(minPx - x) < 4) {
696
- return { feature, edge: 'min' }
697
- }
698
- if (Math.abs(maxPx - x) < 4) {
699
- return { feature, edge: 'max' }
700
- }
701
- if (isCds) {
734
+ if (isCDS) {
702
735
  const transcript = feature.parent
703
736
  if (!transcript?.children) {
704
737
  return
@@ -710,46 +743,205 @@ function getDraggableFeatureInfo(
710
743
  exonChildren.push(child)
711
744
  }
712
745
  }
713
-
714
746
  const overlappingExon = exonChildren.find((child) => {
715
747
  const [start, end] = intersection2(bp - 1, bp, child.min, child.max)
716
748
  return start !== undefined && end !== undefined
717
749
  })
718
-
719
- if (!overlappingExon) {
720
- return
721
- }
722
- const minPxInfo = lgv.bpToPx({
723
- refName,
724
- coord: overlappingExon.min,
725
- regionNumber,
726
- })
727
- const maxPxInfo = lgv.bpToPx({
728
- refName,
729
- coord: overlappingExon.max,
730
- regionNumber,
731
- })
732
- if (minPxInfo === undefined || maxPxInfo === undefined) {
733
- return
750
+ if (overlappingExon) {
751
+ // We are on an exon, are we on the edge of it?
752
+ const minMax = getMinAndMaxPx(overlappingExon, refName, regionNumber, lgv)
753
+ if (minMax) {
754
+ const overlappingEdge = getOverlappingEdge(overlappingExon, x, minMax)
755
+ if (overlappingEdge) {
756
+ return overlappingEdge
757
+ }
758
+ }
734
759
  }
735
- const minPx = minPxInfo.offsetPx - offsetPx
736
- const maxPx = maxPxInfo.offsetPx - offsetPx
737
- if (Math.abs(maxPx - minPx) < 8) {
738
- return
760
+ }
761
+ // End of special cases, let's see if we're on the edge of this CDS or exon
762
+ const minMax = getMinAndMaxPx(feature, refName, regionNumber, lgv)
763
+ if (minMax) {
764
+ const overlappingEdge = getOverlappingEdge(feature, x, minMax)
765
+ if (overlappingEdge) {
766
+ return overlappingEdge
739
767
  }
740
- if (Math.abs(minPx - x) < 4) {
741
- return { feature: overlappingExon, edge: 'min' }
768
+ }
769
+ return
770
+ }
771
+
772
+ function isTranscriptFeature(
773
+ feature: AnnotationFeature,
774
+ session: ApolloSessionModel,
775
+ ): boolean {
776
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
777
+ if (!featureTypeOntology) {
778
+ throw new Error('featureTypeOntology is undefined')
779
+ }
780
+ return (
781
+ featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
782
+ featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
783
+ )
784
+ }
785
+
786
+ function isExonFeature(
787
+ feature: AnnotationFeature,
788
+ session: ApolloSessionModel,
789
+ ): boolean {
790
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
791
+ if (!featureTypeOntology) {
792
+ throw new Error('featureTypeOntology is undefined')
793
+ }
794
+ return featureTypeOntology.isTypeOf(feature.type, 'exon')
795
+ }
796
+
797
+ function isCDSFeature(
798
+ feature: AnnotationFeature,
799
+ session: ApolloSessionModel,
800
+ ): boolean {
801
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
802
+ if (!featureTypeOntology) {
803
+ throw new Error('featureTypeOntology is undefined')
804
+ }
805
+ return featureTypeOntology.isTypeOf(feature.type, 'CDS')
806
+ }
807
+
808
+ function getContextMenuItems(
809
+ display: LinearApolloDisplayMouseEvents,
810
+ mousePosition: MousePositionWithFeatureAndGlyph,
811
+ ): MenuItem[] {
812
+ const {
813
+ apolloInternetAccount: internetAccount,
814
+ apolloHover,
815
+ changeManager,
816
+ regions,
817
+ selectedFeature,
818
+ session,
819
+ } = display
820
+ const [region] = regions
821
+ const currentAssemblyId = display.getAssemblyId(region.assemblyName)
822
+ const menuItems: MenuItem[] = []
823
+ const role = internetAccount ? internetAccount.role : 'admin'
824
+ const admin = role === 'admin'
825
+ if (!apolloHover) {
826
+ return menuItems
827
+ }
828
+
829
+ let featuresUnderClick = getFeaturesUnderClick(mousePosition)
830
+ if (isCDSFeature(mousePosition.featureAndGlyphUnderMouse.feature, session)) {
831
+ featuresUnderClick = getFeaturesUnderClick(mousePosition, true)
832
+ }
833
+
834
+ for (const feature of featuresUnderClick) {
835
+ const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
836
+ display,
837
+ feature,
838
+ )
839
+ if (isExonFeature(feature, session)) {
840
+ contextMenuItemsForFeature.push(
841
+ {
842
+ label: 'Merge exons',
843
+ disabled: !admin,
844
+ onClick: () => {
845
+ ;(session as unknown as AbstractSessionModel).queueDialog(
846
+ (doneCallback) => [
847
+ MergeExons,
848
+ {
849
+ session,
850
+ handleClose: () => {
851
+ doneCallback()
852
+ },
853
+ changeManager,
854
+ sourceFeature: feature,
855
+ sourceAssemblyId: currentAssemblyId,
856
+ selectedFeature,
857
+ setSelectedFeature: (feature?: AnnotationFeature) => {
858
+ display.setSelectedFeature(feature)
859
+ },
860
+ },
861
+ ],
862
+ )
863
+ },
864
+ },
865
+ {
866
+ label: 'Split exon',
867
+ disabled: !admin,
868
+ onClick: () => {
869
+ ;(session as unknown as AbstractSessionModel).queueDialog(
870
+ (doneCallback) => [
871
+ SplitExon,
872
+ {
873
+ session,
874
+ handleClose: () => {
875
+ doneCallback()
876
+ },
877
+ changeManager,
878
+ sourceFeature: feature,
879
+ sourceAssemblyId: currentAssemblyId,
880
+ selectedFeature,
881
+ setSelectedFeature: (feature?: AnnotationFeature) => {
882
+ display.setSelectedFeature(feature)
883
+ },
884
+ },
885
+ ],
886
+ )
887
+ },
888
+ },
889
+ )
742
890
  }
743
- if (Math.abs(maxPx - x) < 4) {
744
- return { feature: overlappingExon, edge: 'max' }
891
+ if (isTranscriptFeature(feature, session)) {
892
+ contextMenuItemsForFeature.push({
893
+ label: 'Merge transcript',
894
+ onClick: () => {
895
+ ;(session as unknown as AbstractSessionModel).queueDialog(
896
+ (doneCallback) => [
897
+ MergeTranscripts,
898
+ {
899
+ session,
900
+ handleClose: () => {
901
+ doneCallback()
902
+ },
903
+ changeManager,
904
+ sourceFeature: feature,
905
+ sourceAssemblyId: currentAssemblyId,
906
+ selectedFeature,
907
+ setSelectedFeature: (feature?: AnnotationFeature) => {
908
+ display.setSelectedFeature(feature)
909
+ },
910
+ },
911
+ ],
912
+ )
913
+ },
914
+ })
915
+ if (isSessionModelWithWidgets(session)) {
916
+ contextMenuItemsForFeature.push({
917
+ label: 'Open transcript details',
918
+ onClick: () => {
919
+ const apolloTranscriptWidget = session.addWidget(
920
+ 'ApolloTranscriptDetails',
921
+ 'apolloTranscriptDetails',
922
+ {
923
+ feature,
924
+ assembly: currentAssemblyId,
925
+ changeManager,
926
+ refName: region.refName,
927
+ },
928
+ )
929
+ session.showWidget(apolloTranscriptWidget)
930
+ },
931
+ })
932
+ }
745
933
  }
934
+ menuItems.push({
935
+ label: feature.type,
936
+ subMenu: contextMenuItemsForFeature,
937
+ })
746
938
  }
747
- return
939
+ return menuItems
748
940
  }
749
941
 
750
942
  // False positive here, none of these functions use "this"
751
943
  /* eslint-disable @typescript-eslint/unbound-method */
752
- const { drawTooltip, getContextMenuItems, onMouseLeave } = boxGlyph
944
+ const { drawTooltip, getContextMenuItemsForFeature, onMouseLeave } = boxGlyph
753
945
  /* eslint-enable @typescript-eslint/unbound-method */
754
946
 
755
947
  export const geneGlyph: Glyph = {
@@ -758,6 +950,7 @@ export const geneGlyph: Glyph = {
758
950
  drawHover,
759
951
  drawTooltip,
760
952
  getContextMenuItems,
953
+ getContextMenuItemsForFeature,
761
954
  getFeatureFromLayout,
762
955
  getRowCount,
763
956
  getRowForFeature,
@@ -1,6 +1,12 @@
1
1
  import { type AnnotationFeature } from '@apollo-annotation/mst'
2
+ import { type MenuItem } from '@jbrowse/core/ui'
2
3
 
4
+ import { getFeaturesUnderClick } from '../../util/annotationFeatureUtils'
3
5
  import { type LinearApolloDisplay } from '../stateModel'
6
+ import {
7
+ type LinearApolloDisplayMouseEvents,
8
+ type MousePositionWithFeatureAndGlyph,
9
+ } from '../stateModel/mouseEvents'
4
10
  import { type LinearApolloDisplayRendering } from '../stateModel/rendering'
5
11
 
6
12
  import { boxGlyph, drawBox, isSelectedFeature } from './BoxGlyph'
@@ -130,12 +136,50 @@ function getRowForFeature(
130
136
  return
131
137
  }
132
138
 
139
+ function getContextMenuItems(
140
+ display: LinearApolloDisplayMouseEvents,
141
+ mousePosition: MousePositionWithFeatureAndGlyph,
142
+ ): MenuItem[] {
143
+ const { apolloHover, session } = display
144
+ const menuItems: MenuItem[] = []
145
+ if (!apolloHover) {
146
+ return menuItems
147
+ }
148
+ const { feature: sourceFeature } = apolloHover
149
+ const { featureTypeOntology } = session.apolloDataStore.ontologyManager
150
+ if (!featureTypeOntology) {
151
+ throw new Error('featureTypeOntology is undefined')
152
+ }
153
+ const sourceFeatureMenuItems = boxGlyph.getContextMenuItems(
154
+ display,
155
+ mousePosition,
156
+ )
157
+ menuItems.push({
158
+ label: sourceFeature.type,
159
+ subMenu: sourceFeatureMenuItems,
160
+ })
161
+ for (const relative of getFeaturesUnderClick(mousePosition)) {
162
+ if (relative._id === sourceFeature._id) {
163
+ continue
164
+ }
165
+ const contextMenuItemsForFeature = boxGlyph.getContextMenuItemsForFeature(
166
+ display,
167
+ relative,
168
+ )
169
+ menuItems.push({
170
+ label: relative.type,
171
+ subMenu: contextMenuItemsForFeature,
172
+ })
173
+ }
174
+ return menuItems
175
+ }
176
+
133
177
  // False positive here, none of these functions use "this"
134
178
  /* eslint-disable @typescript-eslint/unbound-method */
135
179
  const {
136
180
  drawDragPreview,
137
181
  drawTooltip,
138
- getContextMenuItems,
182
+ getContextMenuItemsForFeature,
139
183
  onMouseDown,
140
184
  onMouseLeave,
141
185
  onMouseMove,
@@ -148,6 +192,7 @@ export const genericChildGlyph: Glyph = {
148
192
  drawDragPreview,
149
193
  drawHover,
150
194
  drawTooltip,
195
+ getContextMenuItemsForFeature,
151
196
  getContextMenuItems,
152
197
  getFeatureFromLayout,
153
198
  getRowCount,
@@ -76,5 +76,13 @@ export interface Glyph {
76
76
  context: CanvasRenderingContext2D,
77
77
  ): void
78
78
 
79
- getContextMenuItems(display: LinearApolloDisplayMouseEvents): MenuItem[]
79
+ getContextMenuItemsForFeature(
80
+ display: LinearApolloDisplayMouseEvents,
81
+ sourceFeature: AnnotationFeature,
82
+ ): MenuItem[]
83
+
84
+ getContextMenuItems(
85
+ display: LinearApolloDisplayMouseEvents,
86
+ currentMousePosition: MousePositionWithFeatureAndGlyph,
87
+ ): MenuItem[]
80
88
  }
@@ -10,6 +10,7 @@ import { type AnyConfigurationSchemaType } from '@jbrowse/core/configuration/con
10
10
  import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes'
11
11
  import {
12
12
  type AbstractSessionModel,
13
+ type SessionWithWidgets,
13
14
  getContainingView,
14
15
  getSession,
15
16
  } from '@jbrowse/core/util'
@@ -285,6 +286,34 @@ export function baseModelFactory(
285
286
  self.session as unknown as ApolloSessionModel
286
287
  ).apolloSetSelectedFeature(feature)
287
288
  },
289
+ showFeatureDetailsWidget(
290
+ feature: AnnotationFeature,
291
+ customWidgetNameAndId?: [string, string],
292
+ ) {
293
+ const [region] = self.regions
294
+ const { assemblyName, refName } = region
295
+ const assembly = self.getAssemblyId(assemblyName)
296
+ if (!assembly) {
297
+ return
298
+ }
299
+ const { session } = self
300
+ const { changeManager } = session.apolloDataStore
301
+ const [widgetName, widgetId] = customWidgetNameAndId ?? [
302
+ 'ApolloFeatureDetailsWidget',
303
+ 'apolloFeatureDetailsWidget',
304
+ ]
305
+ const apolloFeatureWidget = (
306
+ session as unknown as SessionWithWidgets
307
+ ).addWidget(widgetName, widgetId, {
308
+ feature,
309
+ assembly,
310
+ refName,
311
+ changeManager,
312
+ })
313
+ ;(session as unknown as SessionWithWidgets).showWidget(
314
+ apolloFeatureWidget,
315
+ )
316
+ },
288
317
  afterAttach() {
289
318
  addDisposer(
290
319
  self,