Skip to content

Commit

Permalink
[skip ci] Reduce re-drawing on alignments track paired read arcs/clou…
Browse files Browse the repository at this point in the history
…d display types (#3695)
  • Loading branch information
cmdcolin committed May 18, 2023
1 parent 3f5e7bf commit dddc616
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 423 deletions.
Original file line number Diff line number Diff line change
@@ -1,56 +1,34 @@
import React from 'react'
import { isAlive } from 'mobx-state-tree'
import { makeStyles } from 'tss-react/mui'
import React, { useCallback } from 'react'
import { observer } from 'mobx-react'
import { getContainingView } from '@jbrowse/core/util'
import { LoadingEllipses } from '@jbrowse/core/ui'
import {
BlockMsg,
LinearGenomeViewModel,
} from '@jbrowse/plugin-linear-genome-view'
import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'

// local
import { LinearReadArcsDisplayModel } from '../model'
import BaseDisplayComponent from '../../shared/BaseDisplayComponent'

type LGV = LinearGenomeViewModel

const useStyles = makeStyles()(theme => {
const bg = theme.palette.action.disabledBackground
return {
loading: {
paddingLeft: '0.6em',
backgroundColor: theme.palette.background.default,
backgroundImage: `repeating-linear-gradient(45deg, transparent, transparent 5px, ${bg} 5px, ${bg} 10px)`,
height: '100%',
width: '100%',
pointerEvents: 'none',
textAlign: 'center',
},
}
})

const Arcs = observer(function ({
model,
}: {
model: LinearReadArcsDisplayModel
}) {
const view = getContainingView(model) as LGV
const width = Math.round(view.dynamicBlocks.totalWidthPx)
const height = model.height
const cb = useCallback(
(ref: HTMLCanvasElement) => model.setRef(ref),
// eslint-disable-next-line react-hooks/exhaustive-deps
[model, width, height],
)
return (
<canvas
data-testid={`Arc-display-${model.drawn}`}
ref={ref => {
if (isAlive(model)) {
model.setRef(ref)
}
}}
style={{
position: 'absolute',
left: -view.offsetPx + model.lastDrawnOffsetPx,
width: view.dynamicBlocks.totalWidthPx,
height: model.height,
}}
width={view.dynamicBlocks.totalWidthPx * 2}
height={model.height * 2}
data-testid="arc-canvas"
ref={cb}
style={{ width, height }}
width={width * 2}
height={height * 2}
/>
)
})
Expand All @@ -60,29 +38,9 @@ export default observer(function ({
}: {
model: LinearReadArcsDisplayModel
}) {
const view = getContainingView(model)
const { classes } = useStyles()
const err = model.error
return err ? (
<BlockMsg
message={`${err}`}
severity="error"
buttonText={'Reload'}
action={model.reload}
/>
) : model.loading ? (
<div
className={classes.loading}
style={{
width: view.dynamicBlocks.totalWidthPx,
height: 20,
position: 'absolute',
left: Math.max(0, -view.offsetPx),
}}
>
<LoadingEllipses message={model.message} />
</div>
) : (
<Arcs model={model} />
return (
<BaseDisplayComponent model={model}>
<Arcs model={model} />
</BaseDisplayComponent>
)
})
90 changes: 51 additions & 39 deletions plugins/alignments/src/LinearReadArcsDisplay/drawFeats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,25 @@ interface CoreFeat {
end: number
}

export default async function drawFeats(
function drawLineAtOffset(
ctx: CanvasRenderingContext2D,
offset: number,
height: number,
color: string,
) {
// draws a vertical line off to middle of nowhere if the second end not found
ctx.strokeStyle = color
ctx.beginPath()
ctx.moveTo(offset, 0)
ctx.lineTo(offset, height)
ctx.stroke()
}

export default function drawFeats(
self: {
setLastDrawnOffsetPx: (n: number) => void
drawInter?: boolean
setDrawn: (arg: boolean) => void
drawLongRange?: boolean
setError: (e: unknown) => void
colorBy?: { type: string }
height: number
chainData?: ChainData
Expand All @@ -61,26 +74,14 @@ export default async function drawFeats(
}
const view = getContainingView(self) as LGV
const { assemblyManager } = getSession(self)
self.setLastDrawnOffsetPx(view.offsetPx)
ctx.lineWidth = lineWidthSetting
const { chains, stats } = chainData
const hasPaired = hasPairedReads(chainData)
const assemblyName = view.assemblyNames[0]
const asm = assemblyManager.get(assemblyName)
const asm = assemblyManager.get(view.assemblyNames[0])
const type = colorBy?.type || 'insertSizeAndOrientation'
if (!asm) {
return
}

function drawLineAtOffset(p: number, c: string) {
// draws a vertical line off to middle of nowhere if the second end not found
ctx.strokeStyle = c
ctx.beginPath()
ctx.moveTo(p, 0)
ctx.lineTo(p, height)
ctx.stroke()
}

ctx.lineWidth = lineWidthSetting
function draw(
k1: CoreFeat & { tlen?: number; pair_orientation?: string },
k2: CoreFeat,
Expand All @@ -104,19 +105,20 @@ export default async function drawFeats(
const absrad = Math.abs(radius)
const p = r1.offsetPx - view.offsetPx
const p2 = r2.offsetPx - view.offsetPx
const drawArcInsteadOfBezier = absrad > 10_000

// bezier (used for non-long-range arcs) requires moveTo before beginPath
// arc (used for long-range) requires moveTo after beginPath (or else a
// unwanted line at y=0 is rendered along with the arc)
if (longRange) {
if (longRange && drawArcInsteadOfBezier) {
ctx.moveTo(p, 0)
ctx.beginPath()
} else {
ctx.beginPath()
ctx.moveTo(p, 0)
}

if (longRange) {
if (longRange && drawArcInsteadOfBezier) {
ctx.strokeStyle = 'red'
} else {
if (hasPaired) {
Expand Down Expand Up @@ -150,11 +152,21 @@ export default async function drawFeats(
// avoid drawing gigantic circles that glitch out the rendering,
// instead draw vertical lines
if (absrad > 100_000) {
drawLineAtOffset(p + jitter(jitterVal), 'red')
drawLineAtOffset(p2 + jitter(jitterVal), 'red')
} else {
drawLineAtOffset(ctx, p + jitter(jitterVal), height, 'red')
drawLineAtOffset(ctx, p2 + jitter(jitterVal), height, 'red')
} else if (drawArcInsteadOfBezier) {
ctx.arc(p + radius + jitter(jitterVal), 0, absrad, 0, Math.PI)
ctx.stroke()
} else {
ctx.bezierCurveTo(
p + jitter(jitterVal),
destY,
destX,
destY,
destX + jitter(jitterVal),
0,
)
ctx.stroke()
}
} else {
ctx.bezierCurveTo(
Expand All @@ -168,25 +180,26 @@ export default async function drawFeats(
ctx.stroke()
}
} else if (r1 && drawInter) {
drawLineAtOffset(r1.offsetPx - view.offsetPx, 'purple')
drawLineAtOffset(ctx, r1.offsetPx - view.offsetPx, height, 'purple')
}
}

for (let i = 0; i < chains.length; i++) {
let chain = chains[i]
for (const chain of chains) {
if (chain.length === 1 && drawLongRange) {
// singleton feature
const f = chain[0]

// special case where we look at RPOS/RNEXT
if (hasPaired) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const refName = f.next_ref!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const coord = f.next_pos!
const coord = f.next_pos || 0
draw(
f,
{ refName, start: coord, end: coord, strand: f.strand },
{
refName: f.next_ref || '',
start: coord,
end: coord,
strand: f.strand,
},
asm,
true,
)
Expand All @@ -203,16 +216,15 @@ export default async function drawFeats(
}
}
} else {
if (!hasPaired) {
chain.sort((a, b) => a.clipPos - b.clipPos)
chain = chain.filter(f => !(f.flags & 256))
} else {
// ignore split/supplementary reads for hasPaired=true for now
chain = chain.filter(f => !(f.flags & 2048))
}
for (let i = 0; i < chain.length - 1; i++) {
draw(chain[i], chain[i + 1], asm, false)
const res = hasPaired
? chain.filter(f => !(f.flags & 2048))
: chain
.sort((a, b) => a.clipPos - b.clipPos)
.filter(f => !(f.flags & 256))
for (let i = 0; i < res.length - 1; i++) {
draw(res[i], res[i + 1], asm, false)
}
}
}
self.setDrawn(true)
}
Loading

0 comments on commit dddc616

Please sign in to comment.