jbrowse-plugin-msaview 3.3.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 +3 -3
- 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.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +32 -11
- 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/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 +5 -5
- 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 +2 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +28 -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/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
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import type { BlastDatabase, MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
1
|
+
import type { BlastDatabase, MsaAlgorithm, PhmmerDatabase, SearchProgram } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
2
|
export interface CachedBlastResult {
|
|
3
3
|
id: string;
|
|
4
4
|
proteinSequence: string;
|
|
5
|
-
blastDatabase: BlastDatabase;
|
|
5
|
+
blastDatabase: BlastDatabase | PhmmerDatabase;
|
|
6
6
|
/**
|
|
7
7
|
* Only ever set on rows cached by a version that still queried NCBI, where
|
|
8
8
|
* the choice between blastp and quick-blastp was real. Kept so those rows
|
|
9
9
|
* still display; never written now.
|
|
10
10
|
*/
|
|
11
11
|
blastProgram?: string;
|
|
12
|
-
|
|
12
|
+
/** absent on rows cached before phmmer existed, which were all blastp */
|
|
13
|
+
searchProgram?: SearchProgram;
|
|
14
|
+
/** absent on phmmer rows, which are aligned by the search itself */
|
|
15
|
+
msaAlgorithm?: MsaAlgorithm;
|
|
13
16
|
msa: string;
|
|
14
17
|
tree: string;
|
|
15
18
|
treeMetadata: string;
|
|
@@ -20,10 +23,11 @@ export interface CachedBlastResult {
|
|
|
20
23
|
transcriptName?: string;
|
|
21
24
|
geneName?: string;
|
|
22
25
|
}
|
|
23
|
-
export declare function saveBlastResult({ proteinSequence, blastDatabase, msaAlgorithm, msa, tree, treeMetadata, rid, geneId, transcriptId, transcriptName, geneName, }: {
|
|
26
|
+
export declare function saveBlastResult({ proteinSequence, blastDatabase, msaAlgorithm, searchProgram, msa, tree, treeMetadata, rid, geneId, transcriptId, transcriptName, geneName, }: {
|
|
24
27
|
proteinSequence: string;
|
|
25
|
-
blastDatabase: BlastDatabase;
|
|
26
|
-
msaAlgorithm
|
|
28
|
+
blastDatabase: BlastDatabase | PhmmerDatabase;
|
|
29
|
+
msaAlgorithm?: MsaAlgorithm;
|
|
30
|
+
searchProgram?: SearchProgram;
|
|
27
31
|
msa: string;
|
|
28
32
|
tree: string;
|
|
29
33
|
treeMetadata: string;
|
package/dist/utils/blastCache.js
CHANGED
|
@@ -10,21 +10,33 @@ const getDB = createDbOpener(DB_NAME, DB_VERSION, (db, oldVersion) => {
|
|
|
10
10
|
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
|
-
function createCacheKey(proteinSequence, blastDatabase, msaAlgorithm, transcriptId) {
|
|
13
|
+
function createCacheKey({ proteinSequence, blastDatabase, msaAlgorithm, searchProgram, transcriptId, }) {
|
|
14
14
|
const idPart = transcriptId ? `:${transcriptId}` : '';
|
|
15
|
+
// phmmer keys are prefixed and blastp keys are left exactly as they were, so
|
|
16
|
+
// results cached before phmmer existed still resolve
|
|
17
|
+
if (searchProgram === 'phmmer') {
|
|
18
|
+
return `phmmer:${blastDatabase}${idPart}:${proteinSequence}`;
|
|
19
|
+
}
|
|
15
20
|
// msaAlgorithm is part of the key because the stored msa/tree are produced by
|
|
16
21
|
// it — without it, re-running the same query under a different algorithm
|
|
17
22
|
// overwrites the earlier result and drops it from the history list
|
|
18
23
|
return `${blastDatabase}:${msaAlgorithm}${idPart}:${proteinSequence}`;
|
|
19
24
|
}
|
|
20
|
-
export async function saveBlastResult({ proteinSequence, blastDatabase, msaAlgorithm, msa, tree, treeMetadata, rid, geneId, transcriptId, transcriptName, geneName, }) {
|
|
25
|
+
export async function saveBlastResult({ proteinSequence, blastDatabase, msaAlgorithm, searchProgram, msa, tree, treeMetadata, rid, geneId, transcriptId, transcriptName, geneName, }) {
|
|
21
26
|
const db = await getDB();
|
|
22
|
-
const id = createCacheKey(
|
|
27
|
+
const id = createCacheKey({
|
|
28
|
+
proteinSequence,
|
|
29
|
+
blastDatabase,
|
|
30
|
+
msaAlgorithm,
|
|
31
|
+
searchProgram,
|
|
32
|
+
transcriptId,
|
|
33
|
+
});
|
|
23
34
|
const entry = {
|
|
24
35
|
id,
|
|
25
36
|
proteinSequence,
|
|
26
37
|
blastDatabase,
|
|
27
38
|
msaAlgorithm,
|
|
39
|
+
searchProgram,
|
|
28
40
|
msa,
|
|
29
41
|
tree,
|
|
30
42
|
treeMetadata,
|
package/dist/utils/ebiBlast.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BlastHit } from './types';
|
|
2
1
|
import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
|
+
import type { BlastHit } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* The subset of EBI's ncbiblast JSON result this plugin reads. The service
|
|
5
5
|
* returns a great deal more per hit (urls, bit scores, e-values, the match
|
package/dist/utils/msa.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
|
+
/**
|
|
3
|
+
* Build a tree from an alignment that already exists, which is what the phmmer
|
|
4
|
+
* path needs: phmmer produces the alignment itself, so there is no aligner run
|
|
5
|
+
* to take a guide tree from — and a guide tree is a byproduct of deciding
|
|
6
|
+
* progressive alignment order, not a phylogeny, so it is not what we would want
|
|
7
|
+
* even if there were one. simple_phylogeny is clustalw2's neighbour-joining on
|
|
8
|
+
* a real distance matrix, Kimura-corrected for protein distances.
|
|
9
|
+
*/
|
|
10
|
+
export declare function launchTree({ alignment, onProgress, }: {
|
|
11
|
+
alignment: string;
|
|
12
|
+
onProgress: (arg: string) => void;
|
|
13
|
+
}): Promise<string>;
|
|
2
14
|
export declare function launchMSA({ algorithm, sequence, onProgress, }: {
|
|
3
15
|
algorithm: MsaAlgorithm;
|
|
4
16
|
sequence: string;
|
package/dist/utils/msa.js
CHANGED
|
@@ -21,6 +21,35 @@ const algorithms = {
|
|
|
21
21
|
treeResult: 'phylotree',
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Build a tree from an alignment that already exists, which is what the phmmer
|
|
26
|
+
* path needs: phmmer produces the alignment itself, so there is no aligner run
|
|
27
|
+
* to take a guide tree from — and a guide tree is a byproduct of deciding
|
|
28
|
+
* progressive alignment order, not a phylogeny, so it is not what we would want
|
|
29
|
+
* even if there were one. simple_phylogeny is clustalw2's neighbour-joining on
|
|
30
|
+
* a real distance matrix, Kimura-corrected for protein distances.
|
|
31
|
+
*/
|
|
32
|
+
export async function launchTree({ alignment, onProgress, }) {
|
|
33
|
+
const tool = 'simple_phylogeny';
|
|
34
|
+
onProgress('Building tree...');
|
|
35
|
+
const jobId = await submitEbiJob({
|
|
36
|
+
tool,
|
|
37
|
+
params: {
|
|
38
|
+
sequence: alignment,
|
|
39
|
+
tree: 'phylip',
|
|
40
|
+
clustering: 'Neighbour-joining',
|
|
41
|
+
kimura: 'true',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
await waitForEbiJob({
|
|
45
|
+
tool,
|
|
46
|
+
jobId,
|
|
47
|
+
onCountdown: s => {
|
|
48
|
+
onProgress(`Re-checking tree status in... ${s}`);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return fetchEbiResult({ tool, jobId, type: 'tree' });
|
|
52
|
+
}
|
|
24
53
|
export async function launchMSA({ algorithm, sequence, onProgress, }) {
|
|
25
54
|
const config = algorithms[algorithm];
|
|
26
55
|
onProgress(`Launching ${algorithm} MSA...`);
|
|
@@ -35,16 +64,10 @@ export async function launchMSA({ algorithm, sequence, onProgress, }) {
|
|
|
35
64
|
onProgress(`Re-checking MSA status in... ${s}`);
|
|
36
65
|
},
|
|
37
66
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
tree: await fetchEbiResult({
|
|
45
|
-
tool: algorithm,
|
|
46
|
-
jobId,
|
|
47
|
-
type: config.treeResult,
|
|
48
|
-
}),
|
|
49
|
-
};
|
|
67
|
+
// one finished job, two result files, neither derived from the other
|
|
68
|
+
const [msa, tree] = await Promise.all([
|
|
69
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult }),
|
|
70
|
+
fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult }),
|
|
71
|
+
]);
|
|
72
|
+
return { msa, tree };
|
|
50
73
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PhmmerRow } from './phmmer';
|
|
2
|
+
import type { TaxonomyInfo } from './taxonomyNames';
|
|
3
|
+
import type { BlastHitDescription } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Turning search results into the rows the view is given, kept free of any
|
|
6
|
+
* jbrowse or network import so the whole assembly can be run and checked
|
|
7
|
+
* outside a browser — see test/phmmerLive.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildRowMetadata(desc: BlastHitDescription, taxonomyInfo: Map<number, TaxonomyInfo>): Record<string, string>;
|
|
10
|
+
/**
|
|
11
|
+
* One target can match the query in several places and phmmer emits a row per
|
|
12
|
+
* matched envelope — four for lamprey albumin against human albumin, which has
|
|
13
|
+
* three domains. Those rows share an accession and so would share a name, and
|
|
14
|
+
* duplicate names silently collapse rows in both the MSA and the tree, so the
|
|
15
|
+
* envelope disambiguates them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function makeRowNames(rows: PhmmerRow[], taxonomyInfo: Map<number, TaxonomyInfo>): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The phmmer alignment as the view receives it: aligned FASTA whose first row
|
|
20
|
+
* is the query, plus the per-row metadata keyed by the same names, which are
|
|
21
|
+
* also what the tree's leaves are labelled with.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildPhmmerMsa({ rows, queryRow, taxonomyInfo, querySeqName, }: {
|
|
24
|
+
rows: PhmmerRow[];
|
|
25
|
+
queryRow: string;
|
|
26
|
+
taxonomyInfo: Map<number, TaxonomyInfo>;
|
|
27
|
+
querySeqName?: string;
|
|
28
|
+
}): {
|
|
29
|
+
msa: string;
|
|
30
|
+
treeMetadata: Record<string, Record<string, string>>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { makeId } from '../LaunchMsaView/components/util';
|
|
2
|
+
/**
|
|
3
|
+
* Turning search results into the rows the view is given, kept free of any
|
|
4
|
+
* jbrowse or network import so the whole assembly can be run and checked
|
|
5
|
+
* outside a browser — see test/phmmerLive.test.ts.
|
|
6
|
+
*/
|
|
7
|
+
export function buildRowMetadata(desc, taxonomyInfo) {
|
|
8
|
+
const metadata = {};
|
|
9
|
+
const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined;
|
|
10
|
+
if (taxInfo?.sciname) {
|
|
11
|
+
metadata['Scientific name'] = taxInfo.sciname;
|
|
12
|
+
}
|
|
13
|
+
if (taxInfo?.commonName) {
|
|
14
|
+
metadata['Common name'] = taxInfo.commonName;
|
|
15
|
+
}
|
|
16
|
+
if (desc.accession) {
|
|
17
|
+
metadata.Accession = desc.accession;
|
|
18
|
+
}
|
|
19
|
+
if (desc.id) {
|
|
20
|
+
metadata.ID = desc.id;
|
|
21
|
+
}
|
|
22
|
+
if (desc.title) {
|
|
23
|
+
metadata.Description = desc.title;
|
|
24
|
+
}
|
|
25
|
+
return metadata;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One target can match the query in several places and phmmer emits a row per
|
|
29
|
+
* matched envelope — four for lamprey albumin against human albumin, which has
|
|
30
|
+
* three domains. Those rows share an accession and so would share a name, and
|
|
31
|
+
* duplicate names silently collapse rows in both the MSA and the tree, so the
|
|
32
|
+
* envelope disambiguates them.
|
|
33
|
+
*/
|
|
34
|
+
export function makeRowNames(rows, taxonomyInfo) {
|
|
35
|
+
const baseNames = rows.map(row => makeId(row, taxonomyInfo));
|
|
36
|
+
const counts = new Map();
|
|
37
|
+
for (const name of baseNames) {
|
|
38
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
const used = new Set();
|
|
41
|
+
return baseNames.map((base, i) => {
|
|
42
|
+
let name = counts.get(base) > 1 ? `${base}_${rows[i].range ?? i + 1}` : base;
|
|
43
|
+
while (used.has(name)) {
|
|
44
|
+
name = `${name}_${i + 1}`;
|
|
45
|
+
}
|
|
46
|
+
used.add(name);
|
|
47
|
+
return name;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The phmmer alignment as the view receives it: aligned FASTA whose first row
|
|
52
|
+
* is the query, plus the per-row metadata keyed by the same names, which are
|
|
53
|
+
* also what the tree's leaves are labelled with.
|
|
54
|
+
*/
|
|
55
|
+
export function buildPhmmerMsa({ rows, queryRow, taxonomyInfo, querySeqName = 'QUERY', }) {
|
|
56
|
+
const treeMetadata = {};
|
|
57
|
+
const rowNames = makeRowNames(rows, taxonomyInfo);
|
|
58
|
+
const sequences = rows.map((row, i) => {
|
|
59
|
+
const rowName = rowNames[i];
|
|
60
|
+
treeMetadata[rowName] = buildRowMetadata(row, taxonomyInfo);
|
|
61
|
+
return `>${rowName}\n${row.aligned}`;
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
msa: [`>${querySeqName}\n${queryRow}`, ...sequences].join('\n'),
|
|
65
|
+
treeMetadata,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
|
+
export interface PhmmerRow {
|
|
3
|
+
accession: string;
|
|
4
|
+
id: string;
|
|
5
|
+
sciname: string;
|
|
6
|
+
taxid?: number;
|
|
7
|
+
title?: string;
|
|
8
|
+
/** the matched envelope on the target, e.g. '503-912', absent if unparseable */
|
|
9
|
+
range?: string;
|
|
10
|
+
/** the row as phmmer aligned it, uppercased with '.' inserts turned into '-' */
|
|
11
|
+
aligned: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PhmmerAlignment {
|
|
14
|
+
rows: PhmmerRow[];
|
|
15
|
+
/**
|
|
16
|
+
* the query, placed into the same columns. phmmer does not put the query in
|
|
17
|
+
* its own output, so this is derived — see buildQueryRow.
|
|
18
|
+
*/
|
|
19
|
+
queryRow: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Human-facing link to a job, shown while it runs and on error.
|
|
23
|
+
*
|
|
24
|
+
* The category has to be sss: jdispatcher serves its shell with a 200 for any
|
|
25
|
+
* category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
|
|
26
|
+
* in a browser.
|
|
27
|
+
*/
|
|
28
|
+
export declare function phmmerResultUrl(jobId: string): string;
|
|
29
|
+
/** EBI job ids are prefixed with the tool that made them */
|
|
30
|
+
export declare function isPhmmerJobId(jobId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Exported for testing against a captured .sto — the annotation names (RF, the
|
|
33
|
+
* DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
|
|
34
|
+
* would notice if HMMER or EBI changed one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parsePhmmerAlignment({ stockholm, query, }: {
|
|
37
|
+
stockholm: string;
|
|
38
|
+
query: string;
|
|
39
|
+
}): PhmmerAlignment;
|
|
40
|
+
export declare function queryPhmmer({ query, database, onProgress, onRid, }: {
|
|
41
|
+
query: string;
|
|
42
|
+
database: PhmmerDatabase;
|
|
43
|
+
onProgress: (arg: string) => void;
|
|
44
|
+
onRid: (arg: string) => void;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
rows: PhmmerRow[];
|
|
47
|
+
/**
|
|
48
|
+
* the query, placed into the same columns. phmmer does not put the query in
|
|
49
|
+
* its own output, so this is derived — see buildQueryRow.
|
|
50
|
+
*/
|
|
51
|
+
queryRow: string;
|
|
52
|
+
rid: string;
|
|
53
|
+
}>;
|
|
@@ -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'
|