Skip to content

Commit

Permalink
Create multi-region arc display type for variant tracks (#4045)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin authored Nov 6, 2023
1 parent c78d575 commit e396d2a
Show file tree
Hide file tree
Showing 30 changed files with 1,392 additions and 270 deletions.
29 changes: 13 additions & 16 deletions plugins/alignments/src/shared/BaseDisplayComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,19 @@ import { LinearReadCloudDisplayModel } from '../LinearReadCloudDisplay/model'
import { LinearReadArcsDisplayModel } from '../LinearReadArcsDisplay/model'
import { getContainingView } from '@jbrowse/core/util'

const useStyles = makeStyles()(theme => {
const bg = theme.palette.action.disabledBackground
return {
loading: {
backgroundColor: theme.palette.background.default,
backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, ${bg} 5px, ${bg} 10px)`,
position: 'absolute',
bottom: 0,
height: 50,
width: 300,
right: 0,
pointerEvents: 'none',
textAlign: 'center',
},
}
})
const useStyles = makeStyles()(theme => ({
loading: {
backgroundColor: theme.palette.background.default,
backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, ${theme.palette.action.disabledBackground} 5px, ${theme.palette.action.disabledBackground} 10px)`,
position: 'absolute',
bottom: 0,
height: 50,
width: 300,
right: 0,
pointerEvents: 'none',
textAlign: 'center',
},
}))

