Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better inversion visualization to read vs reference visualizations #2198

Merged
merged 11 commits into from
Aug 10, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ function SupplementaryAlignments(props: { tag: string; model: any }) {
const locString = `${saRef}:${Math.max(1, start - extra)}-${
end + extra
}`
const displayString = `${saRef}:${start}-${end} (${saStrand})`
const displayStart = start.toLocaleString('en-US')
const displayEnd = end.toLocaleString('en-US')
const displayString = `${saRef}:${displayStart}-${displayEnd} (${saStrand})`
return (
<li key={`${locString}-${index}`}>
<Link
Expand Down
6 changes: 3 additions & 3 deletions plugins/dotplot-view/src/DotplotRenderer/DotplotRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class DotplotRenderer extends ComparativeServerSideRendererType {
;(hview.features || []).forEach(feature => {
let start = feature.get('start')
let end = feature.get('end')
const strand = feature.get('strand')
const strand = feature.get('strand') || 1
const refName = feature.get('refName')
const mate = feature.get('mate')
const mateRef = mate.refName
Expand Down Expand Up @@ -92,10 +92,10 @@ export default class DotplotRenderer extends ComparativeServerSideRendererType {
const prevY = currY

if (op === 'M' || op === '=' || op === 'X') {
currX += val / hview.bpPerPx
currX += (val / hview.bpPerPx) * strand
currY += val / vview.bpPerPx
} else if (op === 'D' || op === 'N') {
currX += val / hview.bpPerPx
currX += (val / hview.bpPerPx) * strand
} else if (op === 'I') {
currY += val / vview.bpPerPx
}
Expand Down
102 changes: 85 additions & 17 deletions plugins/dotplot-view/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,71 @@ function getClip(cigar: string, strand: number) {
: +(cigar.match(/^(\d+)([SH])/) || [])[1] || 0
}

function getTag(f: Feature, tag: string) {
const tags = f.get('tags')
return tags ? tags[tag] : f.get(tag)
}

function mergeIntervals<T extends { start: number; end: number }>(
intervals: T[],
w = 5000,
) {
// test if there are at least 2 intervals
if (intervals.length <= 1) {
return intervals
}

const stack = []
let top = null

// sort the intervals based on their start values
intervals = intervals.sort((a, b) => a.start - b.start)

// push the 1st interval into the stack
stack.push(intervals[0])

// start from the next interval and merge if needed
for (let i = 1; i < intervals.length; i++) {
// get the top element
top = stack[stack.length - 1]

// if the current interval doesn't overlap with the
// stack top element, push it to the stack
if (top.end + w < intervals[i].start - w) {
stack.push(intervals[i])
}
// otherwise update the end value of the top element
// if end of current interval is higher
else if (top.end < intervals[i].end) {
top.end = Math.max(top.end, intervals[i].end)
stack.pop()
stack.push(top)
}
}

return stack
}

interface BasicFeature {
end: number
start: number
refName: string
}

function gatherOverlaps(regions: BasicFeature[]) {
const groups = regions.reduce((memo, x) => {
if (!memo[x.refName]) {
memo[x.refName] = []
}
memo[x.refName].push(x)
return memo
}, {} as { [key: string]: BasicFeature[] })

return Object.values(groups)
.map(group => mergeIntervals(group.sort((a, b) => a.start - b.start)))
.flat()
}

interface ReducedFeature {
refName: string
start: number
Expand Down Expand Up @@ -183,13 +248,11 @@ export default class DotplotPlugin extends Plugin {
icon: AddIcon,
onClick: () => {
const session = getSession(display)
const clipPos = feature.get('clipPos')
const cigar = feature.get('CIGAR')
const clipPos = getClip(cigar, 1)
const flags = feature.get('flags')
const SA: string =
(feature.get('tags')
? feature.get('tags').SA
: feature.get('SA')) || ''
const origStrand = feature.get('strand')
const SA: string = getTag(feature, 'SA') || ''
const readName = feature.get('name')
const readAssembly = `${readName}_assembly`
const [trackAssembly] = getConf(parentTrack, 'assemblyNames')
Expand All @@ -204,7 +267,10 @@ export default class DotplotPlugin extends Plugin {
const saLength = getLength(saCigar)
const saLengthSansClipping = getLengthSansClipping(saCigar)
const saStrandNormalized = saStrand === '-' ? -1 : 1
const saClipPos = getClip(saCigar, saStrandNormalized)
const saClipPos = getClip(
saCigar,
saStrandNormalized * origStrand,
)
const saRealStart = +saStart - 1
return {
refName: saRef,
Expand All @@ -214,7 +280,7 @@ export default class DotplotPlugin extends Plugin {
clipPos: saClipPos,
CIGAR: saCigar,
assemblyName: trackAssembly,
strand: 1, // saStrandNormalized,
strand: origStrand * saStrandNormalized,
uniqueId: `${feature.id()}_SA${index}`,
mate: {
start: saClipPos,
Expand Down Expand Up @@ -257,16 +323,18 @@ export default class DotplotPlugin extends Plugin {
hview: {
offsetPx: 0,
bpPerPx: refLength / 800,
minimumBlockWidth: 0,
interRegionPaddingWidth: 0,
displayedRegions: features.map(f => {
return {
start: f.start,
end: f.end,
refName: f.refName,
assemblyName: trackAssembly,
}
}),
displayedRegions: gatherOverlaps(
features.map((f, index) => {
const { start, end, refName } = f
return {
start,
end,
refName,
index,
assemblyName: trackAssembly,
}
}),
),
},
vview: {
offsetPx: 0,
Expand Down
120 changes: 89 additions & 31 deletions plugins/linear-comparative-view/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,66 @@ function getTag(f: Feature, tag: string) {
return tags ? tags[tag] : f.get(tag)
}

function mergeIntervals<T extends { start: number; end: number }>(
intervals: T[],
w = 5000,
) {
// test if there are at least 2 intervals
if (intervals.length <= 1) {
return intervals
}

const stack = []
let top = null

// sort the intervals based on their start values
intervals = intervals.sort((a, b) => a.start - b.start)

// push the 1st interval into the stack
stack.push(intervals[0])

// start from the next interval and merge if needed
for (let i = 1; i < intervals.length; i++) {
// get the top element
top = stack[stack.length - 1]

// if the current interval doesn't overlap with the
// stack top element, push it to the stack
if (top.end + w < intervals[i].start - w) {
stack.push(intervals[i])
}
// otherwise update the end value of the top element
// if end of current interval is higher
else if (top.end < intervals[i].end) {
top.end = Math.max(top.end, intervals[i].end)
stack.pop()
stack.push(top)
}
}

return stack
}

interface BasicFeature {
end: number
start: number
refName: string
}

function gatherOverlaps(regions: BasicFeature[]) {
const groups = regions.reduce((memo, x) => {
if (!memo[x.refName]) {
memo[x.refName] = []
}
memo[x.refName].push(x)
return memo
}, {} as { [key: string]: BasicFeature[] })

return Object.values(groups)
.map(group => mergeIntervals(group.sort((a, b) => a.start - b.start)))
.flat()
}

function WindowSizeDlg(props: {
feature: Feature
handleClose: () => void
Expand All @@ -179,8 +239,9 @@ function WindowSizeDlg(props: {
useEffect(() => {
let done = false
;(async () => {
if (preFeature.get('flags') & 2048) {
const SA: string = getTag(preFeature, 'SA') || ''
let p = preFeature
if (p.get('flags') & 2048) {
const SA: string = getTag(p, 'SA') || ''
const primaryAln = SA.split(';')[0]
const [saRef, saStart] = primaryAln.split(',')
const { rpcManager } = getSession(track)
Expand All @@ -191,16 +252,12 @@ function WindowSizeDlg(props: {
sessionId,
region: { refName: saRef, start: +saStart - 1, end: +saStart },
})) as any[]
const primaryFeat = feats.find(
f =>
f.get('name') === preFeature.get('name') &&
!(f.get('flags') & 2048),
p = feats.find(
f => f.get('name') === p.get('name') && !(f.get('flags') & 2048),
)
if (!done) {
setPrimaryFeature(primaryFeat)
}
} else {
setPrimaryFeature(preFeature)
}
if (!done) {
setPrimaryFeature(p)
}
})()

Expand All @@ -211,19 +268,22 @@ function WindowSizeDlg(props: {

function onSubmit() {
try {
const feature = primaryFeature || preFeature
if (!primaryFeature) {
return
}
const feature = primaryFeature
const session = getSession(track)
const view = getContainingView(track)
const cigar = feature.get('CIGAR')
const clipPos = getClip(cigar, 1)
const flags = feature.get('flags')
const qual = feature.get('qual') as string
const origStrand = feature.get('strand')
const SA: string = getTag(feature, 'SA') || ''
const readName = feature.get('name')

// the suffix -temp is used in the beforeDetach handler to
// automatically remove itself from the session when this view is
// destroyed
// the suffix -temp is used in the beforeDetach handler to automatically
// remove itself from the session when this view is destroyed
const readAssembly = `${readName}_assembly-temp`
const [trackAssembly] = getConf(track, 'assemblyNames')
const assemblyNames = [trackAssembly, readAssembly]
Expand All @@ -239,12 +299,12 @@ function WindowSizeDlg(props: {
const supplementaryAlignments = SA.split(';')
.filter(aln => !!aln)
.map((aln, index) => {
const [saRef, saStart, , saCigar] = aln.split(',')
const [saRef, saStart, saStrand, saCigar] = aln.split(',')
const saLengthOnRef = getLengthOnRef(saCigar)
const saLength = getLength(saCigar)
const saLengthSansClipping = getLengthSansClipping(saCigar)
// const saStrandNormalized = saStrand === '-' ? -1 : 1
const saClipPos = getClip(saCigar, 1)
const saStrandNormalized = saStrand === '-' ? -1 : 1
const saClipPos = getClip(saCigar, saStrandNormalized * origStrand)
const saRealStart = +saStart - 1
return {
refName: saRef,
Expand All @@ -254,7 +314,7 @@ function WindowSizeDlg(props: {
clipPos: saClipPos,
CIGAR: saCigar,
assemblyName: trackAssembly,
strand: 1, // saStrandNormalized,
strand: origStrand * saStrandNormalized,
uniqueId: `${feature.id()}_SA${index}`,
mate: {
start: saClipPos,
Expand Down Expand Up @@ -309,16 +369,14 @@ function WindowSizeDlg(props: {

const seqTrackId = `${readName}_${Date.now()}`
const sequenceTrackConf = getConf(assembly, 'sequence')
const lgvRegions = features
.map(f => {
return {
...f,
start: Math.max(0, f.start - windowSize),
end: f.end + windowSize,
assemblyName: trackAssembly,
}
})
.sort((a, b) => a.clipPos - b.clipPos)
const lgvRegions = gatherOverlaps(
features.map(f => ({
...f,
start: Math.max(0, f.start - windowSize),
end: f.end + windowSize,
assemblyName: trackAssembly,
})),
)

session.addAssembly?.({
name: `${readAssembly}`,
Expand Down Expand Up @@ -361,7 +419,7 @@ function WindowSizeDlg(props: {
{
id: `${Math.random()}`,
type: 'LinearReferenceSequenceDisplay',
showReverse: false,
showReverse: true,
showTranslation: false,
height: 35,
configuration: `${seqTrackId}-LinearReferenceSequenceDisplay`,
Expand Down Expand Up @@ -392,7 +450,7 @@ function WindowSizeDlg(props: {
{
id: `${Math.random()}`,
type: 'LinearReferenceSequenceDisplay',
showReverse: false,
showReverse: true,
showTranslation: false,
height: 35,
configuration: `${seqTrackId}-LinearReferenceSequenceDisplay`,
Expand Down
7 changes: 7 additions & 0 deletions products/jbrowse-web/src/sessionModelFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ export default function sessionModelFactory(
},

addAssembly(assemblyConfig: AnyConfigurationModel) {
const asm = self.sessionAssemblies.find(
f => f.name === assemblyConfig.name,
)
if (asm) {
console.warn(`Assembly ${assemblyConfig.name} was already existing`)
return asm
}
self.sessionAssemblies.push(assemblyConfig)
},
addSessionPlugin(plugin: JBrowsePlugin) {
Expand Down
Loading