jbrowse-plugin-msaview 3.6.0 → 3.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 (156) hide show
  1. package/README.md +8 -0
  2. package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
  3. package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -32
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +10 -15
  6. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
  7. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +25 -22
  8. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +14 -19
  9. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
  10. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
  11. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
  12. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
  13. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +19 -14
  14. package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
  15. package/dist/LaunchMsaView/components/SubmitCancelActions.js +10 -7
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +20 -2
  17. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +8 -1
  18. package/dist/LaunchMsaView/components/calculateProteinSequence.js +25 -20
  19. package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
  20. package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
  21. package/dist/LaunchMsaView/detectQueryRow.js +4 -7
  22. package/dist/LaunchMsaView/util.d.ts +6 -0
  23. package/dist/LaunchMsaView/util.js +7 -1
  24. package/dist/LaunchMsaViewExtensionPoint/index.test.js +15 -0
  25. package/dist/MsaViewPanel/afterCreateAutoruns.js +12 -3
  26. package/dist/MsaViewPanel/applyRegion.d.ts +23 -0
  27. package/dist/MsaViewPanel/applyRegion.js +19 -0
  28. package/dist/MsaViewPanel/applyRegion.test.d.ts +1 -0
  29. package/dist/MsaViewPanel/applyRegion.test.js +45 -0
  30. package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
  31. package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
  32. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  33. package/dist/MsaViewPanel/genomeToMSA.js +3 -3
  34. package/dist/MsaViewPanel/genomeToMSA.test.js +69 -23
  35. package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
  36. package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
  37. package/dist/MsaViewPanel/model.d.ts +18 -14
  38. package/dist/MsaViewPanel/model.js +17 -0
  39. package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
  40. package/dist/MsaViewPanel/msaDataStore.js +3 -12
  41. package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
  42. package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
  43. package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
  44. package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
  45. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
  46. package/dist/MsaViewPanel/resolveConnectedTranscript.js +103 -68
  47. package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
  48. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
  49. package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
  50. package/dist/MsaViewPanel/runLaunch.js +15 -4
  51. package/dist/MsaViewPanel/runLaunch.test.js +16 -0
  52. package/dist/MsaViewPanel/storedData.test.js +23 -0
  53. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +3 -2
  54. package/dist/MsaViewPanel/util.d.ts +23 -23
  55. package/dist/MsaViewPanel/util.js +23 -24
  56. package/dist/index.js +17 -10
  57. package/dist/jbrowse-plugin-msaview.umd.production.min.js +36 -36
  58. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  59. package/dist/utils/blastCache.d.ts +26 -2
  60. package/dist/utils/blastCache.js +51 -39
  61. package/dist/utils/blastCache.test.js +46 -19
  62. package/dist/utils/domainCache.d.ts +1 -1
  63. package/dist/utils/domainCache.js +18 -14
  64. package/dist/utils/ebiJobDispatcher.d.ts +11 -0
  65. package/dist/utils/ebiJobDispatcher.js +12 -13
  66. package/dist/utils/ebiJobDispatcher.test.js +17 -3
  67. package/dist/utils/eutils.d.ts +14 -0
  68. package/dist/utils/eutils.js +48 -14
  69. package/dist/utils/eutils.test.d.ts +1 -0
  70. package/dist/utils/eutils.test.js +31 -0
  71. package/dist/utils/idb.d.ts +16 -3
  72. package/dist/utils/idb.js +51 -5
  73. package/dist/utils/idb.test.d.ts +1 -0
  74. package/dist/utils/idb.test.js +78 -0
  75. package/dist/utils/ncbiDomains.js +4 -4
  76. package/dist/utils/ncbiOrthologs.d.ts +1 -1
  77. package/dist/utils/ncbiOrthologs.js +18 -17
  78. package/dist/utils/ncbiOrthologs.test.js +17 -1
  79. package/dist/utils/ncbiTaxonomy.js +14 -15
  80. package/dist/utils/taxonomyNames.js +23 -19
  81. package/dist/utils/taxonomyNames.test.d.ts +1 -0
  82. package/dist/utils/taxonomyNames.test.js +33 -0
  83. package/dist/utils/unirefHomologs.js +2 -2
  84. package/dist/utils/unirefHomologs.test.js +8 -1
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +9 -9
  88. package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
  89. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +35 -32
  90. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +10 -14
  91. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
  92. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +34 -21
  93. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +18 -21
  94. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
  95. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
  96. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +19 -13
  97. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +27 -2
  98. package/src/LaunchMsaView/components/SubmitCancelActions.tsx +12 -7
  99. package/src/LaunchMsaView/components/calculateProteinSequence.ts +28 -18
  100. package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
  101. package/src/LaunchMsaView/detectQueryRow.ts +5 -7
  102. package/src/LaunchMsaView/util.ts +7 -1
  103. package/src/LaunchMsaViewExtensionPoint/index.test.ts +16 -0
  104. package/src/LaunchMsaViewExtensionPoint/index.ts +28 -1
  105. package/src/MsaViewPanel/afterCreateAutoruns.ts +12 -3
  106. package/src/MsaViewPanel/applyRegion.test.ts +55 -0
  107. package/src/MsaViewPanel/applyRegion.ts +33 -0
  108. package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
  109. package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
  110. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
  111. package/src/MsaViewPanel/genomeToMSA.test.ts +75 -23
  112. package/src/MsaViewPanel/genomeToMSA.ts +3 -2
  113. package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
  114. package/src/MsaViewPanel/model.ts +22 -0
  115. package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
  116. package/src/MsaViewPanel/msaDataStore.ts +4 -12
  117. package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
  118. package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
  119. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
  120. package/src/MsaViewPanel/resolveConnectedTranscript.ts +154 -71
  121. package/src/MsaViewPanel/runLaunch.test.ts +19 -0
  122. package/src/MsaViewPanel/runLaunch.ts +21 -4
  123. package/src/MsaViewPanel/storedData.test.ts +32 -0
  124. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +3 -2
  125. package/src/MsaViewPanel/util.ts +38 -29
  126. package/src/index.ts +19 -10
  127. package/src/utils/blastCache.test.ts +59 -20
  128. package/src/utils/blastCache.ts +62 -39
  129. package/src/utils/domainCache.ts +27 -15
  130. package/src/utils/ebiJobDispatcher.test.ts +18 -3
  131. package/src/utils/ebiJobDispatcher.ts +12 -16
  132. package/src/utils/eutils.test.ts +39 -0
  133. package/src/utils/eutils.ts +64 -14
  134. package/src/utils/idb.test.ts +106 -0
  135. package/src/utils/idb.ts +57 -5
  136. package/src/utils/ncbiDomains.ts +4 -4
  137. package/src/utils/ncbiOrthologs.test.ts +20 -0
  138. package/src/utils/ncbiOrthologs.ts +26 -28
  139. package/src/utils/ncbiTaxonomy.ts +17 -20
  140. package/src/utils/taxonomyNames.test.ts +41 -0
  141. package/src/utils/taxonomyNames.ts +33 -19
  142. package/src/utils/unirefHomologs.test.ts +11 -1
  143. package/src/utils/unirefHomologs.ts +2 -2
  144. package/src/version.ts +1 -1
  145. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
  146. package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
  147. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
  148. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
  149. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
  150. package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
  151. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -13
  152. package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -22
  153. package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
  154. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
  155. package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
  156. package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -42
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { Button, Checkbox, DialogActions, FormControlLabel, } from '@mui/material';
4
- import { sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
4
+ import { readLaunchPlacement, sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
5
5
  import { useLaunchPlacement } from './launchPlacement';
6
6
  /**
7
7
  * Where the launch puts the view, offered wherever a launch is submitted.
@@ -21,10 +21,9 @@ function PlacementToggle({ checked, onChange, }) {
21
21
  }
22
22
  export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, hint, submitLabel = 'Submit', cancelLabel = 'Cancel', model, }) {
23
23
  const [sideBySide, setSideBySide] = useLaunchPlacement();
24
- // The stored value is what the next launch reads, so it is written on submit
25
- // rather than on the click: ticking the box and then pressing Cancel used to
26
- // change where every future launch landed, from a dialog the user backed out
27
- // of.
24
+ // The stored value is what the next dialog opens on, so it is written on
25
+ // submit rather than on the click: a dialog the user backed out of must not
26
+ // move where every future launch lands.
28
27
  const offerPlacement = !!model && sessionSupportsPlacement(getSession(model));
29
28
  return (React.createElement(DialogActions, { sx: { flexWrap: 'wrap', rowGap: 1 } },
30
29
  offerPlacement ? (React.createElement(PlacementToggle, { checked: sideBySide, onChange: setSideBySide })) : null,
@@ -37,9 +36,13 @@ export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled
37
36
  hint,
38
37
  React.createElement(Button, { sx: { flexShrink: 0 }, color: "primary", variant: "contained", disabled: submitDisabled, onClick: () => {
39
38
  if (offerPlacement) {
40
- writeLaunchPlacement(sideBySide ? 'splitRight' : 'stack');
39
+ const placement = sideBySide ? 'splitRight' : 'stack';
40
+ writeLaunchPlacement(placement);
41
+ onSubmit(placement);
42
+ }
43
+ else {
44
+ onSubmit(readLaunchPlacement());
41
45
  }
42
- onSubmit();
43
46
  } }, submitLabel),
44
47
  React.createElement(Button, { sx: { flexShrink: 0 }, color: "secondary", variant: "contained", onClick: () => {
45
48
  onCancel();
@@ -40,13 +40,31 @@ test('a panel that launches nothing passes no model, and gets no box', () => {
40
40
  render(React.createElement(SubmitCancelActions, { onSubmit: () => { }, onCancel: () => { } }));
41
41
  expect(toggle()).toBeNull();
42
42
  });
43
- test('submitting writes the placement the launch will read', () => {
44
- render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
43
+ test('submitting hands the launch the placement and remembers it', () => {
44
+ const onSubmit = vi.fn();
45
+ render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: onSubmit, onCancel: () => { } }));
45
46
  toggle().click();
46
47
  expect(toggle().checked).toBe(false);
47
48
  screen.getByText('Submit').click();
49
+ expect(onSubmit).toHaveBeenCalledWith('stack');
48
50
  expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
49
51
  });
52
+ // an unticked box used to reach the launch only through storage, so a browser
53
+ // refusing localStorage launched side by side anyway
54
+ test('the box decides the launch even where storage throws', () => {
55
+ const onSubmit = vi.fn();
56
+ const setItem = vi
57
+ .spyOn(Storage.prototype, 'setItem')
58
+ .mockImplementation(() => {
59
+ throw new Error('SecurityError');
60
+ });
61
+ vi.spyOn(console, 'error').mockImplementation(() => { });
62
+ render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: onSubmit, onCancel: () => { } }));
63
+ toggle().click();
64
+ screen.getByText('Submit').click();
65
+ expect(onSubmit).toHaveBeenCalledWith('stack');
66
+ setItem.mockRestore();
67
+ });
50
68
  // the box is a property of this launch until it is launched; a dialog the user
51
69
  // backed out of should not have moved where every future one lands
52
70
  test('cancelling leaves the stored placement alone', () => {
@@ -1,9 +1,11 @@
1
1
  import type { Feat } from './types';
2
2
  import type { Feature } from '@jbrowse/core/util';
3
- export declare function calculateProteinSequence({ cds, sequence, geneticCodeId, }: {
3
+ import type { TranslExcept } from '@jbrowse/core/util/geneticCodes';
4
+ export declare function calculateProteinSequence({ cds, sequence, geneticCodeId, translExcept, }: {
4
5
  cds: Feat[];
5
6
  sequence: string;
6
7
  geneticCodeId?: number;
8
+ translExcept?: TranslExcept[];
7
9
  }): string;
8
10
  export declare function revlist(list: Feat[], seqlen: number): {
9
11
  start: number;
@@ -11,6 +13,11 @@ export declare function revlist(list: Feat[], seqlen: number): {
11
13
  type?: string;
12
14
  phase?: number;
13
15
  }[];
16
+ /**
17
+ * The translation core's own feature panel shows: the contig's or the
18
+ * feature's genetic code, its alternative initiators, and any `transl_except`
19
+ * (RefSeq's selenocysteines), read off the transcript or its CDS as core does.
20
+ */
14
21
  export declare function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }: {
15
22
  seq: string;
16
23
  feature: Feature;
@@ -1,25 +1,20 @@
1
1
  import { revcom } from '@jbrowse/core/util';
2
2
  import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
3
- import { getGeneticCode, parseTranslTable, } from '@jbrowse/core/util/geneticCodes';
3
+ import { getGeneticCode, parseTranslTable, relativizeTranslExcept, } from '@jbrowse/core/util/geneticCodes';
4
4
  // `@jbrowse/core/util/convertCodingSequenceToPeptides` and
5
- // `@jbrowse/core/util/geneticCodes` are deep paths, so unlike the
6
- // `@jbrowse/core/util` barrel they are absent from ReExports and get bundled
7
- // rather than resolved out of the host's JBrowseExports. That is what makes
8
- // reusing core's translation safe across every host a config names: this module
9
- // previously built its codon table at module scope from the barrel's
10
- // `defaultCodonTable`, and a core build that dropped that export turned it into
11
- // `Object.keys(undefined)` while the UMD was still evaluating -- the plugin
12
- // global was never assigned and PluginLoader error-paged the whole app.
13
- export function calculateProteinSequence({ cds, sequence, geneticCodeId, }) {
14
- // `starts` is deliberately not passed: @jbrowse/core 4.3.0's signature has no
15
- // such parameter, so alternative initiators (GTG under table 11, ATA under
16
- // table 2) render as their internal residue rather than M. Core main added it;
17
- // pass it here when msaview's @jbrowse/core floor reaches that release.
18
- const { codonTable } = getGeneticCode(geneticCodeId);
5
+ // `@jbrowse/core/util/geneticCodes` are deep paths absent from ReExports, so
6
+ // they are bundled rather than resolved out of the host's JBrowseExports: every
7
+ // host runs the version this build installs. Importing translation from the
8
+ // `@jbrowse/core/util` barrel instead is what error-paged the whole app when a
9
+ // core build dropped `defaultCodonTable`.
10
+ export function calculateProteinSequence({ cds, sequence, geneticCodeId, translExcept, }) {
11
+ const { codonTable, starts } = getGeneticCode(geneticCodeId);
19
12
  return convertCodingSequenceToPeptides({
20
13
  cds,
21
14
  sequence,
22
15
  codonTable,
16
+ starts,
17
+ translExcept,
23
18
  });
24
19
  }
25
20
  // The CDS list is sorted by start, so adjacent comparison is the whole job.
@@ -38,8 +33,13 @@ export function revlist(list, seqlen) {
38
33
  }))
39
34
  .toSorted((a, b) => a.start - b.start);
40
35
  }
