diff --git a/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx index 76f05c0f5..b0888d323 100644 --- a/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +++ b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx @@ -1,40 +1,22 @@ /* eslint-disable unicorn/no-nested-ternary */ /* eslint-disable unicorn/prefer-at */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import type { AnnotationFeature, TranscriptPart } from '@apollo-annotation/mst' import { LocationEndChange, LocationStartChange, } from '@apollo-annotation/shared' import styled from '@emotion/styled' -import { - type AbstractSessionModel, - defaultCodonTable, - revcom, -} from '@jbrowse/core/util' +import { type AbstractSessionModel, revcom } from '@jbrowse/core/util' import AddIcon from '@mui/icons-material/Add' -import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import ContentCutIcon from '@mui/icons-material/ContentCut' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import RemoveIcon from '@mui/icons-material/Remove' -import { - Accordion, - AccordionDetails, - AccordionSummary, - Grid, - Tooltip, - Typography, -} from '@mui/material' +import { Grid, Typography } from '@mui/material' import { observer } from 'mobx-react' -import React, { useRef } from 'react' import type { OntologyRecord } from '../OntologyManager' import type { ApolloSessionModel } from '../session' -import { copyToClipboard } from '../util/copyToClipboard' import { NumberTextField } from './NumberTextField' +import { Translation } from './Translation' const StyledTextField = styled(NumberTextField)(() => ({ '&.MuiFormControl-root': { @@ -50,29 +32,6 @@ const StyledTextField = styled(NumberTextField)(() => ({ }, })) -const SequenceContainer = styled('div')({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - textAlign: 'left', - width: '100%', - overflowWrap: 'break-word', - wordWrap: 'break-word', - wordBreak: 'break-all', - '& span': { - fontSize: 12, - }, -}) - -const StyledAccordionSummary = styled(AccordionSummary)(() => ({ - minHeight: 30, - maxHeight: 30, - '&.Mui-expanded': { - minHeight: 30, - maxHeight: 30, - }, -})) - const Strand = (props: { strand: 1 | -1 | undefined }) => { const { strand } = props @@ -118,7 +77,6 @@ export const TranscriptWidgetEditLocation = observer( const currentAssembly = session.apolloDataStore.assemblies.get(assembly) const refData = currentAssembly?.getByRefName(refName) const { changeManager } = session.apolloDataStore - const seqRef = useRef(null) const { changeInProgress } = session if (!refData) { @@ -699,263 +657,6 @@ export const TranscriptWidgetEditLocation = observer( ] } - const getTranslationSequence = () => { - let wholeSequence = '' - const [firstLocation] = cdsLocations - const sortedCDSLocations = firstLocation.toSorted( - ({ min: a }, { min: b }) => a - b, - ) - for (const loc of sortedCDSLocations) { - wholeSequence += refData.getSequence(loc.min, loc.max) - } - if (strand === -1) { - // Original: ACGCAT - // Complement: TGCGTA - // Reverse complement: ATGCGT - wholeSequence = revcom(wholeSequence) - } - const elements = [] - for ( - let codonGenomicPos = 0; - codonGenomicPos < wholeSequence.length; - codonGenomicPos += 3 - ) { - const codonSeq = wholeSequence - .slice(codonGenomicPos, codonGenomicPos + 3) - .toUpperCase() - const protein = - defaultCodonTable[codonSeq as keyof typeof defaultCodonTable] || '&' - // highlight start codon and stop codons - if (codonSeq === 'ATG') { - elements.push( - { - if (changeInProgress) { - return - } - // NOTE: codonGenomicPos is important here for calculating the genomic location - // of the start codon. We are using the codonGenomicPos as the key in the typography - // elements to maintain the genomic postion of the codon start - const startCodonGenomicLocation = - getCodonGenomicLocation(codonGenomicPos) - if (startCodonGenomicLocation !== cdsMin && strand === 1) { - updateCDSLocation( - cdsMin, - startCodonGenomicLocation, - feature, - true, - ) - } - if (startCodonGenomicLocation !== cdsMax && strand === -1) { - updateCDSLocation( - cdsMax, - startCodonGenomicLocation, - feature, - false, - ) - } - }} - > - {protein} - , - ) - } else if (['TAA', 'TAG', 'TGA'].includes(codonSeq)) { - elements.push( - - {protein} - , - ) - } else { - elements.push( - // Pass the codonGenomicPos as the key to maintain the genomic position of the codon - - {protein} - , - ) - } - } - return elements - } - - // Codon position is the index of the start codon in the CDS genomic sequence - // Calculate the genomic location of the start codon based on the codon position in the CDS - const getCodonGenomicLocation = (codonGenomicPosition: number) => { - const [firstLocation] = cdsLocations - let cdsLen = 0 - const sortedCDSLocations = firstLocation.toSorted( - ({ min: a }, { min: b }) => a - b, - ) - - // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}] - // and codonGenomicPosition is 25 - // ((10 - 0) + (30 - 20) + (50 - 40)) > 25 - // So, start codon is in (40, 50) - // 40 + (25-20) = 45 is the genomic location of the start codon - if (strand === 1) { - for (const loc of sortedCDSLocations) { - const locLength = loc.max - loc.min - if (cdsLen + locLength > codonGenomicPosition) { - return loc.min + (codonGenomicPosition - cdsLen) - } - cdsLen += locLength - } - } else if (strand === -1) { - for (let i = sortedCDSLocations.length - 1; i >= 0; i--) { - const loc = sortedCDSLocations[i] - const locLength = loc.max - loc.min - if (cdsLen + locLength > codonGenomicPosition) { - return loc.max - (codonGenomicPosition - cdsLen) - } - cdsLen += locLength - } - } - - if (strand === 1) { - return cdsMin - } - - return cdsMax - } - - const trimTranslationSequence = () => { - const sequenceElements = getTranslationSequence() - const translationSequence = sequenceElements - .map((el) => el.props.children) - .join('') - - if ( - translationSequence.startsWith('M') && - translationSequence.endsWith('*') - ) { - return - } - - // NOTE: We are maintaining the genomic location of the codon start as the "key" - // in typography elements. See getTranslationSequence function - const translSeqCodonStartGenomicPosArr = [] - for (const el of sequenceElements) { - translSeqCodonStartGenomicPosArr.push({ - codonGenomicPos: el.key, - sequenceLetter: el.props.children, - }) - } - - if (translSeqCodonStartGenomicPosArr.length === 0) { - return - } - - // Trim any sequence before first start codon and after stop codon - const startCodonIndex = translationSequence.indexOf('M') - const stopCodonIndex = translationSequence.indexOf('*') - - const startCodonPos = - translSeqCodonStartGenomicPosArr[startCodonIndex].codonGenomicPos - const stopCodonPos = - translSeqCodonStartGenomicPosArr[stopCodonIndex].codonGenomicPos - - if (!startCodonPos || !stopCodonPos) { - return - } - const startCodonGenomicLoc = getCodonGenomicLocation( - startCodonPos as unknown as number, - ) - let stopCodonGenomicLoc = getCodonGenomicLocation( - stopCodonPos as unknown as number, - ) - - if (strand === 1) { - if (startCodonGenomicLoc > stopCodonGenomicLoc) { - notify( - 'Start codon genomic location should be less than stop codon genomic location', - 'error', - ) - return - } - let promise - stopCodonGenomicLoc += 3 // move to end of stop codon - if (startCodonGenomicLoc !== cdsMin) { - promise = new Promise((resolve) => { - updateCDSLocation( - cdsMin, - startCodonGenomicLoc, - feature, - true, - () => { - resolve(true) - }, - ) - }) - } - - if (stopCodonGenomicLoc !== cdsMax) { - if (promise) { - void promise.then(() => { - updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false) - }) - } else { - updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false) - } - } - } - - if (strand === -1) { - // reverse strand - if (startCodonGenomicLoc < stopCodonGenomicLoc) { - notify( - 'Start codon genomic location should be less than stop codon genomic location', - 'error', - ) - return - } - let promise - stopCodonGenomicLoc -= 3 // move to end of stop codon - if (startCodonGenomicLoc !== cdsMax) { - promise = new Promise((resolve) => { - updateCDSLocation( - cdsMax, - startCodonGenomicLoc, - feature, - false, - () => { - resolve(true) - }, - ) - }) - } - - if (stopCodonGenomicLoc !== cdsMin) { - if (promise) { - void promise.then(() => { - updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true) - }) - } else { - updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true) - } - } - } - notify('Translation sequence trimmed to start and stop codons', 'success') - } - - const onCopyClick = () => { - const seqDiv = seqRef.current - if (!seqDiv) { - return - } - void copyToClipboard(seqDiv) - } - return (
{cdsPresent && ( @@ -1169,58 +870,17 @@ export const TranscriptWidgetEditLocation = observer( })}
{cdsPresent && ( -
- - } - aria-controls="panel1-content" - id="panel1-header" - > - - Translation - - - - - - {getTranslationSequence()} - - -
- - - - - - -
-
-
-
+ )} ) diff --git a/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/Translation.tsx b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/Translation.tsx new file mode 100644 index 000000000..135da4d38 --- /dev/null +++ b/packages/jbrowse-plugin-apollo/src/FeatureDetailsWidget/Translation.tsx @@ -0,0 +1,358 @@ +import type { + AnnotationFeature, + ApolloRefSeqI, + TranscriptPartCoding, +} from '@apollo-annotation/mst' +import styled from '@emotion/styled' +import { + type AbstractSessionModel, + defaultCodonTable, + revcom, +} from '@jbrowse/core/util' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import ContentCutIcon from '@mui/icons-material/ContentCut' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Tooltip, + Typography, +} from '@mui/material' +import React, { useRef } from 'react' + +import type { ApolloSessionModel } from '../session' +import { copyToClipboard } from '../util/copyToClipboard' + +const SequenceContainer = styled('div')({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + textAlign: 'left', + width: '100%', + overflowWrap: 'break-word', + wordWrap: 'break-word', + wordBreak: 'break-all', + '& span': { + fontSize: 12, + }, +}) + +const StyledAccordionSummary = styled(AccordionSummary)(() => ({ + minHeight: 30, + maxHeight: 30, + '&.Mui-expanded': { + minHeight: 30, + maxHeight: 30, + }, +})) + +export function Translation({ + changeInProgress, + cdsLocations, + refData, + strand, + // eslint-disable-next-line @typescript-eslint/unbound-method + updateCDSLocation, + cdsMin, + cdsMax, + feature, + session, +}: { + changeInProgress: boolean + cdsLocations: TranscriptPartCoding[][] + refData: ApolloRefSeqI + strand: 1 | -1 | undefined + updateCDSLocation( + oldLocation: number, + newLocation: number, + feature: AnnotationFeature, + isMin: boolean, + onComplete?: () => void, + ): boolean + cdsMin: number + cdsMax: number + feature: AnnotationFeature + session: ApolloSessionModel +}) { + const seqRef = useRef(null) + const { notify } = session as unknown as AbstractSessionModel + + const cdsSequences: string[] = [] + const [firstLocation] = cdsLocations + for (const loc of firstLocation) { + const seq = refData.getSequence(loc.min, loc.max) + cdsSequences.push(strand === -1 ? revcom(seq) : seq) + } + const cdsSequence = cdsSequences.join('') + + const proteinSequence: string[] = [] + for ( + let codonGenomicPos = 0; + codonGenomicPos < cdsSequence.length; + codonGenomicPos += 3 + ) { + const codonSeq = cdsSequence + .slice(codonGenomicPos, codonGenomicPos + 3) + .toUpperCase() + const protein = + defaultCodonTable[codonSeq as keyof typeof defaultCodonTable] || '&' + proteinSequence.push(protein) + } + + const onCopyClick = () => { + const seqDiv = seqRef.current + if (!seqDiv) { + return + } + void copyToClipboard(seqDiv) + } + + // Codon position is the index of the start codon in the CDS genomic sequence + // Calculate the genomic location of the start codon based on the codon position in the CDS + const getCodonGenomicLocation = (codonGenomicPosition: number) => { + const [firstLocation] = cdsLocations + let cdsLen = 0 + const sortedCDSLocations = firstLocation.toSorted( + ({ min: a }, { min: b }) => a - b, + ) + + // Suppose CDS locations are [{min: 0, max: 10}, {min: 20, max: 30}, {min: 40, max: 50}] + // and codonGenomicPosition is 25 + // ((10 - 0) + (30 - 20) + (50 - 40)) > 25 + // So, start codon is in (40, 50) + // 40 + (25-20) = 45 is the genomic location of the start codon + if (strand === 1) { + for (const loc of sortedCDSLocations) { + const locLength = loc.max - loc.min + if (cdsLen + locLength > codonGenomicPosition) { + return loc.min + (codonGenomicPosition - cdsLen) + } + cdsLen += locLength + } + } else if (strand === -1) { + for (let i = sortedCDSLocations.length - 1; i >= 0; i--) { + const loc = sortedCDSLocations[i] + const locLength = loc.max - loc.min + if (cdsLen + locLength > codonGenomicPosition) { + return loc.max - (codonGenomicPosition - cdsLen) + } + cdsLen += locLength + } + } + + if (strand === 1) { + return cdsMin + } + + return cdsMax + } + const trimTranslationSequence = () => { + if ( + (proteinSequence.at(0) === 'M' && proteinSequence.at(-1) === '*') || + proteinSequence.length === 0 + ) { + return + } + + // Trim any sequence before first start codon and after stop codon + const startCodonIndex = proteinSequence.indexOf('M') + const stopCodonIndex = proteinSequence.lastIndexOf('*') + + const startCodonPos = startCodonIndex * 3 + const stopCodonPos = stopCodonIndex * 3 + + const startCodonGenomicLoc = getCodonGenomicLocation(startCodonPos) + let stopCodonGenomicLoc = getCodonGenomicLocation(stopCodonPos) + + if (strand === 1) { + if (startCodonGenomicLoc > stopCodonGenomicLoc) { + notify( + 'Start codon genomic location should be less than stop codon genomic location', + 'error', + ) + return + } + let promise + stopCodonGenomicLoc += 3 // move to end of stop codon + if (startCodonGenomicLoc !== cdsMin) { + promise = new Promise((resolve) => { + updateCDSLocation(cdsMin, startCodonGenomicLoc, feature, true, () => { + resolve(true) + }) + }) + } + + if (stopCodonGenomicLoc !== cdsMax) { + if (promise) { + void promise.then(() => { + updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false) + }) + } else { + updateCDSLocation(cdsMax, stopCodonGenomicLoc, feature, false) + } + } + } + + if (strand === -1) { + // reverse strand + if (startCodonGenomicLoc < stopCodonGenomicLoc) { + notify( + 'Start codon genomic location should be less than stop codon genomic location', + 'error', + ) + return + } + let promise + stopCodonGenomicLoc -= 3 // move to end of stop codon + if (startCodonGenomicLoc !== cdsMax) { + promise = new Promise((resolve) => { + updateCDSLocation( + cdsMax, + startCodonGenomicLoc, + feature, + false, + () => { + resolve(true) + }, + ) + }) + } + + if (stopCodonGenomicLoc !== cdsMin) { + if (promise) { + void promise.then(() => { + updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true) + }) + } else { + updateCDSLocation(cdsMin, stopCodonGenomicLoc, feature, true) + } + } + } + notify('Translation sequence trimmed to start and stop codons', 'success') + } + return ( +
+ + } + aria-controls="panel1-content" + id="panel1-header" + > + + Translation + + + + + + {proteinSequence.map((protein, idx) => { + const codonGenomicPos = idx * 3 + if (protein === 'M') { + return ( + { + if (changeInProgress) { + return + } + // NOTE: codonGenomicPos is important here for calculating the genomic location + // of the start codon. We are using the codonGenomicPos as the key in the typography + // elements to maintain the genomic postion of the codon start + const startCodonGenomicLocation = + getCodonGenomicLocation(codonGenomicPos) + if ( + startCodonGenomicLocation !== cdsMin && + strand === 1 + ) { + updateCDSLocation( + cdsMin, + startCodonGenomicLocation, + feature, + true, + ) + } + if ( + startCodonGenomicLocation !== cdsMax && + strand === -1 + ) { + updateCDSLocation( + cdsMax, + startCodonGenomicLocation, + feature, + false, + ) + } + }} + > + {protein} + + ) + } + + if (protein === '*') { + return ( + + {protein} + + ) + } + // Pass the codonGenomicPos as the key to maintain the genomic position of the codon + return ( + + {protein} + + ) + })} + + +
+ + + + + + +
+
+
+
+ ) +}