const BaseDisplayComponent = observer(function ({
model,
Expand Down
13 changes: 13 additions & 0 deletions plugins/arc/src/LinearPairedArcDisplay/afterAttach.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createAutorun } from './util'
import { fetchChains } from './fetchChains'
import { IAnyStateTreeNode } from 'mobx-state-tree'

export function doAfterAttach<T extends IAnyStateTreeNode>(self: T) {
createAutorun(
self,
async () => {
await fetchChains(self)
},
{ delay: 1000 },
)
}
212 changes: 212 additions & 0 deletions plugins/arc/src/LinearPairedArcDisplay/components/Arcs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import React, { useRef, useState } from 'react'
import { observer } from 'mobx-react'
import {
AbstractSessionModel,
Feature,
getContainingView,
getSession,
measureText,
} from '@jbrowse/core/util'
import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
import { Assembly } from '@jbrowse/core/assemblyManager/assembly'
import { getConf } from '@jbrowse/core/configuration'
import { parseBreakend } from '@gmod/vcf'

// local
import { LinearArcDisplayModel } from '../model'
import { Tooltip } from 'react-svg-tooltip'

type LGV = LinearGenomeViewModel

function f(feature: Feature, alt?: string) {
const bnd = alt ? parseBreakend(alt) : undefined
let start = feature.get('start')
let end = feature.get('end')
const strand = feature.get('strand')
const mate = feature.get('mate')
const refName = feature.get('refName')

let mateRefName: string | undefined
let mateEnd = 0
let mateStart = 0

// one sided bracket used, because there could be <INS:ME> and we just check
// startswith below
const symbolicAlleles = ['<TRA', '<DEL', '<INV', '<INS', '<DUP', '<CNV']
if (symbolicAlleles.some(a => alt?.startsWith(a))) {
// END is defined to be a single value, not an array. CHR2 not defined in
// VCF spec, but should be similar
const e = feature.get('INFO')?.END || feature.get('end')
mateEnd = e
mateStart = e - 1
mateRefName = feature.get('INFO')?.CHR2 ?? refName
// re-adjust the arc to be from start to end of feature by re-assigning end
// to the 'mate'
start = feature.get('start')
end = feature.get('start') + 1
} else if (bnd?.MatePosition) {
const matePosition = bnd.MatePosition.split(':')
mateEnd = +matePosition[1]
mateStart = +matePosition[1] - 1
mateRefName = matePosition[0]
}

return {
k1: { refName, start, end, strand },
k2: mate ?? { refName: mateRefName, end: mateEnd, start: mateStart },
}
}

function makeSummary(feature: Feature, alt?: string) {
return [
feature.get('name'),
feature.get('id'),
feature.get('INFO')?.SVTYPE,
alt,
]
.filter(f => !!f)
.join(' - ')
}

const Arc = observer(function ({
model,
feature,
alt,
assembly,
view,
}: {
feature: Feature
alt?: string
model: LinearArcDisplayModel
assembly: Assembly
session: AbstractSessionModel
view: LinearGenomeViewModel
}) {
const [mouseOvered, setMouseOvered] = useState(false)
const { height } = model
const { k1, k2 } = f(feature, alt)
const ref = useRef<SVGPathElement>(null)
const c = getConf(model, 'color', { feature, alt })
const ra1 = assembly.getCanonicalRefName(k1.refName) || k1.refName
const ra2 = assembly.getCanonicalRefName(k2.refName) || k2.refName
const p1 = k1.start
const p2 = k2.start
const r1 = view.bpToPx({ refName: ra1, coord: p1 })?.offsetPx
const r2 = view.bpToPx({ refName: ra2, coord: p2 })?.offsetPx
const caption = makeSummary(feature, alt)
const tooltipWidth = 20 + measureText(caption)

if (r1 !== undefined && r2 !== undefined) {
const radius = (r2 - r1) / 2
const absrad = Math.abs(radius)
const destY = Math.min(height, absrad)
const p1 = r1 - view.offsetPx
const p2 = r2 - view.offsetPx
const left = p1
const right = p2

return (
<>
<path
d={`M ${left} 0 C ${left} ${destY}, ${right} ${destY}, ${right} 0`}
ref={ref}
stroke={mouseOvered ? 'red' : c}
strokeWidth={3}
onMouseOut={() => setMouseOvered(false)}
onMouseOver={() => setMouseOvered(true)}
onClick={() => model.selectFeature(feature)}
fill="none"
pointerEvents="stroke"
/>
<Tooltip triggerRef={ref}>
<rect
x={12}
y={0}
width={tooltipWidth}
height={20}
rx={5}
ry={5}
fill="black"
fillOpacity="50%"
/>
<text
x={22}
y={14}
fontSize={10}
fill="white"
textLength={tooltipWidth - 20}
>
{caption}
</text>
</Tooltip>
</>
)
}
return null
})

const Wrapper = observer(function ({
model,
exportSVG,
children,
}: {
model: LinearArcDisplayModel
exportSVG?: boolean
children: React.ReactNode
}) {
const { height } = model
const view = getContainingView(model) as LGV
const width = Math.round(view.dynamicBlocks.totalWidthPx)
return exportSVG ? (
<>{children}</>
) : (
<svg width={width} height={height}>
{children}
</svg>
)
})

const Arcs = observer(function ({
model,
exportSVG,
}: {
model: LinearArcDisplayModel
exportSVG?: boolean
}) {
const view = getContainingView(model) as LGV
const session = getSession(model)
const { assemblyManager } = session
const { features } = model
const assembly = assemblyManager.get(view.assemblyNames[0])

return assembly ? (
<Wrapper model={model} exportSVG={exportSVG}>
{features?.map(f => {
const alts = f.get('ALT') as string[] | undefined
return (
alts?.map(a => (
<Arc
key={f.id() + '-' + a}
session={session}
feature={f}
alt={a}
view={view}
model={model}
assembly={assembly}
/>
)) ?? (
<Arc
session={session}
feature={f}
view={view}
model={model}
assembly={assembly}
/>
)
)
})}
</Wrapper>
) : null
})

export default Arcs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react'
import { LoadingEllipses } from '@jbrowse/core/ui'
import { BlockMsg } from '@jbrowse/plugin-linear-genome-view'
import { makeStyles } from 'tss-react/mui'
import { observer } from 'mobx-react'

// local
import { LinearArcDisplayModel } from '../model'

const useStyles = makeStyles()(theme => ({
loading: {
backgroundColor: theme.palette.background.default,
backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, ${theme.palette.action.disabledBackground} 5px, ${theme.palette.action.disabledBackground} 10px)`,
position: 'absolute',
bottom: 0,
height: 50,
width: 300,
right: 0,
pointerEvents: 'none',
textAlign: 'center',
},
}))

const BaseDisplayComponent = observer(function ({
model,
children,
}: {
model: LinearArcDisplayModel
children?: React.ReactNode
}) {
const { error, regionTooLarge } = model
return error ? (
<BlockMsg
message={`${error}`}
severity="error"
buttonText="Reload"
action={model.reload}
/>
) : regionTooLarge ? (
model.regionCannotBeRendered()
) : (
<DataDisplay model={model}>{children}</DataDisplay>
)
})

const DataDisplay = observer(function ({
model,
children,
}: {
model: LinearArcDisplayModel
children?: React.ReactNode
}) {
const { loading } = model
return (
<div>
{children}
{loading ? <LoadingBar model={model} /> : null}
</div>
)
})

const LoadingBar = observer(function ({
model,
}: {
model: LinearArcDisplayModel
}) {
const { classes } = useStyles()
const { message } = model
return (
<div className={classes.loading}>
<LoadingEllipses message={message} />
</div>
)
})

export default BaseDisplayComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import { observer } from 'mobx-react'

// local
import { LinearArcDisplayModel } from '../model'
import BaseDisplayComponent from './BaseDisplayComponent'
import Arcs from './Arcs'

const LinearArcReactComponent = observer(function ({
model,
exportSVG,
}: {
model: LinearArcDisplayModel
exportSVG?: boolean
}) {
return (
<BaseDisplayComponent model={model}>
<Arcs model={model} exportSVG={exportSVG} />
</BaseDisplayComponent>
)
})

export default LinearArcReactComponent
29 changes: 29 additions & 0 deletions plugins/arc/src/LinearPairedArcDisplay/configSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ConfigurationSchema } from '@jbrowse/core/configuration'
import { baseLinearDisplayConfigSchema } from '@jbrowse/plugin-linear-genome-view'

/**
* #config LinearArcDisplay
*/
export function configSchemaFactory(name: string) {
return ConfigurationSchema(
name,
{
/**
* #slot
*/
color: {
type: 'color',
description: 'the color of the arcs',
defaultValue: 'jexl:defaultPairedArcColor(feature,alt)',
contextVariable: ['feature', 'alt'],
},
},
{
/**
* #baseConfiguration
*/
baseConfiguration: baseLinearDisplayConfigSchema,
explicitlyTyped: true,
},
)
}
Loading

0 comments on commit e396d2a

Please sign in to comment.