jbrowse-plugin-protein3d 0.6.0 → 0.7.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/AddHighlightModel/GenomeTo1DProteinHoverHighlight.js +5 -2
- package/dist/AddHighlightModel/ProteinToMsaHoverSync.js +4 -0
- package/dist/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.d.ts +17 -17
- package/dist/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.js +18 -37
- package/dist/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.d.ts +16 -21
- package/dist/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.js +21 -60
- package/dist/BaseProteinAnnotationAdapter.d.ts +29 -0
- package/dist/BaseProteinAnnotationAdapter.js +41 -0
- package/dist/LaunchProteinView/components/FoldseekSearch.js +11 -0
- package/dist/LaunchProteinView/hooks/useUniProtSearch.js +1 -0
- package/dist/LaunchProteinView/services/lookupMethods.js +1 -1
- package/dist/LaunchProteinView/utils/launchViewUtils.js +6 -6
- package/dist/LaunchProteinViewExtensionPoint/index.js +4 -5
- package/dist/Protein1DViewRegistry/index.d.ts +6 -1
- package/dist/Protein1DViewRegistry/index.js +16 -23
- package/dist/ProteinView/components/AddStructureDialog.js +1 -1
- package/dist/ProteinView/components/FeatureBar.js +14 -24
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +12 -0
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +8 -0
- package/dist/ProteinView/model.d.ts +12 -38
- package/dist/ProteinView/model.js +31 -82
- package/dist/ProteinView/proteinViewSpec.d.ts +72 -0
- package/dist/ProteinView/proteinViewSpec.js +16 -0
- package/dist/ProteinView/structureModel.d.ts +3 -1
- package/dist/ProteinView/structureModel.js +28 -9
- package/dist/ProteinView/structureSuperposer.d.ts +30 -0
- package/dist/ProteinView/structureSuperposer.js +53 -0
- package/dist/UniProtVariationAdapter/UniProtVariationAdapter.d.ts +77 -13
- package/dist/UniProtVariationAdapter/UniProtVariationAdapter.js +28 -48
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/AddHighlightModel/GenomeTo1DProteinHoverHighlight.tsx +6 -2
- package/src/AddHighlightModel/ProteinToMsaHoverSync.tsx +3 -0
- package/src/AddHighlightModel/msaHoverSyncGuard.test.ts +14 -14
- package/src/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.ts +32 -50
- package/src/AlphaFoldConfidenceAdapter/parseAlphaFoldConfidence.test.ts +26 -0
- package/src/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.ts +30 -74
- package/src/AlphaMissensePathogenicityAdapter/parseAlphaMissense.test.ts +9 -4
- package/src/BaseProteinAnnotationAdapter.ts +70 -0
- package/src/LaunchProteinView/components/FoldseekSearch.tsx +12 -0
- package/src/LaunchProteinView/hooks/useUniProtSearch.ts +1 -0
- package/src/LaunchProteinView/services/lookupMethods.ts +1 -1
- package/src/LaunchProteinView/utils/launchViewUtils.ts +7 -7
- package/src/LaunchProteinViewExtensionPoint/index.ts +28 -23
- package/src/Protein1DViewRegistry/index.ts +20 -35
- package/src/ProteinView/components/AddStructureDialog.tsx +1 -1
- package/src/ProteinView/components/FeatureBar.tsx +14 -26
- package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +12 -0
- package/src/ProteinView/model.ts +48 -117
- package/src/ProteinView/proteinViewSpec.test.ts +48 -0
- package/src/ProteinView/proteinViewSpec.ts +61 -0
- package/src/ProteinView/structureModel.ts +32 -16
- package/src/ProteinView/structureSuperposer.test.ts +108 -0
- package/src/ProteinView/structureSuperposer.ts +70 -0
- package/src/UniProtVariationAdapter/UniProtVariationAdapter.ts +33 -62
- package/src/UniProtVariationAdapter/parseUniProtVariants.test.ts +42 -0
- package/src/version.ts +1 -1
|
@@ -20,11 +20,14 @@ const GenomeTo1DProteinHoverHighlight = observer(function GenomeTo1DProteinHover
|
|
|
20
20
|
if (!checkHovered(hovered)) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
-
const { coord } = hovered.hoverPosition;
|
|
23
|
+
const { coord, refName } = hovered.hoverPosition;
|
|
24
24
|
const feature = new SimpleFeature(protein1DInfo.feature);
|
|
25
25
|
const mapping = genomeToTranscriptSeqMapping(feature);
|
|
26
26
|
const { g2p } = mapping;
|
|
27
|
-
|
|
27
|
+
// g2p is keyed by genomic position on the transcript's own refName; without
|
|
28
|
+
// this gate the same numeric coord on an unrelated chromosome would match a
|
|
29
|
+
// key and light a protein residue for a different locus.
|
|
30
|
+
const proteinPos = refName === mapping.refName ? g2p[coord - 1] : undefined;
|
|
28
31
|
if (proteinPos === undefined) {
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
@@ -43,6 +43,10 @@ const ProteinToMsaHoverSync = observer(function ProteinToMsaHoverSync({ model, }
|
|
|
43
43
|
: seqPos;
|
|
44
44
|
setMousePos(col);
|
|
45
45
|
}
|
|
46
|
+
else {
|
|
47
|
+
// structure went away (e.g. removed); clear the stale MSA hover
|
|
48
|
+
setMousePos(undefined);
|
|
49
|
+
}
|
|
46
50
|
}));
|
|
47
51
|
}
|
|
48
52
|
disposers.push(autorun(() => {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import type { Observable } from 'rxjs';
|
|
5
|
-
export default class AlphaFoldConfidenceAdapter extends BaseFeatureDataAdapter {
|
|
6
|
-
static capabilities: string[];
|
|
7
|
-
feats: Promise<{
|
|
8
|
-
uniqueId: string;
|
|
9
|
-
start: number;
|
|
10
|
-
end: number;
|
|
11
|
-
score: number;
|
|
12
|
-
}[]> | undefined;
|
|
13
|
-
private loadDataP;
|
|
14
|
-
private loadData;
|
|
15
|
-
getRefNames(_opts?: BaseOptions): Promise<never[]>;
|
|
16
|
-
getFeatures(query: Region, _opts?: BaseOptions): Observable<Feature>;
|
|
17
|
-
freeResources(): void;
|
|
1
|
+
import { BaseProteinAnnotationAdapter, type ProteinAnnotationRow } from '../BaseProteinAnnotationAdapter';
|
|
2
|
+
export interface AlphaFoldConfidenceRow extends ProteinAnnotationRow {
|
|
3
|
+
score: number;
|
|
18
4
|
}
|
|
5
|
+
interface AlphaFoldConfidenceJson {
|
|
6
|
+
residueNumber: number[];
|
|
7
|
+
confidenceScore: number[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Converts AlphaFold confidence JSON to features. residueNumber is 1-based, so
|
|
11
|
+
* residue n becomes the 0-based half-open interval [n-1, n) to line up with the
|
|
12
|
+
* interbase protein reference sequence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseAlphaFoldConfidence(json: AlphaFoldConfidenceJson): AlphaFoldConfidenceRow[];
|
|
15
|
+
export default class AlphaFoldConfidenceAdapter extends BaseProteinAnnotationAdapter<AlphaFoldConfidenceRow> {
|
|
16
|
+
protected loadFeatures(): Promise<AlphaFoldConfidenceRow[]>;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -1,40 +1,21 @@
|
|
|
1
|
-
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
2
|
-
import { SimpleFeature, doesIntersect2 } from '@jbrowse/core/util';
|
|
3
1
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return this.feats;
|
|
23
|
-
}
|
|
24
|
-
async getRefNames(_opts = {}) {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
getFeatures(query, _opts = {}) {
|
|
28
|
-
return ObservableCreate(async (observer) => {
|
|
29
|
-
const { start, end, refName } = query;
|
|
30
|
-
const data = await this.loadData();
|
|
31
|
-
for (const f of data) {
|
|
32
|
-
if (doesIntersect2(f.start, f.end, start, end)) {
|
|
33
|
-
observer.next(new SimpleFeature({ ...f, refName }));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
observer.complete();
|
|
37
|
-
});
|
|
2
|
+
import { BaseProteinAnnotationAdapter, } from '../BaseProteinAnnotationAdapter';
|
|
3
|
+
/**
|
|
4
|
+
* Converts AlphaFold confidence JSON to features. residueNumber is 1-based, so
|
|
5
|
+
* residue n becomes the 0-based half-open interval [n-1, n) to line up with the
|
|
6
|
+
* interbase protein reference sequence.
|
|
7
|
+
*/
|
|
8
|
+
export function parseAlphaFoldConfidence(json) {
|
|
9
|
+
return json.residueNumber.map((residue, idx) => ({
|
|
10
|
+
uniqueId: `feat-${idx}`,
|
|
11
|
+
start: residue - 1,
|
|
12
|
+
end: residue,
|
|
13
|
+
score: json.confidenceScore[idx],
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export default class AlphaFoldConfidenceAdapter extends BaseProteinAnnotationAdapter {
|
|
17
|
+
async loadFeatures() {
|
|
18
|
+
const json = JSON.parse(await openLocation(this.getConf('location')).readFile('utf8'));
|
|
19
|
+
return parseAlphaFoldConfidence(json);
|
|
38
20
|
}
|
|
39
|
-
freeResources() { }
|
|
40
21
|
}
|
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseProteinAnnotationAdapter, type ProteinAnnotationRow } from '../BaseProteinAnnotationAdapter';
|
|
2
2
|
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export interface AlphaMissenseRow {
|
|
6
|
-
uniqueId: string;
|
|
7
|
-
start: number;
|
|
8
|
-
end: number;
|
|
3
|
+
import type { Region } from '@jbrowse/core/util';
|
|
4
|
+
export interface AlphaMissenseRow extends ProteinAnnotationRow {
|
|
9
5
|
score: number;
|
|
10
6
|
ref: string;
|
|
11
7
|
variant: string;
|
|
12
8
|
am_class: string;
|
|
13
9
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Parses AlphaMissense CSV text (protein_variant,score,am_class). The
|
|
16
|
-
* protein_variant column looks like "V123L": a ref AA, a 1-based residue
|
|
17
|
-
* coordinate, and a variant AA. Rows that don't parse to a numeric coordinate
|
|
18
|
-
* are skipped rather than emitted as bogus position-0 features.
|
|
19
|
-
*/
|
|
20
10
|
export declare function parseAlphaMissense(text: string): AlphaMissenseRow[];
|
|
21
|
-
export default class AlphaMissensePathogenicityAdapter extends
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
export default class AlphaMissensePathogenicityAdapter extends BaseProteinAnnotationAdapter<AlphaMissenseRow> {
|
|
12
|
+
protected loadFeatures(): Promise<AlphaMissenseRow[]>;
|
|
13
|
+
protected featureData(row: AlphaMissenseRow, refName: string): {
|
|
14
|
+
refName: string;
|
|
15
|
+
source: string;
|
|
16
|
+
score: number;
|
|
17
|
+
ref: string;
|
|
18
|
+
variant: string;
|
|
19
|
+
am_class: string;
|
|
20
|
+
uniqueId: string;
|
|
21
|
+
start: number;
|
|
22
|
+
end: number;
|
|
23
|
+
};
|
|
26
24
|
getGlobalStats(_opts?: BaseOptions): Promise<{
|
|
27
25
|
scoreMin: number;
|
|
28
26
|
scoreMax: number;
|
|
@@ -30,11 +28,8 @@ export default class AlphaMissensePathogenicityAdapter extends BaseFeatureDataAd
|
|
|
30
28
|
getMultiRegionFeatureDensityStats(_regions: Region[]): Promise<{
|
|
31
29
|
featureDensity: number;
|
|
32
30
|
}>;
|
|
33
|
-
getRefNames(_opts?: BaseOptions): Promise<never[]>;
|
|
34
|
-
getFeatures(query: Region, _opts?: BaseOptions): Observable<Feature>;
|
|
35
31
|
getSources(): Promise<{
|
|
36
32
|
name: string;
|
|
37
33
|
__name: string;
|
|
38
34
|
}[]>;
|
|
39
|
-
freeResources(): void;
|
|
40
35
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SimpleFeature, doesIntersect2, max, min } from '@jbrowse/core/util';
|
|
1
|
+
import { max, min } from '@jbrowse/core/util';
|
|
3
2
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
4
|
-
import {
|
|
3
|
+
import { BaseProteinAnnotationAdapter, } from '../BaseProteinAnnotationAdapter';
|
|
5
4
|
/**
|
|
6
5
|
* Parses AlphaMissense CSV text (protein_variant,score,am_class). The
|
|
7
6
|
* protein_variant column looks like "V123L": a ref AA, a 1-based residue
|
|
8
|
-
* coordinate, and a variant AA
|
|
9
|
-
*
|
|
7
|
+
* coordinate, and a variant AA, converted here to a 0-based half-open interval
|
|
8
|
+
* to match the interbase protein reference sequence. Rows that don't match the
|
|
9
|
+
* ref/coord/variant shape are skipped rather than emitted as bogus features (a
|
|
10
|
+
* short string like "VL" would otherwise parse to a position-0 feature).
|
|
10
11
|
*/
|
|
12
|
+
const VARIANT_RE = /^([A-Za-z])(\d+)([A-Za-z])$/;
|
|
11
13
|
export function parseAlphaMissense(text) {
|
|
12
14
|
return text
|
|
13
15
|
.split('\n')
|
|
@@ -16,21 +18,15 @@ export function parseAlphaMissense(text) {
|
|
|
16
18
|
.filter(f => !!f)
|
|
17
19
|
.flatMap((row, idx) => {
|
|
18
20
|
const [protein_variant = '', score, am_class] = row.split(',');
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const coord = +protein_variant.slice(1, -1);
|
|
22
|
-
return ref !== undefined &&
|
|
23
|
-
variant !== undefined &&
|
|
24
|
-
!Number.isNaN(coord) &&
|
|
25
|
-
score !== undefined &&
|
|
26
|
-
am_class !== undefined
|
|
21
|
+
const match = VARIANT_RE.exec(protein_variant);
|
|
22
|
+
return match && score !== undefined && am_class !== undefined
|
|
27
23
|
? [
|
|
28
24
|
{
|
|
29
25
|
uniqueId: `feat-${idx}`,
|
|
30
|
-
ref,
|
|
31
|
-
variant,
|
|
32
|
-
start:
|
|
33
|
-
end:
|
|
26
|
+
ref: match[1],
|
|
27
|
+
variant: match[3],
|
|
28
|
+
start: +match[2] - 1,
|
|
29
|
+
end: +match[2],
|
|
34
30
|
score: +score,
|
|
35
31
|
am_class,
|
|
36
32
|
},
|
|
@@ -38,58 +34,23 @@ export function parseAlphaMissense(text) {
|
|
|
38
34
|
: [];
|
|
39
35
|
});
|
|
40
36
|
}
|
|
41
|
-
export default class AlphaMissensePathogenicityAdapter extends
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
async loadDataP() {
|
|
45
|
-
const scores = await openLocation(this.getConf('location')).readFile('utf8');
|
|
46
|
-
return parseAlphaMissense(scores);
|
|
37
|
+
export default class AlphaMissensePathogenicityAdapter extends BaseProteinAnnotationAdapter {
|
|
38
|
+
async loadFeatures() {
|
|
39
|
+
return parseAlphaMissense(await openLocation(this.getConf('location')).readFile('utf8'));
|
|
47
40
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.feats = undefined;
|
|
51
|
-
throw e;
|
|
52
|
-
});
|
|
53
|
-
return this.feats;
|
|
41
|
+
featureData(row, refName) {
|
|
42
|
+
return { ...row, refName, source: row.variant };
|
|
54
43
|
}
|
|
55
44
|
async getGlobalStats(_opts) {
|
|
56
|
-
const
|
|
57
|
-
const scores = data.map(s => s.score);
|
|
45
|
+
const scores = (await this.loadData()).map(s => s.score);
|
|
58
46
|
return { scoreMin: min(scores), scoreMax: max(scores) };
|
|
59
47
|
}
|
|
60
48
|
// always render bigwig instead of calculating a feature density for it
|
|
61
49
|
async getMultiRegionFeatureDensityStats(_regions) {
|
|
62
50
|
return { featureDensity: 0 };
|
|
63
51
|
}
|
|
64
|
-
async getRefNames(_opts = {}) {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
getFeatures(query, _opts = {}) {
|
|
68
|
-
return ObservableCreate(async (observer) => {
|
|
69
|
-
const { start, end, refName } = query;
|
|
70
|
-
const data = await this.loadData();
|
|
71
|
-
for (const f of data) {
|
|
72
|
-
if (doesIntersect2(f.start, f.end, start, end)) {
|
|
73
|
-
observer.next(new SimpleFeature({
|
|
74
|
-
...f,
|
|
75
|
-
refName,
|
|
76
|
-
source: f.variant,
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
observer.complete();
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
52
|
async getSources() {
|
|
84
|
-
const sources = new Set();
|
|
85
|
-
|
|
86
|
-
for (const f of data) {
|
|
87
|
-
sources.add(f.variant);
|
|
88
|
-
}
|
|
89
|
-
return [...sources].map(s => ({
|
|
90
|
-
name: s,
|
|
91
|
-
__name: s,
|
|
92
|
-
}));
|
|
53
|
+
const sources = new Set((await this.loadData()).map(f => f.variant));
|
|
54
|
+
return [...sources].map(s => ({ name: s, __name: s }));
|
|
93
55
|
}
|
|
94
|
-
freeResources() { }
|
|
95
56
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
2
|
+
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
3
|
+
import type { Feature, Region, SimpleFeatureSerialized } from '@jbrowse/core/util';
|
|
4
|
+
import type { Observable } from 'rxjs';
|
|
5
|
+
export interface ProteinAnnotationRow {
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
uniqueId: string;
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Shared plumbing for the protein-annotation adapters (AlphaFold confidence,
|
|
13
|
+
* AlphaMissense, UniProt variation). Each lives on the temporary protein
|
|
14
|
+
* assembly, exposes no ref names, caches its parsed rows once, and emits the
|
|
15
|
+
* rows that intersect a query region. Subclasses supply the parsing
|
|
16
|
+
* (loadFeatures) and may decorate the emitted feature (featureData).
|
|
17
|
+
*/
|
|
18
|
+
export declare abstract class BaseProteinAnnotationAdapter<T extends ProteinAnnotationRow> extends BaseFeatureDataAdapter {
|
|
19
|
+
static capabilities: string[];
|
|
20
|
+
private feats;
|
|
21
|
+
/** Parse the configured source into rows. */
|
|
22
|
+
protected abstract loadFeatures(): Promise<T[]>;
|
|
23
|
+
/** Fields for the emitted feature; override to add extras (e.g. `source`). */
|
|
24
|
+
protected featureData(row: T, refName: string): SimpleFeatureSerialized;
|
|
25
|
+
protected loadData(): Promise<T[]>;
|
|
26
|
+
getRefNames(_opts?: BaseOptions): Promise<never[]>;
|
|
27
|
+
getFeatures(query: Region, _opts?: BaseOptions): Observable<Feature>;
|
|
28
|
+
freeResources(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
2
|
+
import { SimpleFeature, doesIntersect2 } from '@jbrowse/core/util';
|
|
3
|
+
import { ObservableCreate } from '@jbrowse/core/util/rxjs';
|
|
4
|
+
/**
|
|
5
|
+
* Shared plumbing for the protein-annotation adapters (AlphaFold confidence,
|
|
6
|
+
* AlphaMissense, UniProt variation). Each lives on the temporary protein
|
|
7
|
+
* assembly, exposes no ref names, caches its parsed rows once, and emits the
|
|
8
|
+
* rows that intersect a query region. Subclasses supply the parsing
|
|
9
|
+
* (loadFeatures) and may decorate the emitted feature (featureData).
|
|
10
|
+
*/
|
|
11
|
+
export class BaseProteinAnnotationAdapter extends BaseFeatureDataAdapter {
|
|
12
|
+
static capabilities = ['getFeatures', 'getRefNames'];
|
|
13
|
+
feats;
|
|
14
|
+
/** Fields for the emitted feature; override to add extras (e.g. `source`). */
|
|
15
|
+
featureData(row, refName) {
|
|
16
|
+
return { ...row, refName };
|
|
17
|
+
}
|
|
18
|
+
// Parse once and cache; a failed parse clears the cache so it can retry.
|
|
19
|
+
loadData() {
|
|
20
|
+
this.feats ??= this.loadFeatures().catch((e) => {
|
|
21
|
+
this.feats = undefined;
|
|
22
|
+
throw e;
|
|
23
|
+
});
|
|
24
|
+
return this.feats;
|
|
25
|
+
}
|
|
26
|
+
async getRefNames(_opts = {}) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
getFeatures(query, _opts = {}) {
|
|
30
|
+
return ObservableCreate(async (observer) => {
|
|
31
|
+
const { start, end, refName } = query;
|
|
32
|
+
for (const f of await this.loadData()) {
|
|
33
|
+
if (doesIntersect2(f.start, f.end, start, end)) {
|
|
34
|
+
observer.next(new SimpleFeature(this.featureData(f, refName)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
observer.complete();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
freeResources() { }
|
|
41
|
+
}
|
|
@@ -36,9 +36,19 @@ const FoldseekSearch = observer(function FoldseekSearch({ feature, session, view
|
|
|
36
36
|
? stripStopCodon(selectedIsoformData.seq)
|
|
37
37
|
: '';
|
|
38
38
|
const sequence = userEditedSequence ?? cleanedSequence;
|
|
39
|
+
// Any change to the input sequence makes an existing 3Di prediction (and any
|
|
40
|
+
// results derived from it) stale. Clearing it returns the UI to the Predict
|
|
41
|
+
// step so a search can't silently run against the previously-predicted
|
|
42
|
+
// sequence after the user switches transcript or edits the residues.
|
|
43
|
+
const invalidatePrediction = () => {
|
|
44
|
+
if (di3Sequence !== undefined || results !== undefined) {
|
|
45
|
+
reset();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
39
48
|
const setUserSelectionWithReset = (id) => {
|
|
40
49
|
setUserSelection(id);
|
|
41
50
|
setUserEditedSequence(undefined);
|
|
51
|
+
invalidatePrediction();
|
|
42
52
|
};
|
|
43
53
|
const canPredict = sequence.trim().length > 0 && !isPredicting && !isLoading;
|
|
44
54
|
const canSearch = !!cleanedAaSequence &&
|
|
@@ -55,6 +65,7 @@ const FoldseekSearch = observer(function FoldseekSearch({ feature, session, view
|
|
|
55
65
|
React.createElement(TranscriptSelector, { val: effectiveSelectedTranscriptId, setVal: setUserSelectionWithReset, isoforms: transcripts, isoformSequences: isoformSequences, feature: feature, disabled: isBusy }),
|
|
56
66
|
React.createElement(TextField, { label: "Protein sequence (amino acids)", multiline: true, rows: 4, value: sequence, onChange: e => {
|
|
57
67
|
setUserEditedSequence(e.target.value);
|
|
68
|
+
invalidatePrediction();
|
|
58
69
|
}, placeholder: `MKTVRQERLKSIVRILERSKEPVSGAQLAEEL...`, disabled: isBusy, InputProps: {
|
|
59
70
|
className: classes.sequenceInput,
|
|
60
71
|
} }))) : null,
|
|
@@ -53,7 +53,7 @@ export async function searchUniProtEntries({ recognizedIds = [], geneId, geneNam
|
|
|
53
53
|
let geneNameError;
|
|
54
54
|
if (!entries.some(e => e.isReviewed) && geneName) {
|
|
55
55
|
try {
|
|
56
|
-
const query = `gene:${geneName}
|
|
56
|
+
const query = `gene:${geneName} AND organism_id:${organismId} AND reviewed:true`;
|
|
57
57
|
const geneNameResults = await searchUniProt(query, 5);
|
|
58
58
|
entries = deduplicateEntries([...entries, ...geneNameResults]);
|
|
59
59
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isSessionWithAddTracks } from '@jbrowse/core/util';
|
|
2
2
|
import { maybeLaunchSideBySide } from './sideBySide';
|
|
3
3
|
import { getGeneDisplayName, getTranscriptDisplayName } from './util';
|
|
4
|
+
import { proteinViewSnapshot } from '../../ProteinView/proteinViewSpec';
|
|
4
5
|
import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView';
|
|
5
6
|
export const ALPHAFOLD_VERSION = 'v6';
|
|
6
7
|
export function getAlphaFoldStructureUrl(uniprotId, version = ALPHAFOLD_VERSION) {
|
|
@@ -60,22 +61,21 @@ export function formatViewName(prefix, feature, selectedTranscript, uniprotId) {
|
|
|
60
61
|
.join(' - ');
|
|
61
62
|
}
|
|
62
63
|
export function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }) {
|
|
63
|
-
const snap = {
|
|
64
|
-
type: 'ProteinView',
|
|
64
|
+
const snap = proteinViewSnapshot({
|
|
65
65
|
alignmentAlgorithm,
|
|
66
66
|
connectedMsaViewId,
|
|
67
|
+
displayName: displayName ??
|
|
68
|
+
formatViewName('Protein view', feature, selectedTranscript, uniprotId),
|
|
67
69
|
structures: [
|
|
68
70
|
{
|
|
69
71
|
url,
|
|
70
72
|
data,
|
|
71
|
-
userProvidedTranscriptSequence
|
|
73
|
+
userProvidedTranscriptSequence,
|
|
72
74
|
feature: selectedTranscript?.toJSON(),
|
|
73
75
|
connectedViewId: view.id,
|
|
74
76
|
},
|
|
75
77
|
],
|
|
76
|
-
|
|
77
|
-
formatViewName('Protein view', feature, selectedTranscript, uniprotId),
|
|
78
|
-
};
|
|
78
|
+
});
|
|
79
79
|
const proteinView = session.addView('ProteinView', snap);
|
|
80
80
|
maybeLaunchSideBySide(session, proteinView.id, sideBySide);
|
|
81
81
|
return proteinView;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveShortLaunch } from './resolveShortLaunch';
|
|
2
2
|
import { maybeLaunchSideBySide } from '../LaunchProteinView/utils/sideBySide';
|
|
3
|
+
import { proteinViewSnapshot } from '../ProteinView/proteinViewSpec';
|
|
3
4
|
export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
4
5
|
pluginManager.addToExtensionPoint('LaunchView-ProteinView',
|
|
5
6
|
// LaunchView extension points are typed as transformers `(extendee, props)
|
|
@@ -51,8 +52,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
51
52
|
init: connectedView,
|
|
52
53
|
}).id
|
|
53
54
|
: undefined);
|
|
54
|
-
const proteinView = session.addView('ProteinView', {
|
|
55
|
-
type: 'ProteinView',
|
|
55
|
+
const proteinView = session.addView('ProteinView', proteinViewSnapshot({
|
|
56
56
|
alignmentAlgorithm,
|
|
57
57
|
displayName,
|
|
58
58
|
height,
|
|
@@ -63,14 +63,13 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
63
63
|
{
|
|
64
64
|
url: finalUrl,
|
|
65
65
|
userProvidedTranscriptSequence: resolved?.userProvidedTranscriptSequence ??
|
|
66
|
-
userProvidedTranscriptSequence
|
|
67
|
-
'',
|
|
66
|
+
userProvidedTranscriptSequence,
|
|
68
67
|
feature: resolved?.feature ?? feature,
|
|
69
68
|
connectedViewId: resolvedConnectedViewId,
|
|
70
69
|
initialSelection,
|
|
71
70
|
},
|
|
72
71
|
],
|
|
73
|
-
});
|
|
72
|
+
}));
|
|
74
73
|
if (ownsConnectedView) {
|
|
75
74
|
maybeLaunchSideBySide(session, proteinView.id, sideBySide);
|
|
76
75
|
}
|
|
@@ -15,8 +15,13 @@ declare class Protein1DViewRegistry {
|
|
|
15
15
|
constructor();
|
|
16
16
|
register(info: Protein1DViewInfo): void;
|
|
17
17
|
unregister(viewId: string): void;
|
|
18
|
-
cleanupStaleViews(session: SessionWithViews): void;
|
|
19
18
|
get(viewId: string): Protein1DViewInfo | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Pure lookup. When a session is supplied, entries whose view has since been
|
|
21
|
+
* closed are skipped rather than deleted, so this stays side-effect-free and
|
|
22
|
+
* safe to call from an observer's render (mutating the observable map there
|
|
23
|
+
* would be a MobX anti-pattern).
|
|
24
|
+
*/
|
|
20
25
|
getByUniprotId(uniprotId: string, session?: SessionWithViews): Protein1DViewInfo | undefined;
|
|
21
26
|
get entries(): Protein1DViewInfo[];
|
|
22
27
|
getGenomeHighlightForProteinPosition(uniprotId: string, proteinPos: number, session?: SessionWithViews): {
|
|
@@ -7,7 +7,6 @@ class Protein1DViewRegistry {
|
|
|
7
7
|
makeObservable(this, {
|
|
8
8
|
register: action,
|
|
9
9
|
unregister: action,
|
|
10
|
-
cleanupStaleViews: action,
|
|
11
10
|
entries: computed,
|
|
12
11
|
});
|
|
13
12
|
}
|
|
@@ -17,23 +16,22 @@ class Protein1DViewRegistry {
|
|
|
17
16
|
unregister(viewId) {
|
|
18
17
|
this.views.delete(viewId);
|
|
19
18
|
}
|
|
20
|
-
cleanupStaleViews(session) {
|
|
21
|
-
const activeViewIds = new Set(session.views.map(v => v.id));
|
|
22
|
-
for (const viewId of this.views.keys()) {
|
|
23
|
-
if (!activeViewIds.has(viewId)) {
|
|
24
|
-
this.views.delete(viewId);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
19
|
get(viewId) {
|
|
29
20
|
return this.views.get(viewId);
|
|
30
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Pure lookup. When a session is supplied, entries whose view has since been
|
|
24
|
+
* closed are skipped rather than deleted, so this stays side-effect-free and
|
|
25
|
+
* safe to call from an observer's render (mutating the observable map there
|
|
26
|
+
* would be a MobX anti-pattern).
|
|
27
|
+
*/
|
|
31
28
|
getByUniprotId(uniprotId, session) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const liveViewIds = session
|
|
30
|
+
? new Set(session.views.map(v => v.id))
|
|
31
|
+
: undefined;
|
|
35
32
|
for (const info of this.views.values()) {
|
|
36
|
-
if (info.uniprotId === uniprotId
|
|
33
|
+
if (info.uniprotId === uniprotId &&
|
|
34
|
+
(!liveViewIds || liveViewIds.has(info.viewId))) {
|
|
37
35
|
return info;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
@@ -44,17 +42,12 @@ class Protein1DViewRegistry {
|
|
|
44
42
|
}
|
|
45
43
|
getGenomeHighlightForProteinPosition(uniprotId, proteinPos, session) {
|
|
46
44
|
const info = this.getByUniprotId(uniprotId, session);
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const mapping = genomeToTranscriptSeqMapping(feature);
|
|
52
|
-
if (!mapping) {
|
|
53
|
-
return undefined;
|
|
45
|
+
if (info) {
|
|
46
|
+
const { p2gCodon, refName } = genomeToTranscriptSeqMapping(new SimpleFeature(info.feature));
|
|
47
|
+
const span = codonGenomeSpan(p2gCodon, proteinPos);
|
|
48
|
+
return span ? { refName, start: span[0], end: span[1] } : undefined;
|
|
54
49
|
}
|
|
55
|
-
|
|
56
|
-
const span = codonGenomeSpan(p2gCodon, proteinPos);
|
|
57
|
-
return span ? { refName, start: span[0], end: span[1] } : undefined;
|
|
50
|
+
return undefined;
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
53
|
export const protein1DViewRegistry = new Protein1DViewRegistry();
|
|
@@ -36,7 +36,7 @@ const AddStructureDialog = observer(function AddStructureDialog({ model, }) {
|
|
|
36
36
|
data = await file.text();
|
|
37
37
|
}
|
|
38
38
|
if (url !== undefined || data !== undefined) {
|
|
39
|
-
|
|
39
|
+
model.addStructure({ url, data });
|
|
40
40
|
handleClose();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Tooltip } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
|
-
import { setMolstarLoci } from '../applyLociInteractivity';
|
|
5
4
|
import { HOVERED_BORDER, SELECTED_BORDER } from '../constants';
|
|
5
|
+
import { oneBasedUniProtFeatureToStructureRange } from '../hooks/useProteinFeatureTrackData';
|
|
6
6
|
import { getFeatureColor } from '../hooks/useUniProtFeatures';
|
|
7
7
|
import { clickProteinToGenome } from '../proteinToGenomeMapping';
|
|
8
8
|
function FeatureTooltipContent({ feature }) {
|
|
@@ -18,7 +18,7 @@ function FeatureTooltipContent({ feature }) {
|
|
|
18
18
|
}
|
|
19
19
|
const FeatureBar = observer(function FeatureBar({ layout, top, model, }) {
|
|
20
20
|
const [isHovered, setIsHovered] = useState(false);
|
|
21
|
-
const {
|
|
21
|
+
const { selectedFeatureId } = model;
|
|
22
22
|
const { feature, left, width } = layout;
|
|
23
23
|
const isSelected = selectedFeatureId === feature.uniqueId;
|
|
24
24
|
const handleMouseEnter = () => {
|
|
@@ -32,38 +32,28 @@ const FeatureBar = observer(function FeatureBar({ layout, top, model, }) {
|
|
|
32
32
|
setIsHovered(false);
|
|
33
33
|
model.setAlignmentHoverRange(undefined);
|
|
34
34
|
};
|
|
35
|
+
// The model's `select` autorun owns the magenta molstar selection, deriving
|
|
36
|
+
// it from clickedStructureRange. Setting/clearing that range here (rather than
|
|
37
|
+
// also driving molstar imperatively) keeps a single source of truth: on
|
|
38
|
+
// deselect the autorun correctly falls back to the whole-alignment highlight
|
|
39
|
+
// when showHighlight is on, instead of blanking the selection.
|
|
35
40
|
const handleClick = () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
setMolstarLoci({
|
|
40
|
-
structure,
|
|
41
|
-
plugin: molstarPluginContext,
|
|
42
|
-
channel: 'select',
|
|
43
|
-
entityId: model.mappedEntityId,
|
|
44
|
-
spec: newSelected
|
|
45
|
-
? { kind: 'range', start: feature.start - 1, end: feature.end }
|
|
46
|
-
: undefined,
|
|
47
|
-
}).catch((e) => {
|
|
48
|
-
console.error(e);
|
|
49
|
-
model.setError(e);
|
|
50
|
-
});
|
|
41
|
+
if (isSelected) {
|
|
42
|
+
model.setSelectedFeatureId(undefined);
|
|
43
|
+
model.setClickedStructureRange(undefined);
|
|
51
44
|
}
|
|
52
|
-
|
|
45
|
+
else {
|
|
46
|
+
const { start, end } = oneBasedUniProtFeatureToStructureRange(feature);
|
|
53
47
|
model.setSelectedFeatureId(feature.uniqueId);
|
|
54
48
|
clickProteinToGenome({
|
|
55
49
|
model,
|
|
56
|
-
structureSeqPos:
|
|
57
|
-
structureSeqEndPos:
|
|
50
|
+
structureSeqPos: start,
|
|
51
|
+
structureSeqEndPos: end,
|
|
58
52
|
}).catch((e) => {
|
|
59
53
|
console.error(e);
|
|
60
54
|
model.setError(e);
|
|
61
55
|
});
|
|
62
56
|
}
|
|
63
|
-
else {
|
|
64
|
-
model.setSelectedFeatureId(undefined);
|
|
65
|
-
model.setClickedStructureRange(undefined);
|
|
66
|
-
}
|
|
67
57
|
};
|
|
68
58
|
const color = getFeatureColor(feature.type);
|
|
69
59
|
return (React.createElement(Tooltip, { title: React.createElement(FeatureTooltipContent, { feature: feature }), followCursor: true },
|