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.
Files changed (60) hide show
  1. package/dist/AddHighlightModel/GenomeTo1DProteinHoverHighlight.js +5 -2
  2. package/dist/AddHighlightModel/ProteinToMsaHoverSync.js +4 -0
  3. package/dist/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.d.ts +17 -17
  4. package/dist/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.js +18 -37
  5. package/dist/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.d.ts +16 -21
  6. package/dist/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.js +21 -60
  7. package/dist/BaseProteinAnnotationAdapter.d.ts +29 -0
  8. package/dist/BaseProteinAnnotationAdapter.js +41 -0
  9. package/dist/LaunchProteinView/components/FoldseekSearch.js +11 -0
  10. package/dist/LaunchProteinView/hooks/useUniProtSearch.js +1 -0
  11. package/dist/LaunchProteinView/services/lookupMethods.js +1 -1
  12. package/dist/LaunchProteinView/utils/launchViewUtils.js +6 -6
  13. package/dist/LaunchProteinViewExtensionPoint/index.js +4 -5
  14. package/dist/Protein1DViewRegistry/index.d.ts +6 -1
  15. package/dist/Protein1DViewRegistry/index.js +16 -23
  16. package/dist/ProteinView/components/AddStructureDialog.js +1 -1
  17. package/dist/ProteinView/components/FeatureBar.js +14 -24
  18. package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +12 -0
  19. package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +8 -0
  20. package/dist/ProteinView/model.d.ts +12 -38
  21. package/dist/ProteinView/model.js +31 -82
  22. package/dist/ProteinView/proteinViewSpec.d.ts +72 -0
  23. package/dist/ProteinView/proteinViewSpec.js +16 -0
  24. package/dist/ProteinView/structureModel.d.ts +3 -1
  25. package/dist/ProteinView/structureModel.js +28 -9
  26. package/dist/ProteinView/structureSuperposer.d.ts +30 -0
  27. package/dist/ProteinView/structureSuperposer.js +53 -0
  28. package/dist/UniProtVariationAdapter/UniProtVariationAdapter.d.ts +77 -13
  29. package/dist/UniProtVariationAdapter/UniProtVariationAdapter.js +28 -48
  30. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  31. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +1 -1
  35. package/src/AddHighlightModel/GenomeTo1DProteinHoverHighlight.tsx +6 -2
  36. package/src/AddHighlightModel/ProteinToMsaHoverSync.tsx +3 -0
  37. package/src/AddHighlightModel/msaHoverSyncGuard.test.ts +14 -14
  38. package/src/AlphaFoldConfidenceAdapter/AlphaFoldConfidenceAdapter.ts +32 -50
  39. package/src/AlphaFoldConfidenceAdapter/parseAlphaFoldConfidence.test.ts +26 -0
  40. package/src/AlphaMissensePathogenicityAdapter/AlphaMissensePathogenicityAdapter.ts +30 -74
  41. package/src/AlphaMissensePathogenicityAdapter/parseAlphaMissense.test.ts +9 -4
  42. package/src/BaseProteinAnnotationAdapter.ts +70 -0
  43. package/src/LaunchProteinView/components/FoldseekSearch.tsx +12 -0
  44. package/src/LaunchProteinView/hooks/useUniProtSearch.ts +1 -0
  45. package/src/LaunchProteinView/services/lookupMethods.ts +1 -1
  46. package/src/LaunchProteinView/utils/launchViewUtils.ts +7 -7
  47. package/src/LaunchProteinViewExtensionPoint/index.ts +28 -23
  48. package/src/Protein1DViewRegistry/index.ts +20 -35
  49. package/src/ProteinView/components/AddStructureDialog.tsx +1 -1
  50. package/src/ProteinView/components/FeatureBar.tsx +14 -26
  51. package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +12 -0
  52. package/src/ProteinView/model.ts +48 -117
  53. package/src/ProteinView/proteinViewSpec.test.ts +48 -0
  54. package/src/ProteinView/proteinViewSpec.ts +61 -0
  55. package/src/ProteinView/structureModel.ts +32 -16
  56. package/src/ProteinView/structureSuperposer.test.ts +108 -0
  57. package/src/ProteinView/structureSuperposer.ts +70 -0
  58. package/src/UniProtVariationAdapter/UniProtVariationAdapter.ts +33 -62
  59. package/src/UniProtVariationAdapter/parseUniProtVariants.test.ts +42 -0
  60. 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
