jbrowse-plugin-msaview 5.11.0 → 5.12.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/model.js +2 -0
- package/dist/MsaViewPanel/sessionHighlights.d.ts +57 -0
- package/dist/MsaViewPanel/sessionHighlights.js +102 -0
- package/dist/MsaViewPanel/sessionHighlights.test.d.ts +1 -0
- package/dist/MsaViewPanel/sessionHighlights.test.js +60 -0
- package/dist/chunks/{model-FB6WLJGL.js → model-UTGDKBLI.js} +6 -6
- package/dist/chunks/model-UTGDKBLI.js.map +7 -0
- package/dist/jbrowse-plugin-msaview.esm.js +1 -1
- package/dist/jbrowse-plugin-msaview.esm.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/MsaViewPanel/model.ts +2 -0
- package/src/MsaViewPanel/sessionHighlights.test.ts +81 -0
- package/src/MsaViewPanel/sessionHighlights.ts +172 -0
- package/src/version.ts +1 -1
- package/dist/chunks/model-FB6WLJGL.js.map +0 -7
|
@@ -8,6 +8,7 @@ import { connectedDomains } from './connectedDomains';
|
|
|
8
8
|
import { domainTrackConf } from './domainTrack';
|
|
9
9
|
import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
|
|
10
10
|
import { resolveConnectedTranscriptIfNeeded } from './resolveConnectedTranscript';
|
|
11
|
+
import { syncSessionHighlights } from './sessionHighlights';
|
|
11
12
|
import { transcriptMap } from './transcriptMap';
|
|
12
13
|
/**
|
|
13
14
|
* Where the homolog set comes from. NCBI's ortholog sets cover vertebrates and
|
|
@@ -576,6 +577,7 @@ export default function stateModelFactory() {
|
|
|
576
577
|
// so they're factories returning the autorun body rather than plain fns
|
|
577
578
|
addDisposer(self, autorun(syncGenomeHoverToMsaColumn(self)));
|
|
578
579
|
addDisposer(self, autorun(observeProteinHighlights(self)));
|
|
580
|
+
addDisposer(self, autorun(syncSessionHighlights(self)));
|
|
579
581
|
},
|
|
580
582
|
}));
|
|
581
583
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
+
import type { TranscriptMap } from './transcriptMap';
|
|
3
|
+
/** The `transientHighlights` owner the session's highlights draw under */
|
|
4
|
+
export declare const SESSION_HIGHLIGHT_OWNER = "session-highlights";
|
|
5
|
+
/** A session highlight: a genome region, 0-based half-open */
|
|
6
|
+
export interface GenomeHighlight {
|
|
7
|
+
assemblyName: string;
|
|
8
|
+
refName: string;
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
label?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
/** react-msaview's residue-span highlight: residues of `row`, 1-based */
|
|
15
|
+
export interface RowHighlight {
|
|
16
|
+
row: string;
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
label?: string;
|
|
20
|
+
color?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where the query row sits on the genome: through a transcript, each coding
|
|
24
|
+
* base mapping to its residue, or as the bases of a MAF block
|
|
25
|
+
*/
|
|
26
|
+
export type QueryOnGenome = {
|
|
27
|
+
kind: 'transcript';
|
|
28
|
+
map: Pick<TranscriptMap, 'g2p' | 'refName'>;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'maf';
|
|
31
|
+
refName: string;
|
|
32
|
+
start: number;
|
|
33
|
+
end: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The session's highlights on the query row, one residue span per run of
|
|
37
|
+
* consecutive residues a highlight covers, each keeping the highlight's label
|
|
38
|
+
* and color. A highlight on another assembly or chromosome, or off the
|
|
39
|
+
* transcript, draws nothing here.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sessionHighlightsOnQueryRow({ highlights, onGenome, querySeqName, querySeqOffset, sameAssembly, sameRefName, labels, }: {
|
|
42
|
+
highlights: readonly GenomeHighlight[];
|
|
43
|
+
onGenome: QueryOnGenome;
|
|
44
|
+
querySeqName: string;
|
|
45
|
+
querySeqOffset: number;
|
|
46
|
+
sameAssembly: (assemblyName: string) => boolean;
|
|
47
|
+
sameRefName: (refName: string) => boolean;
|
|
48
|
+
/** whether the session writes labels on its highlights */
|
|
49
|
+
labels: boolean;
|
|
50
|
+
}): RowHighlight[];
|
|
51
|
+
/**
|
|
52
|
+
* Draw the session's highlights on the query row as react-msaview transient
|
|
53
|
+
* highlights, so a codon highlighted on the genome lights and labels its
|
|
54
|
+
* alignment column. They are derived, so the alignment's own snapshot never
|
|
55
|
+
* holds them.
|
|
56
|
+
*/
|
|
57
|
+
export declare function syncSessionHighlights(self: JBrowsePluginMsaViewModel): () => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { hasQueryRow } from './util';
|
|
3
|
+
/** The `transientHighlights` owner the session's highlights draw under */
|
|
4
|
+
export const SESSION_HIGHLIGHT_OWNER = 'session-highlights';
|
|
5
|
+
/**
|
|
6
|
+
* The 0-based query-row positions a genome region covers. A transcript's
|
|
7
|
+
* residues count from its start codon, so `querySeqOffset` moves them onto a
|
|
8
|
+
* query row that holds only part of the protein.
|
|
9
|
+
*/
|
|
10
|
+
function rowPositions({ start, end }, onGenome, querySeqOffset) {
|
|
11
|
+
const positions = new Set();
|
|
12
|
+
if (onGenome.kind === 'maf') {
|
|
13
|
+
for (let base = Math.max(start, onGenome.start); base < Math.min(end, onGenome.end); base++) {
|
|
14
|
+
positions.add(base - onGenome.start);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (let base = start; base < end; base++) {
|
|
19
|
+
const residue = onGenome.map.g2p[base];
|
|
20
|
+
if (residue !== undefined) {
|
|
21
|
+
positions.add(residue - querySeqOffset);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...positions].filter(p => p >= 0).sort((a, b) => a - b);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The session's highlights on the query row, one residue span per run of
|
|
29
|
+
* consecutive residues a highlight covers, each keeping the highlight's label
|
|
30
|
+
* and color. A highlight on another assembly or chromosome, or off the
|
|
31
|
+
* transcript, draws nothing here.
|
|
32
|
+
*/
|
|
33
|
+
export function sessionHighlightsOnQueryRow({ highlights, onGenome, querySeqName, querySeqOffset, sameAssembly, sameRefName, labels, }) {
|
|
34
|
+
return highlights.flatMap(h => {
|
|
35
|
+
if (!sameAssembly(h.assemblyName) || !sameRefName(h.refName)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const runs = [];
|
|
39
|
+
for (const pos of rowPositions(h, onGenome, querySeqOffset)) {
|
|
40
|
+
const last = runs.at(-1);
|
|
41
|
+
if (last?.end === pos) {
|
|
42
|
+
last.end = pos + 1;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
runs.push({
|
|
46
|
+
row: querySeqName,
|
|
47
|
+
start: pos + 1,
|
|
48
|
+
end: pos + 1,
|
|
49
|
+
...(labels && h.label ? { label: h.label } : {}),
|
|
50
|
+
...(h.color ? { color: h.color } : {}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return runs;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// `highlightLabelsVisible` arrived in core after the oldest host this plugin
|
|
58
|
+
// supports, which writes every label
|
|
59
|
+
function labelsVisible(session) {
|
|
60
|
+
return !('highlightLabelsVisible' in session &&
|
|
61
|
+
session.highlightLabelsVisible === false);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Draw the session's highlights on the query row as react-msaview transient
|
|
65
|
+
* highlights, so a codon highlighted on the genome lights and labels its
|
|
66
|
+
* alignment column. They are derived, so the alignment's own snapshot never
|
|
67
|
+
* holds them.
|
|
68
|
+
*/
|
|
69
|
+
export function syncSessionHighlights(self) {
|
|
70
|
+
return () => {
|
|
71
|
+
const session = getSession(self);
|
|
72
|
+
const { connectedView, transcriptToMsaMap, mafRegion } = self;
|
|
73
|
+
const assemblyName = mafRegion?.assemblyName ?? connectedView?.assemblyNames[0];
|
|
74
|
+
const { assemblyManager } = session;
|
|
75
|
+
const assembly = assemblyName
|
|
76
|
+
? assemblyManager.get(assemblyName)
|
|
77
|
+
: undefined;
|
|
78
|
+
const onGenome = mafRegion
|
|
79
|
+
? { kind: 'maf', ...mafRegion }
|
|
80
|
+
: transcriptToMsaMap
|
|
81
|
+
? { kind: 'transcript', map: transcriptToMsaMap }
|
|
82
|
+
: undefined;
|
|
83
|
+
if (!session.highlightsVisible ||
|
|
84
|
+
!session.highlights.length ||
|
|
85
|
+
!onGenome ||
|
|
86
|
+
!assembly?.initialized ||
|
|
87
|
+
!hasQueryRow(self)) {
|
|
88
|
+
self.clearHighlight(SESSION_HIGHLIGHT_OWNER);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const ownRefName = assembly.getCanonicalRefName(onGenome.kind === 'maf' ? onGenome.refName : onGenome.map.refName);
|
|
92
|
+
self.applyHighlight(SESSION_HIGHLIGHT_OWNER, sessionHighlightsOnQueryRow({
|
|
93
|
+
highlights: session.highlights,
|
|
94
|
+
onGenome,
|
|
95
|
+
querySeqName: self.querySeqName,
|
|
96
|
+
querySeqOffset: self.querySeqOffset,
|
|
97
|
+
sameAssembly: name => assemblyManager.get(name) === assembly,
|
|
98
|
+
sameRefName: refName => assembly.getCanonicalRefName(refName) === ownRefName,
|
|
99
|
+
labels: labelsVisible(session),
|
|
100
|
+
}));
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { sessionHighlightsOnQueryRow } from './sessionHighlights';
|
|
3
|
+
// minus strand: residue 0 on bases 109..107, residue 1 on 106..104, residue 2
|
|
4
|
+
// on 103..101
|
|
5
|
+
const g2p = {};
|
|
6
|
+
for (let base = 101; base < 110; base++) {
|
|
7
|
+
g2p[base] = Math.floor((109 - base) / 3);
|
|
8
|
+
}
|
|
9
|
+
const transcript = {
|
|
10
|
+
kind: 'transcript',
|
|
11
|
+
map: { g2p, refName: 'chr12' },
|
|
12
|
+
};
|
|
13
|
+
function onRow(highlights, { onGenome = transcript, querySeqOffset = 0, labels = true } = {}) {
|
|
14
|
+
return sessionHighlightsOnQueryRow({
|
|
15
|
+
highlights,
|
|
16
|
+
onGenome,
|
|
17
|
+
querySeqName: 'human_query',
|
|
18
|
+
querySeqOffset,
|
|
19
|
+
sameAssembly: name => name === 'hg38',
|
|
20
|
+
sameRefName: refName => refName.replace(/^chr/, '') === '12',
|
|
21
|
+
labels,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const codon = (start, label) => ({
|
|
25
|
+
assemblyName: 'hg38',
|
|
26
|
+
refName: 'chr12',
|
|
27
|
+
start,
|
|
28
|
+
end: start + 3,
|
|
29
|
+
label,
|
|
30
|
+
});
|
|
31
|
+
test('a codon labels the residue it codes, 1-based on the query row', () => {
|
|
32
|
+
expect(onRow([codon(104, 'G12')])).toEqual([
|
|
33
|
+
{ row: 'human_query', start: 2, end: 2, label: 'G12' },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
test('a query row holding part of the protein shifts by its offset', () => {
|
|
37
|
+
expect(onRow([codon(101)], { querySeqOffset: 1 })).toEqual([
|
|
38
|
+
{ row: 'human_query', start: 2, end: 2 },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
test('a region over several residues is one span per consecutive run', () => {
|
|
42
|
+
expect(onRow([{ assemblyName: 'hg38', refName: '12', start: 101, end: 110 }])).toEqual([{ row: 'human_query', start: 1, end: 3 }]);
|
|
43
|
+
});
|
|
44
|
+
test('a highlight on another chromosome, assembly or off the CDS draws nothing', () => {
|
|
45
|
+
expect(onRow([
|
|
46
|
+
{ ...codon(104), refName: 'chr7' },
|
|
47
|
+
{ ...codon(104), assemblyName: 'mm39' },
|
|
48
|
+
codon(200),
|
|
49
|
+
])).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
test('a MAF query row counts bases from the block start', () => {
|
|
52
|
+
expect(onRow([codon(1005, 'site')], {
|
|
53
|
+
onGenome: { kind: 'maf', refName: 'chr12', start: 1000, end: 1010 },
|
|
54
|
+
})).toEqual([{ row: 'human_query', start: 6, end: 8, label: 'site' }]);
|
|
55
|
+
});
|
|
56
|
+
test('labels turned off in the session keep the span and drop its text', () => {
|
|
57
|
+
expect(onRow([codon(104, 'G12')], { labels: false })).toEqual([
|
|
58
|
+
{ row: 'human_query', start: 2, end: 2 },
|
|
59
|
+
]);
|
|
60
|
+
});
|