From 64d4b12317e09ec65397615bcbc1c3a8c88b1fa3 Mon Sep 17 00:00:00 2001 From: Colin Date: Tue, 9 Jul 2024 01:22:17 -0400 Subject: [PATCH] Misc refactoring --- eslint.config.mjs | 7 +- .../components/AlphaFoldDBSearch.tsx | 13 +-- .../components/AlphaFoldDBSearchStatus.tsx | 28 +++-- .../components/HelpDialog.tsx | 43 +++++--- .../components/LaunchProteinViewDialog.tsx | 2 +- src/LaunchProteinView/components/MSATable.tsx | 100 ++++++++++++------ .../components/TranscriptSelector.tsx | 3 +- .../components/UserProvidedStructure.tsx | 99 +++++++++++------ .../useLocalStructureFileSequence.ts | 11 +- .../useMyGeneInfoUniprotIdLookup.ts | 4 +- src/LaunchProteinView/components/util.ts | 6 +- src/ProteinView/components/SplitString.tsx | 2 +- src/ProteinView/model.ts | 3 +- 13 files changed, 209 insertions(+), 112 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 9da1e7d..a7698bb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -87,7 +87,12 @@ export default [ ignoreRestSiblings: true, }, ], - + 'no-console': [ + 'warn', + { + allow: ['error', 'warn'], + }, + ], curly: 'error', 'no-extra-semi': 'off', 'unicorn/no-negated-condition': 'off', diff --git a/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx b/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx index 7b09b89..53f27bc 100644 --- a/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +++ b/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { observer } from 'mobx-react' -import { Button, DialogActions, DialogContent } from '@mui/material' +import { Button, DialogActions, DialogContent, Typography } from '@mui/material' import { makeStyles } from 'tss-react/mui' import { AbstractTrackModel, @@ -73,6 +73,7 @@ const AlphaFoldDBSearch = observer(function ({ ? getDisplayName(selectedTranscript) : getDisplayName(feature), }) + const url = uniprotId ? `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_v4.cif` : undefined @@ -86,10 +87,6 @@ const AlphaFoldDBSearch = observer(function ({ useEffect(() => { if (isoformSequences !== undefined) { - console.log( - { structureSequence }, - options.find(f => isoformSequences[f.id()]?.seq == structureSequence), - ) const ret = options.find( f => @@ -104,10 +101,10 @@ const AlphaFoldDBSearch = observer(function ({ <> {e ? : null} -
+ Automatically find AlphaFoldDB entry for given transcript{' '} -
+ {isRemoteStructureSequenceLoading ? ( @@ -57,11 +58,24 @@ export default function AlphaFoldDBSearchStatus({ {structureSequence ? ( - +
+ + {showAllProteinSequences ? ( + + ) : null} +
) : ( )} diff --git a/src/LaunchProteinView/components/HelpDialog.tsx b/src/LaunchProteinView/components/HelpDialog.tsx index 1fadd9c..e4d3104 100644 --- a/src/LaunchProteinView/components/HelpDialog.tsx +++ b/src/LaunchProteinView/components/HelpDialog.tsx @@ -18,26 +18,41 @@ export default function HelpDialog({ handleClose: () => void }) { return ( - + - The automatic lookup performs the following steps: -
    + The procedure for the protein lookup is as follows: +
    • - searches mygene.info for the transcript ID, in order to retrieve - the UniProt ID associated with a given transcript ID + (Automatic lookup) Searches mygene.info for the transcript ID, in + order to retrieve the UniProt ID associated with a given + transcript ID and then, the UniProt ID is used to lookup the + structure in AlphaFoldDB
    • - Then, it uses that UniProt ID to lookup the structure in - AlphaFoldDB because every UniProt ID has been processed by - AlphaFold. + (Manual) Allows you to choose your own structure file from your + local machine (e.g. a PDB file predicted by e.g. ColabFold) or + supply a specific URL
    • -
+
  • + The residues from the structure are downloaded, and then you can + choose the transcript isoform from the selected gene that best + represents the structure. Asterisks are displayed if there is an + exact sequence match +
  • +
  • + The residues from the structure are finally aligned to the to the + selected transcript's protein sequence representation, and + this creates a mapping from the reference genome coordinates to + positions in the 3-D structure +
  • +
  • + Finally the molstar panel is opened, and this contains many + specialized features features, plus additional mouseover and + selection features supplied by the plugin to connect mouse click + actions and mouse hover with coordinates on the linear genome view +
  • +
    If you run into challenges with this workflow e.g. your transcripts diff --git a/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx b/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx index 9f8dcc2..d9b5464 100644 --- a/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx +++ b/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx @@ -26,7 +26,7 @@ export default function LaunchProteinViewDialog({ open > setChoice(val)}> - + diff --git a/src/LaunchProteinView/components/MSATable.tsx b/src/LaunchProteinView/components/MSATable.tsx index a86c365..1d5efc9 100644 --- a/src/LaunchProteinView/components/MSATable.tsx +++ b/src/LaunchProteinView/components/MSATable.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import { TextField } from '@mui/material' +import React, { useState } from 'react' +import { Checkbox, FormControlLabel, TextField } from '@mui/material' import { Feature, max } from '@jbrowse/core/util' import { makeStyles } from 'tss-react/mui' @@ -11,6 +11,9 @@ const useStyles = makeStyles()({ fontFamily: 'Courier New', whiteSpace: 'pre', }, + margin: { + marginLeft: 20, + }, }) export default function MSATable({ @@ -23,42 +26,71 @@ export default function MSATable({ isoformSequences: Record }) { const { classes } = useStyles() - const exactMatchIsoformAndStructureSeq = Object.entries( - isoformSequences, - ).find(([_, val]) => structureSequence === val.seq.replace('*', '')) + const [showInFastaFormat, setShowInFastaFormat] = useState(false) + const removedStars = Object.fromEntries( + Object.entries(isoformSequences).map(([key, val]) => [ + key, + { ...val, seq: val.seq.replaceAll('*', '') }, + ]), + ) + const exactMatchIsoformAndStructureSeq = Object.entries(removedStars).find( + ([_, val]) => structureSequence === val.seq, + ) + const sname = `${structureName || ''} (structure residues)` const maxKeyLen = max([ - structureName?.length ?? 0, - ...Object.entries(isoformSequences).map( + sname.length, + ...Object.entries(removedStars).map( ([_, val]) => getTranscriptDisplayName(val.feature).length, ), ]) + + const l1 = [ + `${sname.padEnd(maxKeyLen)}${exactMatchIsoformAndStructureSeq ? '*' : ' '} ${structureSequence}`, + exactMatchIsoformAndStructureSeq + ? `${getTranscriptDisplayName(exactMatchIsoformAndStructureSeq[1].feature).padEnd(maxKeyLen)}* ${exactMatchIsoformAndStructureSeq[1].seq}` + : undefined, + ...Object.entries(removedStars) + .map( + ([_, val]) => + `${getTranscriptDisplayName(val.feature).padEnd(maxKeyLen)} ${val.seq}`, + ) + .filter(([k]) => k !== exactMatchIsoformAndStructureSeq?.[0]), + ] + .filter(f => !!f) + .join('\n') + + const l2 = [ + `>${sname}\n${structureSequence}`, + ...Object.values(removedStars).map( + ({ feature, seq }) => `>${getTranscriptDisplayName(feature)}\n${seq}`, + ), + ].join('\n') return ( - - `${getTranscriptDisplayName(val.feature).padEnd(maxKeyLen)} ${val.seq}`, - ) - .filter(([k]) => k !== exactMatchIsoformAndStructureSeq?.[0]), - ] - .filter(f => !!f) - .join('\n')} - InputProps={{ - readOnly: true, - classes: { - input: classes.textAreaFont, - }, - }} - /> + <> + setShowInFastaFormat(event.target.checked)} + checked={showInFastaFormat} + /> + } + label="Show in FASTA format?" + /> + + ) } diff --git a/src/LaunchProteinView/components/TranscriptSelector.tsx b/src/LaunchProteinView/components/TranscriptSelector.tsx index 47fa23e..14a7281 100644 --- a/src/LaunchProteinView/components/TranscriptSelector.tsx +++ b/src/LaunchProteinView/components/TranscriptSelector.tsx @@ -45,7 +45,8 @@ export default function TranscriptSelector({ .map(f => ( {getGeneDisplayName(feature)} - {getTranscriptDisplayName(f)} ( - {isoformSequences[f.id()].seq.length}aa) matches structure sequence* + {isoformSequences[f.id()].seq.length}aa) (matches structure + residues) ))} {isoforms diff --git a/src/LaunchProteinView/components/UserProvidedStructure.tsx b/src/LaunchProteinView/components/UserProvidedStructure.tsx index 77c0c6c..f03c32b 100644 --- a/src/LaunchProteinView/components/UserProvidedStructure.tsx +++ b/src/LaunchProteinView/components/UserProvidedStructure.tsx @@ -3,13 +3,13 @@ import { observer } from 'mobx-react' import { Button, DialogActions, - Radio, - RadioGroup, DialogContent, - TextField, FormControlLabel, FormControl, Link, + Radio, + RadioGroup, + TextField, Typography, } from '@mui/material' import { makeStyles } from 'tss-react/mui' @@ -37,6 +37,7 @@ import MSATable from './MSATable' import useIsoformProteinSequences from './useIsoformProteinSequences' import useLocalStructureFileSequence from './useLocalStructureFileSequence' import useRemoteStructureFileSequence from './useRemoteStructureFileSequence' +import HelpButton from './HelpButton' const useStyles = makeStyles()(theme => ({ dialogContent: { @@ -80,24 +81,19 @@ const UserProvidedStructure = observer(function ({ const [choice, setChoice] = useState('file') const [error2, setError] = useState() const [structureURL, setStructureURL] = useState('') - const [selection, setSelection] = useState() + const [userSelection, setUserSelection] = useState() + const [showAllProteinSequences, setShowAllProteinSequences] = useState(false) // check if we are looking at a 'two-level' or 'three-level' feature by // finding exon/CDS subfeatures. we want to select from transcript names const options = getTranscriptFeatures(feature) const view = getContainingView(model) as LGV - const selectedTranscript = options.find(val => getId(val) === selection) + const selectedTranscript = options.find(val => getId(val) === userSelection) const { isoformSequences, error } = useIsoformProteinSequences({ feature, view, }) - const protein = isoformSequences?.[selection ?? ''] - useEffect(() => { - if (selection === undefined && isoformSequences !== undefined) { - setSelection(options.find(f => !!isoformSequences[f.id()])?.id()) - } - }, [options, selection, isoformSequences]) - + const protein = isoformSequences?.[userSelection ?? ''] const { seq: structureSequence1, error: error3 } = useLocalStructureFileSequence({ file }) @@ -109,36 +105,30 @@ const UserProvidedStructure = observer(function ({ 'structureSequence' const structureSequence = structureSequence1 ?? structureSequence2 + useEffect(() => { + if (isoformSequences !== undefined) { + const ret = + options.find( + f => + isoformSequences[f.id()]?.seq.replaceAll('*', '') == + structureSequence, + ) ?? options.find(f => !!isoformSequences[f.id()]) + setUserSelection(ret?.id()) + } + }, [options, structureSequence, userSelection, isoformSequences]) + const e = error || error2 || error3 || error4 return ( <> {e ? : null} - {isoformSequences ? ( - structureSequence ? ( - <> - - - - ) : null - ) : ( -
    - -
    - )} +
    + + Open your structure file + + ) : null}
    +
    + {isoformSequences ? ( + structureSequence ? ( + <> + +
    + + + {showAllProteinSequences ? ( + + ) : null} +
    + + ) : null + ) : ( + + )} +