From b7bc66c35d3d81692d797762004fc088b5f5a4f5 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 20 Mar 2024 20:00:14 -0400 Subject: [PATCH] Update docs --- lib/apidocs/DataModel.md | 2 +- lib/apidocs/DialogQueueSessionMixin.md | 2 +- lib/apidocs/MSAModel.md | 64 +++ lib/apidocs/MsaView.md | 420 ++++++------------ lib/apidocs/Tree.md | 183 ++++++++ lib/src/components/dialogs/FeatureDialog.tsx | 77 ++-- .../components/dialogs/InterProScanDialog.tsx | 231 ++++++++++ lib/src/components/header/Header.tsx | 2 + lib/src/components/header/HeaderInfoArea.tsx | 16 +- lib/src/components/header/HeaderMenu.tsx | 3 +- lib/src/components/header/HeaderMenuExtra.tsx | 87 +++- .../components/header/HeaderStatusArea.tsx | 31 ++ lib/src/ggplotPalettes.ts | 5 + lib/src/launchInterProScan.ts | 53 ++- lib/src/model.ts | 174 +++++--- lib/src/renderToSvg.tsx | 2 +- yarn.lock | 33 +- 17 files changed, 941 insertions(+), 444 deletions(-) create mode 100644 lib/apidocs/MSAModel.md create mode 100644 lib/apidocs/Tree.md create mode 100644 lib/src/components/dialogs/InterProScanDialog.tsx create mode 100644 lib/src/components/header/HeaderStatusArea.tsx diff --git a/lib/apidocs/DataModel.md b/lib/apidocs/DataModel.md index 397aea0a..86c23f9f 100644 --- a/lib/apidocs/DataModel.md +++ b/lib/apidocs/DataModel.md @@ -8,7 +8,7 @@ our source code. ### Source file -[src/DataModel.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/DataModel.ts) +[src/model/DataModel.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/model/DataModel.ts) the data stored for the model. this is sometimes temporary in the case that e.g. msaFilehandle is available on the parent model, because then the msa diff --git a/lib/apidocs/DialogQueueSessionMixin.md b/lib/apidocs/DialogQueueSessionMixin.md index c0ce8b5e..31ae9c9d 100644 --- a/lib/apidocs/DialogQueueSessionMixin.md +++ b/lib/apidocs/DialogQueueSessionMixin.md @@ -8,7 +8,7 @@ our source code. ### Source file -[src/DialogQueue.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/DialogQueue.ts) +[src/model/DialogQueue.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/model/DialogQueue.ts) diff --git a/lib/apidocs/MSAModel.md b/lib/apidocs/MSAModel.md new file mode 100644 index 00000000..6934177f --- /dev/null +++ b/lib/apidocs/MSAModel.md @@ -0,0 +1,64 @@ +--- +id: msamodel +title: MSAModel +--- + +Note: this document is automatically generated from mobx-state-tree objects in +our source code. + +### Source file + +[src/model/msaModel.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/model/msaModel.ts) + + + +### MSAModel - Properties +#### property: bgColor + +draw MSA tiles with a background color + +```js +// type signature +true +// code +bgColor: true +``` + +#### property: colorSchemeName + +default color scheme name + +```js +// type signature +string +// code +colorSchemeName: 'maeditor' +``` + + + + + + + + +### MSAModel - Actions +#### action: setBgColor + + + +```js +// type signature +setBgColor: (arg: boolean) => void +``` + +#### action: setColorSchemeName + +set color scheme name + +```js +// type signature +setColorSchemeName: (name: string) => void +``` + + diff --git a/lib/apidocs/MsaView.md b/lib/apidocs/MsaView.md index 75906212..b06a93f1 100644 --- a/lib/apidocs/MsaView.md +++ b/lib/apidocs/MsaView.md @@ -11,36 +11,14 @@ our source code. [src/model.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/model.ts) extends -- BaseViewModel - DialogQueueSessionMixin +- MSAModel +- Tree ### MsaView - Properties -#### property: bgColor - -draw MSA tiles with a background color - -```js -// type signature -true -// code -bgColor: true -``` - -#### property: boxTracks - -a list of "tracks" to display, as box-like glyphs (e.g. protein -domains) - -```js -// type signature -IArrayType; accession: ISimpleType; name: ISimpleType; associatedRowName: ISimpleType; height: IOptionalIType<...>; }, { ...; } & ... 2 more ... & { ...; }, _NotCustomized, _NotCustomized>> -// code -boxTracks: types.array(UniprotTrack) -``` - #### property: collapsed -array of tree nodes that are 'collapsed' +array of tree parent nodes that are 'collapsed' ```js // type signature @@ -49,15 +27,15 @@ IArrayType> collapsed: types.array(types.string) ``` -#### property: colorSchemeName +#### property: collapsed2 -default color scheme name +array of tree leaf nodes that are 'collapsed' ```js // type signature -string +IArrayType> // code -colorSchemeName: 'maeditor' +collapsed2: types.array(types.string) ``` #### property: colWidth @@ -94,26 +72,26 @@ IOptionalIType>; msa: IMaybe> // code -drawNodeBubbles: true +featureFilters: types.map(types.boolean) ``` -#### property: drawTree +#### property: featureMode + -draw tree, boolean ```js // type signature -true +false // code -drawTree: true +featureMode: false ``` #### property: height @@ -127,29 +105,6 @@ IOptionalIType, [undefined]> height: types.optional(types.number, 550) ``` -#### property: hidden - -array of leaf nodes that are 'hidden', similar to collapsed but for leaf nodes - -```js -// type signature -IArrayType> -// code -hidden: types.array(types.string) -``` - -#### property: highResScaleFactor - -high resolution scale factor, helps make canvas look better on hi-dpi -screens - -```js -// type signature -number -// code -highResScaleFactor: 2 -``` - #### property: id id of view, randomly generated if not provided @@ -161,17 +116,6 @@ IOptionalIType, [undefined]> id: ElementId ``` -#### property: labelsAlignRight - -right-align the labels - -```js -// type signature -false -// code -labelsAlignRight: false -``` - #### property: msaFilehandle filehandle object for the MSA (which could contain a tree e.g. with @@ -217,30 +161,6 @@ number scrollY: 0 ``` -#### property: selectedStructures - -currently "selected" structures, generally PDB 3-D protein structures - -```js -// type signature -IArrayType; structure: IModelType<{ pdb: ISimpleType; startPos: ISimpleType; endPos: ISimpleType; }, {}, _NotCustomized, _NotCustomized>; range: IMaybe<...>; }, {}, _NotCustomized, _NotCustomized>> -// code -selectedStructures: types.array(StructureModel) -``` - -#### property: showBranchLen - -use "branch length" e.g. evolutionary distance to draw tree branch -lengths. if false, the layout is a "cladogram" that does not take into -account evolutionary distances - -```js -// type signature -true -// code -showBranchLen: true -``` - #### property: showOnly focus on particular subtree @@ -252,15 +172,15 @@ IMaybe> showOnly: types.maybe(types.string) ``` -#### property: treeAreaWidth +#### property: subFeatureRows + -width of the area the tree is drawn in, px ```js // type signature -IOptionalIType, [undefined]> +false // code -treeAreaWidth: types.optional(types.number, 400) +subFeatureRows: false ``` #### property: treeFilehandle @@ -285,17 +205,6 @@ IMaybe, [undefined]> -// code -treeWidth: types.optional(types.number, 300) -``` - #### property: turnedOffTracks turned off tracks @@ -335,7 +244,7 @@ NodeWithIds ```js // type -BasicTrack[] +ITextTrack[] ``` #### getter: alignmentNames @@ -383,15 +292,6 @@ any[] any[] ``` -#### getter: boxTrackModels - - - -```js -// type -BasicTrack[] -``` - #### getter: colorScheme @@ -437,24 +337,6 @@ any string[] ``` -#### getter: currentAlignmentName - - - -```js -// type -any -``` - -#### getter: done - - - -```js -// type -string -``` - #### getter: fontSize @@ -491,13 +373,13 @@ HierarchyNode boolean ``` -#### getter: inverseStructures +#### getter: isLoading ```js // type -{ [k: string]: any; } +boolean ``` #### getter: labelsWidth @@ -554,6 +436,15 @@ widget width minus the tree area gives the space for the MSA number ``` +#### getter: noAnnotations + + + +```js +// type +boolean +``` + #### getter: noTree @@ -617,13 +508,31 @@ any any ``` -#### getter: structures +#### getter: tidyFilteredAnnotations + + + +```js +// type +any +``` + +#### getter: tidyFilteredGatheredAnnotations + + + +```js +// type +Record +``` + +#### getter: tidyTypes ```js // type -Record +Map ``` #### getter: totalHeight @@ -659,7 +568,7 @@ number ```js // type -BasicTrack[] +ITextTrack[] ``` #### getter: treeAreaWidthMinusMargin @@ -680,15 +589,6 @@ number any ``` -#### getter: treeWidthMatchesArea - -synchronization that matches treeWidth to treeAreaWidth - -```js -// type -true -``` - #### getter: turnedOnTracks @@ -700,22 +600,13 @@ any ### MsaView - Methods -#### method: getMouseOverResidue - - - -```js -// type signature -getMouseOverResidue: (rowName: string) => any -``` - -#### method: getPos - +#### method: extraViewMenuItems +unused here, but can be used by derived classes to add extra items ```js // type signature -getPos: (pos: number) => number +extraViewMenuItems: () => any[] ``` #### method: getRowData @@ -724,90 +615,38 @@ getPos: (pos: number) => number ```js // type signature -getRowData: (name: string) => { range: { start: number; end: number; }; data: any; } +getRowData: (name: string) => { data: any; } ``` -#### method: globalBpToPx +#### method: globalCoordToRowSpecificSeqCoord - - -```js -// type signature -globalBpToPx: (position: number) => number -``` - -#### method: pxToBp - -returns coordinate in the current relative coordinate scheme +return a row-specific sequence coordinate, skipping gaps, given a global +coordinate ```js // type signature -pxToBp: (coord: number) => number +globalCoordToRowSpecificSeqCoord: (rowName: string, position: number) => number ``` -#### method: globalCoordToRowSpecificCoord - +#### method: seqCoordToRowSpecificGlobalCoord +return a global coordinate given a row-specific sequence coordinate +which does not not include gaps ```js // type signature -globalCoordToRowSpecificCoord: (rowName: string, position: number) => number -``` - -#### method: globalCoordToRowSpecificCoord2 - - - -```js -// type signature -globalCoordToRowSpecificCoord2: (rowName: string, position: number) => number -``` - -#### method: rowSpecificBpToPx - - - -```js -// type signature -rowSpecificBpToPx: (rowName: string, position: number) => number +seqCoordToRowSpecificGlobalCoord: (rowName: string, position: number) => number ``` ### MsaView - Actions -#### action: addStructureToSelection - -add to the selected structures - -```js -// type signature -addStructureToSelection: (elt: ModelCreationType; structure: IModelType<{ pdb: ISimpleType; startPos: ISimpleType; endPos: ISimpleType; }, {}, _NotCustomized, _NotCustomized>; range: IMaybe<...>; }>>) => void -``` - -#### action: addUniprotTrack +#### action: addInterProScanJobId ```js // type signature -addUniprotTrack: (node: { name: string; accession: string; }) => void -``` - -#### action: clearHidden - - - -```js -// type signature -clearHidden: () => void -``` - -#### action: clearSelectedStructures - -clear all selected structures - -```js -// type signature -clearSelectedStructures: () => void +addInterProScanJobId: (arg: string) => void ``` #### action: doScrollX @@ -837,49 +676,49 @@ doScrollY: (deltaY: number) => void exportSVG: (opts: { theme: Theme; includeMinimap?: boolean; exportType: string; }) => Promise ``` -#### action: hideNode - +#### action: incrementRef +internal, used for drawing to canvas ```js // type signature -hideNode: (arg: string) => void +incrementRef: () => void ``` -#### action: incrementRef +#### action: initFilter + -internal, used for drawing to canvas ```js // type signature -incrementRef: () => void +initFilter: (arg: string) => void ``` -#### action: removeStructureFromSelection +#### action: loadInterProScanResults + -remove from the selected structures ```js // type signature -removeStructureFromSelection: (elt: ModelCreationType; structure: IModelType<{ pdb: ISimpleType; startPos: ISimpleType; endPos: ISimpleType; }, {}, _NotCustomized, _NotCustomized>; range: IMaybe<...>; }>>) => void +loadInterProScanResults: (jobId: string) => Promise ``` -#### action: setBgColor +#### action: queryInterProScan ```js // type signature -setBgColor: (arg: boolean) => void +queryInterProScan: (programs: string[]) => Promise ``` -#### action: setColorSchemeName +#### action: reset + -set color scheme name ```js // type signature -setColorSchemeName: (name: string) => void +reset: () => void ``` #### action: setColWidth @@ -909,31 +748,31 @@ setCurrentAlignment: (n: number) => void setData: (data: { msa?: string; tree?: string; }) => void ``` -#### action: setDrawNodeBubbles - +#### action: setError +set error state ```js // type signature -setDrawNodeBubbles: (arg: boolean) => void +setError: (error?: unknown) => void ``` -#### action: setDrawTree +#### action: setFeatureMode ```js // type signature -setDrawTree: (arg: boolean) => void +setFeatureMode: (arg: boolean) => void ``` -#### action: setError +#### action: setFilter + -set error state ```js // type signature -setError: (error?: unknown) => void +setFilter: (arg: string, flag: boolean) => void ``` #### action: setHeight @@ -945,22 +784,40 @@ set the height of the view in px setHeight: (height: number) => void ``` -#### action: setLabelsAlignRight +#### action: setLoadedInterProAnnotations + + + +```js +// type signature +setLoadedInterProAnnotations: (data: Record) => void +``` + +#### action: setLoadingMSA ```js // type signature -setLabelsAlignRight: (arg: boolean) => void +setLoadingMSA: (arg: boolean) => void ``` -#### action: setMouseoveredColumn +#### action: setLoadingTree ```js // type signature -setMouseoveredColumn: (n: number, chain: string, file: string) => void +setLoadingTree: (arg: boolean) => void +``` + +#### action: setMouseClickPos + +set mouse click position (row, column) in the MSA + +```js +// type signature +setMouseClickPos: (col?: number, row?: number) => void ``` #### action: setMousePos @@ -987,7 +844,7 @@ setMSA: (result: string) => void ```js // type signature -setMSAFilehandle: (msaFilehandle?: FileLocation) => Promise +setMSAFilehandle: (msaFilehandle?: FileLocation) => void ``` #### action: setRowHeight @@ -1017,40 +874,40 @@ set scroll Y-offset (px) setScrollY: (n: number) => void ``` -#### action: setShowBranchLen +#### action: setShowOnly ```js // type signature -setShowBranchLen: (arg: boolean) => void +setShowOnly: (node?: string) => void ``` -#### action: setShowOnly +#### action: setStatus ```js // type signature -setShowOnly: (node?: string) => void +setStatus: (status?: { msg: string; url?: string; }) => void ``` -#### action: setTree +#### action: setSubFeatureRows ```js // type signature -setTree: (result: string) => void +setSubFeatureRows: (arg: boolean) => void ``` -#### action: setTreeAreaWidth +#### action: setTree + -set tree area width (px) ```js // type signature -setTreeAreaWidth: (n: number) => void +setTree: (result: string) => void ``` #### action: setTreeFilehandle @@ -1059,7 +916,7 @@ setTreeAreaWidth: (n: number) => void ```js // type signature -setTreeFilehandle: (treeFilehandle?: FileLocation) => Promise +setTreeFilehandle: (treeFilehandle?: FileLocation) => void ``` #### action: setTreeMetadata @@ -1071,49 +928,58 @@ setTreeFilehandle: (treeFilehandle?: FileLocation) => Promise setTreeMetadata: (result: string) => void ``` -#### action: setTreeWidth +#### action: setWidth + + + +```js +// type signature +setWidth: (arg: number) => void +``` + +#### action: toggleCollapsed + -set tree width (px) ```js // type signature -setTreeWidth: (n: number) => void +toggleCollapsed: (node: string) => void ``` -#### action: setTreeWidthMatchesArea +#### action: toggleCollapsed2 + -synchronize the treewidth and treeareawidth ```js // type signature -setTreeWidthMatchesArea: (arg: boolean) => void +toggleCollapsed2: (node: string) => void ``` -#### action: toggleCollapsed +#### action: toggleTrack ```js // type signature -toggleCollapsed: (node: string) => void +toggleTrack: (id: string) => void ``` -#### action: toggleStructureSelection +#### action: zoomIn + -toggle a structure from the selected structures list ```js // type signature -toggleStructureSelection: (elt: { id: string; structure: { startPos: number; endPos: number; pdb: string; }; }) => void +zoomIn: () => void ``` -#### action: toggleTrack +#### action: zoomOut ```js // type signature -toggleTrack: (id: string) => void +zoomOut: () => void ``` diff --git a/lib/apidocs/Tree.md b/lib/apidocs/Tree.md new file mode 100644 index 00000000..e25bda83 --- /dev/null +++ b/lib/apidocs/Tree.md @@ -0,0 +1,183 @@ +--- +id: tree +title: Tree +--- + +Note: this document is automatically generated from mobx-state-tree objects in +our source code. + +### Source file + +[src/model/treeModel.ts](https://github.com/GMOD/react-msaview/blob/main/lib/src/model/treeModel.ts) + + + +### Tree - Properties +#### property: drawLabels + + + +```js +// type signature +true +// code +drawLabels: true +``` + +#### property: drawNodeBubbles + +draw clickable node bubbles on the tree + +```js +// type signature +true +// code +drawNodeBubbles: true +``` + +#### property: drawTree + +draw tree, boolean + +```js +// type signature +true +// code +drawTree: true +``` + +#### property: labelsAlignRight + +right-align the labels + +```js +// type signature +false +// code +labelsAlignRight: false +``` + +#### property: showBranchLen + +use "branch length" e.g. evolutionary distance to draw tree branch +lengths. if false, the layout is a "cladogram" that does not take into +account evolutionary distances + +```js +// type signature +true +// code +showBranchLen: true +``` + +#### property: treeAreaWidth + +width of the area the tree is drawn in, px + +```js +// type signature +IOptionalIType, [undefined]> +// code +treeAreaWidth: types.optional(types.number, 400) +``` + +#### property: treeWidth + +width of the tree within the treeArea, px + +```js +// type signature +IOptionalIType, [undefined]> +// code +treeWidth: types.optional(types.number, 300) +``` + + +### Tree - Getters +#### getter: treeWidthMatchesArea + +synchronization that matches treeWidth to treeAreaWidth + +```js +// type +true +``` + + + + + +### Tree - Actions +#### action: setDrawLabels + + + +```js +// type signature +setDrawLabels: (arg: boolean) => void +``` + +#### action: setDrawNodeBubbles + + + +```js +// type signature +setDrawNodeBubbles: (arg: boolean) => void +``` + +#### action: setDrawTree + + + +```js +// type signature +setDrawTree: (arg: boolean) => void +``` + +#### action: setLabelsAlignRight + + + +```js +// type signature +setLabelsAlignRight: (arg: boolean) => void +``` + +#### action: setShowBranchLen + + + +```js +// type signature +setShowBranchLen: (arg: boolean) => void +``` + +#### action: setTreeAreaWidth + +set tree area width (px) + +```js +// type signature +setTreeAreaWidth: (n: number) => void +``` + +#### action: setTreeWidth + +set tree width (px) + +```js +// type signature +setTreeWidth: (n: number) => void +``` + +#### action: setTreeWidthMatchesArea + +synchronize the treewidth and treeareawidth + +```js +// type signature +setTreeWidthMatchesArea: (arg: boolean) => void +``` + + diff --git a/lib/src/components/dialogs/FeatureDialog.tsx b/lib/src/components/dialogs/FeatureDialog.tsx index ea92449e..ca3d2a9a 100644 --- a/lib/src/components/dialogs/FeatureDialog.tsx +++ b/lib/src/components/dialogs/FeatureDialog.tsx @@ -5,14 +5,7 @@ import { Button, DialogContent } from '@mui/material' // locals import { MsaViewModel } from '../../model' - -function p(n: number) { - const hues = Array.from( - { length: n + 1 }, - (_, i) => 15 + (i * (375 - 15)) / n, - ) - return hues.map(h => `lch(65% 100 ${h})`).slice(0, n) -} +import { getPalette } from '../../ggplotPalettes' const Toggles = observer(function ({ model }: { model: MsaViewModel }) { const { featureFilters } = model @@ -43,7 +36,8 @@ const Toggles = observer(function ({ model }: { model: MsaViewModel }) { const Table = observer(function ({ model }: { model: MsaViewModel }) { const { tidyTypes, featureFilters } = model - const palette = p([...tidyTypes.values()].length) + const values = [...tidyTypes.values()] + const palette = getPalette(values.length - 1) return ( <> @@ -57,36 +51,34 @@ const Table = observer(function ({ model }: { model: MsaViewModel }) { - {[...tidyTypes.values()].map( - ({ accession, name, description }, idx) => ( - - - - model.setFilter( - accession, - !model.featureFilters.get(accession), - ) - } - /> - - {accession} - {name} - {description} - -
- - - ), - )} + {values.map(({ accession, name, description }, idx) => ( + + + + model.setFilter( + accession, + !model.featureFilters.get(accession), + ) + } + /> + + {accession} + {name} + {description} + +
+ + + ))} @@ -101,7 +93,12 @@ const FeatureTypeDialog = observer(function ({ model: MsaViewModel }) { return ( - onClose()} open title="Feature types" maxWidth="xl"> + onClose()} + open + title="Feature filters" + maxWidth="xl" + > diff --git a/lib/src/components/dialogs/InterProScanDialog.tsx b/lib/src/components/dialogs/InterProScanDialog.tsx new file mode 100644 index 00000000..7dae3d33 --- /dev/null +++ b/lib/src/components/dialogs/InterProScanDialog.tsx @@ -0,0 +1,231 @@ +import React, { useState } from 'react' +import { observer } from 'mobx-react' +import { Dialog } from '@jbrowse/core/ui' +import { Button, DialogActions, DialogContent, Typography } from '@mui/material' + +// locals +import { MsaViewModel } from '../../model' +import SequenceTextArea from '../SequenceTextArea' + +const FeatureTypeDialog = observer(function ({ + onClose, + model, +}: { + onClose: () => void + model: MsaViewModel +}) { + const [vals, setVals] = useState([ + { + name: 'NCBIfam', + description: 'NCBI RefSeq FAMs including TIGRFAMs', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'SFLD', + description: 'Structure function linkage database', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'Phobius', + checked: true, + description: + 'A combined transmembrane topology and signal peptide predictor', + category: 'Other sequence features', + }, + { + name: 'SignalP', + checked: true, + category: 'Other sequence features', + }, + { + name: 'SignalP_EUK', + category: 'Other category', + checked: true, + }, + { + name: 'SignalP_GRAM_POSITIVE', + category: 'Other category', + checked: true, + }, + { + name: 'SignalP_GRAM_NEGATIVE', + checked: true, + category: 'Other category', + }, + { + name: 'SUPERFAMILY', + category: 'Structural domains', + checked: true, + }, + { + name: 'PANTHER', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'Gene3D', + category: 'Structural domains', + checked: true, + }, + { + name: 'Hamap', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'ProSiteProfiles', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'ProSitePatterns', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'Coils', + category: 'Other sequence features', + checked: true, + }, + { + name: 'SMART', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'CDD', + description: 'Conserved Domains Database', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'PRINTS', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'Pfam', + category: 'Families, domains, sites & repeats', + checked: true, + }, + { + name: 'MobiDBLite', + checked: true, + category: 'Other sequence features', + }, + { + name: 'PIRSF', + checked: true, + category: 'Other category', + }, + { + name: 'TMHMM', + checked: true, + category: 'Other sequence features', + }, + + { + name: 'AntiFam', + checked: true, + category: 'Other category', + }, + { + name: 'FunFam', + checked: true, + category: 'Other category', + }, + { + name: 'PIRSR', + checked: true, + category: 'Families, domains, sites & repeats', + }, + ]) + + const programs = vals.filter(e => e.checked).map(e => e.name) + const [show, setShow] = useState(false) + + return ( + onClose()} open title="Feature types" maxWidth="xl"> + + + This will run InterProScan on all rows of the current MSA + + + {show ? ( +
+ Select algorithms for InterProScan to run +
+ + +
+
+ + {vals + .sort((a, b) => a.category.localeCompare(b.category)) + .map(({ name, checked, category }) => ( + + + + + + ))} + +
+ + setVals( + vals.map(e => + e.name === name + ? { ...e, checked: !e.checked } + : e, + ), + ) + } + /> + {name}{category}
+ + ) : null} +
+ + + + +
+ ) +}) + +export default FeatureTypeDialog diff --git a/lib/src/components/header/Header.tsx b/lib/src/components/header/Header.tsx index a43eaa44..c062b687 100644 --- a/lib/src/components/header/Header.tsx +++ b/lib/src/components/header/Header.tsx @@ -12,6 +12,7 @@ import Help from '@mui/icons-material/Help' import ZoomControls from './ZoomControls' import MultiAlignmentSelector from './MultiAlignmentSelector' import HeaderInfoArea from './HeaderInfoArea' +import HeaderStatusArea from './HeaderStatusArea' import HeaderMenu from './HeaderMenu' import HeaderMenuExtra from './HeaderMenuExtra' @@ -26,6 +27,7 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) { + model.queueDialog(onClose => [AboutDialog, { onClose }])} > diff --git a/lib/src/components/header/HeaderInfoArea.tsx b/lib/src/components/header/HeaderInfoArea.tsx index 6926d6c0..5528ca05 100644 --- a/lib/src/components/header/HeaderInfoArea.tsx +++ b/lib/src/components/header/HeaderInfoArea.tsx @@ -16,16 +16,12 @@ const useStyles = makeStyles()({ const HeaderInfoArea = observer(({ model }: { model: MsaViewModel }) => { const { mouseOverRowName, mouseCol } = model const { classes } = useStyles() - return ( -
- {mouseOverRowName && mouseCol !== undefined ? ( - - {mouseOverRowName}: - {model.globalCoordToRowSpecificSeqCoord(mouseOverRowName, mouseCol)} - - ) : null} -
- ) + return mouseOverRowName && mouseCol !== undefined ? ( + + {mouseOverRowName}: + {model.globalCoordToRowSpecificSeqCoord(mouseOverRowName, mouseCol)} + + ) : null }) export default HeaderInfoArea diff --git a/lib/src/components/header/HeaderMenu.tsx b/lib/src/components/header/HeaderMenu.tsx index 4d98e229..af0959a1 100644 --- a/lib/src/components/header/HeaderMenu.tsx +++ b/lib/src/components/header/HeaderMenu.tsx @@ -12,6 +12,7 @@ import Menu from '@mui/icons-material/Menu' // locals import { MsaViewModel } from '../../model' +// lazies const SettingsDialog = lazy(() => import('../dialogs/SettingsDialog')) const MetadataDialog = lazy(() => import('../dialogs/MetadataDialog')) const TracklistDialog = lazy(() => import('../dialogs/TracklistDialog')) @@ -38,7 +39,7 @@ const HeaderMenu = observer(function ({ model }: { model: MsaViewModel }) { icon: Assignment, }, { - label: 'Tracks', + label: 'Extra tracks', onClick: () => model.queueDialog(onClose => [TracklistDialog, { model, onClose }]), icon: List, diff --git a/lib/src/components/header/HeaderMenuExtra.tsx b/lib/src/components/header/HeaderMenuExtra.tsx index 4d7f0fa0..0eb8cc4a 100644 --- a/lib/src/components/header/HeaderMenuExtra.tsx +++ b/lib/src/components/header/HeaderMenuExtra.tsx @@ -7,18 +7,27 @@ import { MsaViewModel } from '../../model' // icons import MoreVert from '@mui/icons-material/MoreVert' +import Sort from '@mui/icons-material/Sort' +import Visibility from '@mui/icons-material/Visibility' +import FilterAlt from '@mui/icons-material/FilterAlt' +import Search from '@mui/icons-material/Search' +import PhotoCamera from '@mui/icons-material/PhotoCamera' +import RestartAlt from '@mui/icons-material/RestartAlt' // lazies const ExportSVGDialog = lazy(() => import('../dialogs/ExportSVGDialog')) const FeatureFilterDialog = lazy(() => import('../dialogs/FeatureDialog')) +const InterProScanDialog = lazy(() => import('../dialogs/InterProScanDialog')) const HeaderMenuExtra = observer(function ({ model }: { model: MsaViewModel }) { - const { featureMode, subFeatureRows } = model + const { featureMode, subFeatureRows, noAnnotations, interProScanJobIds } = + model return ( { model.setColWidth(16) model.setRowHeight(20) @@ -26,29 +35,67 @@ const HeaderMenuExtra = observer(function ({ model }: { model: MsaViewModel }) { }, { label: 'Export SVG', + icon: PhotoCamera, onClick: () => model.queueDialog(onClose => [ExportSVGDialog, { onClose, model }]), }, { - label: 'Toggle feature mode', - checked: featureMode, - type: 'checkbox', - onClick: () => model.setFeatureMode(!featureMode), - }, - { - label: 'Toggle sub-feature rows', - checked: subFeatureRows, - type: 'checkbox', - onClick: () => model.setSubFeatureRows(!subFeatureRows), - }, - { - label: 'Protein domains/feature settings', - onClick: () => { - model.queueDialog(onClose => [ - FeatureFilterDialog, - { onClose, model }, - ]) - }, + label: 'Features/protein domains', + type: 'subMenu', + subMenu: [ + { + label: + 'Show domains' + (noAnnotations ? ' (no domains loaded)' : ''), + icon: Visibility, + checked: featureMode, + type: 'checkbox', + onClick: () => model.setFeatureMode(!featureMode), + }, + { + label: 'Use sub-row layout', + checked: subFeatureRows, + icon: Sort, + type: 'checkbox', + onClick: () => model.setSubFeatureRows(!subFeatureRows), + }, + { + label: 'Filter domains', + icon: FilterAlt, + onClick: () => { + model.queueDialog(onClose => [ + FeatureFilterDialog, + { onClose, model }, + ]) + }, + }, + { + label: 'Query InterProScan for domains...', + icon: Search, + onClick: () => + model.queueDialog(onClose => [ + InterProScanDialog, + { onClose, model }, + ]), + }, + { + label: 'Load previous InterProScan results...', + icon: Search, + type: 'subMenu', + subMenu: interProScanJobIds.length + ? interProScanJobIds.map(({ jobId, date }) => ({ + label: + new Date(date).toLocaleString('en-US') + ' - ' + jobId, + onClick: () => model.loadInterProScanResults(jobId), + })) + : [ + { + label: 'No previous searches', + disabled: true, + onClick: () => {}, + }, + ], + }, + ], }, ...(model.extraViewMenuItems?.() || []), ]} diff --git a/lib/src/components/header/HeaderStatusArea.tsx b/lib/src/components/header/HeaderStatusArea.tsx new file mode 100644 index 00000000..8a36a104 --- /dev/null +++ b/lib/src/components/header/HeaderStatusArea.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { Typography } from '@mui/material' +import { observer } from 'mobx-react' +import { makeStyles } from 'tss-react/mui' + +// locals +import { MsaViewModel } from '../../model' + +const useStyles = makeStyles()({ + margin: { + margin: 'auto', + marginLeft: 10, + }, +}) + +const HeaderStatusArea = observer(({ model }: { model: MsaViewModel }) => { + const { status } = model + const { classes } = useStyles() + return status ? ( + + {status.msg}{' '} + {status.url ? ( + + (status) + + ) : null} + + ) : null +}) + +export default HeaderStatusArea diff --git a/lib/src/ggplotPalettes.ts b/lib/src/ggplotPalettes.ts index bb465ed0..1e651af0 100644 --- a/lib/src/ggplotPalettes.ts +++ b/lib/src/ggplotPalettes.ts @@ -17,4 +17,9 @@ const palettes = [ '#FF61CC', ], ] + +export function getPalette(l: number) { + return palettes[Math.min(l, palettes.length - 1)] +} + export default palettes diff --git a/lib/src/launchInterProScan.ts b/lib/src/launchInterProScan.ts index 9320468c..3b1808c2 100644 --- a/lib/src/launchInterProScan.ts +++ b/lib/src/launchInterProScan.ts @@ -1,28 +1,55 @@ import { jsonfetch, textfetch, timeout } from './fetchUtils' -const base = `https://www.ebi.ac.uk/Tools/services/rest/` +const base = `https://www.ebi.ac.uk/Tools/services/rest` + +export interface InterProScanResults { + matches: { + signature: { + entry?: { + name: string + description: string + accession: string + } + } + locations: { start: number; end: number }[] + }[] + xref: { id: string }[] +} +export interface InterProScanResponse { + results: InterProScanResults[] +} async function runInterProScan({ seq, onProgress, + onJobId, + programs, }: { seq: string - onProgress: (arg: string) => void + programs: string[] + onProgress: (arg?: { msg: string; url?: string }) => void + onJobId: (arg: string) => void }) { const jobId = await textfetch(`${base}/iprscan5/run`, { method: 'POST', body: new URLSearchParams({ email: 'colin.diesh@gmail.com', sequence: `${seq}`, + programs: programs.join(','), }), }) + onJobId(jobId) await wait({ jobId, onProgress, }) - return { - result: await jsonfetch(`${base}/iprscan5/result/${jobId}/json`), - } + return loadInterProScanResults(jobId) +} + +export async function loadInterProScanResults(jobId: string) { + return (await jsonfetch( + `${base}/iprscan5/result/${jobId}/json`, + )) as InterProScanResponse } async function wait({ @@ -30,14 +57,14 @@ async function wait({ jobId, }: { jobId: string - onProgress: (arg: string) => void + onProgress: (arg?: { msg: string; url?: string }) => void }) { const url = `${base}/iprscan5/status/${jobId}` // eslint-disable-next-line no-constant-condition while (true) { for (let i = 0; i < 10; i++) { await timeout(1000) - onProgress(`Checking status... ${10 - i} ${url}`) + onProgress({ msg: `Checking status... ${10 - i}`, url }) } const result = await textfetch(url) @@ -50,15 +77,21 @@ async function wait({ export async function launchInterProScan({ algorithm, seq, + programs, + onJobId, onProgress, }: { algorithm: string seq: string - onProgress: (arg: string) => void + programs: string[] + onProgress: (arg?: { msg: string; url?: string }) => void + onJobId: (arg: string) => void }) { - onProgress(`Launching ${algorithm} MSA...`) + onProgress({ msg: `Launching ${algorithm} MSA...` }) if (algorithm === 'interproscan') { - return runInterProScan({ seq, onProgress }) + const result = await runInterProScan({ seq, onJobId, onProgress, programs }) + onProgress() + return result } else { throw new Error('unknown algorithm') } diff --git a/lib/src/model.ts b/lib/src/model.ts index 7e2372b8..e00f1b77 100644 --- a/lib/src/model.ts +++ b/lib/src/model.ts @@ -11,7 +11,13 @@ import { Theme } from '@mui/material' import { FileLocation, ElementId } from '@jbrowse/core/util/types/mst' import { FileLocation as FileLocationType } from '@jbrowse/core/util/types' import { openLocation } from '@jbrowse/core/util/io' -import { groupBy, notEmpty, sum } from '@jbrowse/core/util' +import { + groupBy, + localStorageGetItem, + localStorageSetItem, + notEmpty, + sum, +} from '@jbrowse/core/util' // locals import { @@ -25,7 +31,6 @@ import { NodeWithIdsAndLength, len, } from './util' -import { jsonfetch } from './fetchUtils' import { colord } from 'colord' import { reparseTree } from './reparseTree' import { blocksX, blocksY } from './calculateBlocks' @@ -47,6 +52,11 @@ import { DataModelF } from './model/DataModel' import { DialogQueueSessionMixin } from './model/DialogQueue' import { TreeF } from './model/treeModel' import { MSAModelF } from './model/msaModel' +import { + InterProScanResults, + launchInterProScan, + loadInterProScanResults, +} from './launchInterProScan' interface Accession { accession: string @@ -197,6 +207,7 @@ function stateModelFactory() { }), ) .volatile(() => ({ + status: undefined as { msg: string; url?: string } | undefined, /** * #volatile * high resolution scale factor, helps make canvas look better on hi-dpi @@ -278,53 +289,19 @@ function stateModelFactory() { */ annotPos: undefined as { left: number; right: number } | undefined, - /** - * #volatile - */ - annotationTracks: [ - 'A8CT47_9VIRU%2F46-492.json', - 'B8Y0L1_9VIRU%2F39-459.json', - 'NS1AB_TASV1%2F1126-1583.json', - 'POL1_BAYMY%2F1647-2074.json', - 'POL1_CPMVS%2F1197-1670.json', - 'POLG_EMCVR%2F1853-2275.json', - 'POLG_FCVUR%2F1272-1715.json', - 'POLG_HAVHM%2F1765-2198.json', - 'POLG_NVN68%2F1309-1746.json', - 'POLG_PVYN%2F2312-2736.json', - 'POLG_PYFV1%2F2281-2756.json', - 'POLG_RHDVF%2F1288-1730.json', - 'POLG_VESVA%2F1386-1832.json', - 'Q38L14_9VIRU%2F30-445.json', - 'Q4FCM7_9VIRU%2F30-445.json', - 'Q6YDQ7_9VIRU%2F46-492.json', - 'Q9DLK1_FMDVP%2F1874-2302.json', - 'R1AB_CVHN5%2F4801-5292.json', - 'RDRP_BPPH6%2F35-607.json', - 'RPOA_SHFV%2F2295-2719.json', - 'W1I6K0_9VIRU%2F30-445.json', - ], /** * #volatile * */ loadedInterProAnnotations: undefined as | undefined - | Record< - string, - { - matches: { - signature: { - entry?: { - name: string - description: string - accession: string - } - } - locations: { start: number; end: number }[] - }[] - } - >, + | Record, + /** + * #volatile + */ + interProScanJobIds: JSON.parse( + localStorageGetItem('msaview-interproscanqueries') || '[]', + ) as { jobId: string; date: number }[], })) .actions(self => ({ /** @@ -520,15 +497,8 @@ function stateModelFactory() { * #method */ getRowData(name: string) { - const matches = name.match(/\S+\/(\d+)-(\d+)/) return { - data: this.MSA?.getRowData(name) || ({} as Record), - ...(matches && { - range: { - start: +matches[1], - end: +matches[2], - }, - }), + data: this.MSA?.getRowData(name), } }, @@ -544,6 +514,12 @@ function stateModelFactory() { get noTree() { return !!this._tree.noTree }, + /** + * #getter + */ + get noAnnotations() { + return !self.loadedInterProAnnotations + }, /** * #getter */ @@ -821,8 +797,10 @@ function stateModelFactory() { self.rowHeight = Math.max(1.5, Math.floor(self.rowHeight * 0.75)) self.scrollX = clamp(self.maxScrollX, self.scrollX, 0) }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - setLoadedInterProAnnotations(data: any) { + /** + * #action + */ + setLoadedInterProAnnotations(data: Record) { self.loadedInterProAnnotations = data }, @@ -857,6 +835,21 @@ function stateModelFactory() { self.turnedOffTracks.set(id, true) } }, + /** + * #action + */ + addInterProScanJobId(arg: string) { + self.interProScanJobIds = [ + ...self.interProScanJobIds, + { jobId: arg, date: +Date.now() }, + ] + }, + /** + * #action + */ + setStatus(status?: { msg: string; url?: string }) { + self.status = status + }, })) .views(self => ({ /** @@ -1070,6 +1063,43 @@ function stateModelFactory() { }, })) .actions(self => ({ + /** + * #action + */ + async loadInterProScanResults(jobId: string) { + self.setStatus({ msg: 'Loading ' + jobId }) + const ret = await loadInterProScanResults(jobId) + self.setStatus() + self.setLoadedInterProAnnotations( + Object.fromEntries(ret.results.map(r => [r.xref[0].id, r])), + ) + }, + /** + * #action + */ + async queryInterProScan(programs: string[]) { + const { rows } = self + if (rows.length > 140) { + throw new Error('Too many sequences, please run InterProScan offline') + } + + const ret = await launchInterProScan({ + algorithm: 'interproscan', + programs: programs, + seq: rows + .map(row => `>${row[0]}\n${row[1].replaceAll('-', '')}`) + .join('\n'), + onProgress: arg => self.setStatus(arg), + onJobId: jobId => self.addInterProScanJobId(jobId), + }) + + self.setLoadedInterProAnnotations( + Object.fromEntries(ret.results.map(r => [r.xref[0].id, r])), + ) + }, + /** + * #action + */ reset() { transaction(() => { self.setData({ tree: '', msa: '' }) @@ -1129,20 +1159,20 @@ function stateModelFactory() { addDisposer( self, autorun(async () => { - const res = Object.fromEntries( - await Promise.all( - self.annotationTracks.map(async f => { - const d = await jsonfetch( - `https://jbrowse.org/demos/interproscan/json/${encodeURIComponent(f)}`, - ) - return [ - decodeURIComponent(f).replace('.json', ''), - d.result.results[0], - ] as const - }), - ), - ) - self.setLoadedInterProAnnotations(res) + // const res = Object.fromEntries( + // await Promise.all( + // self.annotationTracks.map(async f => { + // const d = await jsonfetch( + // `https://jbrowse.org/demos/interproscan/json/${encodeURIComponent(f)}`, + // ) + // return [ + // decodeURIComponent(f).replace('.json', ''), + // d.result.results[0], + // ] as const + // }), + // ), + // ) + // self.setLoadedInterProAnnotations(res) }), ) // autorun opens treeFilehandle @@ -1227,6 +1257,16 @@ function stateModelFactory() { } }), ) + + addDisposer( + self, + autorun(() => { + localStorageSetItem( + 'msaview-interproscanqueries', + JSON.stringify(self.interProScanJobIds), + ) + }), + ) }, })) .postProcessSnapshot(result => { diff --git a/lib/src/renderToSvg.tsx b/lib/src/renderToSvg.tsx index 2f38fc50..a8b1b460 100644 --- a/lib/src/renderToSvg.tsx +++ b/lib/src/renderToSvg.tsx @@ -233,5 +233,5 @@ function SvgWrapper({ } function NullWrapper({ children }: { children: React.ReactNode }) { - return <>{children} + return children } diff --git a/yarn.lock b/yarn.lock index 348002bd..6ef389fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1860,14 +1860,15 @@ array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: is-array-buffer "^3.0.4" array-includes@^3.1.6, array-includes@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" - integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" is-string "^1.0.7" array-union@^1.0.1: @@ -2549,9 +2550,9 @@ dompurify@^3.0.0: integrity sha512-WZDL8ZHTliEVP3Lk4phtvjg8SNQ3YMc5WVstxE8cszKZrFjzI4PF4ZTIk9VGAc9vZADO7uGO2V/ZiStcRSAT4Q== electron-to-chromium@^1.4.668: - version "1.4.712" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.712.tgz#2117ea2f4f95e8e0ec96c33dd345134ac00e57ed" - integrity sha512-ncfPC8UnGIyGFrPE03J5Xn6yTZ6R+clkcZbuG1PJbjAaZBFS4Kn3UKfzu8eilzru6SfC8TPsHuwv0p0eYVu+ww== + version "1.4.713" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz#7cd8e4083c948f8d0cc686fcfdde97d97fd76556" + integrity sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw== email-addresses@^5.0.0: version "5.0.0" @@ -4786,9 +4787,9 @@ possible-typed-array-names@^1.0.0: integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== postcss@^8.4.36: - version "8.4.37" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.37.tgz#4505f992cd0c20e03d25f13b31901640b2db731a" - integrity sha512-7iB/v/r7Woof0glKLH8b1SPHrsX7uhdO+Geb41QpF/+mWZHU3uxxSlN+UXGVit1PawOYDToO+AbZzhBzWRDwbQ== + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" @@ -4852,9 +4853,9 @@ punycode@^2.1.0: integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== pure-rand@^6.0.0: - version "6.0.4" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" - integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== queue-microtask@^1.2.2: version "1.2.3"