36
+ /**
37
+ * The translation core's own feature panel shows: the contig's or the
38
+ * feature's genetic code, its alternative initiators, and any `transl_except`
39
+ * (RefSeq's selenocysteines), read off the transcript or its CDS as core does.
40
+ */
41
41
  export function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }) {
42
- const { subfeatures, start, strand } = feature.toJSON();
42
+ const { subfeatures, start, end, strand } = feature.toJSON();
43
43
  const cds = dedupe(subfeatures
44
44
  ?.toSorted((a, b) => a.start - b.start)
45
45
  .map(sub => ({
@@ -48,19 +48,24 @@ export function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCod
48
48
  end: sub.end - start,
49
49
  }))
50
50
  .filter(subfeature => subfeature.type === 'CDS') ?? []);
51
- // RefSeq declares transl_table=2 on a mitochondrial CDS, usually on the CDS
52
- // rather than the transcript. GENCODE and UCSC declare nothing, so without
53
- // the assembly's code all 13 human mitochondrial proteins read TGA as a stop
54
- // and ATA as I.
55
51
  const cdsSubfeature = feature
56
52
  .get('subfeatures')
57
53
  ?.find((f) => f.get('type')?.toLowerCase() === 'cds');
58
54
  const geneticCodeId = parseTranslTable(feature.get('transl_table')) ??
59
55
  parseTranslTable(cdsSubfeature?.get('transl_table')) ??
60
56
  assemblyGeneticCodeId;
57
+ const rawTranslExcept = feature.get('transl_except') ?? cdsSubfeature?.get('transl_except');
61
58
  return calculateProteinSequence({
62
59
  cds: strand === -1 ? revlist(cds, seq.length) : cds,
63
60
  sequence: strand === -1 ? revcom(seq) : seq,
64
61
  geneticCodeId,
62
+ translExcept: rawTranslExcept
63
+ ? relativizeTranslExcept({
64
+ raw: rawTranslExcept,
65
+ featureStart: start,
66
+ featureLength: end - start,
67
+ strand,
68
+ })
69
+ : undefined,
65
70
  });
66
71
  }
