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.
Files changed (97) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +3 -3
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  17. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  18. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  19. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  20. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  21. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  22. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  23. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  24. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  25. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  26. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  27. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  28. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  30. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  31. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  32. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  33. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  34. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  35. package/dist/MsaViewPanel/model.d.ts +32 -11
  36. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  37. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  38. package/dist/MsaViewPanel/util.d.ts +18 -0
  39. package/dist/MsaViewPanel/util.js +17 -0
  40. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  41. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  42. package/dist/utils/blastCache.d.ts +10 -6
  43. package/dist/utils/blastCache.js +15 -3
  44. package/dist/utils/ebiBlast.d.ts +1 -1
  45. package/dist/utils/msa.d.ts +12 -0
  46. package/dist/utils/msa.js +35 -12
  47. package/dist/utils/msaRows.d.ts +31 -0
  48. package/dist/utils/msaRows.js +67 -0
  49. package/dist/utils/phmmer.d.ts +53 -0
  50. package/dist/utils/phmmer.js +118 -0
  51. package/dist/utils/taxonomyNames.d.ts +1 -1
  52. package/dist/utils/taxonomyNames.js +6 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +27 -21
  56. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  57. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  58. package/src/AddHighlightModel/index.tsx +1 -1
  59. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  60. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  61. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  62. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  63. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  64. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  65. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  66. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  67. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +5 -5
  68. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  69. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  70. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  71. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  72. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  73. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  74. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  75. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  76. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  77. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  78. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  79. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  80. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +2 -2
  81. package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
  82. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  83. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  84. package/src/MsaViewPanel/model.ts +28 -5
  85. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  86. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  87. package/src/MsaViewPanel/util.ts +18 -0
  88. package/src/utils/blastCache.ts +33 -12
  89. package/src/utils/ebiBlast.ts +1 -1
  90. package/src/utils/msa.ts +43 -12
  91. package/src/utils/msaRows.ts +95 -0
  92. package/src/utils/phmmer.ts +174 -0
  93. package/src/utils/taxonomyNames.ts +6 -1
  94. package/src/version.ts +1 -1
  95. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  96. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  97. 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
- msaAlgorithm: MsaAlgorithm;
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: MsaAlgorithm;
28
+ blastDatabase: BlastDatabase | PhmmerDatabase;
29
+ msaAlgorithm?: MsaAlgorithm;
30
+ searchProgram?: SearchProgram;
27
31
  msa: string;
28
32
  tree: string;
29
33
  treeMetadata: string;
@@ -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(proteinSequence, blastDatabase, msaAlgorithm, transcriptId);
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,
@@ -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
@@ -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
- return {
39
- msa: await fetchEbiResult({
40
- tool: algorithm,
41
- jobId,
42
- type: config.msaResult,
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(taxids: number[]): Promise<Map<number, TaxonomyInfo>>;
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(taxids) {
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.3.0";
1
+ export declare const version = "3.4.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.3.0';
1
+ export const version = '3.4.0';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.3.0",
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.2.0",
21
- "g2p_mapper": "^2.1.5",
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.2.0",
32
- "@mui/material": "^9.2.0",
33
- "@mui/system": "^9.2.0",
34
- "@mui/x-data-grid": "^9.10.0",
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.1.1",
38
- "@types/react": "^19.2.17",
39
- "esbuild": "^0.28.1",
40
- "eslint": "^10.7.0",
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": "^72.0.0",
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.0.0",
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.3.0",
56
+ "puppeteer": "^25.8.0",
55
57
  "react": "^19.2.8",
56
58
  "react-dom": "^19.2.8",
57
- "react-msaview": "^6.0.0",
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.65.0",
64
- "vitest": "^4.1.10"
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": "pnpm prettier --write .",
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": "eslint src --report-unused-disable-directives --max-warnings 0",
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'