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

Threshold for indicators on SNPCoverage + inverted bargraph of interbase counts for sub-threshold events #1687

Merged
merged 14 commits into from
Feb 19, 2021
Merged
12 changes: 7 additions & 5 deletions plugins/alignments/src/LinearPileupDisplay/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,12 +359,14 @@ const stateModelFactory = (
return filters
},

get rendererConfig() {
const configBlob =
getConf(self, ['renderers', self.rendererTypeName]) || {}
return self.rendererType.configSchema.create(configBlob)
},

get renderProps() {
const view = getContainingView(self) as LGV
const config = self.rendererType.configSchema.create(
getConf(self, ['renderers', self.rendererTypeName]) || {},
)

return {
...self.composedRenderProps,
...getParentRenderProps(self),
Expand All @@ -377,7 +379,7 @@ const stateModelFactory = (
colorTagMap: JSON.parse(JSON.stringify(self.colorTagMap)),
filters: this.filters,
showSoftClip: self.showSoftClipping,
config,
config: this.rendererConfig,
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function SNPCoverageConfigFactory(pluginManager: PluginManager) {
type: 'number',
description:
'round the upper value of the domain scale to the nearest N',
defaultValue: 20,
defaultValue: 10,
},

renderers: ConfigurationSchema('RenderersConfiguration', {
Expand Down
74 changes: 68 additions & 6 deletions plugins/alignments/src/LinearSNPCoverageDisplay/models/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { types, cast } from 'mobx-state-tree'
import { getConf } from '@jbrowse/core/configuration'
import { getConf, readConfObject } from '@jbrowse/core/configuration'
import { getParentRenderProps } from '@jbrowse/core/util/tracks'
import {
linearWiggleDisplayModelFactory,
Expand All @@ -25,6 +25,8 @@ const stateModelFactory = (
linearWiggleDisplayModelFactory(pluginManager, configSchema),
types.model({
type: types.literal('LinearSNPCoverageDisplay'),
drawInterbaseCounts: types.maybe(types.boolean),
drawIndicators: types.maybe(types.boolean),
filterBy: types.optional(
types.model({
flagInclude: types.optional(types.number, 0),
Expand All @@ -51,6 +53,43 @@ const stateModelFactory = (
self.filterBy = cast(filter)
},
}))
.views(self => ({
get rendererConfig() {
const configBlob =
getConf(self, ['renderers', self.rendererTypeName]) || {}

return self.rendererType.configSchema.create({
...configBlob,
drawInterbaseCounts:
self.drawInterbaseCounts === undefined
? configBlob.drawInterbaseCounts
: self.drawInterbaseCounts,
drawIndicators:
self.drawIndicators === undefined
? configBlob.drawIndicators
: self.drawIndicators,
})
},
get drawInterbaseCountsSetting() {
return self.drawInterbaseCounts !== undefined
? self.drawInterbaseCounts
: readConfObject(this.rendererConfig, 'drawInterbaseCounts')
},
get drawIndicatorsSetting() {
return self.drawIndicators !== undefined
? self.drawIndicators
: readConfObject(this.rendererConfig, 'drawIndicators')
},
}))
.actions(self => ({
toggleDrawIndicators() {
self.drawIndicators = !self.drawIndicatorsSetting
},
toggleDrawInterbaseCounts() {
self.drawInterbaseCounts = !self.drawInterbaseCountsSetting
},
}))

.views(self => ({
get TooltipComponent() {
return Tooltip
Expand All @@ -76,6 +115,33 @@ const stateModelFactory = (
return []
},

get composedTrackMenuItems() {
return [
{
type: 'subMenu',
label: 'SNPCoverageTrack settings',
subMenu: [
{
label: 'Draw insertion/clipping indicators',
type: 'checkbox',
checked: self.drawIndicatorsSetting,
onClick: () => {
self.toggleDrawIndicators()
},
},
{
label: 'Draw insertion/clipping counts',
type: 'checkbox',
checked: self.drawInterbaseCountsSetting,
onClick: () => {
self.toggleDrawInterbaseCounts()
},
},
],
},
]
},

// The SNPCoverage filters are called twice because the BAM/CRAM features
// pass filters and then the SNPCoverage score features pass through
// here, and those have no name/flags/tags so those are passed thru
Expand Down Expand Up @@ -125,10 +191,6 @@ const stateModelFactory = (
},

get renderProps() {
const config = self.rendererType.configSchema.create(
getConf(self, ['renderers', self.rendererTypeName]) || {},
)

return {
...self.composedRenderProps,
...getParentRenderProps(self),
Expand All @@ -138,7 +200,7 @@ const stateModelFactory = (
displayModel: self,
scaleOpts: this.scaleOpts,
filters: self.filters,
config,
config: self.rendererConfig,
}
},
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ test('several features', async () => {
bpPerPx: 3,
highResolutionScaling: 1,
height: 100,
config: {},
})

expect(result).toMatchSnapshot({
Expand Down
99 changes: 64 additions & 35 deletions plugins/alignments/src/SNPCoverageRenderer/SNPCoverageRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createJBrowseTheme } from '@jbrowse/core/ui'
import { featureSpanPx } from '@jbrowse/core/util'
import { Feature } from '@jbrowse/core/util/simpleFeature'
import { Region } from '@jbrowse/core/util/types'
import { readConfObject } from '@jbrowse/core/configuration'
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
import {
getOrigin,
Expand All @@ -14,7 +15,6 @@ import { ThemeOptions } from '@material-ui/core'

interface SNPCoverageRendererProps {
features: Map<string, Feature>
layout: any // eslint-disable-line @typescript-eslint/no-explicit-any
config: AnyConfigurationModel
regions: Region[]
bpPerPx: number
Expand Down Expand Up @@ -50,6 +50,7 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
scaleOpts,
height,
theme: configTheme,
config: cfg,
} = props
const theme = createJBrowseTheme(configTheme)

Expand All @@ -61,15 +62,16 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {

const originY = getOrigin(scaleOpts.scaleType)
const snpOriginY = getOrigin('linear')
const indicatorThreshold = readConfObject(cfg, 'indicatorThreshold')
const drawInterbaseCounts = readConfObject(cfg, 'drawInterbaseCounts')
const drawIndicators = readConfObject(cfg, 'drawIndicators')
const width = (region.end - region.start) / bpPerPx

const toY = (rawscore: number, curr = 0) =>
height - viewScale(rawscore) - curr
const snpToY = (rawscore: number, curr = 0) =>
height - snpViewScale(rawscore) - curr
const toHeight = (rawscore: number, curr = 0) =>
toY(originY) - toY(rawscore, curr)
const snpToHeight = (rawscore: number, curr = 0) =>
snpToY(snpOriginY) - snpToY(rawscore, curr)
const toY = (n: number, curr = 0) => height - viewScale(n) - curr
const snpToY = (n: number, curr = 0) => height - snpViewScale(n) - curr
const toHeight = (n: number, curr = 0) => toY(originY) - toY(n, curr)
const snpToHeight = (n: number, curr = 0) =>
snpToY(snpOriginY) - snpToY(n, curr)

const colorForBase: { [key: string]: string } = {
A: theme.palette.bases.A.main,
Expand All @@ -90,39 +92,66 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
const score = feature.get('score') as number
ctx.fillRect(leftPx, toY(score), rightPx - leftPx + 0.3, toHeight(score))
}
ctx.fillStyle = 'grey'
ctx.beginPath()
ctx.lineTo(0, 0)
ctx.moveTo(0, width)
ctx.stroke()

// Second pass: draw the SNP data, and add a minimum feature width of 1px
// which can be wider than the actual bpPerPx This reduces overdrawing of
// the grey background over the SNPs
for (const feature of features.values()) {
const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx)
const infoArray: BaseInfo[] = feature.get('snpinfo') || []
let curr = 0
infoArray.forEach(info => {
if (!info || info.base === 'reference' || info.base === 'total') {
return
}
ctx.fillStyle = colorForBase[info.base]
if (
info.base === 'insertion' ||
info.base === 'softclip' ||
info.base === 'hardclip'
) {
ctx.beginPath()
ctx.moveTo(leftPx - 3, 0)
ctx.lineTo(leftPx + 3, 0)
ctx.lineTo(leftPx, 4.5)
ctx.fill()
} else if (info.base !== 'deletion') {
ctx.fillRect(
leftPx,
snpToY(info.score + curr),
Math.max(rightPx - leftPx + 0.3, 1),
snpToHeight(info.score),
)
curr += info.score
}
})
const totalScore =
infoArray.find(info => info.base === 'total')?.score || 0

const w = Math.max(rightPx - leftPx + 0.3, 1)
infoArray
.filter(
({ base }) =>
base !== 'reference' &&
base !== 'total' &&
base !== 'deletion' &&
base !== 'insertion' &&
base !== 'softclip' &&
base !== 'hardclip',
)
.reduce((curr, info) => {
const { base, score } = info
ctx.fillStyle = colorForBase[base]
ctx.fillRect(leftPx, snpToY(score + curr), w, snpToHeight(score))
return curr + info.score
}, 0)

const interbaseEvents = infoArray.filter(
({ base }) =>
base === 'insertion' || base === 'softclip' || base === 'hardclip',
)

const indicatorHeight = 4.5
if (drawInterbaseCounts) {
interbaseEvents.reduce((curr, info) => {
const { score, base } = info
ctx.fillStyle = colorForBase[base]
ctx.fillRect(leftPx, indicatorHeight + curr, 2, snpToHeight(score))
return curr + info.score
}, 0)
}

if (drawIndicators) {
interbaseEvents.forEach(({ score, base }) => {
if (score > totalScore * indicatorThreshold && totalScore > 10) {
ctx.fillStyle = colorForBase[base]
ctx.beginPath()
ctx.moveTo(leftPx - 3, 0)
ctx.lineTo(leftPx + 3, 0)
ctx.lineTo(leftPx, indicatorHeight)
ctx.fill()
}
})
}
}
}
}
18 changes: 18 additions & 0 deletions plugins/alignments/src/SNPCoverageRenderer/configSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ export default ConfigurationSchema(
description: 'the color of the clipping marker',
defaultValue: 'red',
},
indicatorThreshold: {
type: 'number',
description:
'the proportion of reads containing a insertion/clip indicator',
defaultValue: 0.3,
},
drawInterbaseCounts: {
type: 'boolean',
description:
'draw count "upsidedown histogram" of the interbase events that don\'t contribute to the coverage count so are not drawn in the normal histogram',
defaultValue: true,
},
drawIndicators: {
type: 'boolean',
description:
'draw a triangular indicator where an event has been detected',
defaultValue: true,
},
},
{ explicitlyTyped: true },
)
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const BaseLinearDisplay = types
defaultDisplayHeight,
),
blockState: types.map(BlockState),
userBpPerPxLimit: types.maybe(types.number),
}),
)
.volatile(() => ({
Expand All @@ -56,7 +57,6 @@ export const BaseLinearDisplay = types
contextMenuFeature: undefined as undefined | Feature,
additionalContextMenuItemCallbacks: [] as Function[],
scrollTop: 0,
userBpPerPxLimit: undefined as undefined | number,
}))
.views(self => ({
/**
Expand Down
5 changes: 3 additions & 2 deletions plugins/wiggle/src/LinearWiggleDisplay/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ const stateModelFactory = (
bounds: [minScore, maxScore],
scaleType,
})
const headroom = getConf(self, 'headroom') || 0

// avoid weird scalebar if log value and empty region displayed
if (scaleType === 'log' && ret[1] === Number.MIN_VALUE) {
Expand All @@ -207,10 +208,10 @@ const stateModelFactory = (
// heuristic to just give some extra headroom on bigwig scores if no
// maxScore/minScore specified (they have MAX_VALUE/MIN_VALUE if so)
if (maxScore === Number.MAX_VALUE && ret[1] > 1.0) {
ret[1] = round(ret[1])
ret[1] = round(ret[1] + headroom)
}
if (minScore === Number.MIN_VALUE && ret[0] < -1.0) {
ret[0] = round(ret[0])
ret[0] = round(ret[0] - headroom)
}

// avoid returning a new object if it matches the old value
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading