From 2b97edaef35e0b39734ab2b5ab015fdc4c6448f4 Mon Sep 17 00:00:00 2001 From: Elliot Hershberg Date: Thu, 7 Jan 2021 20:06:37 -0800 Subject: [PATCH] Provide a dialog to add extra genomic context for linear read vs. ref visualization (#1560) * Provide genomic context for linear vs. ref * Add window size dialog * Add explainer and form to increase window size Co-authored-by: Colin --- .../components/ColorByTag.tsx | 65 ++- .../components/FilterByTag.tsx | 166 +++--- .../components/SortByTag.tsx | 54 +- plugins/linear-comparative-view/package.json | 4 +- plugins/linear-comparative-view/src/index.ts | 404 -------------- plugins/linear-comparative-view/src/index.tsx | 495 ++++++++++++++++++ 6 files changed, 634 insertions(+), 554 deletions(-) delete mode 100644 plugins/linear-comparative-view/src/index.ts create mode 100644 plugins/linear-comparative-view/src/index.tsx diff --git a/plugins/alignments/src/LinearPileupDisplay/components/ColorByTag.tsx b/plugins/alignments/src/LinearPileupDisplay/components/ColorByTag.tsx index d33aa1ad33..844966f28b 100644 --- a/plugins/alignments/src/LinearPileupDisplay/components/ColorByTag.tsx +++ b/plugins/alignments/src/LinearPileupDisplay/components/ColorByTag.tsx @@ -55,41 +55,36 @@ export default function ColorByTagDlg(props: { for minimap2 read strand, HP for haplotype, RG for read group, etc. -
- { - setTag(event.target.value) - }} - placeholder="Enter tag name" - inputProps={{ - maxLength: 2, - 'data-testid': 'color-tag-name-input', - }} - error={tag.length === 2 && !validTag} - helperText={ - tag.length === 2 && !validTag ? 'Not a valid tag' : '' - } - autoComplete="off" - data-testid="color-tag-name" - /> - - + { + setTag(event.target.value) + }} + placeholder="Enter tag name" + inputProps={{ + maxLength: 2, + 'data-testid': 'color-tag-name-input', + }} + error={tag.length === 2 && !validTag} + helperText={tag.length === 2 && !validTag ? 'Not a valid tag' : ''} + autoComplete="off" + data-testid="color-tag-name" + /> + diff --git a/plugins/alignments/src/LinearPileupDisplay/components/FilterByTag.tsx b/plugins/alignments/src/LinearPileupDisplay/components/FilterByTag.tsx index fc3703d041..05d9c25c45 100644 --- a/plugins/alignments/src/LinearPileupDisplay/components/FilterByTag.tsx +++ b/plugins/alignments/src/LinearPileupDisplay/components/FilterByTag.tsx @@ -128,92 +128,90 @@ export default observer( for details
-
- -
-
- Read must have ALL these flags - -
-
- Read must have NONE of these flags - -
+ +
+
+ Read must have ALL these flags +
- - - - Filter by tag name and value. Use * in the value field to get - all reads containing any value for that tag. Example: filter - tag name SA with value * to get all split/supplementary reads - - { - setTag(event.target.value) - }} - placeholder="Enter tag name" - inputProps={{ - maxLength: 2, - 'data-testid': 'color-tag-name-input', - }} - error={tag.length === 2 && !validTag} - helperText={ - tag.length === 2 && !validTag ? 'Not a valid tag' : '' - } - data-testid="color-tag-name" - /> - { - setTagValue(event.target.value) - }} - placeholder="Enter tag value" - inputProps={{ - 'data-testid': 'color-tag-name-input', - }} - data-testid="color-tag-value" - /> - - - Filter by read name - { - setReadName(event.target.value) - }} - placeholder="Enter read name" - inputProps={{ - 'data-testid': 'color-tag-readname-input', - }} - data-testid="color-tag-readname" - /> - -
+
+ + + Filter by tag name and value. Use * in the value field to get + all reads containing any value for that tag. Example: filter tag + name SA with value * to get all split/supplementary reads + + { + setTag(event.target.value) + }} + placeholder="Enter tag name" + inputProps={{ + maxLength: 2, + 'data-testid': 'color-tag-name-input', + }} + error={tag.length === 2 && !validTag} + helperText={ + tag.length === 2 && !validTag ? 'Not a valid tag' : '' + } + data-testid="color-tag-name" + /> + { + setTagValue(event.target.value) }} - > - Submit - - + placeholder="Enter tag value" + inputProps={{ + 'data-testid': 'color-tag-name-input', + }} + data-testid="color-tag-value" + /> + + + Filter by read name + { + setReadName(event.target.value) + }} + placeholder="Enter read name" + inputProps={{ + 'data-testid': 'color-tag-readname-input', + }} + data-testid="color-tag-readname" + /> + +
diff --git a/plugins/alignments/src/LinearPileupDisplay/components/SortByTag.tsx b/plugins/alignments/src/LinearPileupDisplay/components/SortByTag.tsx index a006cfbcdf..c3955f9972 100644 --- a/plugins/alignments/src/LinearPileupDisplay/components/SortByTag.tsx +++ b/plugins/alignments/src/LinearPileupDisplay/components/SortByTag.tsx @@ -50,35 +50,31 @@ export default function ColorByTagDlg(props: {
Set the tag to sort by -
- { - setTag(event.target.value) - }} - placeholder="Enter tag name" - inputProps={{ - maxLength: 2, - 'data-testid': 'sort-tag-name-input', - }} - error={tag.length === 2 && !validTag} - helperText={ - tag.length === 2 && !validTag ? 'Not a valid tag' : '' - } - autoComplete="off" - data-testid="sort-tag-name" - /> - - + { + setTag(event.target.value) + }} + placeholder="Enter tag name" + inputProps={{ + maxLength: 2, + 'data-testid': 'sort-tag-name-input', + }} + error={tag.length === 2 && !validTag} + helperText={tag.length === 2 && !validTag ? 'Not a valid tag' : ''} + autoComplete="off" + data-testid="sort-tag-name" + /> +
diff --git a/plugins/linear-comparative-view/package.json b/plugins/linear-comparative-view/package.json index 5463dd7f33..c1a8c27ceb 100644 --- a/plugins/linear-comparative-view/package.json +++ b/plugins/linear-comparative-view/package.json @@ -16,8 +16,8 @@ }, "author": "JBrowse Team", "distMain": "dist/index.js", - "srcMain": "src/index.ts", - "main": "src/index.ts", + "srcMain": "src/index.tsx", + "main": "src/index.tsx", "distModule": "dist/plugin-linear-comparative-view.esm.js", "module": "", "files": [ diff --git a/plugins/linear-comparative-view/src/index.ts b/plugins/linear-comparative-view/src/index.ts deleted file mode 100644 index 9df6929c68..0000000000 --- a/plugins/linear-comparative-view/src/index.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { ConfigurationSchema, getConf } from '@jbrowse/core/configuration' -import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' -import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' -import { - createBaseTrackConfig, - createBaseTrackModel, -} from '@jbrowse/core/pluggableElementTypes/models' -import TrackType from '@jbrowse/core/pluggableElementTypes/TrackType' -import Plugin from '@jbrowse/core/Plugin' -import PluginManager from '@jbrowse/core/PluginManager' -import { - AbstractSessionModel, - getSession, - isAbstractMenuManager, -} from '@jbrowse/core/util' -import { Feature } from '@jbrowse/core/util/simpleFeature' -import { MismatchParser } from '@jbrowse/plugin-alignments' -import AddIcon from '@material-ui/icons/Add' -import CalendarIcon from '@material-ui/icons/CalendarViewDay' -import { autorun } from 'mobx' -import { IAnyStateTreeNode } from 'mobx-state-tree' -import { - configSchemaFactory as linearComparativeDisplayConfigSchemaFactory, - ReactComponent as LinearComparativeDisplayReactComponent, - stateModelFactory as linearComparativeDisplayStateModelFactory, -} from './LinearComparativeDisplay' -import LinearComparativeViewFactory from './LinearComparativeView' -import { - configSchemaFactory as linearSyntenyDisplayConfigSchemaFactory, - stateModelFactory as linearSyntenyDisplayStateModelFactory, -} from './LinearSyntenyDisplay' -import LinearSyntenyRenderer, { - configSchema as linearSyntenyRendererConfigSchema, - ReactComponent as LinearSyntenyRendererReactComponent, -} from './LinearSyntenyRenderer' -import LinearSyntenyViewFactory from './LinearSyntenyView' -import { - AdapterClass as MCScanAnchorsAdapter, - configSchema as MCScanAnchorsConfigSchema, -} from './MCScanAnchorsAdapter' - -const { parseCigar } = MismatchParser - -interface Track { - id: string - type: string - displays: { - addAdditionalContextMenuItemCallback: Function - additionalContextMenuItemCallbacks: Function[] - id: string - type: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - PileupDisplay: any - }[] -} -interface View { - tracks: Track[] - views?: View[] - type: string -} -interface Session { - views: View[] -} -function getLengthOnRef(cigar: string) { - const cigarOps = parseCigar(cigar) - let lengthOnRef = 0 - for (let i = 0; i < cigarOps.length; i += 2) { - const len = +cigarOps[i] - const op = cigarOps[i + 1] - if (op !== 'H' && op !== 'S' && op !== 'I') { - lengthOnRef += len - } - } - return lengthOnRef -} - -function getLength(cigar: string) { - const cigarOps = parseCigar(cigar) - let length = 0 - for (let i = 0; i < cigarOps.length; i += 2) { - const len = +cigarOps[i] - const op = cigarOps[i + 1] - if (op !== 'D' && op !== 'N') { - length += len - } - } - return length -} - -function getLengthSansClipping(cigar: string) { - const cigarOps = parseCigar(cigar) - let length = 0 - for (let i = 0; i < cigarOps.length; i += 2) { - const len = +cigarOps[i] - const op = cigarOps[i + 1] - if (op !== 'H' && op !== 'S' && op !== 'D' && op !== 'N') { - length += len - } - } - return length -} -function getClip(cigar: string, strand: number) { - return strand === -1 - ? +(cigar.match(/(\d+)[SH]$/) || [])[1] || 0 - : +(cigar.match(/^(\d+)([SH])/) || [])[1] || 0 -} - -interface ReducedFeature { - refName: string - start: number - clipPos: number - end: number - seqLength: number - syntenyId?: number - uniqueId: string - mate: { - refName: string - start: number - end: number - syntenyId?: number - uniqueId?: string - } -} - -export default class extends Plugin { - name = 'LinearComparativeViewPlugin' - - install(pluginManager: PluginManager) { - pluginManager.addViewType(() => - pluginManager.jbrequire(LinearComparativeViewFactory), - ) - pluginManager.addViewType(() => - pluginManager.jbrequire(LinearSyntenyViewFactory), - ) - - pluginManager.addTrackType(() => { - const configSchema = ConfigurationSchema( - 'SyntenyTrack', - {}, - { baseConfiguration: createBaseTrackConfig(pluginManager) }, - ) - return new TrackType({ - name: 'SyntenyTrack', - configSchema, - stateModel: createBaseTrackModel( - pluginManager, - 'SyntenyTrack', - configSchema, - ), - }) - }) - pluginManager.addDisplayType(() => { - const configSchema = linearComparativeDisplayConfigSchemaFactory( - pluginManager, - ) - return new DisplayType({ - name: 'LinearComparativeDisplay', - configSchema, - stateModel: linearComparativeDisplayStateModelFactory(configSchema), - trackType: 'SyntenyTrack', - viewType: 'LinearComparativeView', - ReactComponent: LinearComparativeDisplayReactComponent, - }) - }) - pluginManager.addDisplayType(() => { - const configSchema = linearSyntenyDisplayConfigSchemaFactory( - pluginManager, - ) - return new DisplayType({ - name: 'LinearSyntenyDisplay', - configSchema, - stateModel: linearSyntenyDisplayStateModelFactory(configSchema), - trackType: 'SyntenyTrack', - viewType: 'LinearSyntenyView', - ReactComponent: LinearComparativeDisplayReactComponent, - }) - }) - pluginManager.addAdapterType( - () => - new AdapterType({ - name: 'MCScanAnchorsAdapter', - configSchema: MCScanAnchorsConfigSchema, - AdapterClass: MCScanAnchorsAdapter, - }), - ) - pluginManager.addRendererType( - () => - new LinearSyntenyRenderer({ - name: 'LinearSyntenyRenderer', - configSchema: linearSyntenyRendererConfigSchema, - ReactComponent: LinearSyntenyRendererReactComponent, - }), - ) - } - - configure(pluginManager: PluginManager) { - if (isAbstractMenuManager(pluginManager.rootModel)) { - pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { - label: 'Linear synteny view', - icon: CalendarIcon, - onClick: (session: AbstractSessionModel) => { - session.addView('LinearSyntenyView', {}) - }, - }) - } - - const cb = (feature: Feature, track: IAnyStateTreeNode) => { - return feature - ? [ - { - label: 'Linear read vs ref', - icon: AddIcon, - onClick: () => { - const session = getSession(track) - const clipPos = feature.get('clipPos') - const cigar = feature.get('CIGAR') - const flags = feature.get('flags') - const SA: string = - (feature.get('tags') - ? feature.get('tags').SA - : feature.get('SA')) || '' - const readName = feature.get('name') - const readAssembly = `${readName}_assembly` - const trackAssembly = getConf( - // @ts-ignore - track.parentTrack, - 'assemblyNames', - )[0] - const assemblyNames = [trackAssembly, readAssembly] - const trackId = `track-${Date.now()}` - const trackName = `${readName}_vs_${trackAssembly}` - const supplementaryAlignments = SA.split(';') - .filter(aln => !!aln) - .map((aln, index) => { - 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, saStrandNormalized) - const saRealStart = +saStart - 1 + saClipPos - return { - refName: saRef, - start: saRealStart, - end: saRealStart + saLengthOnRef, - seqLength: saLength, - clipPos: saClipPos, - CIGAR: saCigar, - assemblyName: trackAssembly, - strand: saStrandNormalized, - uniqueId: `${feature.id()}_SA${index}`, - mate: { - start: saClipPos, - end: saClipPos + saLengthSansClipping, - refName: readName, - }, - } - }) - - const feat = feature.toJSON() - - feat.mate = { - refName: readName, - start: clipPos, - end: clipPos + getLengthSansClipping(cigar), - } - - // if secondary alignment or supplementary, calculate length from SA[0]'s CIGAR - // which is the primary alignments. otherwise it is the primary alignment just use - // seq.length if primary alignment - const totalLength = - // eslint-disable-next-line no-bitwise - flags & 2048 - ? getLength(supplementaryAlignments[0].CIGAR) - : getLength(cigar) - - const features = [ - feat, - ...supplementaryAlignments, - ] as ReducedFeature[] - - features.forEach((f, index) => { - f.syntenyId = index - f.mate.syntenyId = index - f.mate.uniqueId = `${f.uniqueId}_mate` - }) - features.sort((a, b) => a.clipPos - b.clipPos) - - // the config feature store includes synthetic mate features - // mapped to the read assembly - const configFeatureStore = features.concat( - // @ts-ignore - features.map(f => f.mate), - ) - - const refLength = features.reduce( - (a, f) => a + f.end - f.start, - 0, - ) - - session.addView('LinearSyntenyView', { - type: 'LinearSyntenyView', - views: [ - { - type: 'LinearGenomeView', - hideHeader: true, - offsetPx: 0, - bpPerPx: refLength / 800, - displayedRegions: features.map(f => { - return { - start: f.start, - end: f.end, - refName: f.refName, - assemblyName: trackAssembly, - } - }), - }, - { - type: 'LinearGenomeView', - hideHeader: true, - offsetPx: 0, - bpPerPx: totalLength / 800, - displayedRegions: [ - { - assemblyName: readAssembly, - start: 0, - end: totalLength, - refName: readName, - }, - ], - }, - ], - viewTrackConfigs: [ - { - type: 'SyntenyTrack', - assemblyNames, - adapter: { - type: 'FromConfigAdapter', - features: configFeatureStore, - }, - renderer: { - type: 'LinearSyntenyRenderer', - }, - trackId, - name: trackName, - }, - ], - tracks: [ - { - configuration: trackId, - type: 'SyntenyTrack', - displays: [ - { - type: 'LinearSyntenyDisplay', - configuration: `${trackId}-LinearSyntenyDisplay`, - }, - ], - }, - ], - displayName: `${readName} vs ${trackAssembly}`, - }) - }, - }, - ] - : [] - } - function addContextMenu(view: View) { - if (view.type === 'LinearGenomeView') { - view.tracks.forEach(track => { - if (track.type === 'AlignmentsTrack') { - track.displays.forEach(display => { - if ( - display.type === 'LinearPileupDisplay' && - !display.additionalContextMenuItemCallbacks.includes(cb) - ) { - display.addAdditionalContextMenuItemCallback(cb) - } else if ( - display.type === 'LinearAlignmentsDisplay' && - display.PileupDisplay && - !display.PileupDisplay.additionalContextMenuItemCallbacks.includes( - cb, - ) - ) { - display.PileupDisplay.addAdditionalContextMenuItemCallback(cb) - } - }) - } - }) - } - } - autorun(() => { - const session = pluginManager.rootModel?.session as Session | undefined - if (session) { - session.views.forEach(view => { - if (view.views) { - view.views.forEach(v => addContextMenu(v)) - } else { - addContextMenu(view) - } - }) - } - }) - } -} diff --git a/plugins/linear-comparative-view/src/index.tsx b/plugins/linear-comparative-view/src/index.tsx new file mode 100644 index 0000000000..28b2bfb63f --- /dev/null +++ b/plugins/linear-comparative-view/src/index.tsx @@ -0,0 +1,495 @@ +import { ConfigurationSchema, getConf } from '@jbrowse/core/configuration' +import AdapterType from '@jbrowse/core/pluggableElementTypes/AdapterType' +import DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType' +import { + createBaseTrackConfig, + createBaseTrackModel, +} from '@jbrowse/core/pluggableElementTypes/models' +import TrackType from '@jbrowse/core/pluggableElementTypes/TrackType' +import Plugin from '@jbrowse/core/Plugin' +import PluginManager from '@jbrowse/core/PluginManager' +import { + AbstractSessionModel, + getSession, + getContainingTrack, + getContainingView, + isAbstractMenuManager, +} from '@jbrowse/core/util' +import { Feature } from '@jbrowse/core/util/simpleFeature' +import { MismatchParser } from '@jbrowse/plugin-alignments' +import AddIcon from '@material-ui/icons/Add' +import CalendarIcon from '@material-ui/icons/CalendarViewDay' +import { autorun } from 'mobx' +import { + configSchemaFactory as linearComparativeDisplayConfigSchemaFactory, + ReactComponent as LinearComparativeDisplayReactComponent, + stateModelFactory as linearComparativeDisplayStateModelFactory, +} from './LinearComparativeDisplay' +import LinearComparativeViewFactory from './LinearComparativeView' +import { + configSchemaFactory as linearSyntenyDisplayConfigSchemaFactory, + stateModelFactory as linearSyntenyDisplayStateModelFactory, +} from './LinearSyntenyDisplay' +import LinearSyntenyRenderer, { + configSchema as linearSyntenyRendererConfigSchema, + ReactComponent as LinearSyntenyRendererReactComponent, +} from './LinearSyntenyRenderer' +import LinearSyntenyViewFactory from './LinearSyntenyView' +import { + AdapterClass as MCScanAnchorsAdapter, + configSchema as MCScanAnchorsConfigSchema, +} from './MCScanAnchorsAdapter' + +import React, { useState } from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Button from '@material-ui/core/Button' +import TextField from '@material-ui/core/TextField' +import Typography from '@material-ui/core/Typography' +import Dialog from '@material-ui/core/Dialog' +import DialogContent from '@material-ui/core/DialogContent' +import DialogTitle from '@material-ui/core/DialogTitle' +import IconButton from '@material-ui/core/IconButton' +import CloseIcon from '@material-ui/icons/Close' + +const { parseCigar } = MismatchParser + +interface Track { + id: string + type: string + displays: { + addAdditionalContextMenuItemCallback: Function + additionalContextMenuItemCallbacks: Function[] + id: string + type: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PileupDisplay: any + }[] +} +interface View { + tracks: Track[] + views?: View[] + type: string +} +interface Session { + views: View[] +} +function getLengthOnRef(cigar: string) { + const cigarOps = parseCigar(cigar) + let lengthOnRef = 0 + for (let i = 0; i < cigarOps.length; i += 2) { + const len = +cigarOps[i] + const op = cigarOps[i + 1] + if (op !== 'H' && op !== 'S' && op !== 'I') { + lengthOnRef += len + } + } + return lengthOnRef +} + +function getLength(cigar: string) { + const cigarOps = parseCigar(cigar) + let length = 0 + for (let i = 0; i < cigarOps.length; i += 2) { + const len = +cigarOps[i] + const op = cigarOps[i + 1] + if (op !== 'D' && op !== 'N') { + length += len + } + } + return length +} + +function getLengthSansClipping(cigar: string) { + const cigarOps = parseCigar(cigar) + let length = 0 + for (let i = 0; i < cigarOps.length; i += 2) { + const len = +cigarOps[i] + const op = cigarOps[i + 1] + if (op !== 'H' && op !== 'S' && op !== 'D' && op !== 'N') { + length += len + } + } + return length +} +function getClip(cigar: string, strand: number) { + return strand === -1 + ? +(cigar.match(/(\d+)[SH]$/) || [])[1] || 0 + : +(cigar.match(/^(\d+)([SH])/) || [])[1] || 0 +} + +interface ReducedFeature { + refName: string + start: number + clipPos: number + end: number + seqLength: number + syntenyId?: number + uniqueId: string + mate: { + refName: string + start: number + end: number + syntenyId?: number + uniqueId?: string + } +} + +const useStyles = makeStyles(theme => ({ + root: { + width: 300, + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, +})) + +function WindowSizeDlg(props: { + display: any + handleClose: () => void + track: any +}) { + const classes = useStyles() + const { + track, + display: { feature }, + handleClose, + } = props + const [window, setWindowSize] = useState('0') + const windowSize = +window + + function onSubmit() { + try { + const session = getSession(track) + const view = getContainingView(track) + const clipPos = feature.get('clipPos') + const cigar = feature.get('CIGAR') + const flags = feature.get('flags') + const SA: string = + (feature.get('tags') ? feature.get('tags').SA : feature.get('SA')) || '' + const readName = feature.get('name') + const readAssembly = `${readName}_assembly` + const [trackAssembly] = getConf(track, 'assemblyNames') + const assemblyNames = [trackAssembly, readAssembly] + const trackId = `track-${Date.now()}` + const trackName = `${readName}_vs_${trackAssembly}` + const supplementaryAlignments = SA.split(';') + .filter(aln => !!aln) + .map((aln, index) => { + 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, saStrandNormalized) + const saRealStart = +saStart - 1 + saClipPos + return { + refName: saRef, + start: saRealStart, + end: saRealStart + saLengthOnRef, + seqLength: saLength, + clipPos: saClipPos, + CIGAR: saCigar, + assemblyName: trackAssembly, + strand: saStrandNormalized, + uniqueId: `${feature.id()}_SA${index}`, + mate: { + start: saClipPos, + end: saClipPos + saLengthSansClipping, + refName: readName, + }, + } + }) + + const feat = feature.toJSON() + + feat.mate = { + refName: readName, + start: clipPos, + end: clipPos + getLengthSansClipping(cigar), + } + + // if secondary alignment or supplementary, calculate length from SA[0]'s CIGAR + // which is the primary alignments. otherwise it is the primary alignment just use + // seq.length if primary alignment + const totalLength = + // eslint-disable-next-line no-bitwise + flags & 2048 + ? getLength(supplementaryAlignments[0].CIGAR) + : getLength(cigar) + + const features = [feat, ...supplementaryAlignments] as ReducedFeature[] + + features.forEach((f, index) => { + f.syntenyId = index + f.mate.syntenyId = index + f.mate.uniqueId = `${f.uniqueId}_mate` + }) + features.sort((a, b) => a.clipPos - b.clipPos) + + // the config feature store includes synthetic mate features + // mapped to the read assembly + const configFeatureStore = features.concat( + // @ts-ignore + features.map(f => f.mate), + ) + + const refLength = features.reduce( + (a, f) => a + f.end - f.start + 2 * windowSize, + 0, + ) + + session.addView('LinearSyntenyView', { + type: 'LinearSyntenyView', + views: [ + { + type: 'LinearGenomeView', + hideHeader: true, + offsetPx: 0, + bpPerPx: refLength / view.width, + displayedRegions: features.map(f => { + return { + start: f.start - windowSize, + end: f.end + windowSize, + refName: f.refName, + assemblyName: trackAssembly, + } + }), + }, + { + type: 'LinearGenomeView', + hideHeader: true, + offsetPx: 0, + bpPerPx: totalLength / view.width, + displayedRegions: [ + { + assemblyName: readAssembly, + start: 0, + end: totalLength, + refName: readName, + }, + ], + }, + ], + viewTrackConfigs: [ + { + type: 'SyntenyTrack', + assemblyNames, + adapter: { + type: 'FromConfigAdapter', + features: configFeatureStore, + }, + renderer: { + type: 'LinearSyntenyRenderer', + }, + trackId, + name: trackName, + }, + ], + tracks: [ + { + configuration: trackId, + type: 'SyntenyTrack', + displays: [ + { + type: 'LinearSyntenyDisplay', + configuration: `${trackId}-LinearSyntenyDisplay`, + }, + ], + }, + ], + displayName: `${readName} vs ${trackAssembly}`, + }) + handleClose() + } catch (e) { + console.error(e) + } + } + return ( + + + Set window size + + + + + +
+ + Show an extra window around each part of the split alignment. Using + a larger value can allow you to see more genomic context. + + + { + setWindowSize(event.target.value) + }} + placeholder="Set window size" + /> + +
+
+
+ ) +} + +export default class extends Plugin { + name = 'LinearComparativeViewPlugin' + + install(pluginManager: PluginManager) { + pluginManager.addViewType(() => + pluginManager.jbrequire(LinearComparativeViewFactory), + ) + pluginManager.addViewType(() => + pluginManager.jbrequire(LinearSyntenyViewFactory), + ) + + pluginManager.addTrackType(() => { + const configSchema = ConfigurationSchema( + 'SyntenyTrack', + {}, + { baseConfiguration: createBaseTrackConfig(pluginManager) }, + ) + return new TrackType({ + name: 'SyntenyTrack', + configSchema, + stateModel: createBaseTrackModel( + pluginManager, + 'SyntenyTrack', + configSchema, + ), + }) + }) + pluginManager.addDisplayType(() => { + const configSchema = linearComparativeDisplayConfigSchemaFactory( + pluginManager, + ) + return new DisplayType({ + name: 'LinearComparativeDisplay', + configSchema, + stateModel: linearComparativeDisplayStateModelFactory(configSchema), + trackType: 'SyntenyTrack', + viewType: 'LinearComparativeView', + ReactComponent: LinearComparativeDisplayReactComponent, + }) + }) + pluginManager.addDisplayType(() => { + const configSchema = linearSyntenyDisplayConfigSchemaFactory( + pluginManager, + ) + return new DisplayType({ + name: 'LinearSyntenyDisplay', + configSchema, + stateModel: linearSyntenyDisplayStateModelFactory(configSchema), + trackType: 'SyntenyTrack', + viewType: 'LinearSyntenyView', + ReactComponent: LinearComparativeDisplayReactComponent, + }) + }) + pluginManager.addAdapterType( + () => + new AdapterType({ + name: 'MCScanAnchorsAdapter', + configSchema: MCScanAnchorsConfigSchema, + AdapterClass: MCScanAnchorsAdapter, + }), + ) + pluginManager.addRendererType( + () => + new LinearSyntenyRenderer({ + name: 'LinearSyntenyRenderer', + configSchema: linearSyntenyRendererConfigSchema, + ReactComponent: LinearSyntenyRendererReactComponent, + }), + ) + } + + configure(pluginManager: PluginManager) { + if (isAbstractMenuManager(pluginManager.rootModel)) { + pluginManager.rootModel.appendToSubMenu(['File', 'Add'], { + label: 'Linear synteny view', + icon: CalendarIcon, + onClick: (session: AbstractSessionModel) => { + session.addView('LinearSyntenyView', {}) + }, + }) + } + + const callback = (feature: Feature, display: any) => { + return feature + ? [ + { + label: 'Linear read vs ref', + icon: AddIcon, + onClick: () => { + const track = getContainingTrack(display) + track.setDialogComponent(WindowSizeDlg, { + feature, + }) + }, + }, + ] + : [] + } + + function checkCallback(obj: any) { + return obj.additionalContextMenuItemCallbacks.includes(callback) + } + function addCallback(obj: any) { + obj.addAdditionalContextMenuItemCallback(callback) + } + function addContextMenu(view: View) { + if (view.type === 'LinearGenomeView') { + view.tracks.forEach(track => { + if (track.type === 'AlignmentsTrack') { + track.displays.forEach(display => { + if ( + display.type === 'LinearPileupDisplay' && + !checkCallback(display) + ) { + addCallback(display) + } else if ( + display.type === 'LinearAlignmentsDisplay' && + display.PileupDisplay && + !checkCallback(display.PileupDisplay) + ) { + addCallback(display.PileupDisplay) + } + }) + } + }) + } + } + autorun(() => { + const session = pluginManager.rootModel?.session as Session | undefined + if (session) { + session.views.forEach(view => { + if (view.views) { + view.views.forEach(v => addContextMenu(v)) + } else { + addContextMenu(view) + } + }) + } + }) + } +}