Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LinearApolloSixFrameDisplay #560

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
33c09cd
Copy LinearDisplay as new SixFrameDisplay base
garrettjstevens Jan 7, 2025
693db5f
Break away LinearApolloSixFrameDisplayComponent
haessar Jan 13, 2025
b6ba581
horizontal bar to divide forward/reverse strands
haessar Jan 20, 2025
bec3625
Copy LinearDisplay as new SixFrameDisplay base
garrettjstevens Jan 7, 2025
503b586
Break away LinearApolloSixFrameDisplayComponent
haessar Jan 13, 2025
e998ef9
horizontal bar to divide forward/reverse strands
haessar Jan 20, 2025
62be46d
Merge branch 'non_block_six_frame_display' of https://github.com/GMOD…
haessar Feb 4, 2025
006c091
remove redundant GeneGlyph
haessar Feb 4, 2025
ba16941
make getGlyph an instance method in line with LinearApolloDisplay (bu…
haessar Feb 4, 2025
c1473eb
fix getGlyph in mouseEvents
haessar Feb 4, 2025
b06a7b3
Copy LinearDisplay as new SixFrameDisplay base
garrettjstevens Jan 7, 2025
d420623
Break away LinearApolloSixFrameDisplayComponent
haessar Jan 13, 2025
f26d424
horizontal bar to divide forward/reverse strands
haessar Jan 20, 2025
8824a09
remove redundant GeneGlyph
haessar Feb 4, 2025
61abd1e
make getGlyph an instance method in line with LinearApolloDisplay (bu…
haessar Feb 4, 2025
1878239
fix getGlyph in mouseEvents
haessar Feb 4, 2025
35091ff
Merge branch 'non_block_six_frame_display' of https://github.com/GMOD…
haessar Feb 11, 2025
9ee73d0
reinstate GeneGlyph
haessar Feb 12, 2025
ee1f243
Remove unnecessary seq highighting mouse event
haessar Feb 21, 2025
e9a8068
Remove unnecessary seq rendering in Annotation pane
haessar Feb 23, 2025
110cd43
WIP rendering lines between glyphs
haessar Feb 23, 2025
500c79e
render differently coloured intron lines per mRNA, each with a "hat"
haessar Feb 23, 2025
822c75b
fix CDS features dropping down a row
haessar Mar 5, 2025
2aa3114
one uniform colour for all intron lines (matching Artemis teal)
haessar Mar 5, 2025
5addceb
Fix TrackLine position to permanent number of rows
haessar Mar 5, 2025
152ecb8
move position of getRowCount function to prevent "name not recognised…
haessar Mar 12, 2025
435b62d
comment out redundant code for drawing single-row lines
haessar Mar 12, 2025
1f780bb
new GeneGlyph hover logic
haessar Mar 12, 2025
7ec79be
include OntologyRecord checks in line with LinearApolloDisplay
haessar Mar 28, 2025
993f0c5
migrate filter features menu item
haessar Mar 28, 2025
e28421c
new logic for rendering CDS features and correctly highlighting the o…
haessar Mar 28, 2025
9d7d33e
remove redundant glyphs
haessar Apr 2, 2025
9d90d43
highlight intron lines on mouse hover
haessar Apr 2, 2025
227d0ad
tidy up
haessar Apr 2, 2025
95a9b41
Fixed tooltip to show info about CDS and parent mRNA
haessar Apr 2, 2025
ecf2b30
fix getFeatureLayoutPosition
haessar Apr 2, 2025
369fcd9
Moved looksLikeGene to AnnotationFeatureModel (for both LASFD and LAD)
haessar Apr 2, 2025
a1ea39a
fix to allow scrollSelectedFeatureIntoView to function
haessar Apr 2, 2025
930f965
Replace array with LayoutRow interface
haessar Apr 3, 2025
a861f78
reduce CDS height to ensure a gap between rows
haessar Apr 3, 2025
89dc6d3
fix glyph drawing bug
haessar Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,6 @@ export class JBrowseService {
},
},
},
displays: [
{
type: 'LinearApolloDisplay',
displayId: `${trackId}-LinearApolloDisplay`,
},
{
type: 'SixFrameFeatureDisplay',
displayId: `${trackId}-SixFrameFeatureDisplay`,
},
],
}
})
}
Expand Down
40 changes: 40 additions & 0 deletions packages/apollo-mst/src/AnnotationFeatureModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,46 @@ export const AnnotationFeatureModel = types
transcript.filter((transcriptPart) => transcriptPart.type === 'CDS'),
)
},
get looksLikeGene(): boolean {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
const session = getSession(self) as any
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { apolloDataStore } = session
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const { featureTypeOntology } = apolloDataStore.ontologyManager
if (!featureTypeOntology) {
return false
}
const children = self.children as Children
if (!children?.size) {
return false
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
if (!featureTypeOntology.isTypeOf(self.type, 'gene')) {
return false
}
for (const [, child] of children) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
const { children: grandChildren } = child
if (!grandChildren?.size) {
return false
}
const hasCDS = [...grandChildren.values()].some((grandchild) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
featureTypeOntology.isTypeOf(grandchild.type, 'CDS'),
)
const hasExon = [...grandChildren.values()].some((grandchild) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
featureTypeOntology.isTypeOf(grandchild.type, 'exon'),
)
if (hasCDS && hasExon) {
return true
}
}
}
return false
},
}))
.actions((self) => ({
setAttributes(attributes: Map<string, string[]>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,47 +63,14 @@ export function layoutsModelFactory(
})
},
getGlyph(feature: AnnotationFeature) {
if (this.looksLikeGene(feature)) {
if (feature.looksLikeGene) {
return geneGlyph
}
if (feature.children?.size) {
return genericChildGlyph
}
return boxGlyph
},
looksLikeGene(feature: AnnotationFeature): boolean {
const { featureTypeOntology } =
self.session.apolloDataStore.ontologyManager
if (!featureTypeOntology) {
return false
}
const { children } = feature
if (!children?.size) {
return false
}
const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene')
if (!isGene) {
return false
}
for (const [, child] of children) {
if (featureTypeOntology.isTypeOf(child.type, 'transcript')) {
const { children: grandChildren } = child
if (!grandChildren?.size) {
return false
}
const hasCDS = [...grandChildren.values()].some((grandchild) =>
featureTypeOntology.isTypeOf(grandchild.type, 'CDS'),
)
const hasExon = [...grandChildren.values()].some((grandchild) =>
featureTypeOntology.isTypeOf(grandchild.type, 'exon'),
)
if (hasCDS && hasExon) {
return true
}
}
}
return false
},
}))
.actions((self) => ({
addSeenFeature(feature: AnnotationFeature) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import { Menu, MenuItem } from '@jbrowse/core/ui'
import {
AbstractSessionModel,
doesIntersect2,
getContainingView,
} from '@jbrowse/core/util'
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
import ErrorIcon from '@mui/icons-material/Error'
import { Alert, Avatar, Tooltip, useTheme } from '@mui/material'
import { observer } from 'mobx-react'
import React, { useEffect, useState } from 'react'
import { makeStyles } from 'tss-react/mui'

import { LinearApolloSixFrameDisplay as LinearApolloSixFrameDisplayI } from '../stateModel'
import { TrackLines } from './TrackLines'

interface LinearApolloSixFrameDisplayProps {
model: LinearApolloSixFrameDisplayI
}
export type Coord = [number, number]

const useStyles = makeStyles()((theme) => ({
canvasContainer: {
position: 'relative',
left: 0,
},
canvas: {
position: 'absolute',
left: 0,
},
ellipses: {
textOverflow: 'ellipsis',
overflow: 'hidden',
},
avatar: {
position: 'absolute',
color: theme.palette.warning.light,
backgroundColor: theme.palette.warning.contrastText,
},
}))

export const LinearApolloSixFrameDisplay = observer(
function LinearApolloSixFrameDisplay(
props: LinearApolloSixFrameDisplayProps,
) {
const theme = useTheme()
const { model } = props
const {
apolloRowHeight,
contextMenuItems: getContextMenuItems,
cursor,
featuresHeight,
isShown,
onMouseDown,
onMouseLeave,
onMouseMove,
onMouseUp,
regionCannotBeRendered,
session,
setCanvas,
setCollaboratorCanvas,
setOverlayCanvas,
setSeqTrackCanvas,
setSeqTrackOverlayCanvas,
setTheme,
} = model
const { classes } = useStyles()
const lgv = getContainingView(model) as unknown as LinearGenomeViewModel

useEffect(() => {
setTheme(theme)
}, [theme, setTheme])
const [contextCoord, setContextCoord] = useState<Coord>()
const [contextMenuItems, setContextMenuItems] = useState<MenuItem[]>([])
const message = regionCannotBeRendered()
if (!isShown) {
return null
}
const { assemblyManager } = session as unknown as AbstractSessionModel
return (
<>
{lgv.bpPerPx <= 3 ? (
<div
className={classes.canvasContainer}
style={{
width: lgv.dynamicBlocks.totalWidthPx,
height: lgv.bpPerPx <= 1 ? 125 : 95,
}}
>
<canvas
ref={async (node: HTMLCanvasElement) => {
await Promise.resolve()
setSeqTrackCanvas(node)
}}
width={lgv.dynamicBlocks.totalWidthPx}
height={lgv.bpPerPx <= 1 ? 125 : 95}
className={classes.canvas}
data-testid="seqTrackCanvas"
/>
<canvas
ref={async (node: HTMLCanvasElement) => {
await Promise.resolve()
setSeqTrackOverlayCanvas(node)
}}
width={lgv.dynamicBlocks.totalWidthPx}
height={lgv.bpPerPx <= 1 ? 125 : 95}
className={classes.canvas}
data-testid="seqTrackOverlayCanvas"
/>
</div>
) : null}
<div
className={classes.canvasContainer}
style={{
width: lgv.dynamicBlocks.totalWidthPx,
height: featuresHeight,
}}
onContextMenu={(event) => {
event.preventDefault()
if (contextMenuItems.length > 0) {
// There's already a context menu open, so close it
setContextMenuItems([])
} else {
const coord: [number, number] = [event.clientX, event.clientY]
setContextCoord(coord)
setContextMenuItems(getContextMenuItems(coord))
}
}}
>
{message ? (
<Alert severity="warning" classes={{ message: classes.ellipses }}>
<Tooltip title={message}>
<div>{message}</div>
</Tooltip>
</Alert>
) : (
// Promise.resolve() in these 3 callbacks is to avoid infinite rendering loop
// https://github.com/mobxjs/mobx/issues/3728#issuecomment-1715400931
<>
<TrackLines model={model} />
<canvas
ref={async (node: HTMLCanvasElement) => {
await Promise.resolve()
setCollaboratorCanvas(node)
}}
width={lgv.dynamicBlocks.totalWidthPx}
height={featuresHeight}
className={classes.canvas}
data-testid="collaboratorCanvas"
/>
<canvas
ref={async (node: HTMLCanvasElement) => {
await Promise.resolve()
setCanvas(node)
}}
width={lgv.dynamicBlocks.totalWidthPx}
height={featuresHeight}
className={classes.canvas}
data-testid="canvas"
/>
<canvas
ref={async (node: HTMLCanvasElement) => {
await Promise.resolve()
setOverlayCanvas(node)
}}
width={lgv.dynamicBlocks.totalWidthPx}
height={featuresHeight}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
className={classes.canvas}
style={{ cursor: cursor ?? 'default' }}
data-testid="overlayCanvas"
/>
{lgv.displayedRegions.flatMap((region, idx) => {
const assembly = assemblyManager.get(region.assemblyName)
return [...session.apolloDataStore.checkResults.values()]
.filter(
(checkResult) =>
assembly?.isValidRefName(checkResult.refSeq) &&
assembly.getCanonicalRefName(checkResult.refSeq) ===
region.refName &&
doesIntersect2(
region.start,
region.end,
checkResult.start,
checkResult.end,
),
)
.map((checkResult) => {
const left =
(lgv.bpToPx({
refName: region.refName,
coord: checkResult.start,
regionNumber: idx,
})?.offsetPx ?? 0) - lgv.offsetPx
const [feature] = checkResult.ids
if (!feature || !feature.parent?.looksLikeGene) {
return null
}
const top = 0
const height = apolloRowHeight
return (
<Tooltip
key={checkResult._id}
title={checkResult.message}
>
<Avatar
className={classes.avatar}
style={{ top, left, height, width: height }}
>
<ErrorIcon />
</Avatar>
</Tooltip>
)
})
})}
<Menu
open={contextMenuItems.length > 0}
onMenuItemClick={(_, callback) => {
callback()
setContextMenuItems([])
}}
onClose={() => {
setContextMenuItems([])
}}
TransitionProps={{
onExit: () => {
setContextMenuItems([])
},
}}
anchorReference="anchorPosition"
anchorPosition={
contextCoord
? { top: contextCoord[1], left: contextCoord[0] }
: undefined
}
style={{ zIndex: theme.zIndex.tooltip }}
menuItems={contextMenuItems}
/>
</>
)}
</div>
</>
)
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { observer } from 'mobx-react'
import React from 'react'

import { LinearApolloSixFrameDisplay } from '../stateModel'

export const TrackLines = observer(function TrackLines({
model,
}: {
model: LinearApolloSixFrameDisplay
}) {
const { apolloRowHeight } = model
return (
<div
style={{
position: 'absolute',
left: 0,
top: (apolloRowHeight * 6) / 2,
width: '100%',
}}
>
<hr style={{ margin: 0, top: 0, color: 'black' }} />
</div>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './LinearApolloSixFrameDisplay'
export * from './TrackLines'
Loading