Skip to content

Commit ec9fb8e

Browse files
LASFD: Navigate to prev / next exon (#699)
* move common functionality to utils * replicate prev/next exon functionality in LASFD using common utils --------- Co-authored-by: Garrett Stevens <stevens.garrett.j@gmail.com>
1 parent e1133c9 commit ec9fb8e

File tree

3 files changed

+247
-177
lines changed

3 files changed

+247
-177
lines changed

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/GeneGlyph.ts

Lines changed: 6 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ import {
1010
isSessionModelWithWidgets,
1111
} from '@jbrowse/core/util'
1212
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
13-
import SkipNextRoundedIcon from '@mui/icons-material/SkipNextRounded'
14-
import SkipPreviousRoundedIcon from '@mui/icons-material/SkipPreviousRounded'
1513
import { alpha } from '@mui/material'
1614

1715
import { type OntologyRecord } from '../../OntologyManager'
1816
import { MergeExons, MergeTranscripts, SplitExon } from '../../components'
19-
import { type ApolloSessionModel } from '../../session'
2017
import {
2118
type MousePosition,
2219
type MousePositionWithFeature,
2320
containsSelectedFeature,
21+
getAdjacentExons,
2422
getMinAndMaxPx,
2523
getOverlappingEdge,
24+
getStreamIcon,
25+
isCDSFeature,
26+
isExonFeature,
2627
isMousePositionWithFeature,
28+
isTranscriptFeature,
2729
navToFeatureCenter,
30+
selectFeatureAndOpenWidget,
2831
} from '../../util'
2932
import { getRelatedFeatures } from '../../util/annotationFeatureUtils'
3033
import { type LinearApolloDisplay } from '../stateModel'
@@ -696,45 +699,6 @@ function getRowForFeature(
696699
return
697700
}
698701

699-
function selectFeatureAndOpenWidget(
700-
stateModel: LinearApolloDisplayMouseEvents,
701-
feature: AnnotationFeature,
702-
) {
703-
if (stateModel.apolloDragging) {
704-
return
705-
}
706-
stateModel.setSelectedFeature(feature)
707-
const { session } = stateModel
708-
const { apolloDataStore } = session
709-
const { featureTypeOntology } = apolloDataStore.ontologyManager
710-
if (!featureTypeOntology) {
711-
throw new Error('featureTypeOntology is undefined')
712-
}
713-
714-
let containsCDSOrExon = false
715-
for (const [, child] of feature.children ?? []) {
716-
if (
717-
featureTypeOntology.isTypeOf(child.type, 'CDS') ||
718-
featureTypeOntology.isTypeOf(child.type, 'exon')
719-
) {
720-
containsCDSOrExon = true
721-
break
722-
}
723-
}
724-
if (
725-
(featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
726-
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')) &&
727-
containsCDSOrExon
728-
) {
729-
stateModel.showFeatureDetailsWidget(feature, [
730-
'ApolloTranscriptDetails',
731-
'apolloTranscriptDetails',
732-
])
733-
} else {
734-
stateModel.showFeatureDetailsWidget(feature)
735-
}
736-
}
737-
738702
function onMouseDown(
739703
stateModel: LinearApolloDisplay,
740704
currentMousePosition: MousePositionWithFeature,
@@ -859,131 +823,6 @@ function getDraggableFeatureInfo(
859823
return
860824
}
861825

862-
function isTranscriptFeature(
863-
feature: AnnotationFeature,
864-
session: ApolloSessionModel,
865-
): boolean {
866-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
867-
if (!featureTypeOntology) {
868-
throw new Error('featureTypeOntology is undefined')
869-
}
870-
return (
871-
featureTypeOntology.isTypeOf(feature.type, 'transcript') ||
872-
featureTypeOntology.isTypeOf(feature.type, 'pseudogenic_transcript')
873-
)
874-
}
875-
876-
function isExonFeature(
877-
feature: AnnotationFeature,
878-
session: ApolloSessionModel,
879-
): boolean {
880-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
881-
if (!featureTypeOntology) {
882-
throw new Error('featureTypeOntology is undefined')
883-
}
884-
return featureTypeOntology.isTypeOf(feature.type, 'exon')
885-
}
886-
887-
function isCDSFeature(
888-
feature: AnnotationFeature,
889-
session: ApolloSessionModel,
890-
): boolean {
891-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
892-
if (!featureTypeOntology) {
893-
throw new Error('featureTypeOntology is undefined')
894-
}
895-
return featureTypeOntology.isTypeOf(feature.type, 'CDS')
896-
}
897-
898-
interface AdjacentExons {
899-
upstream: AnnotationFeature | undefined
900-
downstream: AnnotationFeature | undefined
901-
}
902-
903-
function getAdjacentExons(
904-
currentExon: AnnotationFeature,
905-
display: LinearApolloDisplayMouseEvents,
906-
mousePosition: MousePositionWithFeature,
907-
session: ApolloSessionModel,
908-
): AdjacentExons {
909-
const lgv = getContainingView(
910-
display as BaseDisplayModel,
911-
) as unknown as LinearGenomeViewModel
912-
913-
// Genomic coords of current view
914-
const viewGenomicLeft = mousePosition.bp - lgv.bpPerPx * mousePosition.x
915-
const viewGenomicRight = viewGenomicLeft + lgv.coarseTotalBp
916-
if (!currentExon.parent) {
917-
return { upstream: undefined, downstream: undefined }
918-
}
919-
const transcript = currentExon.parent
920-
if (!transcript.children) {
921-
throw new Error(`Error getting children of ${transcript._id}`)
922-
}
923-
const { featureTypeOntology } = session.apolloDataStore.ontologyManager
924-
if (!featureTypeOntology) {
925-
throw new Error('featureTypeOntology is undefined')
926-
}
927-
928-
let exons = []
929-
for (const [, child] of transcript.children) {
930-
if (featureTypeOntology.isTypeOf(child.type, 'exon')) {
931-
exons.push(child)
932-
}
933-
}
934-
const adjacentExons: AdjacentExons = {
935-
upstream: undefined,
936-
downstream: undefined,
937-
}
938-
exons = exons.sort((a, b) => (a.min < b.min ? -1 : 1))
939-
for (const exon of exons) {
940-
if (exon.min > viewGenomicRight) {
941-
adjacentExons.downstream = exon
942-
break
943-
}
944-
}
945-
exons = exons.sort((a, b) => (a.min > b.min ? -1 : 1))
946-
for (const exon of exons) {
947-
if (exon.max < viewGenomicLeft) {
948-
adjacentExons.upstream = exon
949-
break
950-
}
951-
}
952-
if (transcript.strand === -1) {
953-
const newUpstream = adjacentExons.downstream
954-
adjacentExons.downstream = adjacentExons.upstream
955-
adjacentExons.upstream = newUpstream
956-
}
957-
return adjacentExons
958-
}
959-
960-
function getStreamIcon(
961-
strand: 1 | -1 | undefined,
962-
isUpstream: boolean,
963-
isFlipped: boolean | undefined,
964-
) {
965-
// This is the icon you would use for strand=1, downstream, straight
966-
// (non-flipped) view
967-
let icon = SkipNextRoundedIcon
968-
969-
if (strand === -1) {
970-
icon = SkipPreviousRoundedIcon
971-
}
972-
if (isUpstream) {
973-
icon =
974-
icon === SkipPreviousRoundedIcon
975-
? SkipNextRoundedIcon
976-
: SkipPreviousRoundedIcon
977-
}
978-
if (isFlipped) {
979-
icon =
980-
icon === SkipPreviousRoundedIcon
981-
? SkipNextRoundedIcon
982-
: SkipPreviousRoundedIcon
983-
}
984-
return icon
985-
}
986-
987826
function getContextMenuItems(
988827
display: LinearApolloDisplayMouseEvents,
989828
mousePosition: MousePositionWithFeature,

packages/jbrowse-plugin-apollo/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import {
22
type AnnotationFeature,
33
type TranscriptPartCoding,
44
} from '@apollo-annotation/mst'
5+
import { type BaseDisplayModel } from '@jbrowse/core/pluggableElementTypes'
56
import { type MenuItem } from '@jbrowse/core/ui'
67
import {
78
type AbstractSessionModel,
9+
getContainingView,
810
getFrame,
911
intersection2,
1012
measureText,
1113
} from '@jbrowse/core/util'
14+
import { type LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
1215
import { alpha } from '@mui/material'
1316
import equal from 'fast-deep-equal/es6'
1417
import { getSnapshot } from 'mobx-state-tree'
@@ -18,12 +21,19 @@ import { FilterTranscripts } from '../../components/FilterTranscripts'
1821
import {
1922
type MousePosition,
2023
type MousePositionWithFeature,
24+
getAdjacentExons,
2125
getContextMenuItemsForFeature,
2226
getMinAndMaxPx,
2327
getOverlappingEdge,
2428
getRelatedFeatures,
29+
getStreamIcon,
30+
isCDSFeature,
31+
isExonFeature,
2532
isMousePositionWithFeature,
33+
// isTranscriptFeature,
2634
isSelectedFeature,
35+
navToFeatureCenter,
36+
selectFeatureAndOpenWidget,
2737
} from '../../util'
2838
import { type LinearApolloSixFrameDisplay } from '../stateModel'
2939
import { type LinearApolloSixFrameDisplayMouseEvents } from '../stateModel/mouseEvents'
@@ -848,18 +858,62 @@ function getContextMenuItems(
848858
}
849859
if (isMousePositionWithFeature(mousePosition)) {
850860
const { bp, feature } = mousePosition
851-
for (const relatedFeature of getRelatedFeatures(feature, bp)) {
852-
const featureID: string | undefined = relatedFeature.attributes
861+
let featuresUnderClick = getRelatedFeatures(feature, bp)
862+
if (isCDSFeature(feature, session)) {
863+
featuresUnderClick = getRelatedFeatures(feature, bp, true)
864+
}
865+
866+
for (const feature of featuresUnderClick) {
867+
const featureID: string | undefined = feature.attributes
853868
.get('gff_id')
854869
?.toString()
855870
if (featureID && filteredTranscripts.includes(featureID)) {
856871
continue
857872
}
858873
const contextMenuItemsForFeature = getContextMenuItemsForFeature(
859874
display,
860-
relatedFeature,
875+
feature,
861876
)
862-
if (featureTypeOntology.isTypeOf(relatedFeature.type, 'exon')) {
877+
if (isExonFeature(feature, session)) {
878+
const adjacentExons = getAdjacentExons(
879+
feature,
880+
display,
881+
mousePosition,
882+
session,
883+
)
884+
const lgv = getContainingView(
885+
display as BaseDisplayModel,
886+
) as unknown as LinearGenomeViewModel
887+
if (adjacentExons.upstream) {
888+
const exon = adjacentExons.upstream
889+
contextMenuItemsForFeature.push({
890+
label: 'Go to upstream exon',
891+
icon: getStreamIcon(
892+
feature.strand,
893+
true,
894+
lgv.displayedRegions.at(0)?.reversed,
895+
),
896+
onClick: () => {
897+
lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
898+
selectFeatureAndOpenWidget(display, exon)
899+
},
900+
})
901+
}
902+
if (adjacentExons.downstream) {
903+
const exon = adjacentExons.downstream
904+
contextMenuItemsForFeature.push({
905+
label: 'Go to downstream exon',
906+
icon: getStreamIcon(
907+
feature.strand,
908+
false,
909+
lgv.displayedRegions.at(0)?.reversed,
910+
),
911+
onClick: () => {
912+
lgv.navTo(navToFeatureCenter(exon, 0.1, lgv.totalBp))
913+
selectFeatureAndOpenWidget(display, exon)
914+
},
915+
})
916+
}
863917
contextMenuItemsForFeature.push(
864918
{
865919
label: 'Merge exons',
@@ -874,7 +928,7 @@ function getContextMenuItems(
874928
doneCallback()
875929
},
876930
changeManager,
877-
sourceFeature: relatedFeature,
931+
sourceFeature: feature,
878932
sourceAssemblyId: currentAssemblyId,
879933
selectedFeature,
880934
setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -898,7 +952,7 @@ function getContextMenuItems(
898952
doneCallback()
899953
},
900954
changeManager,
901-
sourceFeature: relatedFeature,
955+
sourceFeature: feature,
902956
sourceAssemblyId: currentAssemblyId,
903957
selectedFeature,
904958
setSelectedFeature: (feature?: AnnotationFeature) => {
@@ -911,7 +965,7 @@ function getContextMenuItems(
911965
},
912966
)
913967
}
914-
if (featureTypeOntology.isTypeOf(relatedFeature.type, 'gene')) {
968+
if (featureTypeOntology.isTypeOf(feature.type, 'gene')) {
915969
contextMenuItemsForFeature.push({
916970
label: 'Filter alternate transcripts',
917971
onClick: () => {
@@ -922,7 +976,7 @@ function getContextMenuItems(
922976
handleClose: () => {
923977
doneCallback()
924978
},
925-
sourceFeature: relatedFeature,
979+
sourceFeature: feature,
926980
filteredTranscripts: getSnapshot(filteredTranscripts),
927981
onUpdate: (forms: string[]) => {
928982
display.updateFilteredTranscripts(forms)
@@ -934,7 +988,7 @@ function getContextMenuItems(
934988
})
935989
}
936990
menuItems.push({
937-
label: relatedFeature.type,
991+
label: feature.type,
938992
subMenu: contextMenuItemsForFeature,
939993
})
940994
}

0 commit comments

Comments
 (0)