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.
@@ -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
+ });