diff --git a/packages/core/BaseFeatureWidget/BaseFeatureDetail.tsx b/packages/core/BaseFeatureWidget/BaseFeatureDetail.tsx index b8dfb6794b..e80d1f936b 100644 --- a/packages/core/BaseFeatureWidget/BaseFeatureDetail.tsx +++ b/packages/core/BaseFeatureWidget/BaseFeatureDetail.tsx @@ -1,20 +1,34 @@ /* eslint-disable @typescript-eslint/no-explicit-any,react/prop-types */ -import Accordion from '@material-ui/core/Accordion' -import AccordionDetails from '@material-ui/core/AccordionDetails' -import AccordionSummary from '@material-ui/core/AccordionSummary' -import Typography from '@material-ui/core/Typography' +import React, { useEffect, useState } from 'react' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Typography, + Divider, + Tooltip, + Select, + MenuItem, +} from '@material-ui/core' import ExpandMore from '@material-ui/icons/ExpandMore' -import Divider from '@material-ui/core/Divider' -import Paper from '@material-ui/core/Paper' -import Tooltip from '@material-ui/core/Tooltip' import { makeStyles } from '@material-ui/core/styles' import { DataGrid } from '@material-ui/data-grid' import { observer } from 'mobx-react' import clsx from 'clsx' -import React, { FunctionComponent } from 'react' import isObject from 'is-object' +import { getConf } from '../configuration' +import { + measureText, + getSession, + defaultCodonTable, + generateCodonTable, + revcom, +} from '../util' +import { Feature } from '../util/simpleFeature' import SanitizedHTML from '../ui/SanitizedHTML' +type Feat = { start: number; end: number } + const globalOmit = [ 'name', 'start', @@ -76,19 +90,28 @@ export const useStyles = makeStyles(theme => ({ boxSizing: 'border-box', overflow: 'auto', }, + + accordionBorder: { + marginTop: '4px', + border: '1px solid #444', + }, })) interface BaseCardProps { title?: string defaultExpanded?: boolean + children?: React.ReactNode } -export const BaseCard: FunctionComponent = props => { +export function BaseCard({ + children, + title, + defaultExpanded = true, +}: BaseCardProps) { const classes = useStyles() - const { children, title, defaultExpanded = true } = props return ( @@ -146,6 +169,7 @@ const BasicValue = ({ value }: { value: string | React.ReactNode }) => { } const SimpleValue = ({ name, + value, description, prefix, @@ -195,9 +219,245 @@ const ArrayValue = ({ interface BaseProps extends BaseCardProps { feature: any descriptions?: Record + model?: any +} + +function stitch(subfeats: any, sequence: string) { + return subfeats + .map((sub: any) => { + return sequence.slice(sub.start, sub.end) + }) + .join('') +} + +// display the stitched-together sequence of a gene's CDS, cDNA, or protein +// sequence. this is a best effort and weird genomic phenomena could lead these +// to not be 100% accurate +function SequenceFeatureDetails(props: BaseProps) { + const { model, feature } = props + const { assemblyManager, rpcManager } = getSession(model) + const { assemblyNames } = model.view + const [preseq, setSequence] = useState() + const [error, setError] = useState() + const [assemblyName] = assemblyNames + const subfeatures = feature.subfeatures as { + start: number + end: number + type: string + }[] + const hasCDS = subfeatures.find(sub => sub.type === 'CDS') + const [mode, setMode] = useState(hasCDS ? 'cds' : 'cdna') + const loading = !preseq + const codonTable = generateCodonTable(defaultCodonTable) + + useEffect(() => { + ;(async () => { + const assembly = await assemblyManager.waitForAssembly(assemblyName) + if (!assembly) { + setError('assembly not found') + return + } + const adapterConfig = getConf(assembly, ['sequence', 'adapter']) + const sessionId = 'getSequence' + const region = { + start: feature.start, + end: feature.end, + refName: assembly?.getCanonicalRefName(feature.refName), + } + const feats = await rpcManager.call(sessionId, 'CoreGetFeatures', { + adapterConfig, + region, + sessionId, + }) + const [feat] = feats as Feature[] + if (!feat) { + setError('sequence not found') + } + + setSequence(feat.get('seq')) + })() + }, [feature, assemblyManager, rpcManager, assemblyName]) + + const text: React.ReactNode[] = [] + if (preseq && feature) { + const children = subfeatures + .sort((a, b) => a.start - b.start) + .map(sub => { + return { + ...sub, + start: sub.start - feature.start, + end: sub.end - feature.start, + } + }) + + const cdsColor = 'rgba(150,150,0,0.3)' + const utrColor = 'rgba(0,150,150,0.3)' + const proteinColor = 'rgba(150,0,150,0.3)' + + // filter duplicate entries in cds and exon lists duplicate entries may be + // rare but was seen in Gencode v36 track NCList, likely a bug on GFF3 or + // probably worth ignoring here (produces broken protein translations if + // included) + // position 1:224,800,006..225,203,064 gene ENSG00000185842.15 first + // transcript ENST00000445597.6 + // http://localhost:3000/?config=test_data%2Fconfig.json&session=share-FUl7G1isvF&password=HXh5Y + const fid = (feat: Feat) => `${feat.start}-${feat.end}` + + let cds = children + .filter(sub => sub.type === 'CDS') + .filter((item, pos, ary) => !pos || fid(item) !== fid(ary[pos - 1])) + + let exons = children + .filter(sub => sub.type === 'exon') + .filter((item, pos, ary) => !pos || fid(item) !== fid(ary[pos - 1])) + const revstrand = feature.strand === -1 + const sequence = revstrand ? revcom(preseq) : preseq + const seqlen = sequence.length + if (revstrand) { + cds = cds + .map(sub => ({ + ...sub, + start: seqlen - sub.end, + end: seqlen - sub.start, + })) + .sort((a, b) => a.start - b.start) + exons = exons + .map(sub => ({ + ...sub, + start: seqlen - sub.end, + end: seqlen - sub.start, + })) + .sort((a, b) => a.start - b.start) + } + + if (mode === 'cds') { + text.push( +
+ {stitch(cds, sequence)} +
, + ) + } else if (mode === 'cdna') { + // if we have CDS, it is a real gene, color the difference between the + // start and end of the CDS as UTR and the rest as CDS + if (cds.length) { + const firstCds = cds[0] + const lastCds = cds[cds.length - 1] + const firstCdsIdx = exons.findIndex(exon => { + return exon.end >= firstCds.start && exon.start <= firstCds.start + }) + const lastCdsIdx = exons.findIndex(exon => { + return exon.end >= lastCds.end && exon.start <= lastCds.end + }) + const lastCdsExon = exons[lastCdsIdx] + const firstCdsExon = exons[firstCdsIdx] + + // logic: there can be "UTR exons" that are just UTRs, so we stitch + // those together until we get to a CDS that overlaps and exon + text.push( +
+ {stitch(exons.slice(0, firstCdsIdx), sequence)} + {sequence.slice(firstCdsExon.start, firstCds.start)} +
, + ) + + text.push( +
+ {stitch(cds, sequence)} +
, + ) + + text.push( +
+ {sequence.slice(lastCds.end, lastCdsExon.end)} + {stitch(exons.slice(lastCdsIdx), sequence)} +
, + ) + } + // no CDS, probably a pseudogene, color whole thing as "UTR" + else { + const cdna = stitch(exons, sequence) + text.push( +
+ {cdna} +
, + ) + } + } else if (mode === 'protein') { + const str = stitch(cds, sequence) + let protein = '' + for (let i = 0; i < str.length; i += 3) { + // use & symbol for undefined codon, or partial slice + protein += codonTable[str.slice(i, i + 3)] || '&' + } + text.push( +
+ {protein} +
, + ) + } + } + + return ( +
+ +
+ {error ? ( + {error} + ) : ( +
+ {text} +
+ )} + {loading ?
Loading gene sequence...
: null} +
+
+ ) } -const CoreDetails = (props: BaseProps) => { +function CoreDetails(props: BaseProps) { const { feature } = props const { refName, start, end, strand } = feature const strandMap: Record = { @@ -211,7 +471,7 @@ const CoreDetails = (props: BaseProps) => { const displayRef = refName ? `${refName}:` : '' const displayedDetails: Record = { ...feature, - length: end - start, + length: (end - start).toLocaleString('en-US'), position: `${displayRef}${displayStart}..${displayEnd} ${strandStr}`, } @@ -234,21 +494,6 @@ const CoreDetails = (props: BaseProps) => { ) } -// xref https://gist.github.com/tophtucker/62f93a4658387bb61e4510c37e2e97cf -function measureText(str: string, fontSize = 10) { - // prettier-ignore - const widths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.2796875,0.2765625,0.3546875,0.5546875,0.5546875,0.8890625,0.665625,0.190625,0.3328125,0.3328125,0.3890625,0.5828125,0.2765625,0.3328125,0.2765625,0.3015625,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.2765625,0.2765625,0.584375,0.5828125,0.584375,0.5546875,1.0140625,0.665625,0.665625,0.721875,0.721875,0.665625,0.609375,0.7765625,0.721875,0.2765625,0.5,0.665625,0.5546875,0.8328125,0.721875,0.7765625,0.665625,0.7765625,0.721875,0.665625,0.609375,0.721875,0.665625,0.94375,0.665625,0.665625,0.609375,0.2765625,0.3546875,0.2765625,0.4765625,0.5546875,0.3328125,0.5546875,0.5546875,0.5,0.5546875,0.5546875,0.2765625,0.5546875,0.5546875,0.221875,0.240625,0.5,0.221875,0.8328125,0.5546875,0.5546875,0.5546875,0.5546875,0.3328125,0.5,0.2765625,0.5546875,0.5,0.721875,0.5,0.5,0.5,0.3546875,0.259375,0.353125,0.5890625] - const avg = 0.5279276315789471 - return ( - str - .split('') - .map(c => - c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg, - ) - .reduce((cur, acc) => acc + cur) * fontSize - ) -} - export const BaseCoreDetails = (props: BaseProps) => { return ( @@ -265,7 +510,7 @@ interface AttributeProps { prefix?: string } -export const Attributes: FunctionComponent = props => { +export const Attributes: React.FunctionComponent = props => { const { attributes, omit = [], @@ -410,54 +655,69 @@ export interface BaseInputProps extends BaseCardProps { formatter?: (val: unknown, key: string) => JSX.Element } -const Subfeature = (props: BaseProps) => { - const { feature } = props - const { type, name, id } = feature - const displayName = name || id - return ( - - - - - {feature.subfeatures && feature.subfeatures.length ? ( - - {feature.subfeatures.map((subfeature: any) => ( - - ))} - - ) : null} - - ) -} - function isEmpty(obj: Record) { return Object.keys(obj).length === 0 } export const BaseFeatureDetails = observer((props: BaseInputProps) => { - const classes = useStyles() const { model } = props + const { featureData } = model - if (!model.featureData) { + if (!featureData) { return null } - const feature = JSON.parse(JSON.stringify(model.featureData)) + const feature = JSON.parse(JSON.stringify(featureData)) if (isEmpty(feature)) { return null } + return +}) + +export const FeatureDetails = (props: { + model: any + feature: any + depth?: number + omit?: string[] + formatter?: (val: unknown, key: string) => JSX.Element +}) => { + const { model, feature, depth = 0 } = props + const { name, id, type, subfeatures } = feature + const displayName = name || id + const ellipsedDisplayName = + displayName && displayName.length > 20 ? '' : displayName + const session = getSession(model) + const defSeqTypes = ['mRNA', 'transcript'] + const sequenceTypes = + getConf(session, ['featureDetails', 'sequenceTypes']) || defSeqTypes + return ( - - + +
Core details
+ - - {feature.subfeatures && feature.subfeatures.length ? ( - - {feature.subfeatures.map((subfeature: any) => ( - +
Attributes
+ + {sequenceTypes.includes(feature.type) ? ( + + ) : null} + {subfeatures && subfeatures.length ? ( + + {subfeatures.map((sub: any) => ( + ))} ) : null} -
+
) -}) +} diff --git a/packages/core/BaseFeatureWidget/__snapshots__/index.test.js.snap b/packages/core/BaseFeatureWidget/__snapshots__/index.test.js.snap index 00242248f6..7208b0b225 100644 --- a/packages/core/BaseFeatureWidget/__snapshots__/index.test.js.snap +++ b/packages/core/BaseFeatureWidget/__snapshots__/index.test.js.snap @@ -2,179 +2,115 @@ exports[`open up a widget 1`] = `
-
- - - Primary data - -
- + + + +
+
+
+
+ Core details +
-
- Position -
-
-
- ctgA:3..102 (+) -
-
+ Position
-
- Length +
+ ctgA:3..102 (+)
-
-
- 100 -
+
+
+
+
+ Length +
+
+
+ 100
-
-
-
-
-
-
-
-
-
- - - Attributes - -
- -
-
-
-
-
+
+ Attributes +
-
- score -
-
-
- 37 -
+ score +
+
+
+ 37
diff --git a/packages/core/BaseFeatureWidget/index.test.js b/packages/core/BaseFeatureWidget/index.test.js index ed00a241d6..27ea385543 100644 --- a/packages/core/BaseFeatureWidget/index.test.js +++ b/packages/core/BaseFeatureWidget/index.test.js @@ -1,5 +1,7 @@ import { render } from '@testing-library/react' import React from 'react' +import { types } from 'mobx-state-tree' +import { ConfigurationSchema } from '@jbrowse/core/configuration' import PluginManager from '../PluginManager' import { stateModelFactory } from '.' import { BaseFeatureDetails as ReactComponent } from './BaseFeatureDetail' @@ -7,11 +9,19 @@ import { BaseFeatureDetails as ReactComponent } from './BaseFeatureDetail' test('open up a widget', () => { console.warn = jest.fn() const pluginManager = new PluginManager([]) - const model = stateModelFactory(pluginManager).create({ - type: 'BaseFeatureWidget', + + const Session = types.model({ + pluginManager: types.optional(types.frozen(), {}), + configuration: ConfigurationSchema('test', {}), + widget: stateModelFactory(pluginManager), + }) + const model = Session.create({ + widget: { type: 'BaseFeatureWidget' }, }) - const { container, getByText } = render() - model.setFeatureData({ + const { container, getByText } = render( + , + ) + model.widget.setFeatureData({ start: 2, end: 102, strand: 1, diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index 86f6b4c278..f98df6fa9d 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -865,3 +865,118 @@ export const rIC = (typeof jest === 'undefined' ? (cb: Function) => setTimeout(() => cb(), 1) : (cb: Function) => cb()) + +// xref https://gist.github.com/tophtucker/62f93a4658387bb61e4510c37e2e97cf +export function measureText(str: string, fontSize = 10) { + // prettier-ignore + const widths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.2796875,0.2765625,0.3546875,0.5546875,0.5546875,0.8890625,0.665625,0.190625,0.3328125,0.3328125,0.3890625,0.5828125,0.2765625,0.3328125,0.2765625,0.3015625,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.5546875,0.2765625,0.2765625,0.584375,0.5828125,0.584375,0.5546875,1.0140625,0.665625,0.665625,0.721875,0.721875,0.665625,0.609375,0.7765625,0.721875,0.2765625,0.5,0.665625,0.5546875,0.8328125,0.721875,0.7765625,0.665625,0.7765625,0.721875,0.665625,0.609375,0.721875,0.665625,0.94375,0.665625,0.665625,0.609375,0.2765625,0.3546875,0.2765625,0.4765625,0.5546875,0.3328125,0.5546875,0.5546875,0.5,0.5546875,0.5546875,0.2765625,0.5546875,0.5546875,0.221875,0.240625,0.5,0.221875,0.8328125,0.5546875,0.5546875,0.5546875,0.5546875,0.3328125,0.5,0.2765625,0.5546875,0.5,0.721875,0.5,0.5,0.5,0.3546875,0.259375,0.353125,0.5890625] + const avg = 0.5279276315789471 + return ( + str + .split('') + .map(c => + c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg, + ) + .reduce((cur, acc) => acc + cur) * fontSize + ) +} + +export const defaultStarts = ['ATG'] +export const defaultStops = ['TAA', 'TAG', 'TGA'] +export const defaultCodonTable = { + TCA: 'S', + TCC: 'S', + TCG: 'S', + TCT: 'S', + TTC: 'F', + TTT: 'F', + TTA: 'L', + TTG: 'L', + TAC: 'Y', + TAT: 'Y', + TAA: '*', + TAG: '*', + TGC: 'C', + TGT: 'C', + TGA: '*', + TGG: 'W', + CTA: 'L', + CTC: 'L', + CTG: 'L', + CTT: 'L', + CCA: 'P', + CCC: 'P', + CCG: 'P', + CCT: 'P', + CAC: 'H', + CAT: 'H', + CAA: 'Q', + CAG: 'Q', + CGA: 'R', + CGC: 'R', + CGG: 'R', + CGT: 'R', + ATA: 'I', + ATC: 'I', + ATT: 'I', + ATG: 'M', + ACA: 'T', + ACC: 'T', + ACG: 'T', + ACT: 'T', + AAC: 'N', + AAT: 'N', + AAA: 'K', + AAG: 'K', + AGC: 'S', + AGT: 'S', + AGA: 'R', + AGG: 'R', + GTA: 'V', + GTC: 'V', + GTG: 'V', + GTT: 'V', + GCA: 'A', + GCC: 'A', + GCG: 'A', + GCT: 'A', + GAC: 'D', + GAT: 'D', + GAA: 'E', + GAG: 'E', + GGA: 'G', + GGC: 'G', + GGG: 'G', + GGT: 'G', +} + +/** + * take CodonTable above and generate larger codon table that includes + * all permutations of upper and lower case nucleotides + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function generateCodonTable(table: any) { + const tempCodonTable: { [key: string]: string } = {} + Object.keys(table).forEach(codon => { + const aa = table[codon] + const nucs: string[][] = [] + for (let i = 0; i < 3; i++) { + const nuc = codon.charAt(i) + nucs[i] = [] + nucs[i][0] = nuc.toUpperCase() + nucs[i][1] = nuc.toLowerCase() + } + for (let i = 0; i < 2; i++) { + const n0 = nucs[0][i] + for (let j = 0; j < 2; j++) { + const n1 = nucs[1][j] + for (let k = 0; k < 2; k++) { + const n2 = nucs[2][k] + const triplet = n0 + n1 + n2 + tempCodonTable[triplet] = aa + } + } + } + }) + return tempCodonTable +} diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx b/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx index da3bf3b023..de4e8f4684 100644 --- a/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +++ b/plugins/alignments/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx @@ -4,7 +4,7 @@ import { getSession } from '@jbrowse/core/util' import React, { useState } from 'react' import copy from 'copy-to-clipboard' import { - BaseFeatureDetails, + FeatureDetails, BaseCard, useStyles, } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail' @@ -162,9 +162,10 @@ function AlignmentFeatureDetails(props: { model: any }) { const SA = (feat.tags && feat.tags.SA) || feat.SA return ( - { return key === 'next_segment_position' ? ( diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.js.snap b/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.js.snap index 9b44bbfdf0..b7a6e767e1 100644 --- a/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.js.snap +++ b/plugins/alignments/src/AlignmentsFeatureDetail/__snapshots__/index.test.js.snap @@ -6,339 +6,273 @@ exports[`open up a widget 1`] = ` data-testid="alignment-side-drawer" >
-
- - - Primary data - -
- + + + + +
+
+
+
+ Core details +
+ Position +
+
-
- Position -
-
-
- ctgA:3..102 (+) -
+
+ ctgA:3..102 (+)
+
+
-
- Name -
-
-
- ctgA_3_555_0:0:0_2:0:0_102d -
-
+ Name
-
- Length -
-
-
- 100 -
+
+ ctgA_3_555_0:0:0_2:0:0_102d
+
+
-
- Type + Length +
+
+
+ 100
-
-
- match -
+
+
+
+
+ Type +
+
+
+ match
-
-
-
-
-
-
-
-
-
- - - Attributes - -
- -
-
-
-
-
+
+ Attributes +
-
- seq -
-
-
- TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG -
-
+ seq
-
- score -
-
-
- 37 -
+
+ TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG
+
+
-
- qual -
-
- - -
- 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... -
-
+ score
-
- MQ -
-
-
- 37 -
+
+ 37
+
+
+
+ qual +
-
- CIGAR -
-
+ +
+ 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...
+
+
+
+ MQ +
-
- length_on_ref -
-
-
- 100 -
+
+ 37
+
+
+
+ CIGAR +
-
- template_length +
+ 100M
-
-
- 0 -
+
+
+
+
+ length_on_ref +
+
+
+ 100
+
+
-
- seq_length + template_length +
+
+
+ 0
-
-
- 100 -
+
+
+
+
+ seq_length +
+
+
+ 100
@@ -349,8 +283,7 @@ exports[`open up a widget 1`] = `
{ console.warn = jest.fn() const pluginManager = new PluginManager([]) - const model = stateModelFactory(pluginManager).create({ - type: 'AlignmentsFeatureWidget', + + const Session = types.model({ + pluginManager: types.optional(types.frozen(), {}), + configuration: ConfigurationSchema('test', {}), + widget: stateModelFactory(pluginManager), + }) + const session = Session.create({ + widget: { type: 'AlignmentsFeatureWidget' }, }) - model.setFeatureData({ + session.widget.setFeatureData({ seq: 'TTGTTGCGGAGTTGAACAACGGCATTAGGAACACTTCCGTCTCTCACTTTTATACGATTATGATTGGTTCTTTAGCCTTGGTTTAGATTGGTAGTAGTAG', start: 2, @@ -28,7 +36,9 @@ test('open up a widget', () => { refName: 'ctgA', type: 'match', }) - const { container, getByText } = render() + const { container, getByText } = render( + , + ) expect(container.firstChild).toMatchSnapshot() expect(getByText('ctgA:3..102 (+)')).toBeTruthy() }) diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/Category.js b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/Category.js index 35634546c9..fca028f036 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/Category.js +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/Category.js @@ -17,6 +17,10 @@ const useStyles = makeStyles(theme => ({ expandIcon: { color: '#FFFFFF', }, + accordionBorder: { + marginTop: 4, + border: '1px solid #444', + }, })) function Category({ @@ -45,7 +49,7 @@ function Category({ return ( model.toggleCategory(pathName)} > diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.js.snap b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.js.snap index d749f0d068..df8bf04cca 100644 --- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.js.snap +++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/__snapshots__/HierarchicalTrackSelector.test.js.snap @@ -134,8 +134,7 @@ exports[`HierarchicalTrackSelector widget renders with a couple of categorized t class="MuiFormGroup-root" />
@@ -20,105 +28,6 @@ interface MyProps { showTranslation: boolean } -const defaultStarts = ['ATG'] -const defaultStops = ['TAA', 'TAG', 'TGA'] -const defaultCodonTable = { - TCA: 'S', - TCC: 'S', - TCG: 'S', - TCT: 'S', - TTC: 'F', - TTT: 'F', - TTA: 'L', - TTG: 'L', - TAC: 'Y', - TAT: 'Y', - TAA: '*', - TAG: '*', - TGC: 'C', - TGT: 'C', - TGA: '*', - TGG: 'W', - CTA: 'L', - CTC: 'L', - CTG: 'L', - CTT: 'L', - CCA: 'P', - CCC: 'P', - CCG: 'P', - CCT: 'P', - CAC: 'H', - CAT: 'H', - CAA: 'Q', - CAG: 'Q', - CGA: 'R', - CGC: 'R', - CGG: 'R', - CGT: 'R', - ATA: 'I', - ATC: 'I', - ATT: 'I', - ATG: 'M', - ACA: 'T', - ACC: 'T', - ACG: 'T', - ACT: 'T', - AAC: 'N', - AAT: 'N', - AAA: 'K', - AAG: 'K', - AGC: 'S', - AGT: 'S', - AGA: 'R', - AGG: 'R', - GTA: 'V', - GTC: 'V', - GTG: 'V', - GTT: 'V', - GCA: 'A', - GCC: 'A', - GCG: 'A', - GCT: 'A', - GAC: 'D', - GAT: 'D', - GAA: 'E', - GAG: 'E', - GGA: 'G', - GGC: 'G', - GGG: 'G', - GGT: 'G', -} - -/** - * take CodonTable above and generate larger codon table that includes - * all permutations of upper and lower case nucleotides - */ -function generateCodonTable(table: any) { - const tempCodonTable: { [key: string]: string } = {} - Object.keys(table).forEach(codon => { - const aa = table[codon] - const nucs: string[][] = [] - for (let i = 0; i < 3; i++) { - const nuc = codon.charAt(i) - nucs[i] = [] - nucs[i][0] = nuc.toUpperCase() - nucs[i][1] = nuc.toLowerCase() - } - for (let i = 0; i < 2; i++) { - const n0 = nucs[0][i] - for (let j = 0; j < 2; j++) { - const n1 = nucs[1][j] - for (let k = 0; k < 2; k++) { - const n2 = nucs[2][k] - const triplet = n0 + n1 + n2 - tempCodonTable[triplet] = aa - } - } - } - }) - return tempCodonTable -} - function Translation(props: { codonTable: any seq: string diff --git a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.js b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.js index ff2843798a..818a142cba 100644 --- a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.js +++ b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.test.js @@ -1,27 +1,38 @@ import React from 'react' import { render } from '@testing-library/react' -import { observable } from 'mobx' +import { types } from 'mobx-state-tree' +import { ConfigurationSchema } from '@jbrowse/core/configuration' +import PluginManager from '@jbrowse/core/PluginManager' +import { stateModelFactory } from '.' import VariantFeatureDetails from './VariantFeatureWidget' describe('VariantTrack widget', () => { it('renders with just the required model elements', () => { - const f = observable({ - featureData: { - refName: 'ctgA', - start: 176, - end: 177, - name: 'rs123', - REF: 'A', - ALT: '', - QUAL: 10.4, - INFO: { - MQ: 5, - }, + console.warn = jest.fn() + const pluginManager = new PluginManager([]) + const Session = types.model({ + pluginManager: types.optional(types.frozen(), {}), + configuration: ConfigurationSchema('test', {}), + widget: stateModelFactory(pluginManager), + }) + const model = Session.create({ + widget: { type: 'VariantFeatureWidget' }, + }) + model.widget.setFeatureData({ + refName: 'ctgA', + start: 176, + end: 177, + name: 'rs123', + REF: 'A', + ALT: '', + QUAL: 10.4, + INFO: { + MQ: 5, }, }) - const { container } = render() + const { container } = render() expect(container.firstChild).toMatchSnapshot() }) }) diff --git a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx index 7096098207..86bbb4cc2f 100644 --- a/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx +++ b/plugins/variants/src/VariantFeatureWidget/VariantFeatureWidget.tsx @@ -15,7 +15,7 @@ import SimpleFeature, { import { DataGrid } from '@material-ui/data-grid' import { observer } from 'mobx-react' import { - BaseFeatureDetails, + FeatureDetails, BaseCard, } from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail' import { getSession } from '@jbrowse/core/util' @@ -210,11 +210,7 @@ function VariantFeatureDetails(props: any) { return ( - + {feat.type === 'breakend' ? (
-
- - - Primary data - -
- + + + + +
+
+
+
+ Core details +
-
- Position -
-
-
- ctgA:177..177 -
-
+ Position
-
- Name -
-
-
- rs123 -
+
+ ctgA:177..177
+
+
+
+ Name +
-
- Length +
+ rs123
-
-
- 1 -
+
+
+
+
+ Length +
+
+
+ 1
-
-
-
-
-
-
-
-
-
- - - Attributes - -
- -
-
-
-
-
+
+ Attributes +
-
- REF -
-
-
- A -
-
+ REF
-
- ALT -
-
-
- <TRA> -
+
+ A
+
+
-
- QUAL -
-
-
- 10.4 -
+ ALT +
+
+
+ <TRA>
+
+
+
+ QUAL +
-
- INFO.MQ +
+ 10.4
-
-
- 5 -
+
+
+
+
+ INFO.MQ +
+
+
+ 5
diff --git a/products/jbrowse-web/src/jbrowseModel.js b/products/jbrowse-web/src/jbrowseModel.js index a3e948a176..e3b45502ec 100644 --- a/products/jbrowse-web/src/jbrowseModel.js +++ b/products/jbrowse-web/src/jbrowseModel.js @@ -34,6 +34,12 @@ export default function JBrowseWeb( defaultValue: 'https://g5um1mrb0i.execute-api.us-east-1.amazonaws.com/api/v1/', }, + featureDetails: ConfigurationSchema('FeatureDetails', { + sequenceTypes: { + type: 'stringArray', + defaultValue: ['mRNA', 'transcript'], + }, + }), disableAnalytics: { type: 'boolean', defaultValue: false, diff --git a/products/jbrowse-web/src/tests/CircularView.test.js b/products/jbrowse-web/src/tests/CircularView.test.js index c6515cd2e9..cee5ffe248 100644 --- a/products/jbrowse-web/src/tests/CircularView.test.js +++ b/products/jbrowse-web/src/tests/CircularView.test.js @@ -66,9 +66,14 @@ describe('circular views', () => { fireEvent.click(await findByTestId('htsTrackEntry-volvox_sv_test')) // expect the chord track to render eventually - await wait(() => { - expect(getByTestId('structuralVariantChordRenderer')).toBeInTheDocument() - }) + await wait( + () => { + expect( + getByTestId('structuralVariantChordRenderer'), + ).toBeInTheDocument() + }, + { timeout: 10000 }, + ) // make sure a chord is rendered await wait(() => { expect(getByTestId('chord-test-vcf-66132')).toBeInTheDocument() @@ -90,5 +95,5 @@ describe('circular views', () => { await wait(() => { expect(getByTestId('chord-test-vcf-62852')).toBeInTheDocument() }) - }, 10000) + }, 15000) }) diff --git a/products/jbrowse-web/src/tests/JBrowse.test.js b/products/jbrowse-web/src/tests/JBrowse.test.js index 29a20ac2ea..0aefea5002 100644 --- a/products/jbrowse-web/src/tests/JBrowse.test.js +++ b/products/jbrowse-web/src/tests/JBrowse.test.js @@ -249,7 +249,7 @@ test('looks at about this track dialog', async () => { // load track fireEvent.click(await findByTestId('htsTrackEntry-volvox-long-reads-cram')) - fireEvent.click(await findByTestId('track_menu_icon')) + fireEvent.click(await findByTestId('track_menu_icon', {}, { timeout: 10000 })) fireEvent.click(await findByText('About this track')) await findAllByText(/SQ/, {}, { timeout: 10000 }) -}) +}, 15000)