Skip to content

Commit

Permalink
Add clickable navigation links to supplementary alignments/paired end…
Browse files Browse the repository at this point in the history
…s locations and BND/TRA endpoints in detail widgets (#1701)

* Add ability to navigate to individual split alignment sections from AlignmentsFeatureDetails

* Add view reference to feature details

* Ignore some any

* Fixup tests to use stateModelFactory

* Add some console warn ignores

* Add view to variantmodelfactory

* Connect to breakend endpoints

* Link to breakend endpoints for both tra and bnd types

* Add links to automatically launch breakpoints split view from variant detail panel

* Fix lint/tsc

* Try not too add too much data to the virtual dom with long keys

* Add formatter for bam/cram paired end links
  • Loading branch information
cmdcolin authored Feb 19, 2021
1 parent abb6514 commit 9123f2c
Show file tree
Hide file tree
Showing 21 changed files with 314 additions and 99 deletions.
6 changes: 3 additions & 3 deletions packages/core/BaseFeatureWidget/BaseFeatureDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export const BaseCoreDetails = (props: BaseProps) => {
interface AttributeProps {
attributes: Record<string, any>
omit?: string[]
formatter?: (val: unknown) => JSX.Element
formatter?: (val: unknown, key: string) => JSX.Element
descriptions?: Record<string, React.ReactNode>
prefix?: string
}
Expand Down Expand Up @@ -384,7 +384,7 @@ export const Attributes: FunctionComponent<AttributeProps> = props => {
<SimpleValue
key={key}
name={key}
value={formatter(value)}
value={formatter(value, key)}
description={description}
prefix={prefix}
/>
Expand All @@ -407,7 +407,7 @@ export interface BaseInputProps extends BaseCardProps {
omit?: string[]
model: any
descriptions?: Record<string, React.ReactNode>
formatter?: (val: unknown) => JSX.Element
formatter?: (val: unknown, key: string) => JSX.Element
}

const Subfeature = (props: BaseProps) => {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/BaseFeatureWidget/index.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { render } from '@testing-library/react'
import React from 'react'
import { stateModel } from '.'
import PluginManager from '../PluginManager'
import { stateModelFactory } from '.'
import { BaseFeatureDetails as ReactComponent } from './BaseFeatureDetail'

test('open up a widget', () => {
const model = stateModel.create({ type: 'BaseFeatureWidget' })
console.warn = jest.fn()
const pluginManager = new PluginManager([])
const model = stateModelFactory(pluginManager).create({
type: 'BaseFeatureWidget',
})
const { container, getByText } = render(<ReactComponent model={model} />)
model.setFeatureData({
start: 2,
Expand Down
36 changes: 21 additions & 15 deletions packages/core/BaseFeatureWidget/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { types } from 'mobx-state-tree'
import PluginManager from '../PluginManager'
import { ConfigurationSchema } from '../configuration'
import { ElementId } from '../util/types/mst'

const configSchema = ConfigurationSchema('BaseFeatureWidget', {})

const stateModel = types
.model('BaseFeatureWidget', {
id: ElementId,
type: types.literal('BaseFeatureWidget'),
featureData: types.frozen(),
})
.actions(self => ({
setFeatureData(data: Record<string, unknown>) {
self.featureData = data
},
clearFeatureData() {
self.featureData = undefined
},
}))
export default function stateModelFactory(pluginManager: PluginManager) {
return types
.model('BaseFeatureWidget', {
id: ElementId,
type: types.literal('BaseFeatureWidget'),
featureData: types.frozen(),
view: types.safeReference(
pluginManager.pluggableMstType('view', 'stateModel'),
),
})
.actions(self => ({
setFeatureData(data: Record<string, unknown>) {
self.featureData = data
},
clearFeatureData() {
self.featureData = undefined
},
}))
}

export { configSchema, stateModel }
export { configSchema, stateModelFactory }
export { BaseFeatureDetails as ReactComponent } from './BaseFeatureDetail'
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import Paper from '@material-ui/core/Paper'
import { observer, PropTypes as MobxPropTypes } from 'mobx-react'
import PropTypes from 'prop-types'
import React, { useState, FunctionComponent } from 'react'
import { Typography, Link, Paper } from '@material-ui/core'
import { observer } from 'mobx-react'
import { getSession } from '@jbrowse/core/util'
import React, { useState } from 'react'
import copy from 'copy-to-clipboard'
import {
BaseFeatureDetails,
BaseCard,
useStyles,
} from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'

interface AlnCardProps {
title?: string
}

interface AlnProps extends AlnCardProps {
feature: Record<string, any> // eslint-disable-line @typescript-eslint/no-explicit-any
}
import { parseCigar } from '../BamAdapter/MismatchParser'

const omit = ['clipPos', 'flags']

const AlignmentFlags: FunctionComponent<AlnProps> = props => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function AlignmentFlags(props: { feature: any }) {
const classes = useStyles()
const { feature } = props
const { flags } = feature
const flagNames = [
'read paired',
'read mapped in proper pair',
Expand All @@ -36,7 +31,6 @@ const AlignmentFlags: FunctionComponent<AlnProps> = props => {
'read is PCR or optical duplicate',
'supplementary alignment',
]
const { flags } = feature
return (
<BaseCard {...props} title="Flags">
<div style={{ display: 'flex' }}>
Expand All @@ -57,13 +51,6 @@ const AlignmentFlags: FunctionComponent<AlnProps> = props => {
</BaseCard>
)
}
AlignmentFlags.propTypes = {
feature: PropTypes.objectOf(PropTypes.any).isRequired,
}

interface AlnInputProps {
model: any // eslint-disable-line @typescript-eslint/no-explicit-any
}

function Formatter({ value }: { value: unknown }) {
const [show, setShow] = useState(false)
Expand All @@ -84,22 +71,112 @@ function Formatter({ value }: { value: unknown }) {
return <div>{display}</div>
}

const AlignmentFeatureDetails: FunctionComponent<AlnInputProps> = props => {
// utility function to get length of alignment from cigar
function getLengthOnRef(cigar: string) {
const cigarOps = parseCigar(cigar)
let lengthOnRef = 0
for (let i = 0; i < cigarOps.length; i += 2) {
const len = +cigarOps[i]
const op = cigarOps[i + 1]
if (op !== 'H' && op !== 'S' && op !== 'I') {
lengthOnRef += len
}
}
return lengthOnRef
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function SupplementaryAlignments(props: { tag: string; model: any }) {
const { tag, model } = props
const session = getSession(model)
return (
<BaseCard {...props} title="Supplementary alignments">
<Typography>List of supplementary alignment locations</Typography>
<ul>
{tag
.split(';')
.filter(SA => !!SA)
.map((SA, index) => {
const [saRef, saStart, saStrand, saCigar] = SA.split(',')
const saLength = getLengthOnRef(saCigar)
const extra = Math.floor(saLength / 5)
const start = +saStart
const end = +saStart + saLength
const locString = `${saRef}:${Math.max(1, start - extra)}-${
end + extra
}`
const displayString = `${saRef}:${start}-${end} (${saStrand})`
return (
<li key={`${locString}-${index}`}>
<Link
onClick={() => {
const { view } = model
if (view) {
view.navToLocString(locString)
} else {
session.notify(
'No view associated with this feature detail panel anymore',
'warning',
)
}
}}
href="#"
>
{displayString}
</Link>
</li>
)
})}
</ul>
</BaseCard>
)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function PairLink({ locString, model }: { locString: string; model: any }) {
const session = getSession(model)
return (
<Link
onClick={() => {
const { view } = model
if (view) {
view.navToLocString(locString)
} else {
session.notify(
'No view associated with this feature detail panel anymore',
'warning',
)
}
}}
href="#"
>
{locString}
</Link>
)
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function AlignmentFeatureDetails(props: { model: any }) {
const { model } = props
const feat = JSON.parse(JSON.stringify(model.featureData))
const SA = (feat.tags && feat.tags.SA) || feat.SA
return (
<Paper data-testid="alignment-side-drawer">
<BaseFeatureDetails
{...props}
omit={omit}
formatter={(value: unknown) => <Formatter value={value} />}
formatter={(value: unknown, key: string) => {
return key === 'next_segment_position' ? (
<PairLink model={model} locString={value as string} />
) : (
<Formatter value={value} />
)
}}
/>
{SA ? <SupplementaryAlignments model={model} tag={SA} /> : null}
<AlignmentFlags feature={feat} {...props} />
</Paper>
)
}
AlignmentFeatureDetails.propTypes = {
model: MobxPropTypes.objectOrObservableObject.isRequired,
}

export default observer(AlignmentFeatureDetails)
35 changes: 20 additions & 15 deletions plugins/alignments/src/AlignmentsFeatureDetail/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import { types } from 'mobx-state-tree'

const configSchema = ConfigurationSchema('AlignmentsFeatureWidget', {})

const stateModel = types
.model('AlignmentsFeatureWidget', {
id: ElementId,
type: types.literal('AlignmentsFeatureWidget'),
featureData: types.frozen(),
})
.actions(self => ({
setFeatureData(data) {
self.featureData = data
},
clearFeatureData() {
self.featureData = undefined
},
}))
export default function stateModelFactory(pluginManager) {
return types
.model('AlignmentsFeatureWidget', {
id: ElementId,
type: types.literal('AlignmentsFeatureWidget'),
featureData: types.frozen(),
view: types.safeReference(
pluginManager.pluggableMstType('view', 'stateModel'),
),
})
.actions(self => ({
setFeatureData(data) {
self.featureData = data
},
clearFeatureData() {
self.featureData = undefined
},
}))
}

export { configSchema, stateModel }
export { configSchema, stateModelFactory }
export { default as ReactComponent } from './AlignmentsFeatureDetail'
9 changes: 7 additions & 2 deletions plugins/alignments/src/AlignmentsFeatureDetail/index.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { render } from '@testing-library/react'
import React from 'react'
import { stateModel } from '.'
import PluginManager from '@jbrowse/core/PluginManager'
import { stateModelFactory } from '.'
import ReactComponent from './AlignmentsFeatureDetail'

test('open up a widget', () => {
const model = stateModel.create({ type: 'AlignmentsFeatureWidget' })
console.warn = jest.fn()
const pluginManager = new PluginManager([])
const model = stateModelFactory(pluginManager).create({
type: 'AlignmentsFeatureWidget',
})
model.setFeatureData({
seq:
'TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG',
Expand Down
2 changes: 1 addition & 1 deletion plugins/alignments/src/LinearPileupDisplay/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const stateModelFactory = (
const featureWidget = session.addWidget(
'AlignmentsFeatureWidget',
'alignmentFeature',
{ featureData: feature.toJSON() },
{ featureData: feature.toJSON(), view: getContainingView(self) },
)
session.showWidget(featureWidget)
}
Expand Down
1 change: 1 addition & 0 deletions plugins/alignments/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getSnapshot } from 'mobx-state-tree'
import ThisPlugin from '.'

test('plugin in a stock JBrowse', () => {
console.warn = jest.fn()
const pluginManager = new PluginManager([new ThisPlugin(), new SVG()])
pluginManager.createPluggableElements()
pluginManager.configure()
Expand Down
4 changes: 2 additions & 2 deletions plugins/alignments/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { LinearWiggleDisplayReactComponent } from '@jbrowse/plugin-wiggle'
import {
configSchema as alignmentsFeatureDetailConfigSchema,
ReactComponent as AlignmentsFeatureDetailReactComponent,
stateModel as alignmentsFeatureDetailStateModel,
stateModelFactory as alignmentsFeatureDetailStateModelFactory,
} from './AlignmentsFeatureDetail'
import BamAdapterF from './BamAdapter'
import * as MismatchParser from './BamAdapter/MismatchParser'
Expand Down Expand Up @@ -126,7 +126,7 @@ export default class AlignmentsPlugin extends Plugin {
name: 'AlignmentsFeatureWidget',
heading: 'Feature Details',
configSchema: alignmentsFeatureDetailConfigSchema,
stateModel: alignmentsFeatureDetailStateModel,
stateModel: alignmentsFeatureDetailStateModelFactory(pluginManager),
ReactComponent: AlignmentsFeatureDetailReactComponent,
}),
)
Expand Down
15 changes: 11 additions & 4 deletions plugins/breakpoint-split-view/src/BreakpointSplitView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ class BreakpointSplitViewType extends ViewType {

// TODO: Figure this out for multiple assembly names
const { assemblyName } = view.displayedRegions[0]
const assembly = getSession(view).assemblyManager.get(assemblyName)
const { assemblyManager } = getSession(view)
const assembly = assemblyManager.get(assemblyName)

if (!assembly) {
throw new Error(`assembly ${assemblyName} not found`)
}
if (!assembly.regions) {
throw new Error(`assembly ${assemblyName} regions not loaded`)
}
const { getCanonicalRefName } = assembly as Assembly
const featureRefName = getCanonicalRefName(feature.get('refName'))

const topRegion = view.displayedRegions.find(
const topRegion = assembly.regions.find(
f => f.refName === String(featureRefName),
)

Expand Down Expand Up @@ -61,7 +68,7 @@ class BreakpointSplitViewType extends ViewType {
return {}
}

const bottomRegion = view.displayedRegions.find(
const bottomRegion = assembly.regions.find(
f => f.refName === String(mateRefName),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe('ConfigurationEditor widget', () => {
})

it('renders with defaults of the PileupTrack schema', () => {
console.warn = jest.fn()
const pluginManager = new PluginManager([new Alignments(), new SVG()])
pluginManager.createPluggableElements()
pluginManager.configure()
Expand Down
Loading

0 comments on commit 9123f2c

Please sign in to comment.