@@ -0,0 +1,23 @@
1
+ import type { MsaViewPlacement } from '../../utils/workspaces';
2
+ import type { Feature } from '@jbrowse/core/util';
3
+ import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
4
+ /** how a view whose alignment the plugin builds opens */
5
+ export declare const builtAlignmentLook: {
6
+ drawNodeBubbles: boolean;
7
+ colWidth: number;
8
+ rowHeight: number;
9
+ };
10
+ /**
11
+ * Every dialog launch: an MSA view tied to the genome view it came from, and
12
+ * through `feature` to the transcript whose codons its query row is read by.
13
+ */
14
+ export declare function launchConnectedView({ view, feature, placement, ...snapshot }: {
15
+ view: LinearGenomeViewModel;
16
+ feature?: Feature;
17
+ placement: MsaViewPlacement;
18
+ } & Record<string, unknown>): void;
19
+ /** runs a panel's launch, closing the dialog on success and keeping the error */
20
+ export declare function useLaunchSubmit(handleClose: () => void): {
21
+ launchError: unknown;
22
+ submit: (launch: () => void) => void;
23
+ };
@@ -0,0 +1,39 @@
1
+ import { useState } from 'react';
2
+ import { getSession } from '@jbrowse/core/util';
3
+ import { launchMsaView } from '../../utils/launchMsaView';
4
+ /** how a view whose alignment the plugin builds opens */
5
+ export const builtAlignmentLook = {
6
+ drawNodeBubbles: true,
7
+ colWidth: 10,
8
+ rowHeight: 12,
9
+ };
10
+ /**
11
+ * Every dialog launch: an MSA view tied to the genome view it came from, and
12
+ * through `feature` to the transcript whose codons its query row is read by.
13
+ */
14
+ export function launchConnectedView({ view, feature, placement, ...snapshot }) {
15
+ launchMsaView(getSession(view), {
16
+ placement,
17
+ connectedViewId: view.id,
18
+ connectedFeature: feature?.toJSON(),
19
+ ...snapshot,
20
+ });
21
+ }
22
+ /** runs a panel's launch, closing the dialog on success and keeping the error */
23
+ export function useLaunchSubmit(handleClose) {
24
+ const [launchError, setLaunchError] = useState();
25
+ return {
26
+ launchError,
27
+ submit: (launch) => {
28
+ try {
29
+ setLaunchError(undefined);
30
+ launch();
31
+ handleClose();
32
+ }
33
+ catch (e) {
34
+ console.error(e);
35
+ setLaunchError(e);
36
+ }
37
+ },
38
+ };
39
+ }
@@ -1,14 +1,11 @@
1
1
  import { getUngappedSequence, parseMSA } from 'msa-parsers';
