jbrowse-plugin-msaview 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
- package/dist/AddHighlightModel/index.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +9 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
- package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
- package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
- package/dist/LaunchMsaView/detectQueryRow.js +20 -21
- package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
- package/dist/LaunchMsaView/useQueryRowName.js +5 -8
- package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
- package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
- package/dist/MsaViewPanel/components/JobLink.js +7 -1
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
- package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +41 -11
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
- package/dist/MsaViewPanel/util.d.ts +18 -0
- package/dist/MsaViewPanel/util.js +17 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +10 -6
- package/dist/utils/blastCache.js +15 -3
- package/dist/utils/ebiBlast.d.ts +1 -1
- package/dist/utils/msa.d.ts +12 -0
- package/dist/utils/msa.js +35 -12
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/utils/phmmer.d.ts +53 -0
- package/dist/utils/phmmer.js +118 -0
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +27 -21
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
- package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
- package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
- package/src/LaunchMsaView/detectQueryRow.ts +34 -23
- package/src/LaunchMsaView/useQueryRowName.ts +6 -9
- package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +38 -5
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.ts +33 -12
- package/src/utils/ebiBlast.ts +1 -1
- package/src/utils/msa.ts +43 -12
- package/src/utils/msaRows.ts +95 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/utils/phmmer.ts +174 -0
- package/src/utils/taxonomyNames.ts +6 -1
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
- package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { StockholmMSA } from 'msa-parsers';
|
|
2
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher';
|
|
3
|
+
const TOOL = 'hmmer3_phmmer';
|
|
4
|
+
/**
|
|
5
|
+
* Human-facing link to a job, shown while it runs and on error.
|
|
6
|
+
*
|
|
7
|
+
* The category has to be sss: jdispatcher serves its shell with a 200 for any
|
|
8
|
+
* category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
|
|
9
|
+
* in a browser.
|
|
10
|
+
*/
|
|
11
|
+
export function phmmerResultUrl(jobId) {
|
|
12
|
+
return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`;
|
|
13
|
+
}
|
|
14
|
+
/** EBI job ids are prefixed with the tool that made them */
|
|
15
|
+
export function isPhmmerJobId(jobId) {
|
|
16
|
+
return jobId.startsWith(`${TOOL}-`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* phmmer aligns every hit to a profile built from the query, one match state
|
|
20
|
+
* per query residue, and marks those columns 'x' in #=GC RF. So the query's own
|
|
21
|
+
* row is exactly recoverable: walk RF, consume a query residue at each match
|
|
22
|
+
* column, gap everywhere else.
|
|
23
|
+
*
|
|
24
|
+
* This is the one piece of real logic here rather than a library call, and it
|
|
25
|
+
* is checked hard: if the match columns do not account for the query exactly,
|
|
26
|
+
* the columns and the query have drifted apart, and a query row that is off by
|
|
27
|
+
* even one residue would silently mis-map every column to the genome. Throwing
|
|
28
|
+
* is much better than drawing that.
|
|
29
|
+
*/
|
|
30
|
+
function buildQueryRow({ rf, query }) {
|
|
31
|
+
let consumed = 0;
|
|
32
|
+
const row = Array.from(rf, c => c === 'x' ? (query[consumed++] ?? '-') : '-').join('');
|
|
33
|
+
if (consumed !== query.length) {
|
|
34
|
+
throw new Error(`phmmer alignment has ${consumed} match columns for a query of ${query.length} residues, so the query row cannot be placed`);
|
|
35
|
+
}
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* '[subseq from] Albumin OS=Homo sapiens OX=9606 GN=ALB PE=1 SV=2' is what a
|
|
40
|
+
* UniProt target's #=GS DE looks like. Hits from the non-UniProt databases
|
|
41
|
+
* phmmer also offers (PDB, AlphaFold, MEROPS...) carry no OS=/OX= at all, so
|
|
42
|
+
* every field here is optional.
|
|
43
|
+
*/
|
|
44
|
+
function parseDescription(de) {
|
|
45
|
+
const text = (de ?? '').replace('[subseq from] ', '');
|
|
46
|
+
const sciname = /OS=(.*?)\s+(?:OX|GN|PE|SV)=/.exec(text)?.[1];
|
|
47
|
+
const ox = /OX=(\d+)/.exec(text)?.[1];
|
|
48
|
+
return {
|
|
49
|
+
sciname: sciname ?? 'unknown',
|
|
50
|
+
taxid: ox ? Number.parseInt(ox, 10) : undefined,
|
|
51
|
+
title: text.split(' OS=')[0] || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Target names look like 'sp|P02768|ALBU_HUMAN/1-609' for UniProt databases and
|
|
56
|
+
* like anything at all for the others, so an unrecognized name becomes its own
|
|
57
|
+
* accession rather than being dropped.
|
|
58
|
+
*/
|
|
59
|
+
function parseName(name) {
|
|
60
|
+
const slash = name.lastIndexOf('/');
|
|
61
|
+
const range = slash === -1 ? undefined : /^\d+-\d+$/.exec(name.slice(slash + 1))?.[0];
|
|
62
|
+
const bare = range === undefined ? name : name.slice(0, slash);
|
|
63
|
+
const parts = bare.split('|');
|
|
64
|
+
return parts.length === 3
|
|
65
|
+
? { accession: parts[1], id: parts[2], range }
|
|
66
|
+
: { accession: bare, id: bare, range };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Exported for testing against a captured .sto — the annotation names (RF, the
|
|
70
|
+
* DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
|
|
71
|
+
* would notice if HMMER or EBI changed one.
|
|
72
|
+
*/
|
|
73
|
+
export function parsePhmmerAlignment({ stockholm, query, }) {
|
|
74
|
+
const { gc, gs, seqdata, seqname } = new StockholmMSA(stockholm, 0).getMSA();
|
|
75
|
+
const rf = gc.RF;
|
|
76
|
+
if (!rf) {
|
|
77
|
+
throw new Error('phmmer alignment has no #=GC RF line');
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
queryRow: buildQueryRow({ rf, query }),
|
|
81
|
+
rows: seqname.map(name => ({
|
|
82
|
+
...parseName(name),
|
|
83
|
+
...parseDescription(gs.DE?.[name]?.[0]),
|
|
84
|
+
// insert columns come back lowercase with '.' for gaps; the MSA renderer
|
|
85
|
+
// looks colors up by the literal letter, so lowercase would draw
|
|
86
|
+
// uncolored. The insert columns stay visible as gaps in the query row.
|
|
87
|
+
aligned: (seqdata[name] ?? '').replaceAll('.', '-').toUpperCase(),
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function queryPhmmer({ query, database, onProgress, onRid, }) {
|
|
92
|
+
onProgress('Submitting to EBI phmmer...');
|
|
93
|
+
const jobId = await submitEbiJob({
|
|
94
|
+
tool: TOOL,
|
|
95
|
+
params: {
|
|
96
|
+
database,
|
|
97
|
+
sequence: query,
|
|
98
|
+
// the alignment is the whole point of using phmmer here
|
|
99
|
+
alignView: 'true',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
onRid(jobId);
|
|
103
|
+
await waitForEbiJob({
|
|
104
|
+
tool: TOOL,
|
|
105
|
+
jobId,
|
|
106
|
+
onCountdown: s => {
|
|
107
|
+
onProgress(`Re-checking phmmer status in... ${s}`);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const alignment = parsePhmmerAlignment({
|
|
111
|
+
stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto' }),
|
|
112
|
+
query,
|
|
113
|
+
});
|
|
114
|
+
if (alignment.rows.length === 0) {
|
|
115
|
+
throw new Error('No hits found');
|
|
116
|
+
}
|
|
117
|
+
return { rid: jobId, ...alignment };
|
|
118
|
+
}
|
|
@@ -2,4 +2,4 @@ export interface TaxonomyInfo {
|
|
|
2
2
|
sciname: string;
|
|
3
3
|
commonName?: string;
|
|
4
4
|
}
|
|
5
|
-
export declare function fetchTaxonomyInfo(
|
|
5
|
+
export declare function fetchTaxonomyInfo(taxidsWithRepeats: number[]): Promise<Map<number, TaxonomyInfo>>;
|
|
@@ -25,7 +25,12 @@ async function saveTaxonomyCache(entries) {
|
|
|
25
25
|
}
|
|
26
26
|
await tx.done;
|
|
27
27
|
}
|
|
28
|
-
export async function fetchTaxonomyInfo(
|
|
28
|
+
export async function fetchTaxonomyInfo(taxidsWithRepeats) {
|
|
29
|
+
// callers pass one taxid per alignment row, and a BLAST hit list is several
|
|
30
|
+
// rows per species: 100 albumin hits are maybe 50 taxa, and asking as they
|
|
31
|
+
// came did 100 IndexedDB reads and sent eutils 100 ids for 50 answers. The
|
|
32
|
+
// result is keyed by taxid, so no caller can tell the difference
|
|
33
|
+
const taxids = [...new Set(taxidsWithRepeats)];
|
|
29
34
|
const result = new Map();
|
|
30
35
|
const uncachedTaxids = [];
|
|
31
36
|
const cachedResults = await getCachedTaxonomies(taxids);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "3.
|
|
1
|
+
export declare const version = "3.4.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.
|
|
1
|
+
export const version = '3.4.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.4.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@emotion/styled": "^11.14.1",
|
|
20
|
-
"@gmod/bgzf-filehandle": "^6.
|
|
21
|
-
"g2p_mapper": "^2.1.
|
|
20
|
+
"@gmod/bgzf-filehandle": "^6.6.0",
|
|
21
|
+
"g2p_mapper": "^2.1.7",
|
|
22
22
|
"idb": "^8.0.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
@@ -28,51 +28,57 @@
|
|
|
28
28
|
"@jbrowse/core": "^4.3.0",
|
|
29
29
|
"@jbrowse/mobx-state-tree": "^5.13.0",
|
|
30
30
|
"@jbrowse/plugin-linear-genome-view": "^4.3.0",
|
|
31
|
-
"@mui/icons-material": "^9.
|
|
32
|
-
"@mui/material": "^9.
|
|
33
|
-
"@mui/system": "^9.
|
|
34
|
-
"@mui/x-data-grid": "^9.
|
|
31
|
+
"@mui/icons-material": "^9.3.1",
|
|
32
|
+
"@mui/material": "^9.3.1",
|
|
33
|
+
"@mui/system": "^9.3.0",
|
|
34
|
+
"@mui/x-data-grid": "^9.12.0",
|
|
35
35
|
"@testing-library/dom": "^10.4.1",
|
|
36
36
|
"@testing-library/react": "^16.3.2",
|
|
37
|
-
"@types/node": "^26.
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
-
"esbuild": "^0.28.
|
|
40
|
-
"eslint": "^10.
|
|
37
|
+
"@types/node": "^26.3.0",
|
|
38
|
+
"@types/react": "^19.2.18",
|
|
39
|
+
"esbuild": "^0.28.2",
|
|
40
|
+
"eslint": "^10.9.1",
|
|
41
41
|
"eslint-plugin-import-x": "^4.17.1",
|
|
42
42
|
"eslint-plugin-react": "^7.37.5",
|
|
43
43
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
44
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-unicorn": "^73.0.0",
|
|
45
45
|
"git-cliff": "^2.13.1",
|
|
46
46
|
"jsdom": "^30.0.1",
|
|
47
47
|
"mobx": "^6.16.1",
|
|
48
48
|
"mobx-react": "^9.2.2",
|
|
49
|
-
"msa-parsers": "^6.
|
|
49
|
+
"msa-parsers": "^6.1.0",
|
|
50
|
+
"oxfmt": "^0.65.0",
|
|
51
|
+
"oxlint": "^1.80.0",
|
|
52
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
50
53
|
"pixelmatch": "^7.2.0",
|
|
51
54
|
"pngjs": "^7.0.0",
|
|
52
|
-
"prettier": "^3.9.6",
|
|
53
55
|
"pretty-bytes": "^7.1.1",
|
|
54
|
-
"puppeteer": "^25.
|
|
56
|
+
"puppeteer": "^25.8.0",
|
|
55
57
|
"react": "^19.2.8",
|
|
56
58
|
"react-dom": "^19.2.8",
|
|
57
|
-
"react-msaview": "^6.
|
|
59
|
+
"react-msaview": "^6.1.0",
|
|
58
60
|
"rimraf": "^6.1.3",
|
|
59
61
|
"rxjs": "^7.8.2",
|
|
60
62
|
"serve": "^14.2.6",
|
|
61
63
|
"tss-react": "^4.9.21",
|
|
62
64
|
"typescript": "^6.0.2",
|
|
63
|
-
"typescript-eslint": "^8.
|
|
64
|
-
"vitest": "^4.1.
|
|
65
|
+
"typescript-eslint": "^8.68.0",
|
|
66
|
+
"vitest": "^4.1.11"
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
69
|
"clean": "rimraf dist",
|
|
68
70
|
"start": "node esbuild.mjs --watch",
|
|
69
|
-
"format": "
|
|
71
|
+
"format": "oxfmt",
|
|
72
|
+
"check-format": "oxfmt --check",
|
|
70
73
|
"build": "tsc && NODE_ENV=production node esbuild.mjs && cp distconfig.json dist/config.json",
|
|
71
74
|
"prebuild": "pnpm clean",
|
|
72
|
-
"lint": "
|
|
75
|
+
"lint": "oxlint --type-aware --deny-warnings",
|
|
76
|
+
"lint:fast": "oxlint --deny-warnings",
|
|
77
|
+
"lint:eslint": "eslint src --report-unused-disable-directives --max-warnings 0",
|
|
73
78
|
"check-text-source": "node scripts/check-text-source.mjs",
|
|
74
79
|
"check-ebi-params": "node scripts/check-ebi-params.mjs",
|
|
75
80
|
"check-mui-imports": "node scripts/check-mui-imports.mjs",
|
|
81
|
+
"check-host-dep-pins": "node scripts/check-host-dep-pins.mjs",
|
|
76
82
|
"pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
|
|
77
83
|
"test": "vitest run",
|
|
78
84
|
"test:watch": "vitest",
|
|
@@ -82,7 +88,7 @@
|
|
|
82
88
|
"test:version": "node scripts/test-versions.mjs run",
|
|
83
89
|
"host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
|
|
84
90
|
"check-ci": "node scripts/require-green-ci.mjs",
|
|
85
|
-
"preversion": "pnpm check-ci && pnpm lint && pnpm build && pnpm host-compat",
|
|
91
|
+
"preversion": "pnpm check-ci && pnpm check-host-dep-pins && pnpm lint && pnpm build && pnpm host-compat",
|
|
86
92
|
"version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add src/version.ts CHANGELOG.md",
|
|
87
93
|
"postversion": "git push --follow-tags"
|
|
88
94
|
}
|
|
@@ -3,8 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import { hasHoverPosition, useStyles } from './util'
|
|
7
6
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
7
|
+
import { hasHoverPosition, useStyles } from './util'
|
|
8
8
|
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
10
10
|
|
|
@@ -3,8 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import { hasHoverPosition, useStyles } from './util'
|
|
7
6
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
7
|
+
import { hasHoverPosition, useStyles } from './util'
|
|
8
8
|
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
10
10
|
|
|
@@ -2,8 +2,8 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
|
|
5
|
-
import HighlightComponents from './HighlightComponents'
|
|
6
5
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
6
|
+
import HighlightComponents from './HighlightComponents'
|
|
7
7
|
|
|
8
8
|
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
@@ -11,11 +11,6 @@ import {
|
|
|
11
11
|
import { observer } from 'mobx-react'
|
|
12
12
|
import { makeStyles } from 'tss-react/mui'
|
|
13
13
|
|
|
14
|
-
import CachedBlastResults from './CachedBlastResults'
|
|
15
|
-
import MsaAlgorithmSelect from './MsaAlgorithmSelect'
|
|
16
|
-
import { blastLaunchView } from './blastLaunchView'
|
|
17
|
-
import { blastDatabaseOptions, defaultBlastDatabase } from './consts'
|
|
18
|
-
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
19
14
|
import TextField2 from '../../../components/TextField2'
|
|
20
15
|
import {
|
|
21
16
|
getBlastViewTitle,
|
|
@@ -26,14 +21,30 @@ import LaunchPanelContent from '../LaunchPanelContent'
|
|
|
26
21
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
27
22
|
import TranscriptSelector from '../TranscriptSelector'
|
|
28
23
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
24
|
+
import CachedBlastResults from './CachedBlastResults'
|
|
25
|
+
import MsaAlgorithmSelect from './MsaAlgorithmSelect'
|
|
26
|
+
import { blastLaunchView } from './blastLaunchView'
|
|
27
|
+
import {
|
|
28
|
+
databaseOptionsFor,
|
|
29
|
+
defaultBlastDatabase,
|
|
30
|
+
defaultSearchFor,
|
|
31
|
+
searchPrograms,
|
|
32
|
+
} from './consts'
|
|
33
|
+
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
29
34
|
|
|
30
|
-
import type {
|
|
35
|
+
import type { MsaAlgorithm, SearchChoice, SearchProgram } from './consts'
|
|
31
36
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
32
37
|
|
|
33
38
|
const useStyles = makeStyles()({
|
|
34
39
|
selectField: {
|
|
35
40
|
width: 150,
|
|
36
41
|
},
|
|
42
|
+
// wider than the rest because the values are what the user came to read, and
|
|
43
|
+
// `uniprotkb_swissprot` is 19 characters — at 150 the field showed
|
|
44
|
+
// `uniprotkb_swis…`, which does not distinguish it from `uniprotkb_trembl`
|
|
45
|
+
databaseField: {
|
|
46
|
+
width: 230,
|
|
47
|
+
},
|
|
37
48
|
cachedResultsAccordion: {
|
|
38
49
|
marginTop: 20,
|
|
39
50
|
},
|
|
@@ -56,10 +67,16 @@ const BlastAutomaticPanel = observer(function ({
|
|
|
56
67
|
const { classes } = useStyles()
|
|
57
68
|
const view = getLinearGenomeView(model)
|
|
58
69
|
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
// one piece of state, not two: a program and a database that program does not
|
|
71
|
+
// have is a 400 from EBI minutes after Submit, and holding them apart is what
|
|
72
|
+
// would let them drift into that
|
|
73
|
+
const [search, setSearch] = useState<SearchChoice>({
|
|
74
|
+
program: 'blastp',
|
|
75
|
+
database: defaultBlastDatabase,
|
|
76
|
+
})
|
|
61
77
|
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] =
|
|
62
78
|
useState<MsaAlgorithm>('clustalo')
|
|
79
|
+
const isPhmmer = search.program === 'phmmer'
|
|
63
80
|
|
|
64
81
|
const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature])
|
|
65
82
|
const { results: cachedResults, error: cachedResultsError } =
|
|
@@ -74,37 +91,69 @@ const BlastAutomaticPanel = observer(function ({
|
|
|
74
91
|
{children}
|
|
75
92
|
<TextField2
|
|
76
93
|
variant="outlined"
|
|
77
|
-
label="
|
|
94
|
+
label="Search program"
|
|
78
95
|
className={classes.selectField}
|
|
79
96
|
select
|
|
80
|
-
value={
|
|
97
|
+
value={search.program}
|
|
81
98
|
onChange={event => {
|
|
82
|
-
|
|
99
|
+
// the two services name their databases differently, so switching
|
|
100
|
+
// program replaces the database rather than keeping a name the new
|
|
101
|
+
// one has never heard of
|
|
102
|
+
setSearch(defaultSearchFor(event.target.value as SearchProgram))
|
|
83
103
|
}}
|
|
84
104
|
>
|
|
85
|
-
{
|
|
105
|
+
{searchPrograms.map(val => (
|
|
86
106
|
<MenuItem value={val} key={val}>
|
|
87
107
|
{val}
|
|
88
108
|
</MenuItem>
|
|
89
109
|
))}
|
|
90
110
|
</TextField2>
|
|
91
111
|
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
<TextField2
|
|
113
|
+
variant="outlined"
|
|
114
|
+
label="Database"
|
|
115
|
+
className={classes.databaseField}
|
|
116
|
+
select
|
|
117
|
+
value={search.database}
|
|
118
|
+
onChange={event => {
|
|
119
|
+
setSearch({
|
|
120
|
+
program: search.program,
|
|
121
|
+
database: event.target.value,
|
|
122
|
+
} as SearchChoice)
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{databaseOptionsFor(search.program).map(val => (
|
|
126
|
+
<MenuItem value={val} key={val}>
|
|
127
|
+
{val}
|
|
128
|
+
</MenuItem>
|
|
129
|
+
))}
|
|
130
|
+
</TextField2>
|
|
131
|
+
|
|
132
|
+
{isPhmmer ? null : (
|
|
133
|
+
<MsaAlgorithmSelect
|
|
134
|
+
className={classes.selectField}
|
|
135
|
+
value={selectedMsaAlgorithm}
|
|
136
|
+
onChange={setSelectedMsaAlgorithm}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
97
139
|
|
|
98
140
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
99
141
|
|
|
100
142
|
<Typography className={classes.infoText}>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
143
|
+
{isPhmmer
|
|
144
|
+
? `phmmer searches UniProtKB with a profile HMM built from the query,
|
|
145
|
+
so it aligns the hits as it finds them and that alignment is used
|
|
146
|
+
directly — nothing is realigned afterwards. The tree is then built
|
|
147
|
+
from it by neighbour-joining. A hit matching the query in more
|
|
148
|
+
than one place appears once per matched region.`
|
|
149
|
+
: `This panel will automatically submit a blastp query to EBI, which
|
|
150
|
+
searches UniProtKB. Searches usually finish in under a minute, and
|
|
151
|
+
swissprot returns curated sequences that align more cleanly than
|
|
152
|
+
the many near-identical entries a TrEMBL search brings back. After
|
|
153
|
+
completion, all the hits will be run through a multiple sequence
|
|
154
|
+
alignment.`}{' '}
|
|
155
|
+
Searching NCBI's nr needs the manual approach: NCBI no longer lets a
|
|
156
|
+
browser read responses from Blast.cgi.
|
|
108
157
|
</Typography>
|
|
109
158
|
|
|
110
159
|
{cachedResults.length > 0 ? (
|
|
@@ -133,12 +182,21 @@ const BlastAutomaticPanel = observer(function ({
|
|
|
133
182
|
feature: selectedTranscript,
|
|
134
183
|
view,
|
|
135
184
|
newViewTitle: getBlastViewTitle(feature, selectedTranscript),
|
|
136
|
-
blastParams:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
blastParams:
|
|
186
|
+
search.program === 'phmmer'
|
|
187
|
+
? {
|
|
188
|
+
searchProgram: 'phmmer',
|
|
189
|
+
blastDatabase: search.database,
|
|
190
|
+
selectedTranscript,
|
|
191
|
+
proteinSequence,
|
|
192
|
+
}
|
|
193
|
+
: {
|
|
194
|
+
searchProgram: 'blastp',
|
|
195
|
+
blastDatabase: search.database,
|
|
196
|
+
msaAlgorithm: selectedMsaAlgorithm,
|
|
197
|
+
selectedTranscript,
|
|
198
|
+
proteinSequence,
|
|
199
|
+
},
|
|
142
200
|
})
|
|
143
201
|
handleClose()
|
|
144
202
|
}
|
|
@@ -5,7 +5,6 @@ import { Alert, Typography } from '@mui/material'
|
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
|
-
import { BASE_BLAST_URL } from './consts'
|
|
9
8
|
import ExternalLink from '../../../components/ExternalLink'
|
|
10
9
|
import TextField2 from '../../../components/TextField2'
|
|
11
10
|
import { useQueryRowName } from '../../useQueryRowName'
|
|
@@ -20,6 +19,7 @@ import QueryRowSelector from '../QueryRowSelector'
|
|
|
20
19
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
21
20
|
import TranscriptSelector from '../TranscriptSelector'
|
|
22
21
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
22
|
+
import { BASE_BLAST_URL } from './consts'
|
|
23
23
|
|
|
24
24
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
25
25
|
|
|
@@ -4,15 +4,15 @@ import SettingsIcon from '@mui/icons-material/Settings'
|
|
|
4
4
|
import { IconButton } from '@mui/material'
|
|
5
5
|
import { makeStyles } from 'tss-react/mui'
|
|
6
6
|
|
|
7
|
-
import BlastAutomaticPanel from './BlastAutomaticPanel'
|
|
8
|
-
import BlastManualPanel from './BlastManualPanel'
|
|
9
|
-
import BlastMethodSelector from './BlastMethodSelector'
|
|
10
|
-
import BlastSettingsDialog from './BlastSettingsDialog'
|
|
11
7
|
import {
|
|
12
8
|
DEFAULT_EBI_EMAIL,
|
|
13
9
|
EBI_EMAIL_STORAGE_KEY,
|
|
14
10
|
} from '../../../utils/ebiJobDispatcher'
|
|
15
11
|
import { useLocalStorage } from '../../../utils/useLocalStorage'
|
|
12
|
+
import BlastAutomaticPanel from './BlastAutomaticPanel'
|
|
13
|
+
import BlastManualPanel from './BlastManualPanel'
|
|
14
|
+
import BlastMethodSelector from './BlastMethodSelector'
|
|
15
|
+
import BlastSettingsDialog from './BlastSettingsDialog'
|
|
16
16
|
|
|
17
17
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
18
18
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { describeSearch } from './CachedBlastResults'
|
|
4
|
+
|
|
5
|
+
import type { CachedBlastResult } from '../../../utils/blastCache'
|
|
6
|
+
|
|
7
|
+
const base: CachedBlastResult = {
|
|
8
|
+
id: 'k',
|
|
9
|
+
proteinSequence: 'MKV',
|
|
10
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
11
|
+
msa: '',
|
|
12
|
+
tree: '',
|
|
13
|
+
treeMetadata: '{}',
|
|
14
|
+
rid: 'r',
|
|
15
|
+
timestamp: 0,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function row(fields: Partial<CachedBlastResult>): CachedBlastResult {
|
|
19
|
+
return { ...base, ...fields }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('a blastp row names its database, program and aligner', () => {
|
|
23
|
+
expect(
|
|
24
|
+
describeSearch(row({ searchProgram: 'blastp', msaAlgorithm: 'muscle' })),
|
|
25
|
+
).toBe('uniprotkb_swissprot / blastp / muscle')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// phmmer aligns as it searches, so its rows carry no msaAlgorithm at all --
|
|
29
|
+
// which read as "(undefined)" while this assumed one
|
|
30
|
+
test('a phmmer row names no aligner, because none ran', () => {
|
|
31
|
+
expect(
|
|
32
|
+
describeSearch(
|
|
33
|
+
row({ blastDatabase: 'swissprot', searchProgram: 'phmmer' }),
|
|
34
|
+
),
|
|
35
|
+
).toBe('swissprot / phmmer')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('a row cached before searchProgram existed reads as blastp', () => {
|
|
39
|
+
expect(describeSearch(row({ msaAlgorithm: 'clustalo' }))).toBe(
|
|
40
|
+
'uniprotkb_swissprot / blastp / clustalo',
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('a row from the NCBI era keeps the program it recorded', () => {
|
|
45
|
+
expect(
|
|
46
|
+
describeSearch(
|
|
47
|
+
row({ blastProgram: 'quick-blastp', msaAlgorithm: 'clustalo' }),
|
|
48
|
+
),
|
|
49
|
+
).toBe('uniprotkb_swissprot / quick-blastp / clustalo')
|
|
50
|
+
})
|
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
import { observer } from 'mobx-react'
|
|
15
15
|
import { makeStyles } from 'tss-react/mui'
|
|
16
16
|
|
|
17
|
-
import { blastLaunchViewFromCache } from './blastLaunchView'
|
|
18
|
-
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
19
17
|
import {
|
|
20
18
|
featureMatchesId,
|
|
21
19
|
getGeneIdentifiers,
|
|
22
20
|
getLinearGenomeView,
|
|
23
21
|
getSortedTranscriptFeatures,
|
|
24
22
|
} from '../../util'
|
|
23
|
+
import { blastLaunchViewFromCache } from './blastLaunchView'
|
|
24
|
+
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
25
25
|
|
|
26
26
|
import type { CachedBlastResult } from '../../../utils/blastCache'
|
|
27
27
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
@@ -51,6 +51,26 @@ function getResultDisplayName(result: CachedBlastResult): string {
|
|
|
51
51
|
: (result.geneId ?? result.transcriptId ?? 'Unknown')
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* How the row was produced: `uniprotkb_swissprot / blastp / clustalo`, or
|
|
56
|
+
* `swissprot / phmmer` for a row phmmer aligned as it searched and that
|
|
57
|
+
* therefore ran no aligner. Each part is dropped when absent rather than
|
|
58
|
+
* printed empty — `msaAlgorithm` became optional when phmmer arrived, and a
|
|
59
|
+
* phmmer row read `(undefined)` until this stopped assuming one.
|
|
60
|
+
*
|
|
61
|
+
* `blastProgram` is the older field, written only while the plugin still
|
|
62
|
+
* queried NCBI directly and blastp/quick-blastp was a real choice.
|
|
63
|
+
*/
|
|
64
|
+
export function describeSearch(result: CachedBlastResult) {
|
|
65
|
+
return [
|
|
66
|
+
result.blastDatabase,
|
|
67
|
+
result.searchProgram ?? result.blastProgram ?? 'blastp',
|
|
68
|
+
result.msaAlgorithm,
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(' / ')
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
const CachedBlastResults = observer(function ({
|
|
55
75
|
model,
|
|
56
76
|
handleClose,
|
|
@@ -149,7 +169,7 @@ const CachedBlastResults = observer(function ({
|
|
|
149
169
|
}}
|
|
150
170
|
>
|
|
151
171
|
<ListItemText
|
|
152
|
-
primary={`${getResultDisplayName(result)} - ${
|
|
172
|
+
primary={`${getResultDisplayName(result)} - ${describeSearch(result)}`}
|
|
153
173
|
secondary={`${new Date(result.timestamp).toLocaleString()} - Seq: ${result.proteinSequence.slice(0, 30)}...`}
|
|
154
174
|
/>
|
|
155
175
|
</ListItemButton>
|
|
@@ -2,8 +2,8 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { MenuItem } from '@mui/material'
|
|
4
4
|
|
|
5
|
-
import { msaAlgorithms } from './consts'
|
|
6
5
|
import TextField2 from '../../../components/TextField2'
|
|
6
|
+
import { msaAlgorithms } from './consts'
|
|
7
7
|
|
|
8
8
|
import type { MsaAlgorithm } from './consts'
|
|
9
9
|
|
|
@@ -27,3 +27,43 @@ export type BlastDatabase = (typeof blastDatabaseOptions)[number]
|
|
|
27
27
|
// curated, so it returns roughly one good sequence per species rather than the
|
|
28
28
|
// many near-identical TrEMBL entries an alignment reads poorly
|
|
29
29
|
export const defaultBlastDatabase: BlastDatabase = 'uniprotkb_swissprot'
|
|
30
|
+
|
|
31
|
+
export const searchPrograms = ['blastp', 'phmmer'] as const
|
|
32
|
+
export type SearchProgram = (typeof searchPrograms)[number]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
|
|
36
|
+
* targets outside UniProt carry no OS=/OX= in their description, so those rows
|
|
37
|
+
* would lose their species and common name. Only the databases that label their
|
|
38
|
+
* hits are offered.
|
|
39
|
+
*/
|
|
40
|
+
export const phmmerDatabaseOptions = [
|
|
41
|
+
'swissprot',
|
|
42
|
+
'uniprotkb',
|
|
43
|
+
'uniprotrefprot',
|
|
44
|
+
] as const
|
|
45
|
+
export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number]
|
|
46
|
+
|
|
47
|
+
export const defaultPhmmerDatabase: PhmmerDatabase = 'swissprot'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A program together with a database that program actually has.
|
|
51
|
+
*
|
|
52
|
+
* The pair travels as one value because neither service knows the other's
|
|
53
|
+
* database names — `swissprot` is a phmmer database and `uniprotkb_swissprot` a
|
|
54
|
+
* blastp one — so a program held apart from its database can drift into a
|
|
55
|
+
* combination EBI answers with a 400, minutes after the user pressed Submit.
|
|
56
|
+
*/
|
|
57
|
+
export type SearchChoice =
|
|
58
|
+
| { program: 'blastp'; database: BlastDatabase }
|
|
59
|
+
| { program: 'phmmer'; database: PhmmerDatabase }
|
|
60
|
+
|
|
61
|
+
export function defaultSearchFor(program: SearchProgram): SearchChoice {
|
|
62
|
+
return program === 'phmmer'
|
|
63
|
+
? { program, database: defaultPhmmerDatabase }
|
|
64
|
+
: { program, database: defaultBlastDatabase }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function databaseOptionsFor(program: SearchProgram) {
|
|
68
|
+
return program === 'phmmer' ? phmmerDatabaseOptions : blastDatabaseOptions
|
|
69
|
+
}
|