Skip to content

Commit

Permalink
Use ontology to compare equivalent feature type terms (GMOD#477)
Browse files Browse the repository at this point in the history
* Load ontology synonyms into state tree

* Use isTypeOf for checking equivalent types

In `packages/apollo-mst/src/AnnotationFeatureModel.ts` we set the
session onject to type `any` and ignore the warnings and ignore warning

Add test file so_types.gff3 with unconventional types.

* Fix lint

* Change message looked for in test

* Update test indexeddb loading code

---------

Co-authored-by: Garrett Stevens <[email protected]>
  • Loading branch information
dariober and garrettjstevens authored Jan 22, 2025
1 parent dd3ace7 commit 8f1c5d7
Show file tree
Hide file tree
Showing 16 changed files with 279 additions and 94 deletions.
17 changes: 13 additions & 4 deletions packages/apollo-mst/src/AnnotationFeatureModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { intersection2 } from '@jbrowse/core/util'
import { getSession, intersection2 } from '@jbrowse/core/util'
import {
IAnyModelType,
IMSTMap,
Expand Down Expand Up @@ -127,7 +127,14 @@ export const AnnotationFeatureModel = types
return false
},
get transcriptParts(): TranscriptParts[] {
if (self.type !== 'mRNA') {
// 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
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
if (!featureTypeOntology.isTypeOf(self.type, 'mRNA')) {
throw new Error(
'Only features of type "mRNA" or equivalent can calculate CDS locations',
)
Expand All @@ -137,7 +144,8 @@ export const AnnotationFeatureModel = types
throw new Error('no CDS or exons in mRNA')
}
const cdsChildren = [...children.values()].filter(
(child) => child.type === 'CDS',
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(child) => featureTypeOntology.isTypeOf(child.type, 'CDS'),
)
if (cdsChildren.length === 0) {
throw new Error('no CDS in mRNA')
Expand All @@ -149,7 +157,8 @@ export const AnnotationFeatureModel = types
let hasIntersected = false
const exonLocations: TranscriptPartLocation[] = []
for (const [, child] of children) {
if (child.type === 'exon') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
if (!featureTypeOntology.isTypeOf(child.type, 'exon')) {
exonLocations.push({ min: child.min, max: child.max })
}
}
Expand Down
Binary file modified packages/jbrowse-plugin-apollo/cypress/data/go.json.gz
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Add Assembly', () => {

cy.intercept('/changes').as('changes')
cy.contains('Submit').click()
cy.contains('is being added', { timeout: 10_000 })
cy.contains('added successfully', { timeout: 10_000 })
cy.wait('@changes').its('response.statusCode').should('match', /2../)
})

Expand Down
19 changes: 18 additions & 1 deletion packages/jbrowse-plugin-apollo/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ async function loadOntology(
OntologyKey,
unknown[]
>
// @ts-expect-error could use more typing
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
ontologyData.meta[0].storeOptions.prefixes = new Map(
// @ts-expect-error could use more typing
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
Object.entries(ontologyData.meta[0].storeOptions.prefixes),
)
await openDB(name, version, {
async upgrade(database: IDBPDatabase): Promise<void> {
const meta = database.createObjectStore('meta')
Expand Down Expand Up @@ -76,7 +83,7 @@ Cypress.Commands.add('addOntologies', () => {
},
{
name: 'Sequence Ontology',
version: '3.1',
version: 'unversioned',
source: {
uri: 'http://localhost:9000/test_data/so-v3.1.json',
locationType: 'UriLocation',
Expand All @@ -94,6 +101,16 @@ Cypress.Commands.add('addOntologies', () => {
{ timeout: 120_000 },
)
})
cy.readFile('cypress/data/so.json.gz', null).then((soGZip: Buffer) => {
cy.wrap<Promise<void>>(
loadOntology(
soGZip,
'Apollo Ontology "Sequence Ontology" "unversioned"',
2,
),
{ timeout: 120_000 },
)
})
})

Cypress.Commands.add('addAssemblyFromGff', (assemblyName, fin) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { CanvasMouseEvent } from '../types'
import { Glyph } from './Glyph'
import { boxGlyph } from './BoxGlyph'
import { LinearApolloDisplayRendering } from '../stateModel/rendering'
import { OntologyRecord } from '../../OntologyManager'

let forwardFillLight: CanvasPattern | null = null
let backwardFillLight: CanvasPattern | null = null
Expand Down Expand Up @@ -80,6 +81,11 @@ function draw(
return
}
const { apolloSelectedFeature } = session
const { apolloDataStore } = session
const { featureTypeOntology } = apolloDataStore.ontologyManager
if (!featureTypeOntology) {
throw new Error('featureTypeOntology is undefined')
}

// Draw background for gene
const topLevelFeatureMinX =
Expand All @@ -93,7 +99,8 @@ function draw(
? topLevelFeatureMinX - topLevelFeatureWidthPx
: topLevelFeatureMinX
const topLevelFeatureTop = row * rowHeight
const topLevelFeatureHeight = getRowCount(feature) * rowHeight
const topLevelFeatureHeight =
getRowCount(feature, featureTypeOntology) * rowHeight

ctx.fillStyle = alpha(theme?.palette.background.paper ?? '#ffffff', 0.6)
ctx.fillRect(
Expand All @@ -106,7 +113,8 @@ function draw(
// Draw lines on different rows for each mRNA
let currentRow = 0
for (const [, mrna] of children) {
if (mrna.type !== 'mRNA') {
const isMrna = featureTypeOntology.isTypeOf(mrna.type, 'mRNA')
if (!isMrna) {
currentRow += 1
continue
}
Expand All @@ -115,7 +123,7 @@ function draw(
continue
}
for (const [, cds] of childrenOfmRNA) {
if (cds.type !== 'CDS') {
if (!featureTypeOntology.isTypeOf(cds.type, 'CDS')) {
continue
}
const minX =
Expand Down Expand Up @@ -144,7 +152,7 @@ function draw(
// Draw exon and CDS for each mRNA
currentRow = 0
for (const [, child] of children) {
if (child.type !== 'mRNA') {
if (!featureTypeOntology.isTypeOf(child.type, 'mRNA')) {
boxGlyph.draw(ctx, child, row, stateModel, displayedRegionIndex)
currentRow += 1
continue
Expand All @@ -155,7 +163,7 @@ function draw(
continue
}
for (const [, exon] of childrenOfmRNA) {
if (exon.type !== 'exon') {
if (!featureTypeOntology.isTypeOf(exon.type, 'exon')) {
continue
}
const minX =
Expand Down Expand Up @@ -296,7 +304,9 @@ function drawHover(
stateModel: LinearApolloDisplay,
ctx: CanvasRenderingContext2D,
) {
const { apolloHover, apolloRowHeight, lgv, theme } = stateModel
const { apolloHover, apolloRowHeight, lgv, session, theme } = stateModel
const { featureTypeOntology } = session.apolloDataStore.ontologyManager

if (!apolloHover) {
return
}
Expand All @@ -320,16 +330,26 @@ function drawHover(
const top = row * apolloRowHeight
const widthPx = length / bpPerPx
ctx.fillStyle = theme?.palette.action.selected ?? 'rgba(0,0,0,04)'
ctx.fillRect(startPx, top, widthPx, apolloRowHeight * getRowCount(feature))

if (!featureTypeOntology) {
throw new Error('featureTypeOntology is undefined')
}
ctx.fillRect(
startPx,
top,
widthPx,
apolloRowHeight * getRowCount(feature, featureTypeOntology),
)
}

function getFeatureFromLayout(
feature: AnnotationFeature,
bp: number,
row: number,
featureTypeOntology: OntologyRecord,
): AnnotationFeature | undefined {
const featureInThisRow: AnnotationFeature[] =
featuresForRow(feature)[row] || []
featuresForRow(feature, featureTypeOntology)[row] || []
for (const f of featureInThisRow) {
let featureObj
if (bp >= f.min && bp <= f.max && f.parent) {
Expand All @@ -339,9 +359,9 @@ function getFeatureFromLayout(
continue
}
if (
featureObj.type === 'CDS' &&
featureTypeOntology.isTypeOf(featureObj.type, 'CDS') &&
featureObj.parent &&
featureObj.parent.type === 'mRNA'
featureTypeOntology.isTypeOf(featureObj.parent.type, 'mRNA')
) {
const { cdsLocations } = featureObj.parent
for (const cdsLoc of cdsLocations) {
Expand All @@ -361,22 +381,28 @@ function getFeatureFromLayout(
return feature
}

function getRowCount(feature: AnnotationFeature, _bpPerPx?: number): number {
function getRowCount(
feature: AnnotationFeature,
featureTypeOntology: OntologyRecord,
_bpPerPx?: number,
): number {
const { children, type } = feature
if (!children) {
return 1
}
const isMrna = featureTypeOntology.isTypeOf(type, 'mRNA')
let rowCount = 0
if (type === 'mRNA') {
if (isMrna) {
for (const [, child] of children) {
if (child.type === 'CDS') {
const isCds = featureTypeOntology.isTypeOf(child.type, 'CDS')
if (isCds) {
rowCount += 1
}
}
return rowCount
}
for (const [, child] of children) {
rowCount += getRowCount(child)
rowCount += getRowCount(child, featureTypeOntology)
}
return rowCount
}
Expand All @@ -387,8 +413,12 @@ function getRowCount(feature: AnnotationFeature, _bpPerPx?: number): number {
* If the row contains an mRNA, the order is CDS -\> exon -\> mRNA -\> gene
* If the row does not contain an mRNA, the order is subfeature -\> gene
*/
function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] {
if (feature.type !== 'gene') {
function featuresForRow(
feature: AnnotationFeature,
featureTypeOntology: OntologyRecord,
): AnnotationFeature[][] {
const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene')
if (!isGene) {
throw new Error('Top level feature for GeneGlyph must have type "gene"')
}
const { children } = feature
Expand All @@ -397,7 +427,7 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] {
}
const features: AnnotationFeature[][] = []
for (const [, child] of children) {
if (child.type !== 'mRNA') {
if (!featureTypeOntology.isTypeOf(child.type, 'mRNA')) {
features.push([child, feature])
continue
}
Expand All @@ -407,9 +437,9 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] {
const cdss: AnnotationFeature[] = []
const exons: AnnotationFeature[] = []
for (const [, grandchild] of child.children) {
if (grandchild.type === 'CDS') {
if (featureTypeOntology.isTypeOf(grandchild.type, 'CDS')) {
cdss.push(grandchild)
} else if (grandchild.type === 'exon') {
} else if (featureTypeOntology.isTypeOf(grandchild.type, 'exon')) {
exons.push(grandchild)
}
}
Expand All @@ -423,8 +453,9 @@ function featuresForRow(feature: AnnotationFeature): AnnotationFeature[][] {
function getRowForFeature(
feature: AnnotationFeature,
childFeature: AnnotationFeature,
featureTypeOntology: OntologyRecord,
) {
const rows = featuresForRow(feature)
const rows = featuresForRow(feature, featureTypeOntology)
for (const [idx, row] of rows.entries()) {
if (row.some((feature) => feature._id === childFeature._id)) {
return idx
Expand Down Expand Up @@ -496,7 +527,16 @@ function getDraggableFeatureInfo(
feature: AnnotationFeature,
stateModel: LinearApolloDisplay,
): { feature: AnnotationFeature; edge: 'min' | 'max' } | undefined {
if (feature.type === 'gene' || feature.type === 'mRNA') {
const { session } = stateModel
const { apolloDataStore } = session
const { featureTypeOntology } = apolloDataStore.ontologyManager
if (!featureTypeOntology) {
throw new Error('featureTypeOntology is undefined')
}
const isGene = featureTypeOntology.isTypeOf(feature.type, 'gene')
const isMrna = featureTypeOntology.isTypeOf(feature.type, 'mRNA')
const isCds = featureTypeOntology.isTypeOf(feature.type, 'CDS')
if (isGene || isMrna) {
return
}
const { bp, refName, regionNumber, x } = mousePosition
Expand All @@ -519,14 +559,19 @@ function getDraggableFeatureInfo(
if (Math.abs(maxPx - x) < 4) {
return { feature, edge: 'max' }
}
if (feature.type === 'CDS') {
if (isCds) {
const mRNA = feature.parent
if (!mRNA?.children) {
return
}
const exonChildren = [...mRNA.children.values()].filter(
(child) => child.type === 'exon',
)
const exonChildren: AnnotationFeature[] = []
for (const child of mRNA.children.values()) {
const childIsExon = featureTypeOntology.isTypeOf(child.type, 'exon')
if (childIsExon) {
exonChildren.push(child)
}
}

const overlappingExon = exonChildren.find((child) => {
const [start, end] = intersection2(bp, bp + 1, child.min, child.max)
return start !== undefined && end !== undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import {
} from '../stateModel/mouseEvents'
import { LinearApolloDisplayRendering } from '../stateModel/rendering'
import { CanvasMouseEvent } from '../types'
import { OntologyRecord } from '../../OntologyManager'

export interface Glyph {
/** @returns number of layout rows used by this glyph with this feature and zoom level */
getRowCount(feature: AnnotationFeature, bpPerPx: number): number
getRowCount(
feature: AnnotationFeature,
featureTypeOntology: OntologyRecord,
bpPerPx: number,
): number
/** draw the feature's primary rendering on the canvas */
draw(
ctx: CanvasRenderingContext2D,
Expand All @@ -24,10 +29,12 @@ export interface Glyph {
feature: AnnotationFeature,
bp: number,
row: number,
featureTypeOntology: OntologyRecord,
): AnnotationFeature | undefined
getRowForFeature(
feature: AnnotationFeature,
childFeature: AnnotationFeature,
featureTypeOntology: OntologyRecord,
): number | undefined

drawHover(
Expand Down
Loading

0 comments on commit 8f1c5d7

Please sign in to comment.