2
+ import { cleanProteinSequence } from './util';
2
3
  /**
3
- * A stop codon is present in the transcript's translation and absent from
4
- * anything an aligner returns, and case is not meaningful in either.
4
+ * The translation as a search launch would have sent it, so a row that launch
5
+ * built matches exactly; case means nothing on either side.
5
6
  */
6
7
  function normalize(seq) {
7
- return seq
8
- .replaceAll('*', '')
9
- .replaceAll('-', '')
10
- .replaceAll('.', '')
11
- .toUpperCase();
8
+ return cleanProteinSequence(seq).toUpperCase();
12
9
  }
13
10
  function identityOverOverlap(a, b) {
14
11
  const len = Math.min(a.length, b.length);
@@ -13,5 +13,11 @@ export declare function getTranscriptDisplayName(val?: Feature): string;
13
13
  export declare function getGeneDisplayName(val?: Feature): string;
14
14
  export declare function getBlastViewTitle(feature: Feature, transcript: Feature): string;
15
15
  export declare function getSortedTranscriptFeatures(feature: Feature): Feature[];
16
+ /**
17
+ * A translation as a query, still one residue per codon: g2p numbers codons,
18
+ * and the query row is read through it. So the partial first codon (`&`), an
19
+ * internal stop and an unreadable codon become X, and only what trails the
20
+ * last whole codon is dropped.
21
+ */
16
22
  export declare function cleanProteinSequence(seq: string): string;
17
23
  export declare function getGeneIdentifiers(feature: Feature): string[];
@@ -70,8 +70,14 @@ export function getSortedTranscriptFeatures(feature) {
70
70
  const transcripts = getTranscriptFeatures(feature);
71
71
  return transcripts.toSorted((a, b) => getTranscriptLength(b).len - getTranscriptLength(a).len);
72
72
  }
73
+ /**
74
+ * A translation as a query, still one residue per codon: g2p numbers codons,
75
+ * and the query row is read through it. So the partial first codon (`&`), an
76
+ * internal stop and an unreadable codon become X, and only what trails the
77
+ * last whole codon is dropped.
78
+ */
73
79
  export function cleanProteinSequence(seq) {
74
- return seq.replaceAll('*', '').replaceAll('&', '');
80
+ return seq.replace(/[*&]+$/, '').replaceAll(/[*&]/g, 'X');
75
81
  }
76
82
  export function getGeneIdentifiers(feature) {
77
83
  return uniqueDefined([
@@ -53,6 +53,21 @@ test('searchParams is a source, stored under the name the dialog uses', () => {
53
53
  test('a launch naming no source at all is refused', () => {
54
54
  expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
55
55
  });
56
+ test('region and the data-layer keys reach the view as snapshot properties', () => {
57
+ const region = { row: 'Human', start: 245, end: 249 };
58
+ const clades = [{ mrca: ['Human', 'Mouse'], tips: 2, mark: 'bracket' }];
59
+ const snapshot = launch({
60
+ data: { msa: '>Human\nMEEP' },
61
+ region,
62
+ clades,
63
+ relativeTo: 'Human',
64
+ gffFilehandle: { uri: 'http://example.com/a.gff' },
65
+ });
66
+ expect(snapshot.region).toEqual(region);
67
+ expect(snapshot.clades).toEqual(clades);
68
+ expect(snapshot.relativeTo).toBe('Human');
69
+ expect(snapshot.gffFilehandle).toEqual({ uri: 'http://example.com/a.gff' });
70
+ });
56
71
  test('one field set is enough to need init', () => {
57
72
  const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
58
73
  expect(snapshot.init).toEqual({
@@ -32,6 +32,9 @@ export function loadStoredData(self) {
32
32
  if (storedData.treeMetadata) {
33
33
  self.setTreeMetadata(storedData.treeMetadata);
34
34
  }
35
+ if (storedData.gff) {
36
+ self.setGFF(storedData.gff);
37
+ }
35
38
  self.setLastStoredData(currentData(self));
36
39
  });
37
40
  }
@@ -59,15 +62,21 @@ export function loadStoredData(self) {
59
62
  })();
60
63
  }
61
64
  }
65
+ /**
66
+ * Everything react-msaview drops from the snapshot once it passes 50kB. The
67
+ * GFF is among them: a local one's filehandle is cleared once it loads, so
68
+ * this row is the only copy.
69
+ */
62
70
  function currentData(self) {
63
- const { msa, tree, treeMetadata } = self.data;
64
- return { msa, tree, treeMetadata };
71
+ const { msa, tree, treeMetadata, gff } = self.data;
72
+ return { msa, tree, treeMetadata, gff };
65
73
  }
66
74
  function sameData(a, b) {
67
75
  return (!!a &&
68
76
  a.msa === b.msa &&
69
77
  a.tree === b.tree &&
70
- a.treeMetadata === b.treeMetadata);
78
+ a.treeMetadata === b.treeMetadata &&
79
+ a.gff === b.gff);
71
80
  }
72
81
  /**
73
82
  * Keep IndexedDB holding what the view holds. The first run writes a new row and
@@ -0,0 +1,23 @@
1
+ import type { Region } from 'react-msaview';
2
+ interface RegionModel {
3
+ region?: Region;
4
+ viewInitialized: boolean;
5
+ numColumns: number;
6
+ treeFilehandle?: unknown;
7
+ data: {
8
+ tree?: string;
9
+ };
10
+ zoomToRegion: (region: Region) => void;
11
+ setRegion: (region?: Region) => void;
12
+ }
13
+ /**
14
+ * Zoom onto the launch's `region` once, then forget it, so a reloaded session
15
+ * opens where the reader left it rather than back on the region.
16
+ *
17
+ * zoomToRegion is a no-op until the view has a width, and it resolves residues
18
+ * through the visible columns, so it waits for the alignment and for a tree
19
+ * file too: a clade the tree collapses hides columns and would move the
20
+ * target after the zoom.
21
+ */
22
+ export declare function applyRegion(self: RegionModel): void;
23
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Zoom onto the launch's `region` once, then forget it, so a reloaded session
3
+ * opens where the reader left it rather than back on the region.
4
+ *
5
+ * zoomToRegion is a no-op until the view has a width, and it resolves residues
6
+ * through the visible columns, so it waits for the alignment and for a tree
7
+ * file too: a clade the tree collapses hides columns and would move the
8
+ * target after the zoom.
9
+ */
10
+ export function applyRegion(self) {
11
+ const { region } = self;
12
+ if (region &&
13
+ self.viewInitialized &&
14
+ self.numColumns > 0 &&
15
+ !(self.treeFilehandle && !self.data.tree)) {
16
+ self.zoomToRegion(region);
17
+ self.setRegion(undefined);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ import { expect, test, vi } from 'vitest';
2
+ import { applyRegion } from './applyRegion';
3
+ const REGION = { row: 'Human', start: 245, end: 249 };
4
+ function makeModel(over = {}) {
5
+ const model = {
6
+ region: REGION,
7
+ viewInitialized: true,
8
+ numColumns: 400,
9
+ treeFilehandle: undefined,
10
+ data: {},
11
+ zoomToRegion: vi.fn(),
12
+ setRegion: vi.fn((arg) => {
13
+ model.region = arg;
14
+ }),
15
+ ...over,
16
+ };
17
+ return model;
18
+ }
19
+ test('zooms onto the region once, then forgets it', () => {
20
+ const model = makeModel();
21
+ applyRegion(model);
22
+ applyRegion(model);
23
+ expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
24
+ expect(model.zoomToRegion).toHaveBeenCalledWith(REGION);
25
+ expect(model.region).toBeUndefined();
26
+ });
27
+ test('waits for the view to have a width', () => {
28
+ const model = makeModel({ viewInitialized: false });
29
+ applyRegion(model);
30
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
31
+ expect(model.region).toEqual(REGION);
32
+ });
33
+ test('waits for the alignment', () => {
34
+ const model = makeModel({ numColumns: 0 });
35
+ applyRegion(model);
36
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
37
+ });
38
+ test('waits for a tree file, whose collapsed clades move the columns', () => {
39
+ const model = makeModel({ treeFilehandle: { uri: 'x.nh' } });
40
+ applyRegion(model);
41
+ expect(model.zoomToRegion).not.toHaveBeenCalled();
42
+ model.data.tree = '(a,b);';
43
+ applyRegion(model);
44
+ expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
45
+ });
@@ -84,8 +84,9 @@ export async function doLaunchBlast({ self, scope, }) {
84
84
  await saveBlastResult({
85
85
  proteinSequence: query,
86
86
  blastDatabase: params.blastDatabase,
87
- msaAlgorithm: params.msaAlgorithm,
87
+ msaAlgorithm: queryRow ? undefined : (params.msaAlgorithm ?? 'browser'),
88
88
  searchProgram: params.searchProgram,
89
+ maxHits,
89
90
  msa,
90
91
  tree,
91
92
  treeMetadata: treeMetadataJson,
@@ -67,6 +67,18 @@ test('bare hits go to the chosen aligner, with the query first', async () => {
67
67
  expect(result.msa).toBe('aligned');
68
68
  expect(result.tree).toBe('tree');
69
69
  });
70
+ // the history names the aligner that ran, which for a spec naming none is the
71
+ // in-browser one -- not a key reading `db:undefined:...`
72
+ test('a search naming no aligner is saved as aligned in the browser', async () => {
73
+ blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
74
+ mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
75
+ await launch(makeModel({
76
+ blastDatabase: 'uniprotkb_swissprot',
77
+ maxHits: 20,
78
+ proteinSequence: 'MKWVTF',
79
+ }));
80
+ expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({ msaAlgorithm: 'browser', maxHits: 20 }));
81
+ });
70
82
  test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
71
83
  phmmer.mockResolvedValue({
72
84
  rid: 'job',
@@ -175,7 +175,7 @@ describe('the query row sequence', () => {
175
175
  await launch({
176
176
  self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
177
177
  });
178
- expect(queryRowSent()).toBe('MAGGAWGR');
178
+ expect(queryRowSent()).toBe('MAGGXAWGR');
179
179
  });
180
180
  test('throws when neither a sequence nor a representative is available', async () => {
181
181
  mockFetchProtein.mockResolvedValue(undefined);
@@ -1,8 +1,8 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
- import { hasHoverPosition, hasQueryRow, transcriptPosToVisibleCol, } from './util';
2
+ import { hasHoverPosition, hasQueryRow, querySeqPosToVisibleCol, transcriptPosToVisibleCol, } from './util';
3
3
  export function genomeToMSA({ model }) {
4
4
  const { assemblyManager, hovered } = getSession(model);
5
- const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
5
+ const { transcriptToMsaMap, connectedView, mafRegion } = model;
6
6
  if (!connectedView?.initialized ||
7
7
  !hasHoverPosition(hovered) ||
8
8
  !hasQueryRow(model)) {
@@ -36,7 +36,7 @@ export function genomeToMSA({ model }) {
36
36
  genomePos >= mafRegion.end) {
37
37
  return undefined;
38
38
  }
39
- return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start);
39
+ return querySeqPosToVisibleCol(model, genomePos - mafRegion.start);
40
40
  }
41
41
  // session.hovered is global -- set by whichever LinearGenomeView the cursor
42
42
  // was last over, on any assembly -- so the refName gate is load bearing: