Skip to content

Commit

Permalink
Add ability to launch breakpoint split view from alignments feature d…
Browse files Browse the repository at this point in the history
…etails (#4215)
  • Loading branch information
cmdcolin committed Feb 19, 2024
1 parent cdd6716 commit 6883fc4
Show file tree
Hide file tree
Showing 30 changed files with 1,245 additions and 723 deletions.
5 changes: 3 additions & 2 deletions packages/core/BaseFeatureWidget/BaseFeatureDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ import {
getEnv,
getSession,
assembleLocString,
toLocale,
ParsedLocString,
SimpleFeatureSerialized,
} from '../../util'
import { ErrorMessage } from '../../ui'
import SequenceFeatureDetails from '../SequenceFeatureDetails'
import { BaseCardProps, BaseProps } from '../types'
import { SimpleFeatureSerialized } from '../../util'
import SimpleField from './SimpleField'
import Attributes from './Attributes'
import { generateTitle, isEmpty, toLocale } from './util'
import { generateTitle, isEmpty } from './util'

// coreDetails are omitted in some circumstances
const coreDetails = [
Expand Down
4 changes: 0 additions & 4 deletions packages/core/BaseFeatureWidget/BaseFeatureDetail/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export function generateMaxWidth(array: unknown[][], prefix: string[]) {
)
}

export function toLocale(n: number) {
return n.toLocaleString('en-US')
}

// pick using a path from an object, similar to _.get from lodash with special
// logic for Descriptions from e.g. VCF headers
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,131 +1,24 @@
import React, { useState } from 'react'
import { Link, Paper } from '@mui/material'
import React from 'react'
import { Paper } from '@mui/material'
import { observer } from 'mobx-react'
import copy from 'copy-to-clipboard'
import clone from 'clone'
import { FeatureDetails } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
import { IAnyStateTreeNode } from 'mobx-state-tree'

// locals
import { getTag, navToLoc } from './util'
import SupplementaryAlignments from './AlignmentsFeatureSuppAligns'
import AlignmentFlags from './AlignmentsFeatureFlags'
import { getTag } from './util'
import { tags } from './tagInfo'
import { AlignmentFeatureWidgetModel } from './stateModelFactory'

const omit = ['clipPos', 'flags']

const tags = {
AM: 'The smallest template-independent mapping quality in the template',
AS: 'Alignment score generated by aligner',
BC: 'Barcode sequence identifying the sample',
BQ: 'Offset to base alignment quality (BAQ)',
BZ: 'Phred quality of the unique molecular barcode bases in the {OX} tag',
CB: 'Cell identifier',
CC: 'Reference name of the next hit',
CM: 'Edit distance between the color sequence and the color reference (see also {NM})',
CO: 'Free-text comments',
CP: 'Leftmost coordinate of the next hit',
CQ: 'Color read base qualities',
CR: 'Cellular barcode sequence bases (uncorrected)',
CS: 'Color read sequence',
CT: 'Complete read annotation tag, used for consensus annotation dummy features',
CY: 'Phred quality of the cellular barcode sequence in the {CR} tag',
E2: 'The 2nd most likely base calls',
FI: 'The index of segment in the template',
FS: 'Segment suffix',
FZ: 'Flow signal intensities',
GC: 'Reserved for backwards compatibility reasons',
GQ: 'Reserved for backwards compatibility reasons',
GS: 'Reserved for backwards compatibility reasons',
H0: 'Number of perfect hits',
H1: 'Number of 1-difference hits (see also {NM})',
H2: 'Number of 2-difference hits',
HI: 'Query hit index',
IH: 'Query hit total count',
LB: 'Library',
MC: 'CIGAR string for mate/next segment',
MD: 'String encoding mismatched and deleted reference bases',
MF: 'Reserved for backwards compatibility reasons',
MI: 'Molecular identifier; a string that uniquely identifies the molecule from which the record was derived',
ML: 'Base modification probabilities',
MM: 'Base modifications / methylation ',
MQ: 'Mapping quality of the mate/next segment',
NH: 'Number of reported alignments that contain the query in the current record',
NM: 'Edit distance to the reference',
OA: 'Original alignment',
OC: 'Original CIGAR (deprecated; use {OA} instead)',
OP: 'Original mapping position (deprecated; use {OA} instead)',
OQ: 'Original base quality',
OX: 'Original unique molecular barcode bases',
PG: 'Program',
PQ: 'Phred likelihood of the template',
PT: 'Read annotations for parts of the padded read sequence',
PU: 'Platform unit',
Q2: 'Phred quality of the mate/next segment sequence in the {R2} tag',
QT: 'Phred quality of the sample barcode sequence in the {BC} tag',
QX: 'Quality score of the unique molecular identifier in the {RX} tag',
R2: 'Sequence of the mate/next segment in the template',
RG: 'Read group',
RT: 'Reserved for backwards compatibility reasons',
RX: 'Sequence bases of the (possibly corrected) unique molecular identifier',
S2: 'Reserved for backwards compatibility reasons',
SA: 'Other canonical alignments in a chimeric alignment',
SM: 'Template-independent mapping quality',
SQ: 'Reserved for backwards compatibility reasons',
TC: 'The number of segments in the template',
TS: 'Transcript strand',
U2: 'Phred probability of the 2nd call being wrong conditional on the best being wrong',
UQ: 'Phred likelihood of the segment, conditional on the mapping being correct',
}

function Formatter({ value }: { value: unknown }) {
const [show, setShow] = useState(false)
const [copied, setCopied] = useState(false)
const display = String(value)
return display.length > 100 ? (
<>
<button
type="button"
onClick={() => {
copy(display)
setCopied(true)
setTimeout(() => setCopied(false), 700)
}}
>
{copied ? 'Copied to clipboard' : 'Copy'}
</button>
<button type="button" onClick={() => setShow(val => !val)}>
{show ? 'Show less' : 'Show more'}
</button>
<div>{show ? display : `${display.slice(0, 100)}...`}</div>
</>
) : (
<div>{display}</div>
)
}
// local components
import SuppAlignments from './SuppAlignments'
import Flags from './Flags'
import PairLink from './PairLink'
import Formatter from './Formatter'

function PairLink({
locString,
model,
}: {
locString: string
model: IAnyStateTreeNode
}) {
return (
<Link
onClick={event => {
event.preventDefault()
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navToLoc(locString, model)
}}
href="#"
>
{locString}
</Link>
)
}
const omit = ['clipPos', 'flags']

const AlignmentsFeatureDetails = observer(function (props: {
model: IAnyStateTreeNode
model: AlignmentFeatureWidgetModel
}) {
const { model } = props
const feat = clone(model.featureData)
Expand All @@ -136,7 +29,7 @@ const AlignmentsFeatureDetails = observer(function (props: {
{...props}
omit={omit}
// @ts-expect-error
descriptions={{ ...tags, tags }}
descriptions={{ ...tags, tags: tags }}
feature={feat}
formatter={(value, key) =>
key === 'next_segment_position' ? (
Expand All @@ -146,10 +39,8 @@ const AlignmentsFeatureDetails = observer(function (props: {
)
}
/>
{SA ? <SupplementaryAlignments model={model} tag={SA} /> : null}
{feat.flags !== undefined ? (
<AlignmentFlags feature={feat} {...props} />
) : null}
{SA ? <SuppAlignments model={model} tag={SA} feature={feat} /> : null}
{feat.flags !== undefined ? <Flags feature={feat} {...props} /> : null}
</Paper>
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, { useState } from 'react'
import { observer } from 'mobx-react'
import {
Button,
Checkbox,
DialogActions,
DialogContent,
FormControlLabel,
} from '@mui/material'
import { makeStyles } from 'tss-react/mui'
import { getSession } from '@jbrowse/core/util'
import { Dialog } from '@jbrowse/core/ui'
import { ViewType } from '@jbrowse/core/pluggableElementTypes'

// locals
import { AlignmentFeatureWidgetModel } from './stateModelFactory'
import { getBreakpointSplitView } from './launchBreakpointSplitView'
import { getSnapshot } from 'mobx-state-tree'
import { ReducedFeature } from './getSAFeatures'

const useStyles = makeStyles()({
block: {
display: 'block',
},
})

interface Track {
id: string
displays: { id: string; [key: string]: unknown }[]
[key: string]: unknown
}

function stripIds(arr: Track[]) {
return arr.map(({ id, displays, ...rest }) => ({
...rest,
displays: displays.map(({ id, ...rest }) => rest),
}))
}

function Checkbox2({
checked,
label,
onChange,
}: {
checked: boolean
label: string
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
}) {
const { classes } = useStyles()
return (
<FormControlLabel
className={classes.block}
control={<Checkbox checked={checked} onChange={onChange} />}
label={label}
/>
)
}

const BreakendOptionDialog = observer(function ({
model,
handleClose,
f1,
f2,
}: {
model: AlignmentFeatureWidgetModel
handleClose: () => void
f1: ReducedFeature
f2: ReducedFeature
viewType: ViewType
}) {
const [copyTracks, setCopyTracks] = useState(true)
const [mirror, setMirror] = useState(true)

return (
<Dialog open onClose={handleClose} title="Breakpoint split view options">
<DialogContent>
<Checkbox2
checked={copyTracks}
onChange={event => setCopyTracks(event.target.checked)}
label="Copy tracks into the new view"
/>
<Checkbox2
checked={mirror}
onChange={event => setMirror(event.target.checked)}
label="Mirror tracks vertically in vertically stacked view"
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
const { view } = model
const session = getSession(model)
try {
const viewSnapshot = getBreakpointSplitView({ view, f1, f2 })
const [view1, view2] = viewSnapshot.views
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const viewTracks = getSnapshot(view.tracks) as Track[]

session.addView('BreakpointSplitView', {
...viewSnapshot,
views: [
{
...view1,
tracks: stripIds(viewTracks),
offsetPx: view1.offsetPx - view.width / 2 + 100,
},
{
...view2,
tracks: stripIds(
mirror ? [...viewTracks].reverse() : viewTracks,
),
offsetPx: view2.offsetPx - view.width / 2 + 100,
},
],
})
} catch (e) {
console.error(e)
session.notify(`${e}`)
}
handleClose()
}}
variant="contained"
color="primary"
autoFocus
>
OK
</Button>
<Button
onClick={() => handleClose()}
color="secondary"
variant="contained"
>
Cancel
</Button>
</DialogActions>
</Dialog>
)
})

export default BreakendOptionDialog
31 changes: 31 additions & 0 deletions plugins/alignments/src/AlignmentsFeatureDetail/Formatter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState } from 'react'
import copy from 'copy-to-clipboard'

// this 'show more...' used specifically as a formatter on alignments feature
// details because long SEQ or CRAM files, even a single div full of a ton of
// data from a long read, can slow down the rest of the app
export default function Formatter({ value }: { value: unknown }) {
const [show, setShow] = useState(false)
const [copied, setCopied] = useState(false)
const display = String(value)
return display.length > 100 ? (
<>
<button
type="button"
onClick={() => {
copy(display)
setCopied(true)
setTimeout(() => setCopied(false), 700)
}}
>
{copied ? 'Copied to clipboard' : 'Copy'}
</button>
<button type="button" onClick={() => setShow(val => !val)}>
{show ? 'Show less' : 'Show more'}
</button>
<div>{show ? display : `${display.slice(0, 100)}...`}</div>
</>
) : (
<div>{display}</div>
)
}
Loading

0 comments on commit 6883fc4

Please sign in to comment.