jbrowse-plugin-msaview 5.3.0 → 5.5.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/MsaViewPanel/afterCreateAutoruns.d.ts +1 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +8 -0
- package/dist/MsaViewPanel/connectedDomains.d.ts +40 -0
- package/dist/MsaViewPanel/connectedDomains.js +74 -0
- package/dist/MsaViewPanel/connectedDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/connectedDomains.test.js +108 -0
- package/dist/MsaViewPanel/domainTrack.d.ts +85 -0
- package/dist/MsaViewPanel/domainTrack.js +64 -0
- package/dist/MsaViewPanel/domainTrack.test.d.ts +1 -0
- package/dist/MsaViewPanel/domainTrack.test.js +67 -0
- package/dist/MsaViewPanel/model.d.ts +44 -22
- package/dist/MsaViewPanel/model.js +78 -2
- package/dist/MsaViewPanel/model.test.js +85 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +11 -3
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +21 -14
- package/dist/chunks/{AboutDialog-447TIFAW.js → AboutDialog-HCCCEJ6V.js} +2 -2
- package/dist/chunks/{AboutDialog-447TIFAW.js.map → AboutDialog-HCCCEJ6V.js.map} +1 -1
- package/dist/chunks/AnnotationFileDialog-CPJ4X7XW.js +2 -0
- package/dist/chunks/{AnnotationFileDialog-S3N5EHHE.js.map → AnnotationFileDialog-CPJ4X7XW.js.map} +2 -2
- package/dist/chunks/{ExportSVGDialog-7ZHKKQ7W.js → ExportSVGDialog-YUFLYBYW.js} +2 -2
- package/dist/chunks/{ExportSVGDialog-7ZHKKQ7W.js.map → ExportSVGDialog-YUFLYBYW.js.map} +1 -1
- package/dist/chunks/{FeatureDialog-EEOGSLOG.js → FeatureDialog-NBDB5SWU.js} +1 -1
- package/dist/chunks/{FeatureDialog-EEOGSLOG.js.map → FeatureDialog-NBDB5SWU.js.map} +1 -1
- package/dist/chunks/{HelpDialog-TCWNBZOV.js → HelpDialog-UCUYPCOI.js} +2 -2
- package/dist/chunks/{HelpDialog-TCWNBZOV.js.map → HelpDialog-UCUYPCOI.js.map} +1 -1
- package/dist/chunks/{LaunchMsaViewDialog-R6CNRASN.js → LaunchMsaViewDialog-BG4MX2BW.js} +2 -2
- package/dist/chunks/{LaunchMsaViewDialog-R6CNRASN.js.map → LaunchMsaViewDialog-BG4MX2BW.js.map} +2 -2
- package/dist/chunks/MetadataDialog-DSA3545E.js +2 -0
- package/dist/chunks/{MetadataDialog-MNSI6NEW.js.map → MetadataDialog-DSA3545E.js.map} +2 -2
- package/dist/chunks/MsaViewPanel-XSXFJWWY.js +2 -0
- package/dist/chunks/{MsaViewPanel-R2WAER5O.js.map → MsaViewPanel-XSXFJWWY.js.map} +1 -1
- package/dist/chunks/{TrackInfoDialog-W2F77IPG.js → TrackInfoDialog-YXCIGFYM.js} +2 -2
- package/dist/chunks/{TrackInfoDialog-W2F77IPG.js.map → TrackInfoDialog-YXCIGFYM.js.map} +1 -1
- package/dist/chunks/TreeNodeInfoDialog-QCWDJTM4.js +2 -0
- package/dist/chunks/{TreeNodeInfoDialog-T5XMPG3R.js.map → TreeNodeInfoDialog-QCWDJTM4.js.map} +2 -2
- package/dist/chunks/{chunk-JF6CW6MO.js → chunk-35FWLG3N.js} +1 -1
- package/dist/chunks/{chunk-JF6CW6MO.js.map → chunk-35FWLG3N.js.map} +1 -1
- package/dist/chunks/{chunk-BAM67QHH.js → chunk-4TEPHD3L.js} +3 -3
- package/dist/chunks/{chunk-BAM67QHH.js.map → chunk-4TEPHD3L.js.map} +2 -2
- package/dist/chunks/{chunk-NWHZOZMG.js → chunk-5OSSOQH4.js} +1 -1
- package/dist/chunks/{chunk-NWHZOZMG.js.map → chunk-5OSSOQH4.js.map} +1 -1
- package/dist/chunks/{chunk-FEUZKN5V.js → chunk-666BV764.js} +2 -2
- package/dist/chunks/{chunk-FEUZKN5V.js.map → chunk-666BV764.js.map} +1 -1
- package/dist/chunks/{chunk-YUKJI2AH.js → chunk-AQ2THRTR.js} +2 -2
- package/dist/chunks/{chunk-YUKJI2AH.js.map → chunk-AQ2THRTR.js.map} +1 -1
- package/dist/chunks/{chunk-L6CBIH6H.js → chunk-ASUKJX5J.js} +2 -2
- package/dist/chunks/{chunk-L6CBIH6H.js.map → chunk-ASUKJX5J.js.map} +1 -1
- package/dist/chunks/chunk-F726IWCW.js +2 -0
- package/dist/chunks/chunk-F726IWCW.js.map +7 -0
- package/dist/chunks/{chunk-CSCUNTGJ.js → chunk-FFHB7WP7.js} +4 -4
- package/dist/chunks/chunk-FFHB7WP7.js.map +7 -0
- package/dist/chunks/{chunk-ERCMNTEQ.js → chunk-GAY2PLS6.js} +1 -1
- package/dist/chunks/{chunk-ERCMNTEQ.js.map → chunk-GAY2PLS6.js.map} +1 -1
- package/dist/chunks/{chunk-TSHN6J7U.js → chunk-JDQUBCHM.js} +1 -1
- package/dist/chunks/{chunk-TSHN6J7U.js.map → chunk-JDQUBCHM.js.map} +1 -1
- package/dist/chunks/{chunk-IBBQUV2I.js → chunk-KSCGTF4R.js} +1 -1
- package/dist/chunks/{chunk-IBBQUV2I.js.map → chunk-KSCGTF4R.js.map} +1 -1
- package/dist/chunks/{chunk-6YFCSBEE.js → chunk-NLAW2OYS.js} +2 -2
- package/dist/chunks/{chunk-6YFCSBEE.js.map → chunk-NLAW2OYS.js.map} +1 -1
- package/dist/chunks/chunk-QQKPT6EL.js +2 -0
- package/dist/chunks/{chunk-XVNCZQMR.js.map → chunk-QQKPT6EL.js.map} +1 -1
- package/dist/chunks/{chunk-WAPIOD25.js → chunk-WMDOEOAH.js} +5 -5
- package/dist/chunks/{chunk-WAPIOD25.js.map → chunk-WMDOEOAH.js.map} +1 -1
- package/dist/chunks/chunk-XTQQNBQ5.js +2 -0
- package/dist/chunks/{chunk-6EOYBBT4.js.map → chunk-XTQQNBQ5.js.map} +1 -1
- package/dist/chunks/chunk-XXCNVFGP.js +11 -0
- package/dist/chunks/chunk-XXCNVFGP.js.map +7 -0
- package/dist/chunks/{chunk-OVAXNTPN.js → chunk-ZQSWX57C.js} +1 -1
- package/dist/chunks/{chunk-OVAXNTPN.js.map → chunk-ZQSWX57C.js.map} +1 -1
- package/dist/chunks/dist-GZKCGB2N.js +2 -0
- package/dist/chunks/model-7RBDANXP.js +12 -0
- package/dist/chunks/model-7RBDANXP.js.map +7 -0
- package/dist/chunks/{renderToSvg-53FEV6IJ.js → renderToSvg-5DMIQ72S.js} +2 -2
- package/dist/jbrowse-plugin-msaview.esm.js +2 -2
- package/dist/jbrowse-plugin-msaview.esm.js.map +3 -3
- package/dist/utils/domainCache.js +16 -2
- package/dist/utils/ncbiDomains.js +15 -25
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -17
- package/src/LaunchMsaViewExtensionPoint/index.ts +5 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +11 -0
- package/src/MsaViewPanel/connectedDomains.test.ts +153 -0
- package/src/MsaViewPanel/connectedDomains.ts +107 -0
- package/src/MsaViewPanel/domainTrack.test.ts +73 -0
- package/src/MsaViewPanel/domainTrack.ts +79 -0
- package/src/MsaViewPanel/model.test.ts +92 -0
- package/src/MsaViewPanel/model.ts +90 -1
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +37 -25
- package/src/utils/domainCache.ts +19 -2
- package/src/utils/ncbiDomains.ts +15 -26
- package/src/version.ts +1 -1
- package/dist/chunks/AnnotationFileDialog-S3N5EHHE.js +0 -2
- package/dist/chunks/MetadataDialog-MNSI6NEW.js +0 -2
- package/dist/chunks/MsaViewPanel-R2WAER5O.js +0 -2
- package/dist/chunks/TreeNodeInfoDialog-T5XMPG3R.js +0 -2
- package/dist/chunks/chunk-6EOYBBT4.js +0 -2
- package/dist/chunks/chunk-A63PUWQZ.js +0 -6
- package/dist/chunks/chunk-A63PUWQZ.js.map +0 -7
- package/dist/chunks/chunk-CSCUNTGJ.js.map +0 -7
- package/dist/chunks/chunk-DEFUNIWW.js +0 -8
- package/dist/chunks/chunk-DEFUNIWW.js.map +0 -7
- package/dist/chunks/chunk-K55ZVN73.js +0 -5
- package/dist/chunks/chunk-K55ZVN73.js.map +0 -7
- package/dist/chunks/chunk-XVNCZQMR.js +0 -2
- package/dist/chunks/dist-N4ZJK63Q.js +0 -2
- package/dist/chunks/model-TCPB6WNJ.js +0 -12
- package/dist/chunks/model-TCPB6WNJ.js.map +0 -7
- /package/dist/chunks/{dist-N4ZJK63Q.js.map → dist-GZKCGB2N.js.map} +0 -0
- /package/dist/chunks/{renderToSvg-53FEV6IJ.js.map → renderToSvg-5DMIQ72S.js.map} +0 -0
|
@@ -57,3 +57,4 @@ export declare function syncGenomeHoverToMsaColumn(self: JBrowsePluginMsaViewMod
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function observeProteinHighlights(self: JBrowsePluginMsaViewModel): () => void;
|
|
59
59
|
export declare function runCleanup(): void;
|
|
60
|
+
export declare function showDomainsOnGenomeIfAsked(self: JBrowsePluginMsaViewModel): void;
|
|
@@ -390,3 +390,11 @@ export function runCleanup() {
|
|
|
390
390
|
console.error('Failed to cleanup old MSA data:', e);
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
|
+
export function showDomainsOnGenomeIfAsked(self) {
|
|
394
|
+
if (self.domainsOnGenome &&
|
|
395
|
+
self.connectedView &&
|
|
396
|
+
self.connectedDomains.length > 0) {
|
|
397
|
+
self.showDomainsOnGenome();
|
|
398
|
+
self.setDomainsOnGenome(false);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { GenomeRegion, SeqPosModel } from './msaCoordToGenomeCoord';
|
|
2
|
+
import type { Annotation } from 'react-msaview';
|
|
3
|
+
/**
|
|
4
|
+
* One accession the alignment draws on the query row, on the genome. A
|
|
5
|
+
* cross-plugin contract: jbrowse-plugin-protein3d reads it off the view.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConnectedDomain {
|
|
8
|
+
/** the label the alignment's legend shows for the domain's accession */
|
|
9
|
+
name: string;
|
|
10
|
+
accession: string;
|
|
11
|
+
/** the fill the alignment draws the domain in */
|
|
12
|
+
color: string;
|
|
13
|
+
/**
|
|
14
|
+
* 0-based half-open, ascending, contiguous bases merged: one region per exon
|
|
15
|
+
* piece of a domain, or per run of a site's scattered residues, in the
|
|
16
|
+
* transcript's refName spelling
|
|
17
|
+
*/
|
|
18
|
+
regions: GenomeRegion[];
|
|
19
|
+
}
|
|
20
|
+
export interface DomainSource extends SeqPosModel {
|
|
21
|
+
showDomains: boolean;
|
|
22
|
+
querySeqName: string;
|
|
23
|
+
annotationsByRow: Record<string, Annotation[]>;
|
|
24
|
+
annotationTypes: Map<string, Annotation>;
|
|
25
|
+
featureColors: Map<Annotation, {
|
|
26
|
+
fill: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The genome regions under the query row's residues `positions`, 0-based
|
|
31
|
+
* ungapped.
|
|
32
|
+
*/
|
|
33
|
+
export declare function residuesToGenomeRegions(model: SeqPosModel, positions: Iterable<number>): GenomeRegion[];
|
|
34
|
+
/**
|
|
35
|
+
* The query row's domains as the alignment shows them, one per accession, so
|
|
36
|
+
* a CDD site's scattered residues are one entry. Largest first by residues
|
|
37
|
+
* covered, so a site painted later lands on the domain it sits in. Empty
|
|
38
|
+
* while the domain overlay is off.
|
|
39
|
+
*/
|
|
40
|
+
export declare function connectedDomains(model: DomainSource): ConnectedDomain[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { seqPosToGenomeRegions } from './msaCoordToGenomeCoord';
|
|
2
|
+
function mergeAdjacent(regions) {
|
|
3
|
+
const merged = [];
|
|
4
|
+
for (const region of regions.toSorted((a, b) => a.start - b.start)) {
|
|
5
|
+
const last = merged.at(-1);
|
|
6
|
+
if (last && last.refName === region.refName && region.start <= last.end) {
|
|
7
|
+
last.end = Math.max(last.end, region.end);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
merged.push({ ...region });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The genome regions under the query row's residues `positions`, 0-based
|
|
17
|
+
* ungapped.
|
|
18
|
+
*/
|
|
19
|
+
export function residuesToGenomeRegions(model, positions) {
|
|
20
|
+
const pieces = [];
|
|
21
|
+
for (const pos of positions) {
|
|
22
|
+
pieces.push(...seqPosToGenomeRegions(model, pos));
|
|
23
|
+
}
|
|
24
|
+
return mergeAdjacent(pieces);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The query row's domains as the alignment shows them, one per accession, so
|
|
28
|
+
* a CDD site's scattered residues are one entry. Largest first by residues
|
|
29
|
+
* covered, so a site painted later lands on the domain it sits in. Empty
|
|
30
|
+
* while the domain overlay is off.
|
|
31
|
+
*/
|
|
32
|
+
export function connectedDomains(model) {
|
|
33
|
+
if (!model.showDomains) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const byAccession = new Map();
|
|
37
|
+
for (const annotation of model.annotationsByRow[model.querySeqName] ?? []) {
|
|
38
|
+
const list = byAccession.get(annotation.accession);
|
|
39
|
+
if (list) {
|
|
40
|
+
list.push(annotation);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
byAccession.set(annotation.accession, [annotation]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...byAccession]
|
|
47
|
+
.flatMap(([accession, annotations]) => {
|
|
48
|
+
// annotations count residues 1-based inclusive
|
|
49
|
+
const positions = new Set();
|
|
50
|
+
for (const { start, end } of annotations) {
|
|
51
|
+
for (let pos = start - 1; pos < end; pos++) {
|
|
52
|
+
positions.add(pos);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const first = annotations[0];
|
|
56
|
+
const color = model.featureColors.get(first)?.fill;
|
|
57
|
+
const regions = residuesToGenomeRegions(model, positions);
|
|
58
|
+
return color && regions.length > 0
|
|
59
|
+
? [
|
|
60
|
+
{
|
|
61
|
+
domain: {
|
|
62
|
+
name: model.annotationTypes.get(accession)?.name ?? first.name,
|
|
63
|
+
accession,
|
|
64
|
+
color,
|
|
65
|
+
regions,
|
|
66
|
+
},
|
|
67
|
+
residues: positions.size,
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
: [];
|
|
71
|
+
})
|
|
72
|
+
.toSorted((a, b) => b.residues - a.residues)
|
|
73
|
+
.map(({ domain }) => domain);
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { connectedDomains } from './connectedDomains';
|
|
4
|
+
// five codons read from 207 down: 207-205, 204-202, 201-200 then 106 across
|
|
5
|
+
// the intron, 105-103, 102-100
|
|
6
|
+
const reverseTranscript = genomeToTranscriptSeqMapping({
|
|
7
|
+
refName: 'chr1',
|
|
8
|
+
strand: -1,
|
|
9
|
+
start: 100,
|
|
10
|
+
end: 208,
|
|
11
|
+
subfeatures: [
|
|
12
|
+
{ refName: 'chr1', type: 'CDS', start: 100, end: 107, phase: 0 },
|
|
13
|
+
{ refName: 'chr1', type: 'CDS', start: 200, end: 208, phase: 0 },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
function annotation(accession, start, end, id = 'QUERY') {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
accession,
|
|
20
|
+
name: `name ${accession}`,
|
|
21
|
+
description: '',
|
|
22
|
+
start,
|
|
23
|
+
end,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function source(annotations, overrides = {}) {
|
|
27
|
+
return {
|
|
28
|
+
showDomains: true,
|
|
29
|
+
querySeqName: 'QUERY',
|
|
30
|
+
transcriptToMsaMap: reverseTranscript,
|
|
31
|
+
annotationsByRow: Object.groupBy(annotations, a => a.id),
|
|
32
|
+
annotationTypes: new Map(annotations.map(a => [
|
|
33
|
+
a.accession,
|
|
34
|
+
{ ...a, name: `legend ${a.accession}` },
|
|
35
|
+
])),
|
|
36
|
+
featureColors: new Map(annotations.map(a => [a, { fill: `fill-${a.accession}` }])),
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
describe('connectedDomains on a reverse-strand transcript', () => {
|
|
41
|
+
test('a domain crossing the exon boundary is one region per exon piece', () => {
|
|
42
|
+
expect(connectedDomains(source([annotation('site', 2, 4)]))).toEqual([
|
|
43
|
+
{
|
|
44
|
+
name: 'legend site',
|
|
45
|
+
accession: 'site',
|
|
46
|
+
color: 'fill-site',
|
|
47
|
+
regions: [
|
|
48
|
+
{ refName: 'chr1', start: 103, end: 107 },
|
|
49
|
+
{ refName: 'chr1', start: 200, end: 205 },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
test('the longest comes first, so a nested site paints over it', () => {
|
|
55
|
+
const domains = connectedDomains(source([annotation('site', 2, 4), annotation('domain', 1, 5)]));
|
|
56
|
+
expect(domains.map(d => d.accession)).toEqual(['domain', 'site']);
|
|
57
|
+
expect(domains[0].regions).toEqual([
|
|
58
|
+
{ refName: 'chr1', start: 100, end: 107 },
|
|
59
|
+
{ refName: 'chr1', start: 200, end: 208 },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
test('a scattered site is one entry, its residues merged where they touch', () => {
|
|
63
|
+
const domains = connectedDomains(source([
|
|
64
|
+
annotation('small', 4, 5),
|
|
65
|
+
annotation('heme', 1, 1),
|
|
66
|
+
annotation('heme', 3, 3),
|
|
67
|
+
annotation('heme', 4, 4),
|
|
68
|
+
]));
|
|
69
|
+
expect(domains.map(d => d.accession)).toEqual(['heme', 'small']);
|
|
70
|
+
expect(domains[0]).toEqual({
|
|
71
|
+
name: 'legend heme',
|
|
72
|
+
accession: 'heme',
|
|
73
|
+
color: 'fill-heme',
|
|
74
|
+
regions: [
|
|
75
|
+
{ refName: 'chr1', start: 103, end: 107 },
|
|
76
|
+
{ refName: 'chr1', start: 200, end: 202 },
|
|
77
|
+
{ refName: 'chr1', start: 205, end: 208 },
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test('a trimmed query row shifts by querySeqOffset', () => {
|
|
82
|
+
const [domain] = connectedDomains(source([annotation('site', 1, 2)], { querySeqOffset: 1 }));
|
|
83
|
+
expect(domain.regions).toEqual([
|
|
84
|
+
{ refName: 'chr1', start: 106, end: 107 },
|
|
85
|
+
{ refName: 'chr1', start: 200, end: 205 },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
test('residues past the transcript drop out, and a domain with none left', () => {
|
|
89
|
+
expect(connectedDomains(source([annotation('tail', 5, 9), annotation('off', 7, 9)]))).toEqual([
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
accession: 'tail',
|
|
92
|
+
regions: [{ refName: 'chr1', start: 100, end: 103 }],
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
test('only the query row, and nothing while the overlay is off', () => {
|
|
97
|
+
const annotations = [annotation('other', 1, 5, 'Mouse')];
|
|
98
|
+
expect(connectedDomains(source(annotations))).toEqual([]);
|
|
99
|
+
expect(connectedDomains(source([annotation('domain', 1, 5)], { showDomains: false }))).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
test('a MAF row maps one base per residue', () => {
|
|
103
|
+
const [domain] = connectedDomains(source([annotation('d', 3, 6)], {
|
|
104
|
+
transcriptToMsaMap: undefined,
|
|
105
|
+
mafRegion: { refName: 'chr2', start: 50, end: 100, assemblyName: 'hg38' },
|
|
106
|
+
}));
|
|
107
|
+
expect(domain.regions).toEqual([{ refName: 'chr2', start: 52, end: 56 }]);
|
|
108
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ConnectedDomain } from './connectedDomains';
|
|
2
|
+
export declare function domainTrackId(viewId: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* A session FeatureTrack drawing `domains` on the genome: one feature per
|
|
5
|
+
* domain, one subfeature per region, so a domain spanning exons draws as boxes
|
|
6
|
+
* joined across the intron. Each feature paints the `color` it carries, and
|
|
7
|
+
* the key names each color with its domain.
|
|
8
|
+
*/
|
|
9
|
+
export declare function domainTrackConf({ viewId, assemblyName, name, strand, domains, }: {
|
|
10
|
+
viewId: string;
|
|
11
|
+
assemblyName: string;
|
|
12
|
+
name: string;
|
|
13
|
+
strand?: number;
|
|
14
|
+
domains: ConnectedDomain[];
|
|
15
|
+
}): {
|
|
16
|
+
type: string;
|
|
17
|
+
trackId: string;
|
|
18
|
+
name: string;
|
|
19
|
+
assemblyNames: string[];
|
|
20
|
+
adapter: {
|
|
21
|
+
type: string;
|
|
22
|
+
features: ({
|
|
23
|
+
type: string;
|
|
24
|
+
name: string;
|
|
25
|
+
accession: string;
|
|
26
|
+
color: string;
|
|
27
|
+
subfeatures: ({
|
|
28
|
+
type: string;
|
|
29
|
+
strand?: undefined;
|
|
30
|
+
uniqueId: string;
|
|
31
|
+
refName: string;
|
|
32
|
+
start: number;
|
|
33
|
+
end: number;
|
|
34
|
+
} | {
|
|
35
|
+
type: string;
|
|
36
|
+
strand: number;
|
|
37
|
+
uniqueId: string;
|
|
38
|
+
refName: string;
|
|
39
|
+
start: number;
|
|
40
|
+
end: number;
|
|
41
|
+
})[];
|
|
42
|
+
strand?: undefined;
|
|
43
|
+
uniqueId: string;
|
|
44
|
+
refName: string;
|
|
45
|
+
start: number;
|
|
46
|
+
end: number;
|
|
47
|
+
} | {
|
|
48
|
+
type: string;
|
|
49
|
+
name: string;
|
|
50
|
+
accession: string;
|
|
51
|
+
color: string;
|
|
52
|
+
subfeatures: ({
|
|
53
|
+
type: string;
|
|
54
|
+
strand?: undefined;
|
|
55
|
+
uniqueId: string;
|
|
56
|
+
refName: string;
|
|
57
|
+
start: number;
|
|
58
|
+
end: number;
|
|
59
|
+
} | {
|
|
60
|
+
type: string;
|
|
61
|
+
strand: number;
|
|
62
|
+
uniqueId: string;
|
|
63
|
+
refName: string;
|
|
64
|
+
start: number;
|
|
65
|
+
end: number;
|
|
66
|
+
})[];
|
|
67
|
+
strand: number;
|
|
68
|
+
uniqueId: string;
|
|
69
|
+
refName: string;
|
|
70
|
+
start: number;
|
|
71
|
+
end: number;
|
|
72
|
+
})[];
|
|
73
|
+
};
|
|
74
|
+
displays: {
|
|
75
|
+
type: string;
|
|
76
|
+
displayId: string;
|
|
77
|
+
color: {
|
|
78
|
+
field: string;
|
|
79
|
+
domain: string[];
|
|
80
|
+
range: string[];
|
|
81
|
+
labels: string[];
|
|
82
|
+
title: string;
|
|
83
|
+
};
|
|
84
|
+
}[];
|
|
85
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function domainTrackId(viewId) {
|
|
2
|
+
return `msaview-domains-${viewId}`;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* A session FeatureTrack drawing `domains` on the genome: one feature per
|
|
6
|
+
* domain, one subfeature per region, so a domain spanning exons draws as boxes
|
|
7
|
+
* joined across the intron. Each feature paints the `color` it carries, and
|
|
8
|
+
* the key names each color with its domain.
|
|
9
|
+
*/
|
|
10
|
+
export function domainTrackConf({ viewId, assemblyName, name, strand, domains, }) {
|
|
11
|
+
const trackId = domainTrackId(viewId);
|
|
12
|
+
const labelOf = new Map();
|
|
13
|
+
for (const { color, name } of domains) {
|
|
14
|
+
if (!labelOf.has(color)) {
|
|
15
|
+
labelOf.set(color, name);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const colors = [...labelOf.keys()];
|
|
19
|
+
const stranded = strand === undefined ? {} : { strand };
|
|
20
|
+
return {
|
|
21
|
+
type: 'FeatureTrack',
|
|
22
|
+
trackId,
|
|
23
|
+
name,
|
|
24
|
+
assemblyNames: [assemblyName],
|
|
25
|
+
adapter: {
|
|
26
|
+
type: 'FromConfigAdapter',
|
|
27
|
+
features: domains.map(({ name, accession, color, regions }, i) => {
|
|
28
|
+
const uniqueId = `${trackId}-${i}`;
|
|
29
|
+
return {
|
|
30
|
+
uniqueId,
|
|
31
|
+
refName: regions[0].refName,
|
|
32
|
+
start: Math.min(...regions.map(r => r.start)),
|
|
33
|
+
end: Math.max(...regions.map(r => r.end)),
|
|
34
|
+
...stranded,
|
|
35
|
+
type: 'protein_domain',
|
|
36
|
+
name,
|
|
37
|
+
accession,
|
|
38
|
+
color,
|
|
39
|
+
subfeatures: regions.map((r, j) => ({
|
|
40
|
+
uniqueId: `${uniqueId}-${j}`,
|
|
41
|
+
refName: r.refName,
|
|
42
|
+
start: r.start,
|
|
43
|
+
end: r.end,
|
|
44
|
+
...stranded,
|
|
45
|
+
type: 'protein_domain_part',
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
displays: [
|
|
51
|
+
{
|
|
52
|
+
type: 'LinearBasicDisplay',
|
|
53
|
+
displayId: `${trackId}-LinearBasicDisplay`,
|
|
54
|
+
color: {
|
|
55
|
+
field: 'color',
|
|
56
|
+
domain: colors,
|
|
57
|
+
range: colors,
|
|
58
|
+
labels: colors.map(c => labelOf.get(c)),
|
|
59
|
+
title: 'Domains',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { domainTrackConf } from './domainTrack';
|
|
3
|
+
const domains = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Hb beta-like',
|
|
6
|
+
accession: 'cd1',
|
|
7
|
+
color: '#1b9e77',
|
|
8
|
+
regions: [
|
|
9
|
+
{ refName: 'chr11', start: 100, end: 107 },
|
|
10
|
+
{ refName: 'chr11', start: 200, end: 208 },
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'heme binding site',
|
|
15
|
+
accession: 'cd2',
|
|
16
|
+
color: '#d95f02',
|
|
17
|
+
regions: [{ refName: 'chr11', start: 103, end: 107 }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Hb beta-like',
|
|
21
|
+
accession: 'cd1',
|
|
22
|
+
color: '#1b9e77',
|
|
23
|
+
regions: [{ refName: 'chr11', start: 300, end: 330 }],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const conf = domainTrackConf({
|
|
27
|
+
viewId: 'msa1',
|
|
28
|
+
assemblyName: 'hg38',
|
|
29
|
+
name: 'HBB domains (from the alignment)',
|
|
30
|
+
strand: -1,
|
|
31
|
+
domains,
|
|
32
|
+
});
|
|
33
|
+
test('one feature per domain, one subfeature per region, spanning them all', () => {
|
|
34
|
+
const [first] = conf.adapter.features;
|
|
35
|
+
expect(conf.trackId).toBe('msaview-domains-msa1');
|
|
36
|
+
expect(conf.adapter.features).toHaveLength(3);
|
|
37
|
+
expect(first).toMatchObject({
|
|
38
|
+
refName: 'chr11',
|
|
39
|
+
start: 100,
|
|
40
|
+
end: 208,
|
|
41
|
+
strand: -1,
|
|
42
|
+
name: 'Hb beta-like',
|
|
43
|
+
color: '#1b9e77',
|
|
44
|
+
});
|
|
45
|
+
expect(first.subfeatures.map(s => [s.start, s.end])).toEqual([
|
|
46
|
+
[100, 107],
|
|
47
|
+
[200, 208],
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
test('the display paints each feature its own color and names it in the key', () => {
|
|
51
|
+
expect(conf.displays[0].color).toEqual({
|
|
52
|
+
field: 'color',
|
|
53
|
+
domain: ['#1b9e77', '#d95f02'],
|
|
54
|
+
range: ['#1b9e77', '#d95f02'],
|
|
55
|
+
labels: ['Hb beta-like', 'heme binding site'],
|
|
56
|
+
title: 'Domains',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
test('a MAF view knows no strand, and the features carry none', () => {
|
|
60
|
+
const unstranded = domainTrackConf({
|
|
61
|
+
viewId: 'msa1',
|
|
62
|
+
assemblyName: 'hg38',
|
|
63
|
+
name: 'x',
|
|
64
|
+
domains,
|
|
65
|
+
});
|
|
66
|
+
expect('strand' in unstranded.adapter.features[0]).toBe(false);
|
|
67
|
+
});
|