- const proteinPos = g2p[coord - 1];
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 { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
- import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
3
- import type { Feature, Region } from '@jbrowse/core/util';
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 { ObservableCreate } from '@jbrowse/core/util/rxjs';
5
- export default class AlphaFoldConfidenceAdapter extends BaseFeatureDataAdapter {
6
- static capabilities = ['getFeatures', 'getRefNames'];
7
- feats;
8
- async loadDataP() {
9
- const scores = JSON.parse(await openLocation(this.getConf('location')).readFile('utf8'));
10
- return scores.residueNumber.map((value, idx) => ({
11
- uniqueId: `feat-${idx}`,
12
- start: value,
13
- end: value + 1,
14
- score: scores.confidenceScore[idx],
15
- }));
16
- }
17
- async loadData(_opts = {}) {
18
- this.feats ??= this.loadDataP().catch((e) => {
19
- this.feats = undefined;
20
- throw e;
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 { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
1
+ import { BaseProteinAnnotationAdapter, type ProteinAnnotationRow } from '../BaseProteinAnnotationAdapter';
2
2
  import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter';
3
- import type { Feature, Region } from '@jbrowse/core/util';
4
- import type { Observable } from 'rxjs';
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 BaseFeatureDataAdapter {
22
- static capabilities: string[];
23
- feats: Promise<AlphaMissenseRow[]> | undefined;
24
- private loadDataP;
25
- private loadData;
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 { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
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 { ObservableCreate } from '@jbrowse/core/util/rxjs';
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. Rows that don't parse to a numeric coordinate
9
- * are skipped rather than emitted as bogus position-0 features.
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 ref = protein_variant[0];
20
- const variant = protein_variant.at(-1);
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: coord,
33
- end: coord + 1,
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 BaseFeatureDataAdapter {
42
- static capabilities = ['getFeatures', 'getRefNames'];
43
- feats;
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
- async loadData(_opts = {}) {
49
- this.feats ??= this.loadDataP().catch((e) => {
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 data = await this.loadData();
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
- const data = await this.loadData();
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,
@@ -24,6 +24,7 @@ export default function useUniProtSearch({ recognizedIds = [], geneId, geneName,
24
24
  selectedQueryId,
25
25
  idsToSearch.join(','),
26
26
  geneNameToSearch,
27
+ geneId,
27
28
  ]
28
29
  : null, async () => searchUniProtEntries({
29
30
  recognizedIds: idsToSearch,
@@ -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}+AND+organism_id:${organismId}+AND+reviewed:true`;
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: userProvidedTranscriptSequence ?? '',
73
+ userProvidedTranscriptSequence,
72
74
  feature: selectedTranscript?.toJSON(),
73
75
  connectedViewId: view.id,
74
76
  },
75
77
  ],
76
- displayName: displayName ??
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
- if (session) {
33
- this.cleanupStaleViews(session);
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 (!info) {
48
- return undefined;
49
- }
50
- const feature = new SimpleFeature(info.feature);
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
- const { p2gCodon, refName } = mapping;
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
- await model.addStructureAndSuperpose({ url, data });
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 { molstarPluginContext, selectedFeatureId } = model;
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
- const structure = model.molstarStructure;
37
- const newSelected = !isSelected;
38
- if (structure && molstarPluginContext) {
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
- if (newSelected) {
45
+ else {
46
+ const { start, end } = oneBasedUniProtFeatureToStructureRange(feature);
53
47
  model.setSelectedFeatureId(feature.uniqueId);
54
48
  clickProteinToGenome({
55
49
  model,
56
- structureSeqPos: feature.start - 1,
57
- structureSeqEndPos: feature.end,
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 },