-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create multi-region arc display type for variant tracks (#4045)
- Loading branch information
Showing
30 changed files
with
1,392 additions
and
270 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
212
plugins/arc/src/LinearPairedArcDisplay/components/Arcs.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
76 changes: 76 additions & 0 deletions
76
plugins/arc/src/LinearPairedArcDisplay/components/BaseDisplayComponent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
23 changes: 23 additions & 0 deletions
23
plugins/arc/src/LinearPairedArcDisplay/components/ReactComponent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
) | ||
} |
Oops, something went wrong.