diff --git a/packages/core/BaseFeatureWidget/BaseFeatureDetail/index.tsx b/packages/core/BaseFeatureWidget/BaseFeatureDetail/index.tsx
index a4f971a383..753f380d3c 100644
--- a/packages/core/BaseFeatureWidget/BaseFeatureDetail/index.tsx
+++ b/packages/core/BaseFeatureWidget/BaseFeatureDetail/index.tsx
@@ -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 = [
diff --git a/packages/core/BaseFeatureWidget/BaseFeatureDetail/util.ts b/packages/core/BaseFeatureWidget/BaseFeatureDetail/util.ts
index 843b215679..055cbf5ed4 100644
--- a/packages/core/BaseFeatureWidget/BaseFeatureDetail/util.ts
+++ b/packages/core/BaseFeatureWidget/BaseFeatureDetail/util.ts
@@ -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
//
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx
index e974df17e8..65f86ee5c6 100644
--- a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx
@@ -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 ? (
- <>
- {
- copy(display)
- setCopied(true)
- setTimeout(() => setCopied(false), 700)
- }}
- >
- {copied ? 'Copied to clipboard' : 'Copy'}
-
- setShow(val => !val)}>
- {show ? 'Show less' : 'Show more'}
-
-
{show ? display : `${display.slice(0, 100)}...`}
- >
- ) : (
- {display}
- )
-}
+// 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 (
- {
- event.preventDefault()
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- navToLoc(locString, model)
- }}
- href="#"
- >
- {locString}
-
- )
-}
+const omit = ['clipPos', 'flags']
const AlignmentsFeatureDetails = observer(function (props: {
- model: IAnyStateTreeNode
+ model: AlignmentFeatureWidgetModel
}) {
const { model } = props
const feat = clone(model.featureData)
@@ -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' ? (
@@ -146,10 +39,8 @@ const AlignmentsFeatureDetails = observer(function (props: {
)
}
/>
- {SA ? : null}
- {feat.flags !== undefined ? (
-
- ) : null}
+ {SA ? : null}
+ {feat.flags !== undefined ? : null}
)
})
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/BreakendOptionDialog.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/BreakendOptionDialog.tsx
new file mode 100644
index 0000000000..e4eb8dd3bf
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/BreakendOptionDialog.tsx
@@ -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) => void
+}) {
+ const { classes } = useStyles()
+ return (
+ }
+ 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 (
+
+
+ setCopyTracks(event.target.checked)}
+ label="Copy tracks into the new view"
+ />
+ setMirror(event.target.checked)}
+ label="Mirror tracks vertically in vertically stacked view"
+ />
+
+
+ {
+ 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
+
+ handleClose()}
+ color="secondary"
+ variant="contained"
+ >
+ Cancel
+
+
+
+ )
+})
+
+export default BreakendOptionDialog
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureFlags.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/Flags.tsx
similarity index 100%
rename from plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureFlags.tsx
rename to plugins/alignments/src/AlignmentsFeatureDetail/Flags.tsx
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/Formatter.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/Formatter.tsx
new file mode 100644
index 0000000000..9b79ea580f
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/Formatter.tsx
@@ -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 ? (
+ <>
+ {
+ copy(display)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 700)
+ }}
+ >
+ {copied ? 'Copied to clipboard' : 'Copy'}
+
+ setShow(val => !val)}>
+ {show ? 'Show less' : 'Show more'}
+
+ {show ? display : `${display.slice(0, 100)}...`}
+ >
+ ) : (
+ {display}
+ )
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/LaunchBreakpointSplitViewPanel.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/LaunchBreakpointSplitViewPanel.tsx
new file mode 100644
index 0000000000..22116d634a
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/LaunchBreakpointSplitViewPanel.tsx
@@ -0,0 +1,100 @@
+import React, { lazy, useEffect, useState } from 'react'
+import { Typography, Link, Tooltip } from '@mui/material'
+import {
+ SimpleFeature,
+ SimpleFeatureSerialized,
+ getSession,
+ toLocale,
+} from '@jbrowse/core/util'
+import { makeStyles } from 'tss-react/mui'
+import { ErrorMessage } from '@jbrowse/core/ui'
+import { ViewType } from '@jbrowse/core/pluggableElementTypes'
+
+// locals
+import { AlignmentFeatureWidgetModel } from './stateModelFactory'
+import { ReducedFeature, getSAFeatures } from './getSAFeatures'
+
+// lazies
+const BreakendOptionDialog = lazy(() => import('./BreakendOptionDialog'))
+
+const useStyles = makeStyles()({
+ cursor: {
+ cursor: 'pointer',
+ },
+})
+export default function LaunchBreakpointSplitViewPanel({
+ model,
+ feature,
+ viewType,
+}: {
+ model: AlignmentFeatureWidgetModel
+ feature: SimpleFeatureSerialized
+ viewType: ViewType
+}) {
+ const { classes } = useStyles()
+ const session = getSession(model)
+ const { view } = model
+ const [res, setRes] = useState()
+ const [error, setError] = useState()
+ useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ ;(async () => {
+ try {
+ const feats = await getSAFeatures({
+ view,
+ feature: new SimpleFeature(feature),
+ })
+ setRes(feats)
+ } catch (e) {
+ setError(e)
+ console.error(e)
+ }
+ })()
+ }, [feature, view])
+
+ const ret = [] as [ReducedFeature, ReducedFeature][]
+ if (res) {
+ for (let i = 0; i < res.length - 1; i++) {
+ ret.push([res[i], res[i + 1]] as const)
+ }
+ }
+ return (
+ <>
+ {ret.length ? (
+
+
+ Launch split views with breakend source and target
+
+ {error ?
: null}
+
+ {ret.map((arg, index) => {
+ const [f1, f2] = arg
+ return (
+
+
+ {
+ event.preventDefault()
+ session.queueDialog(handleClose => [
+ BreakendOptionDialog,
+ { handleClose, f1, f2, model, viewType },
+ ])
+ }}
+ >
+ {f1.refName}:
+ {toLocale(f1.strand === 1 ? f1.end : f1.start)} ->{' '}
+ {f2.refName}:
+ {toLocale(f2.strand === 1 ? f2.start : f2.end)}
+
+
+
+ )
+ })}
+
+
+ ) : null}
+ >
+ )
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/PairLink.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/PairLink.tsx
new file mode 100644
index 0000000000..4bea71a1d4
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/PairLink.tsx
@@ -0,0 +1,27 @@
+import React from 'react'
+import { Link } from '@mui/material'
+
+// locals
+import { AlignmentFeatureWidgetModel } from './stateModelFactory'
+import { navToLoc } from './util'
+
+export default function PairLink({
+ locString,
+ model,
+}: {
+ locString: string
+ model: AlignmentFeatureWidgetModel
+}) {
+ return (
+ {
+ event.preventDefault()
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ navToLoc(locString, model)
+ }}
+ href="#"
+ >
+ {locString}
+
+ )
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignments.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignments.tsx
new file mode 100644
index 0000000000..d8b1f128c0
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignments.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import { BaseCard } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
+import { SimpleFeatureSerialized, getEnv, getSession } from '@jbrowse/core/util'
+import { ViewType } from '@jbrowse/core/pluggableElementTypes'
+
+// locals
+import { AlignmentFeatureWidgetModel } from './stateModelFactory'
+import SuppAlignmentsLocStrings from './SuppAlignmentsLocStrings'
+import LaunchBreakpointSplitViewPanel from './LaunchBreakpointSplitViewPanel'
+
+export default function SuppAlignments(props: {
+ tag: string
+ model: AlignmentFeatureWidgetModel
+ feature: SimpleFeatureSerialized
+}) {
+ const { model, tag, feature } = props
+ const session = getSession(model)
+ const { pluginManager } = getEnv(session)
+ let viewType: ViewType | undefined
+
+ try {
+ viewType = pluginManager.getViewType('BreakpointSplitView')
+ } catch (e) {
+ // ignore
+ }
+
+ return (
+
+
+ {viewType ? (
+
+ ) : null}
+
+ )
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureSuppAligns.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignmentsLocStrings.tsx
similarity index 74%
rename from plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureSuppAligns.tsx
rename to plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignmentsLocStrings.tsx
index 4c3d11c98a..006ea61ede 100644
--- a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureSuppAligns.tsx
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/SuppAlignmentsLocStrings.tsx
@@ -1,17 +1,19 @@
import React from 'react'
import { Typography, Link } from '@mui/material'
-import { BaseCard } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
+// locals
import { getLengthOnRef } from '../MismatchParser'
-import { IAnyStateTreeNode } from 'mobx-state-tree'
import { navToLoc } from './util'
+import { AlignmentFeatureWidgetModel } from './stateModelFactory'
-export default function SupplementaryAlignments(props: {
+export default function SuppAlignmentsLocStrings({
+ tag,
+ model,
+}: {
tag: string
- model: IAnyStateTreeNode
+ model: AlignmentFeatureWidgetModel
}) {
- const { tag, model } = props
return (
-
+
List of supplementary alignment locations
{tag
@@ -23,9 +25,9 @@ export default function SupplementaryAlignments(props: {
const extra = Math.floor(saLength / 5)
const start = +saStart
const end = +saStart + saLength
- const locString = `${saRef}:${Math.max(1, start - extra)}-${
- end + extra
- }`
+ const sp = start - extra
+ const ep = end + extra
+ const locString = `${saRef}:${Math.max(1, sp)}-${ep}`
const displayStart = start.toLocaleString('en-US')
const displayEnd = end.toLocaleString('en-US')
const displayString = `${saRef}:${displayStart}-${displayEnd} (${saStrand}) [${saLength}bp]`
@@ -38,7 +40,6 @@ export default function SupplementaryAlignments(props: {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
navToLoc(locString, model)
}}
- href="#"
>
{displayString}
@@ -46,6 +47,6 @@ export default function SupplementaryAlignments(props: {
)
})}
-
+
)
}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.tsx.snap b/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.tsx.snap
index d34f8d8c6f..57cf48360c 100644
--- a/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.tsx.snap
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.tsx.snap
@@ -1,320 +1,322 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`open up a widget 1`] = `
-
+
-
-
- Core details
-
-
- Position
-
+ Core details
+
- ctgA:3..102 (+)
-
-
-
-
- Name
-
-
-
- ctgA_3_555_0:0:0_2:0:0_102d
-
-
-
-
-
- Length
-
-
-
- 100
-
-
-
-
-
- Type
-
-
-
- match
-
-
-
-
-
- Attributes
-
-
-
- seq
-
-
-
- TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG
+
+ Position
+
+
+ ctgA:3..102 (+)
-
-
-
-
- score
-
- 37
+
+ Name
+
+
+
+ ctgA_3_555_0:0:0_2:0:0_102d
+
-
-
- qual
+
+ Length
+
+
+
+ 100
+
+
-
- Copy
-
-
+
- Show more
-
-
- 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 1...
+
+ match
+
-
-
+ Attributes
+
-
- 37
+
+ seq
+
+
+
+ TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG
+
-
-
-
-
- CIGAR
-
- length_on_ref
+
+ qual
+
+
+
+ Copy
+
+
+ Show more
+
+
+ 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 1...
+
+
-
- template_length
+
+ CIGAR
+
+
-
- 0
+
+ length_on_ref
+
+
-
-
- seq_length
+
+ template_length
+
+
-
-
- Show feature sequence
-
-
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/configSchema.ts b/plugins/alignments/src/AlignmentsFeatureDetail/configSchema.ts
new file mode 100644
index 0000000000..d0cd96fcf7
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/configSchema.ts
@@ -0,0 +1,3 @@
+import { ConfigurationSchema } from '@jbrowse/core/configuration'
+
+export const configSchema = ConfigurationSchema('AlignmentsFeatureWidget', {})
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/getSAFeatures.ts b/plugins/alignments/src/AlignmentsFeatureDetail/getSAFeatures.ts
new file mode 100644
index 0000000000..89180a8de7
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/getSAFeatures.ts
@@ -0,0 +1,69 @@
+import { Feature, getSession } from '@jbrowse/core/util'
+
+// locals
+import { featurizeSA, getClip, getLengthSansClipping } from '../MismatchParser'
+import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
+import { getTag } from '../util'
+
+export interface ReducedFeature {
+ refName: string
+ start: number
+ clipPos: number
+ end: number
+ strand: number
+ seqLength: number
+ syntenyId?: number
+ uniqueId: string
+ mate: {
+ refName: string
+ start: number
+ end: number
+ syntenyId?: number
+ uniqueId?: string
+ }
+}
+
+export async function getSAFeatures({
+ view,
+ feature,
+}: {
+ view: LinearGenomeViewModel
+ feature: Feature
+}) {
+ const { assemblyManager } = getSession(view)
+ const cigar = feature.get('CIGAR') as string
+ const origStrand = feature.get('strand') as number
+ const SA = (getTag(feature, 'SA') as string) || ''
+ const readName = feature.get('name') as string
+ const clipPos = getClip(cigar, 1)
+
+ // get the canonical refname for the read because if the
+ // read.get('refName') is chr1 and the actual fasta refName is 1 then no
+ // tracks can be opened on the top panel of the linear read vs ref
+ const assembly = await assemblyManager.waitForAssembly(view.assemblyNames[0])
+ if (!assembly) {
+ throw new Error('assembly not found')
+ }
+
+ const suppAlns = featurizeSA(SA, feature.id(), origStrand, readName, true)
+
+ const feat = feature.toJSON()
+ feat.clipPos = clipPos
+ feat.strand = 1
+
+ feat.mate = {
+ refName: readName,
+ start: clipPos,
+ end: clipPos + getLengthSansClipping(cigar),
+ }
+ const features = [feat, ...suppAlns] as ReducedFeature[]
+
+ features.forEach((f, idx) => {
+ f.refName = assembly?.getCanonicalRefName(f.refName) || f.refName
+ f.syntenyId = idx
+ f.mate.syntenyId = idx
+ f.mate.uniqueId = `${f.uniqueId}_mate`
+ })
+ features.sort((a, b) => a.clipPos - b.clipPos)
+ return features
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/index.test.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/index.test.tsx
index 0bc088efde..fafbf1ddb8 100644
--- a/plugins/alignments/src/AlignmentsFeatureDetail/index.test.tsx
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/index.test.tsx
@@ -5,7 +5,7 @@ import PluginManager from '@jbrowse/core/PluginManager'
import { ConfigurationSchema } from '@jbrowse/core/configuration'
// locals
-import { stateModelFactory } from '.'
+import { stateModelFactory } from './stateModelFactory'
import ReactComponent from './AlignmentsFeatureDetail'
test('open up a widget', () => {
@@ -42,6 +42,6 @@ test('open up a widget', () => {
const { container, getByText } = render(
,
)
- expect(container.firstChild).toMatchSnapshot()
+ expect(container).toMatchSnapshot()
expect(getByText('ctgA:3..102 (+)')).toBeTruthy()
})
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/index.ts b/plugins/alignments/src/AlignmentsFeatureDetail/index.ts
index fffaf31b1b..11863eb336 100644
--- a/plugins/alignments/src/AlignmentsFeatureDetail/index.ts
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/index.ts
@@ -1,33 +1,18 @@
import { lazy } from 'react'
import PluginManager from '@jbrowse/core/PluginManager'
-import { ConfigurationSchema } from '@jbrowse/core/configuration'
-import { types } from 'mobx-state-tree'
import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType'
-import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
+import { configSchema } from './configSchema'
+import { stateModelFactory } from './stateModelFactory'
-const configSchema = ConfigurationSchema('AlignmentsFeatureWidget', {})
-
-export function stateModelFactory(pluginManager: PluginManager) {
- const baseModel = baseModelFactory(pluginManager)
- return types.compose(
- baseModel,
- types.model('AlignmentsFeatureWidget', {
- type: types.literal('AlignmentsFeatureWidget'),
- }),
- )
-}
-
-export default function register(pluginManager: PluginManager) {
+export default function AlignmentFeatureDetailsF(pluginManager: PluginManager) {
pluginManager.addWidgetType(
() =>
new WidgetType({
name: 'AlignmentsFeatureWidget',
heading: 'Feature details',
- configSchema,
+ configSchema: configSchema,
stateModel: stateModelFactory(pluginManager),
ReactComponent: lazy(() => import('./AlignmentsFeatureDetail')),
}),
)
}
-
-export { configSchema }
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/launchBreakpointSplitView.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/launchBreakpointSplitView.tsx
new file mode 100644
index 0000000000..9802681227
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/launchBreakpointSplitView.tsx
@@ -0,0 +1,64 @@
+import { getSession } from '@jbrowse/core/util'
+import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
+
+// locals
+import { ReducedFeature } from './getSAFeatures'
+
+export function getBreakpointSplitView({
+ f1,
+ f2,
+ view,
+}: {
+ f1: ReducedFeature
+ f2: ReducedFeature
+ view: LinearGenomeViewModel
+}) {
+ const { assemblyName } = view.displayedRegions[0]
+ const { assemblyManager } = getSession(view)
+ const assembly = assemblyManager.get(assemblyName)
+ if (!assembly) {
+ throw new Error(`assembly ${assemblyName} not found`)
+ }
+ if (!assembly.regions) {
+ throw new Error(`assembly ${assemblyName} regions not loaded`)
+ }
+ const topRegion = assembly.regions.find(f => f.refName === f1.refName)
+ const bottomRegion = assembly.regions.find(f => f.refName === f2.refName)
+
+ if (!topRegion || !bottomRegion) {
+ throw new Error(
+ `unable to find the refName for the top or bottom of the breakpoint view`,
+ )
+ }
+ const topMarkedRegion = [{ ...topRegion }, { ...topRegion }]
+ const bottomMarkedRegion = [{ ...bottomRegion }, { ...bottomRegion }]
+
+ const s = f1.strand === 1 ? f1.end : f1.start
+ const e = f2.strand === 1 ? f2.start : f2.end
+
+ topMarkedRegion[0].end = s
+ topMarkedRegion[1].start = s + 1
+ bottomMarkedRegion[0].end = e
+ bottomMarkedRegion[1].start = e + 1
+ const bpPerPx = 10
+ return {
+ type: 'BreakpointSplitView',
+ views: [
+ {
+ type: 'LinearGenomeView',
+ displayedRegions: topMarkedRegion,
+ hideHeader: true,
+ bpPerPx,
+ offsetPx: (topRegion.start + s) / bpPerPx,
+ },
+ {
+ type: 'LinearGenomeView',
+ displayedRegions: bottomMarkedRegion,
+ hideHeader: true,
+ bpPerPx,
+ offsetPx: (bottomRegion.start + e) / bpPerPx,
+ },
+ ],
+ displayName: `breakend split detail`,
+ }
+}
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/stateModelFactory.ts b/plugins/alignments/src/AlignmentsFeatureDetail/stateModelFactory.ts
new file mode 100644
index 0000000000..ab41015898
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/stateModelFactory.ts
@@ -0,0 +1,19 @@
+import PluginManager from '@jbrowse/core/PluginManager'
+import { Instance, types } from 'mobx-state-tree'
+import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
+
+export function stateModelFactory(pluginManager: PluginManager) {
+ const baseModel = baseModelFactory(pluginManager)
+ return types.compose(
+ baseModel,
+ types.model('AlignmentsFeatureWidget', {
+ type: types.literal('AlignmentsFeatureWidget'),
+ }),
+ )
+}
+
+export type AlignmentFeatureWidgetStateModel = ReturnType<
+ typeof stateModelFactory
+>
+export type AlignmentFeatureWidgetModel =
+ Instance
diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts b/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts
new file mode 100644
index 0000000000..e668521114
--- /dev/null
+++ b/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts
@@ -0,0 +1,63 @@
+export 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',
+}
diff --git a/plugins/alignments/src/LinearSNPCoverageDisplay/components/Tooltip.tsx b/plugins/alignments/src/LinearSNPCoverageDisplay/components/Tooltip.tsx
index 16e582ce24..627a078256 100644
--- a/plugins/alignments/src/LinearSNPCoverageDisplay/components/Tooltip.tsx
+++ b/plugins/alignments/src/LinearSNPCoverageDisplay/components/Tooltip.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import { observer } from 'mobx-react'
-import { Feature } from '@jbrowse/core/util'
+import { Feature, toLocale } from '@jbrowse/core/util'
import { Tooltip } from '@jbrowse/plugin-wiggle'
type Count = Record<
@@ -27,12 +27,14 @@ interface SNPInfo {
'1': number
}
-const en = (n: number) => n.toLocaleString('en-US')
const toP = (s = 0) => +(+s).toFixed(1)
+
const pct = (n: number, total: number) => `${toP((n / (total || 1)) * 100)}%`
+
interface Props {
feature: Feature
}
+
const TooltipContents = React.forwardRef(
function TooltipContents2({ feature }, reactRef) {
const start = feature.get('start')
@@ -48,7 +50,10 @@ const TooltipContents = React.forwardRef(
'0': r0,
...info
} = feature.get('snpinfo') as SNPInfo
- const loc = [name, start === end ? en(start) : `${en(start)}..${en(end)}`]
+ const loc = [
+ name,
+ start === end ? toLocale(start) : `${toLocale(start)}..${toLocale(end)}`,
+ ]
.filter(f => !!f)
.join(':')
diff --git a/plugins/variants/src/VariantFeatureWidget/BreakendOptionDialog.tsx b/plugins/variants/src/VariantFeatureWidget/BreakendOptionDialog.tsx
index a5021cc363..685e8a9282 100644
--- a/plugins/variants/src/VariantFeatureWidget/BreakendOptionDialog.tsx
+++ b/plugins/variants/src/VariantFeatureWidget/BreakendOptionDialog.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState } from 'react'
import { observer } from 'mobx-react'
import {
@@ -10,9 +9,11 @@ import {
} from '@mui/material'
import { makeStyles } from 'tss-react/mui'
import { getSnapshot } from 'mobx-state-tree'
-// jbrowse
import { Dialog } from '@jbrowse/core/ui'
import { getSession, Feature } from '@jbrowse/core/util'
+import { ViewType } from '@jbrowse/core/pluggableElementTypes'
+// locals
+import { VariantFeatureWidgetModel } from './stateModelFactory'
const useStyles = makeStyles()({
block: {
@@ -20,43 +21,63 @@ const useStyles = makeStyles()({
},
})
+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) => void
+}) {
+ const { classes } = useStyles()
+ return (
+ }
+ label={label}
+ />
+ )
+}
+
const BreakendOptionDialog = observer(function ({
model,
handleClose,
feature,
viewType,
}: {
- model: any
+ model: VariantFeatureWidgetModel
handleClose: () => void
feature: Feature
- viewType: any
+ viewType: ViewType
}) {
- const { classes } = useStyles()
const [copyTracks, setCopyTracks] = useState(true)
- const [mirrorTracks, setMirrorTracks] = useState(true)
+ const [mirror, setMirror] = useState(true)
return (
- setCopyTracks(event.target.checked)}
- />
- }
+ setCopyTracks(event.target.checked)}
label="Copy tracks into the new view"
/>
-
- setMirrorTracks(event.target.checked)}
- />
- }
+ setMirror(event.target.checked)}
label="Mirror tracks vertically in vertically stacked view"
/>
@@ -66,32 +87,32 @@ const BreakendOptionDialog = observer(function ({
const { view } = model
const session = getSession(model)
try {
+ // @ts-expect-error
const viewSnapshot = viewType.snapshotFromBreakendFeature(
feature,
view,
)
-
- interface Track {
- trackId: string
- [key: string]: unknown
- }
- function remapIds(arr: Track[]) {
- return arr.map(v => ({
- ...v,
- id: `${v.trackId}-${Math.random()}`,
- }))
- }
- viewSnapshot.views[0].offsetPx -= view.width / 2 + 100
- viewSnapshot.views[1].offsetPx -= view.width / 2 + 100
- viewSnapshot.featureData = feature
+ const [view1, view2] = viewSnapshot.views
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const viewTracks = getSnapshot(view.tracks) as Track[]
- viewSnapshot.views[0].tracks = remapIds(viewTracks)
- viewSnapshot.views[1].tracks = remapIds(
- mirrorTracks ? [...viewTracks].reverse() : viewTracks,
- )
- session.addView('BreakpointSplitView', viewSnapshot)
+ 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}`)
diff --git a/plugins/variants/src/VariantFeatureWidget/BreakendPanel.tsx b/plugins/variants/src/VariantFeatureWidget/BreakendPanel.tsx
index 1cfd2fa353..ac4efce128 100644
--- a/plugins/variants/src/VariantFeatureWidget/BreakendPanel.tsx
+++ b/plugins/variants/src/VariantFeatureWidget/BreakendPanel.tsx
@@ -1,37 +1,33 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import React, { useState } from 'react'
+import React, { lazy } from 'react'
import { Link, Typography } from '@mui/material'
-import SimpleFeature, {
+import {
+ getEnv,
+ getSession,
+ SimpleFeature,
SimpleFeatureSerialized,
-} from '@jbrowse/core/util/simpleFeature'
-import { getEnv, getSession } from '@jbrowse/core/util'
+} from '@jbrowse/core/util'
import { BaseCard } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
-import BreakendOptionDialog from './BreakendOptionDialog'
+import { ViewType } from '@jbrowse/core/pluggableElementTypes'
-export default function BreakendPanel(props: {
+import { VariantFeatureWidgetModel } from './stateModelFactory'
+
+// lazies
+const BreakendOptionDialog = lazy(() => import('./BreakendOptionDialog'))
+
+function LocStringList({
+ locStrings,
+ model,
+}: {
locStrings: string[]
- model: any
- feature: SimpleFeatureSerialized
+ model: VariantFeatureWidgetModel
}) {
- const { model, locStrings, feature } = props
const session = getSession(model)
- const { pluginManager } = getEnv(session)
- const [breakpointDialog, setBreakpointDialog] = useState(false)
- let viewType
-
- try {
- viewType = pluginManager.getViewType('BreakpointSplitView')
- } catch (e) {
- // ignore
- }
-
- const simpleFeature = new SimpleFeature(feature)
return (
-
+
Link to linear view of breakend endpoints
- {locStrings.map(locString => (
-
+ {locStrings.map((locString, index) => (
+
{
@@ -56,37 +52,76 @@ export default function BreakendPanel(props: {
))}
- {viewType ? (
-
-
- Launch split views with breakend source and target
-
-
- {locStrings.map(locString => (
-
- {
- event.preventDefault()
- setBreakpointDialog(true)
- }}
- >
- {`${feature.refName}:${feature.start} // ${locString} (split view)`}
-
-
- ))}
-
- {breakpointDialog ? (
-
{
- setBreakpointDialog(false)
+
+ )
+}
+
+function LaunchBreakpointSplitViewPanel({
+ locStrings,
+ model,
+ feature,
+ viewType,
+}: {
+ locStrings: string[]
+ model: VariantFeatureWidgetModel
+ feature: SimpleFeatureSerialized
+ viewType: ViewType
+}) {
+ const session = getSession(model)
+ const simpleFeature = new SimpleFeature(feature)
+ return (
+
+
+ Launch split views with breakend source and target
+
+
+ {locStrings.map(locString => (
+
+ {
+ event.preventDefault()
+ session.queueDialog(handleClose => [
+ BreakendOptionDialog,
+ { handleClose, model, feature: simpleFeature, viewType },
+ ])
}}
- />
- ) : null}
-
+ >
+ {`${feature.refName}:${feature.start} // ${locString} (split view)`}
+
+
+ ))}
+
+
+ )
+}
+
+export default function BreakendPanel(props: {
+ locStrings: string[]
+ model: VariantFeatureWidgetModel
+ feature: SimpleFeatureSerialized
+}) {
+ const { model, locStrings, feature } = props
+ const session = getSession(model)
+ const { pluginManager } = getEnv(session)
+ let viewType: ViewType | undefined
+
+ try {
+ viewType = pluginManager.getViewType('BreakpointSplitView')
+ } catch (e) {
+ // ignore
+ }
+
+ return (
+
+
+ {viewType ? (
+
) : null}
)
diff --git a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.tsx b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.tsx
index fcb6d7b041..7f15efd849 100644
--- a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.tsx
+++ b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.tsx
@@ -5,7 +5,7 @@ import { ConfigurationSchema } from '@jbrowse/core/configuration'
import PluginManager from '@jbrowse/core/PluginManager'
// locals
-import { stateModelFactory } from '.'
+import { stateModelFactory } from './stateModelFactory'
import VariantFeatureDetails from './VariantFeatureWidget'
test('renders with just the required model elements', () => {
@@ -38,5 +38,5 @@ test('renders with just the required model elements', () => {
})
const { container } = render( )
- expect(container.firstChild).toMatchSnapshot()
+ expect(container).toMatchSnapshot()
})
diff --git a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx
index 93a91d1e1f..cbf9652eb3 100644
--- a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx
+++ b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import { observer } from 'mobx-react'
-import { Divider, Paper } from '@mui/material'
+import { Paper } from '@mui/material'
import { FeatureDetails } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail'
import { parseBreakend } from '@gmod/vcf'
@@ -8,18 +8,8 @@ import { parseBreakend } from '@gmod/vcf'
import VariantSampleGrid from './VariantSampleGrid'
import BreakendPanel from './BreakendPanel'
import VariantAnnotationTable from './VariantAnnotationTable'
-import { SimpleFeatureSerialized } from '@jbrowse/core/util'
-
-const basicDescriptions = {
- CHROM: 'chromosome: An identifier from the reference genome',
- POS: 'position: The reference position, with the 1st base having position 1',
- ID: 'identifier: Semi-colon separated list of unique identifiers where available',
- REF: 'reference base(s): Each base must be one of A,C,G,T,N (case insensitive).',
- ALT: 'alternate base(s): Comma-separated list of alternate non-reference alleles',
- QUAL: 'quality: Phred-scaled quality score for the assertion made in ALT',
- FILTER:
- 'filter status: PASS if this position has passed all filters, otherwise a semicolon-separated list of codes for filters that fail',
-}
+import { VariantFeatureWidgetModel } from './stateModelFactory'
+import { variantFieldDescriptions } from './variantFieldDescriptions'
function AnnPanel({
descriptions,
@@ -62,10 +52,7 @@ function CsqPanel({
}
const VariantFeatureWidget = observer(function (props: {
- model: {
- featureData: SimpleFeatureSerialized
- descriptions: Record
- }
+ model: VariantFeatureWidgetModel
}) {
const { model } = props
const { featureData, descriptions } = model
@@ -76,14 +63,11 @@ const VariantFeatureWidget = observer(function (props: {
-
-
-
{feat.type === 'breakend' ? (
+
-
-
- Core details
-
-
-
- Position
-
-
- ctgA:177
-
-
-
- Name
-
+ Core details
+
-
- rs123
-
-
-
-
-
- Length
-
-
-
- 1
-
-
-
-
-
- Attributes
-
-
-
- REF
-
-
-
- A
-
+
+ Position
+
+
+ ctgA:177
+
-
-
- ALT
+
+ Name
+
+
+
+ rs123
+
+
-
- <TRA>
-
+
+ Length
+
+
+
+ 1
+
+
-
-
+
+
+ Attributes
+
- QUAL
+
+ REF
+
+
+
+ A
+
+
-
- 10.4
-
+
+ ALT
+
+
+
+ <TRA>
+
+
-
-
- INFO.MQ
+
+ QUAL
+
+
+
+ 10.4
+
+
-
- 5
-
+
+ INFO.MQ
+
+
+
+ 5
+
+
-
-
-
- Show feature sequence
-
-
@@ -231,14 +233,5 @@ exports[`renders with just the required model elements 1`] = `
-
-
-
`;
diff --git a/plugins/variants/src/VariantFeatureWidget/configSchema.ts b/plugins/variants/src/VariantFeatureWidget/configSchema.ts
new file mode 100644
index 0000000000..70f00d578f
--- /dev/null
+++ b/plugins/variants/src/VariantFeatureWidget/configSchema.ts
@@ -0,0 +1,3 @@
+import { ConfigurationSchema } from '@jbrowse/core/configuration'
+
+export const configSchema = ConfigurationSchema('VariantFeatureWidget', {})
diff --git a/plugins/variants/src/VariantFeatureWidget/index.ts b/plugins/variants/src/VariantFeatureWidget/index.ts
index e2f4b8b1e6..7d64f8ebf1 100644
--- a/plugins/variants/src/VariantFeatureWidget/index.ts
+++ b/plugins/variants/src/VariantFeatureWidget/index.ts
@@ -1,22 +1,8 @@
import { lazy } from 'react'
-import { ConfigurationSchema } from '@jbrowse/core/configuration'
import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType'
import PluginManager from '@jbrowse/core/PluginManager'
-import { types } from 'mobx-state-tree'
-import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
-
-export const configSchema = ConfigurationSchema('VariantFeatureWidget', {})
-
-export function stateModelFactory(pluginManager: PluginManager) {
- const baseModel = baseModelFactory(pluginManager)
- return types.compose(
- baseModel,
- types.model('VariantFeatureWidget', {
- type: types.literal('VariantFeatureWidget'),
- descriptions: types.frozen(),
- }),
- )
-}
+import { stateModelFactory } from './stateModelFactory'
+import { configSchema } from './configSchema'
export default (pluginManager: PluginManager) => {
pluginManager.addWidgetType(
@@ -24,7 +10,7 @@ export default (pluginManager: PluginManager) => {
new WidgetType({
name: 'VariantFeatureWidget',
heading: 'Feature details',
- configSchema,
+ configSchema: configSchema,
stateModel: stateModelFactory(pluginManager),
ReactComponent: lazy(() => import('./VariantFeatureWidget')),
}),
diff --git a/plugins/variants/src/VariantFeatureWidget/stateModelFactory.ts b/plugins/variants/src/VariantFeatureWidget/stateModelFactory.ts
new file mode 100644
index 0000000000..2ae01c8e24
--- /dev/null
+++ b/plugins/variants/src/VariantFeatureWidget/stateModelFactory.ts
@@ -0,0 +1,19 @@
+import PluginManager from '@jbrowse/core/PluginManager'
+import { Instance, types } from 'mobx-state-tree'
+import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
+
+export function stateModelFactory(pluginManager: PluginManager) {
+ const baseModel = baseModelFactory(pluginManager)
+ return types.compose(
+ baseModel,
+ types.model('VariantFeatureWidget', {
+ type: types.literal('VariantFeatureWidget'),
+ descriptions: types.frozen(),
+ }),
+ )
+}
+
+export type VariantFeatureWidgetStateModel = ReturnType<
+ typeof stateModelFactory
+>
+export type VariantFeatureWidgetModel = Instance
diff --git a/plugins/variants/src/VariantFeatureWidget/variantFieldDescriptions.ts b/plugins/variants/src/VariantFeatureWidget/variantFieldDescriptions.ts
new file mode 100644
index 0000000000..0b38539885
--- /dev/null
+++ b/plugins/variants/src/VariantFeatureWidget/variantFieldDescriptions.ts
@@ -0,0 +1,10 @@
+export const variantFieldDescriptions = {
+ CHROM: 'chromosome: An identifier from the reference genome',
+ POS: 'position: The reference position, with the 1st base having position 1',
+ ID: 'identifier: Semi-colon separated list of unique identifiers where available',
+ REF: 'reference base(s): Each base must be one of A,C,G,T,N (case insensitive).',
+ ALT: 'alternate base(s): Comma-separated list of alternate non-reference alleles',
+ QUAL: 'quality: Phred-scaled quality score for the assertion made in ALT',
+ FILTER:
+ 'filter status: PASS if this position has passed all filters, otherwise a semicolon-separated list of codes for filters that fail',
+}
diff --git a/test_data/config_demo.json b/test_data/config_demo.json
index 27a25cd365..9688be84b7 100644
--- a/test_data/config_demo.json
+++ b/test_data/config_demo.json
@@ -1342,6 +1342,41 @@
}
}
},
+ {
+ "type": "VariantTrack",
+ "trackId": "breast_cancer_sniffles_hg19_traonly",
+ "name": "SKBR3 pacbio (sniffles VCF, TRA only)",
+ "adapter": {
+ "type": "VcfAdapter",
+ "vcfLocation": {
+ "uri": "https://jbrowse.org/genomes/hg19/SKBR3/reads_lr_skbr3.fa_ngmlr-0.2.3_mapped.bam.sniffles1kb_auto_l8_s5_noalt.filtered.vcf",
+ "locationType": "UriLocation"
+ }
+ },
+ "category": ["SKBR3"],
+ "assemblyNames": ["hg19"]
+ },
+ {
+ "type": "VariantTrack",
+ "trackId": "breast_cancer_sniffles_hg19_traonly_tabix",
+ "name": "SKBR3 pacbio (sniffles VCF, TRA only tabix)",
+ "adapter": {
+ "type": "VcfTabixAdapter",
+ "vcfGzLocation": {
+ "uri": "https://jbrowse.org/genomes/hg19/SKBR3/reads_lr_skbr3.fa_ngmlr-0.2.3_mapped.bam.sniffles1kb_auto_l8_s5_noalt.filtered.vcf.gz",
+ "locationType": "UriLocation"
+ },
+ "index": {
+ "location": {
+ "uri": "https://jbrowse.org/genomes/hg19/SKBR3/reads_lr_skbr3.fa_ngmlr-0.2.3_mapped.bam.sniffles1kb_auto_l8_s5_noalt.filtered.vcf.gz.tbi",
+ "locationType": "UriLocation"
+ },
+ "indexType": "TBI"
+ }
+ },
+ "category": ["SKBR3"],
+ "assemblyNames": ["hg19"]
+ },
{
"type": "VariantTrack",
"trackId": "1000genomes_indels",
diff --git a/yarn.lock b/yarn.lock
index 84c0c104e7..a7fb7c90e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3259,7 +3259,7 @@
wordwrap "^1.0.0"
wrap-ansi "^7.0.0"
-"@oclif/core@^3.18.2", "@oclif/core@^3.19.1", "@oclif/core@^3.19.2":
+"@oclif/core@^3.18.2", "@oclif/core@^3.19.2":
version "3.19.2"
resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.19.2.tgz#1672061e7309f0048f9f9991607b303a9e5082fc"
integrity sha512-/kUZZQZO92esToSTn+fnfMQdAj8uDJ/lsMNXBRBCiLQpwnIskHXy8JzYpynxxkzY2wbGREBceQhxCyDV0XpY4g==
@@ -14548,13 +14548,13 @@ obuf@^1.0.0, obuf@^1.1.2:
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
oclif@^4.0.0:
- version "4.4.15"
- resolved "https://registry.yarnpkg.com/oclif/-/oclif-4.4.15.tgz#497debe9acb9cacd8c0c59a5beb1cfc7ef7ef987"
- integrity sha512-L2X3I+iTWpuMTzr1McDf2k8YwS57P+C9Yeqvo2jzXZ/sotkhoNW0RwDO4nZFqHvyn6I9Dj4Q8OVGVa85c6M0EQ==
+ version "4.4.16"
+ resolved "https://registry.yarnpkg.com/oclif/-/oclif-4.4.16.tgz#30384a03e20b7294a048a5f36ed7a755e988c0fd"
+ integrity sha512-4qNgN8ZhLZV2k92HXBiIvFGDM2HKgXrB8auVe9EpNQPlZr/PXeW7CTYpkEm3VYrmtohe2p3ma5jIR/Nyuu/v6g==
dependencies:
"@aws-sdk/client-cloudfront" "^3.511.0"
"@aws-sdk/client-s3" "^3.515.0"
- "@oclif/core" "^3.19.1"
+ "@oclif/core" "^3.19.2"
"@oclif/plugin-help" "^6.0.12"
"@oclif/plugin-not-found" "^3.0.10"
"@oclif/plugin-warn-if-update-available" "^3.0.10"