jbrowse-plugin-msaview 2.5.0 → 2.5.2
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/LaunchMsaViewExtensionPoint/index.js +2 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +22 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.js +83 -26
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +16 -0
- package/dist/MsaViewPanel/loadProteinDomains.js +33 -0
- package/dist/MsaViewPanel/model.d.ts +189 -158
- package/dist/MsaViewPanel/model.js +17 -5
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +28 -28
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/domainCache.d.ts +8 -0
- package/dist/utils/domainCache.js +28 -0
- package/dist/utils/eutils.d.ts +3 -0
- package/dist/utils/eutils.js +14 -0
- package/dist/utils/msa.js +18 -15
- package/dist/utils/ncbiBlast.js +22 -24
- package/dist/utils/ncbiDomains.d.ts +39 -0
- package/dist/utils/ncbiDomains.js +156 -0
- package/dist/utils/poll.d.ts +11 -0
- package/dist/utils/poll.js +19 -0
- package/dist/utils/taxonomyNames.js +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +22 -19
- package/src/LaunchMsaViewExtensionPoint/index.ts +3 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +86 -29
- package/src/MsaViewPanel/loadProteinDomains.ts +57 -0
- package/src/MsaViewPanel/model.ts +22 -4
- package/src/utils/domainCache.ts +43 -0
- package/src/utils/eutils.ts +16 -0
- package/src/utils/msa.ts +18 -16
- package/src/utils/ncbiBlast.ts +27 -31
- package/src/utils/ncbiDomains.ts +173 -0
- package/src/utils/poll.ts +28 -0
- package/src/utils/taxonomyNames.ts +3 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DomainMatch } from './ncbiDomains';
|
|
2
|
+
interface CachedDomain {
|
|
3
|
+
accession: string;
|
|
4
|
+
matches: DomainMatch[];
|
|
5
|
+
}
|
|
6
|
+
export declare function getCachedDomains(accessions: string[]): Promise<(CachedDomain | undefined)[]>;
|
|
7
|
+
export declare function saveDomains(entries: CachedDomain[]): Promise<void>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { openDB } from 'idb';
|
|
2
|
+
const DB_NAME = 'jbrowse-msaview-domain-cache';
|
|
3
|
+
const STORE_NAME = 'domains';
|
|
4
|
+
const DB_VERSION = 1;
|
|
5
|
+
async function getDB() {
|
|
6
|
+
return openDB(DB_NAME, DB_VERSION, {
|
|
7
|
+
upgrade(db) {
|
|
8
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
9
|
+
db.createObjectStore(STORE_NAME, { keyPath: 'accession' });
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export async function getCachedDomains(accessions) {
|
|
15
|
+
const db = await getDB();
|
|
16
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
17
|
+
const results = await Promise.all(accessions.map(accession => tx.store.get(accession)));
|
|
18
|
+
await tx.done;
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
export async function saveDomains(entries) {
|
|
22
|
+
const db = await getDB();
|
|
23
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
await tx.store.put(entry);
|
|
26
|
+
}
|
|
27
|
+
await tx.done;
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// NCBI asks that programmatic E-utilities requests identify themselves with a
|
|
2
|
+
// tool name and contact email so they can reach out before throttling, rather
|
|
3
|
+
// than silently rate-limiting. https://www.ncbi.nlm.nih.gov/books/NBK25497/
|
|
4
|
+
export const NCBI_TOOL = 'jbrowse-plugin-msaview';
|
|
5
|
+
export const NCBI_EMAIL = 'colin.diesh@gmail.com';
|
|
6
|
+
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
7
|
+
export function efetchUrl(params) {
|
|
8
|
+
const search = new URLSearchParams({
|
|
9
|
+
...params,
|
|
10
|
+
tool: NCBI_TOOL,
|
|
11
|
+
email: NCBI_EMAIL,
|
|
12
|
+
});
|
|
13
|
+
return `${EUTILS}/efetch.fcgi?${search.toString()}`;
|
|
14
|
+
}
|
package/dist/utils/msa.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { textfetch
|
|
1
|
+
import { textfetch } from './fetch';
|
|
2
|
+
import { pollLoop } from './poll';
|
|
2
3
|
const base = `https://www.ebi.ac.uk/Tools/services/rest`;
|
|
3
4
|
const email = 'colin.diesh@gmail.com';
|
|
4
5
|
const algorithms = {
|
|
@@ -24,20 +25,22 @@ const algorithms = {
|
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
async function wait({ onProgress, jobId, algorithm, }) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
await pollLoop({
|
|
29
|
+
intervalSeconds: 10,
|
|
30
|
+
onCountdown: s => {
|
|
31
|
+
onProgress(`Re-checking MSA status in... ${s}`);
|
|
32
|
+
},
|
|
33
|
+
check: async () => {
|
|
34
|
+
const result = await textfetch(`${base}/${algorithm}/status/${jobId}`);
|
|
35
|
+
if (result.includes('FINISHED')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (result.includes('FAILURE')) {
|
|
39
|
+
throw new Error(`Failed to run: jobId ${jobId}`);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
41
44
|
}
|
|
42
45
|
export async function launchMSA({ algorithm, sequence, onProgress, }) {
|
|
43
46
|
const config = algorithms[algorithm];
|
package/dist/utils/ncbiBlast.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { jsonfetch, textfetch
|
|
1
|
+
import { jsonfetch, textfetch } from './fetch';
|
|
2
|
+
import { pollLoop } from './poll';
|
|
2
3
|
export async function queryBlastFromRid({ rid, baseUrl, onProgress, }) {
|
|
3
4
|
onProgress(`Checking BLAST status for RID: ${rid}...`);
|
|
4
5
|
await waitForRid({
|
|
@@ -56,31 +57,28 @@ async function initialQuery({ query, blastProgram, blastDatabase, baseUrl, }) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
async function waitForRid({ rid, onProgress, baseUrl, }) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
await pollLoop({
|
|
61
|
+
intervalSeconds: 20,
|
|
62
|
+
onCountdown: s => {
|
|
63
|
+
onProgress(`Re-checking BLAST status in... ${s}`);
|
|
64
|
+
},
|
|
65
|
+
check: async () => {
|
|
66
|
+
const res = await textfetch(`${baseUrl}?CMD=Get&FORMAT_OBJECT=SearchInfo&RID=${rid}`);
|
|
67
|
+
const status = /\s+Status=(\S+)/m.exec(res)?.[1];
|
|
68
|
+
const hasHits = /\s+ThereAreHits=yes/m.test(res);
|
|
69
|
+
if (status === 'WAITING') {
|
|
70
|
+
return false;
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (status === 'FAILED') {
|
|
74
|
-
throw new Error(`BLAST ${rid} failed`);
|
|
75
|
-
}
|
|
76
|
-
if (status === 'READY') {
|
|
77
|
-
if (hasHits) {
|
|
78
|
-
return true;
|
|
72
|
+
if (status === 'FAILED') {
|
|
73
|
+
throw new Error(`BLAST ${rid} failed`);
|
|
79
74
|
}
|
|
80
|
-
|
|
75
|
+
if (status === 'READY') {
|
|
76
|
+
if (hasHits) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
81
79
|
throw new Error('No hits found');
|
|
82
80
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
81
|
+
throw new Error(`BLAST ${rid} returned unexpected status: ${status ?? 'unknown'}`);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
86
84
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { InterProScanResults } from 'react-msaview';
|
|
2
|
+
export type DomainMatch = InterProScanResults['matches'][number];
|
|
3
|
+
/**
|
|
4
|
+
* Parse a GenPept (efetch db=protein&rettype=gp&retmode=xml) document into CDD
|
|
5
|
+
* domain and site annotations, keyed by both the versioned and primary
|
|
6
|
+
* accession so callers can look up by whichever NCBI returned.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseCddDomains(xml: string): Map<string, {
|
|
9
|
+
signature: {
|
|
10
|
+
entry?: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
accession: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
locations: {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
}[];
|
|
20
|
+
}[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch pre-computed CDD domain and site annotations for NCBI protein
|
|
23
|
+
* accessions. These come baked into the GenPept records, so a single batched
|
|
24
|
+
* efetch returns them with no job submission or polling. Results are cached in
|
|
25
|
+
* IndexedDB so reopening a view doesn't refetch.
|
|
26
|
+
*/
|
|
27
|
+
export declare function fetchProteinDomains(accessions: string[]): Promise<Map<string, {
|
|
28
|
+
signature: {
|
|
29
|
+
entry?: {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
accession: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
locations: {
|
|
36
|
+
start: number;
|
|
37
|
+
end: number;
|
|
38
|
+
}[];
|
|
39
|
+
}[]>>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { getCachedDomains, saveDomains } from './domainCache';
|
|
2
|
+
import { efetchUrl } from './eutils';
|
|
3
|
+
import { textfetch } from './fetch';
|
|
4
|
+
function field(xml, tag) {
|
|
5
|
+
return new RegExp(`<${tag}>(.*?)</${tag}>`, 's').exec(xml)?.[1];
|
|
6
|
+
}
|
|
7
|
+
function parseQualifiers(featureXml) {
|
|
8
|
+
const quals = {};
|
|
9
|
+
const re = /<GBQualifier>([\s\S]*?)<\/GBQualifier>/g;
|
|
10
|
+
let m;
|
|
11
|
+
while ((m = re.exec(featureXml)) !== null) {
|
|
12
|
+
const name = field(m[1], 'GBQualifier_name');
|
|
13
|
+
const value = field(m[1], 'GBQualifier_value');
|
|
14
|
+
// keep the first occurrence: NCBI lists the canonical value first
|
|
15
|
+
if (name && value !== undefined && quals[name] === undefined) {
|
|
16
|
+
quals[name] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return quals;
|
|
20
|
+
}
|
|
21
|
+
// A feature can span several intervals: domains are usually one contiguous
|
|
22
|
+
// range, but CDD Sites (e.g. an active site) are a set of scattered residues
|
|
23
|
+
// expressed as multiple GBInterval ranges and single GBInterval_point residues.
|
|
24
|
+
// We collapse those to a single bounding span so a site renders as one box
|
|
25
|
+
// rather than a spray of 1px specks.
|
|
26
|
+
function parseBoundingSpan(featureXml) {
|
|
27
|
+
const starts = [];
|
|
28
|
+
const ends = [];
|
|
29
|
+
const re = /<GBInterval>([\s\S]*?)<\/GBInterval>/g;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = re.exec(featureXml)) !== null) {
|
|
32
|
+
const block = m[1];
|
|
33
|
+
const from = field(block, 'GBInterval_from');
|
|
34
|
+
const to = field(block, 'GBInterval_to');
|
|
35
|
+
const point = field(block, 'GBInterval_point');
|
|
36
|
+
if (from && to) {
|
|
37
|
+
starts.push(Number(from));
|
|
38
|
+
ends.push(Number(to));
|
|
39
|
+
}
|
|
40
|
+
else if (point) {
|
|
41
|
+
starts.push(Number(point));
|
|
42
|
+
ends.push(Number(point));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return starts.length > 0
|
|
46
|
+
? { start: Math.min(...starts), end: Math.max(...ends) }
|
|
47
|
+
: undefined;
|
|
48
|
+
}
|
|
49
|
+
// Drop single-residue specks (acetylation/phospho points) but keep every
|
|
50
|
+
// genuine domain and functional site; react-msaview draws longest-first, so
|
|
51
|
+
// smaller features (binding sites, loops) layer on top of the domain they sit
|
|
52
|
+
// inside.
|
|
53
|
+
const MIN_FEATURE_LENGTH = 2;
|
|
54
|
+
function parseFeature(featureXml) {
|
|
55
|
+
const key = field(featureXml, 'GBFeature_key');
|
|
56
|
+
const quals = parseQualifiers(featureXml);
|
|
57
|
+
const xref = quals.db_xref;
|
|
58
|
+
const span = parseBoundingSpan(featureXml);
|
|
59
|
+
// only CDD-backed Regions/Sites are conserved-domain annotations; Regions and
|
|
60
|
+
// Sites without a CDD xref are UniProt-propagated point motifs we don't want
|
|
61
|
+
if ((key === 'Region' || key === 'Site') &&
|
|
62
|
+
xref?.startsWith('CDD:') &&
|
|
63
|
+
span &&
|
|
64
|
+
span.end - span.start + 1 >= MIN_FEATURE_LENGTH) {
|
|
65
|
+
const cddId = xref.replace('CDD:', '');
|
|
66
|
+
const isDomain = key === 'Region';
|
|
67
|
+
// a site's note (e.g. "ATP binding site [chemical binding]") is more
|
|
68
|
+
// specific than its generic site_type ("other"), so prefer it for the name
|
|
69
|
+
// — that gives each functional site its own color/legend/filter entry
|
|
70
|
+
const noteName = quals.note?.split(/[[(]/)[0]?.trim();
|
|
71
|
+
const name = isDomain
|
|
72
|
+
? (quals.region_name ?? cddId)
|
|
73
|
+
: noteName || quals.site_type || 'site';
|
|
74
|
+
const accession = isDomain ? cddId : `${cddId}:${name}`;
|
|
75
|
+
return {
|
|
76
|
+
signature: {
|
|
77
|
+
entry: { name, description: quals.note ?? name, accession },
|
|
78
|
+
},
|
|
79
|
+
locations: [span],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse a GenPept (efetch db=protein&rettype=gp&retmode=xml) document into CDD
|
|
86
|
+
* domain and site annotations, keyed by both the versioned and primary
|
|
87
|
+
* accession so callers can look up by whichever NCBI returned.
|
|
88
|
+
*/
|
|
89
|
+
export function parseCddDomains(xml) {
|
|
90
|
+
const byAccession = new Map();
|
|
91
|
+
const seqRe = /<GBSeq>([\s\S]*?)<\/GBSeq>/g;
|
|
92
|
+
let seqMatch;
|
|
93
|
+
while ((seqMatch = seqRe.exec(xml)) !== null) {
|
|
94
|
+
const seqXml = seqMatch[1];
|
|
95
|
+
const matches = [];
|
|
96
|
+
const featRe = /<GBFeature>([\s\S]*?)<\/GBFeature>/g;
|
|
97
|
+
let featMatch;
|
|
98
|
+
while ((featMatch = featRe.exec(seqXml)) !== null) {
|
|
99
|
+
const match = parseFeature(featMatch[1]);
|
|
100
|
+
if (match) {
|
|
101
|
+
matches.push(match);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const acc of [
|
|
105
|
+
field(seqXml, 'GBSeq_accession-version'),
|
|
106
|
+
field(seqXml, 'GBSeq_primary-accession'),
|
|
107
|
+
]) {
|
|
108
|
+
if (acc) {
|
|
109
|
+
byAccession.set(acc, matches);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return byAccession;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Fetch pre-computed CDD domain and site annotations for NCBI protein
|
|
117
|
+
* accessions. These come baked into the GenPept records, so a single batched
|
|
118
|
+
* efetch returns them with no job submission or polling. Results are cached in
|
|
119
|
+
* IndexedDB so reopening a view doesn't refetch.
|
|
120
|
+
*/
|
|
121
|
+
export async function fetchProteinDomains(accessions) {
|
|
122
|
+
const unique = [...new Set(accessions)].filter(Boolean);
|
|
123
|
+
const byAccession = new Map();
|
|
124
|
+
const cached = await getCachedDomains(unique);
|
|
125
|
+
const uncached = [];
|
|
126
|
+
unique.forEach((acc, i) => {
|
|
127
|
+
const hit = cached[i];
|
|
128
|
+
if (hit) {
|
|
129
|
+
byAccession.set(acc, hit.matches);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
uncached.push(acc);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const toCache = [];
|
|
136
|
+
const batchSize = 100;
|
|
137
|
+
for (let i = 0; i < uncached.length; i += batchSize) {
|
|
138
|
+
const batch = uncached.slice(i, i + batchSize);
|
|
139
|
+
const xml = await textfetch(efetchUrl({
|
|
140
|
+
db: 'protein',
|
|
141
|
+
id: batch.join(','),
|
|
142
|
+
rettype: 'gp',
|
|
143
|
+
retmode: 'xml',
|
|
144
|
+
}));
|
|
145
|
+
const parsed = parseCddDomains(xml);
|
|
146
|
+
for (const acc of batch) {
|
|
147
|
+
const matches = parsed.get(acc) ?? [];
|
|
148
|
+
byAccession.set(acc, matches);
|
|
149
|
+
toCache.push({ accession: acc, matches });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (toCache.length > 0) {
|
|
153
|
+
await saveDomains(toCache);
|
|
154
|
+
}
|
|
155
|
+
return byAccession;
|
|
156
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a remote job until it reports done. `check` returns true when finished,
|
|
3
|
+
* false when still pending, and throws on failure. Between checks it counts down
|
|
4
|
+
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
5
|
+
* progress.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pollLoop({ check, intervalSeconds, onCountdown, }: {
|
|
8
|
+
check: () => Promise<boolean>;
|
|
9
|
+
intervalSeconds: number;
|
|
10
|
+
onCountdown: (secondsRemaining: number) => void;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { timeout } from './fetch';
|
|
2
|
+
/**
|
|
3
|
+
* Poll a remote job until it reports done. `check` returns true when finished,
|
|
4
|
+
* false when still pending, and throws on failure. Between checks it counts down
|
|
5
|
+
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
6
|
+
* progress.
|
|
7
|
+
*/
|
|
8
|
+
export async function pollLoop({ check, intervalSeconds, onCountdown, }) {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
10
|
+
while (true) {
|
|
11
|
+
if (await check()) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (let i = intervalSeconds; i > 0; i--) {
|
|
15
|
+
onCountdown(i);
|
|
16
|
+
await timeout(1000);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openDB } from 'idb';
|
|
2
|
+
import { efetchUrl } from './eutils';
|
|
2
3
|
const DB_NAME = 'jbrowse-msaview-taxonomy-cache';
|
|
3
4
|
const STORE_NAME = 'common-names';
|
|
4
5
|
const DB_VERSION = 2;
|
|
@@ -53,7 +54,7 @@ export async function fetchTaxonomyInfo(taxids) {
|
|
|
53
54
|
const batch = uncachedTaxids.slice(i, i + batchSize);
|
|
54
55
|
const idsParam = batch.join(',');
|
|
55
56
|
try {
|
|
56
|
-
const response = await fetch(
|
|
57
|
+
const response = await fetch(efetchUrl({ db: 'taxonomy', id: idsParam, retmode: 'xml' }));
|
|
57
58
|
const text = await response.text();
|
|
58
59
|
// Build a map of taxid -> taxon block by finding Taxon elements.
|
|
59
60
|
// Prefer entries with <LineageEx> (full top-level entries) over nested
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.5.
|
|
1
|
+
export declare const version = "2.5.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.5.
|
|
1
|
+
export const version = '2.5.2';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.5.
|
|
2
|
+
"version": "2.5.2",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -20,44 +20,47 @@
|
|
|
20
20
|
"g2p_mapper": "^2.1.5",
|
|
21
21
|
"idb": "^8.0.3",
|
|
22
22
|
"pako-esm2": "^2.0.2",
|
|
23
|
-
"react-msaview": "^5.
|
|
24
|
-
"swr": "^2.4.
|
|
23
|
+
"react-msaview": "^5.4.1",
|
|
24
|
+
"swr": "^2.4.2"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@emotion/react": "^11.14.0",
|
|
28
28
|
"@eslint/js": "^10.0.1",
|
|
29
29
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
|
30
30
|
"@jbrowse/core": "^4.3.0",
|
|
31
|
-
"@jbrowse/mobx-state-tree": "^5.
|
|
31
|
+
"@jbrowse/mobx-state-tree": "^5.11.1",
|
|
32
32
|
"@jbrowse/plugin-linear-genome-view": "^4.3.0",
|
|
33
33
|
"@mui/icons-material": "^7.3.11",
|
|
34
34
|
"@mui/material": "^7.3.11",
|
|
35
35
|
"@mui/system": "^7.3.11",
|
|
36
|
-
"@mui/x-data-grid": "^8.
|
|
37
|
-
"@types/node": "^25.9.
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
40
|
-
"@typescript-eslint/parser": "^8.
|
|
41
|
-
"esbuild": "^0.28.
|
|
42
|
-
"eslint": "^10.
|
|
43
|
-
"eslint-plugin-import-x": "^4.
|
|
36
|
+
"@mui/x-data-grid": "^8.29.1",
|
|
37
|
+
"@types/node": "^25.9.4",
|
|
38
|
+
"@types/react": "^19.2.17",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.62.0",
|
|
41
|
+
"esbuild": "^0.28.1",
|
|
42
|
+
"eslint": "^10.5.0",
|
|
43
|
+
"eslint-plugin-import-x": "^4.17.0",
|
|
44
44
|
"eslint-plugin-react": "^7.37.5",
|
|
45
45
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
46
46
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
47
|
-
"mobx": "^6.
|
|
47
|
+
"mobx": "^6.16.1",
|
|
48
48
|
"mobx-react": "^9.2.2",
|
|
49
|
-
"
|
|
49
|
+
"msa-parsers": "5.4.1",
|
|
50
|
+
"pixelmatch": "^7.2.0",
|
|
51
|
+
"pngjs": "^7.0.0",
|
|
52
|
+
"prettier": "^3.8.4",
|
|
50
53
|
"pretty-bytes": "^7.1.0",
|
|
51
|
-
"puppeteer": "^25.1
|
|
52
|
-
"react": "^19.2.
|
|
53
|
-
"react-dom": "^19.2.
|
|
54
|
+
"puppeteer": "^25.2.1",
|
|
55
|
+
"react": "^19.2.7",
|
|
56
|
+
"react-dom": "^19.2.7",
|
|
54
57
|
"rimraf": "^6.1.3",
|
|
55
58
|
"rxjs": "^7.8.2",
|
|
56
59
|
"serve": "^14.2.6",
|
|
57
60
|
"tss-react": "^4.9.21",
|
|
58
61
|
"typescript": "^6.0.3",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
-
"vitest": "^4.1.
|
|
62
|
+
"typescript-eslint": "^8.62.0",
|
|
63
|
+
"vitest": "^4.1.9"
|
|
61
64
|
},
|
|
62
65
|
"scripts": {
|
|
63
66
|
"clean": "rimraf dist",
|
|
@@ -24,6 +24,7 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
24
24
|
labelsAlignRight,
|
|
25
25
|
showBranchLen,
|
|
26
26
|
querySeqName,
|
|
27
|
+
highlightColumns,
|
|
27
28
|
}: {
|
|
28
29
|
session: AbstractSessionModel
|
|
29
30
|
data?: { msa: string; tree?: string }
|
|
@@ -41,6 +42,7 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
41
42
|
labelsAlignRight?: boolean
|
|
42
43
|
showBranchLen?: boolean
|
|
43
44
|
querySeqName?: string
|
|
45
|
+
highlightColumns?: number[]
|
|
44
46
|
}) => {
|
|
45
47
|
if (!data && !msaFileLocation) {
|
|
46
48
|
throw new Error(
|
|
@@ -61,6 +63,7 @@ export default function LaunchMsaViewExtensionPointF(
|
|
|
61
63
|
drawNodeBubbles,
|
|
62
64
|
labelsAlignRight,
|
|
63
65
|
showBranchLen,
|
|
66
|
+
highlightColumns,
|
|
64
67
|
init: {
|
|
65
68
|
msaData: data?.msa,
|
|
66
69
|
treeData: data?.tree,
|
|
@@ -2,6 +2,7 @@ import { getSession } from '@jbrowse/core/util'
|
|
|
2
2
|
|
|
3
3
|
import { doLaunchBlast } from './doLaunchBlast'
|
|
4
4
|
import { genomeToMSA } from './genomeToMSA'
|
|
5
|
+
import { loadProteinDomains } from './loadProteinDomains'
|
|
5
6
|
import {
|
|
6
7
|
cleanupOldData,
|
|
7
8
|
generateDataStoreId,
|
|
@@ -30,6 +31,9 @@ export function loadStoredData(self: JBrowsePluginMsaViewModel) {
|
|
|
30
31
|
if (storedData.tree) {
|
|
31
32
|
self.setTree(storedData.tree)
|
|
32
33
|
}
|
|
34
|
+
if (storedData.treeMetadata) {
|
|
35
|
+
self.setTreeMetadata(storedData.treeMetadata)
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
} catch (e) {
|
|
35
39
|
console.error('Failed to load MSA data from IndexedDB:', e)
|
|
@@ -95,6 +99,34 @@ export function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel) {
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Once an accession-bearing alignment is present (fresh from BLAST or restored
|
|
104
|
+
* from cache), fetch NCBI CDD domains for those accessions and overlay them.
|
|
105
|
+
* Runs once per view; the domainsRequested guard prevents refiring when NCBI
|
|
106
|
+
* returns no domains (which leaves interProAnnotations undefined).
|
|
107
|
+
*/
|
|
108
|
+
export function autoLoadProteinDomains(self: JBrowsePluginMsaViewModel) {
|
|
109
|
+
const { rows, domainsRequested, interProAnnotations } = self
|
|
110
|
+
const hasAccessions = self.data.treeMetadata?.includes('"Accession"') ?? false
|
|
111
|
+
if (
|
|
112
|
+
rows.length > 0 &&
|
|
113
|
+
hasAccessions &&
|
|
114
|
+
!interProAnnotations &&
|
|
115
|
+
!domainsRequested
|
|
116
|
+
) {
|
|
117
|
+
self.setDomainsRequested(true)
|
|
118
|
+
void (async () => {
|
|
119
|
+
try {
|
|
120
|
+
await loadProteinDomains(self)
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('[msaview-domains] auto-load failed:', e)
|
|
123
|
+
} finally {
|
|
124
|
+
self.setProgress('')
|
|
125
|
+
}
|
|
126
|
+
})()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
98
130
|
export function processInit(self: JBrowsePluginMsaViewModel) {
|
|
99
131
|
const { init } = self
|
|
100
132
|
if (init) {
|
|
@@ -250,46 +282,71 @@ export function autoConnectStructures(self: JBrowsePluginMsaViewModel) {
|
|
|
250
282
|
}
|
|
251
283
|
}
|
|
252
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Mirror a connected 3D protein view's hovered residue onto the MSA's
|
|
287
|
+
* highlighted columns. Returns the autorun body and keeps a flag tracking
|
|
288
|
+
* whether the current highlight was set by THIS sync: when a protein hover ends
|
|
289
|
+
* we restore the declarative highlightColumns seed (or clear) rather than
|
|
290
|
+
* blindly wiping it.
|
|
291
|
+
*
|
|
292
|
+
* Without the flag this autorun fires once on creation — with the view connected
|
|
293
|
+
* to a *genome* LGV but no 3D protein structure attached — computes zero columns,
|
|
294
|
+
* and calls setHighlightedColumns(undefined), clobbering the seed that
|
|
295
|
+
* MSAModelF.afterCreate just set from the declarative `highlightColumns`. That is
|
|
296
|
+
* the bug that made the BRAF/TP53 genome-browser links open with no V600/R248
|
|
297
|
+
* column lit (SRC has no highlightColumns, so nothing was there to wipe).
|
|
298
|
+
*/
|
|
253
299
|
export function observeProteinHighlights(self: JBrowsePluginMsaViewModel) {
|
|
254
|
-
|
|
300
|
+
let proteinDriven = false
|
|
301
|
+
return () => {
|
|
302
|
+
const { connectedViewId, transcriptToMsaMap, querySeqName } = self
|
|
255
303
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
304
|
+
if (!connectedViewId || !transcriptToMsaMap) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
259
307
|
|
|
260
|
-
|
|
308
|
+
const columns = new Set<number>()
|
|
261
309
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
310
|
+
for (const view of getProteinViews(getSession(self).views)) {
|
|
311
|
+
for (const structure of view.structures) {
|
|
312
|
+
if (structure.connectedViewId !== connectedViewId) {
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
267
315
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
316
|
+
const highlights = structure.hoverGenomeHighlights
|
|
317
|
+
if (!highlights || highlights.length === 0) {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
272
320
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
321
|
+
const { g2p } = transcriptToMsaMap
|
|
322
|
+
for (const highlight of highlights) {
|
|
323
|
+
for (let coord = highlight.start; coord < highlight.end; coord++) {
|
|
324
|
+
const proteinPos = g2p[coord]
|
|
325
|
+
if (proteinPos !== undefined) {
|
|
326
|
+
const col = self.seqPosToGlobalCol(querySeqName, proteinPos)
|
|
327
|
+
columns.add(col)
|
|
328
|
+
}
|
|
280
329
|
}
|
|
281
330
|
}
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
|
-
}
|
|
285
333
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
334
|
+
const visibleColumns = Array.from(columns)
|
|
335
|
+
.map(col => self.globalColToVisibleCol(col))
|
|
336
|
+
.filter((col): col is number => col !== undefined)
|
|
337
|
+
|
|
338
|
+
if (visibleColumns.length > 0) {
|
|
339
|
+
self.setHighlightedColumns(visibleColumns)
|
|
340
|
+
proteinDriven = true
|
|
341
|
+
} else if (proteinDriven) {
|
|
342
|
+
// our protein-hover highlight ended — fall back to the declarative seed
|
|
343
|
+
// instead of wiping a column the URL/user asked to keep lit
|
|
344
|
+
self.setHighlightedColumns(
|
|
345
|
+
self.highlightColumns?.length ? self.highlightColumns : undefined,
|
|
346
|
+
)
|
|
347
|
+
proteinDriven = false
|
|
348
|
+
}
|
|
349
|
+
}
|
|
293
350
|
}
|
|
294
351
|
|
|
295
352
|
export function runCleanup() {
|