jbrowse-plugin-msaview 5.16.0 → 5.16.2

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 (106) hide show
  1. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.d.ts +19 -1
  2. package/dist/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.js +55 -3
  3. package/dist/BgzipFastaMsaAdapter/configSchema.d.ts +10 -0
  4. package/dist/BgzipFastaMsaAdapter/configSchema.js +5 -0
  5. package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +23 -13
  6. package/dist/LaunchMsaView/components/PreLoadedMSA/fetchMSAData.d.ts +11 -4
  7. package/dist/LaunchMsaView/components/PreLoadedMSA/fetchMSAData.js +4 -4
  8. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +2 -2
  9. package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -6
  10. package/dist/LaunchMsaView/components/pickSelectedId.test.d.ts +1 -0
  11. package/dist/LaunchMsaView/components/pickSelectedId.test.js +17 -0
  12. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +5 -3
  13. package/dist/LaunchMsaView/components/useTranscriptSelection.js +10 -11
  14. package/dist/LaunchMsaView/detectQueryRow.d.ts +1 -1
  15. package/dist/LaunchMsaView/detectQueryRow.js +37 -25
  16. package/dist/LaunchMsaView/detectQueryRow.test.js +9 -0
  17. package/dist/LaunchMsaView/featureMatchesId.test.d.ts +1 -0
  18. package/dist/LaunchMsaView/featureMatchesId.test.js +74 -0
  19. package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -1
  20. package/dist/LaunchMsaView/useQueryRowName.js +2 -2
  21. package/dist/LaunchMsaView/util.d.ts +2 -0
  22. package/dist/LaunchMsaView/util.js +38 -0
  23. package/dist/MsaViewPanel/displayedRefName.d.ts +19 -0
  24. package/dist/MsaViewPanel/displayedRefName.js +16 -0
  25. package/dist/MsaViewPanel/fetchIndexedMsa.js +10 -23
  26. package/dist/MsaViewPanel/handleMsaClick.test.d.ts +1 -0
  27. package/dist/MsaViewPanel/handleMsaClick.test.js +59 -0
  28. package/dist/MsaViewPanel/model.js +13 -11
  29. package/dist/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.d.ts +22 -0
  30. package/dist/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.js +92 -0
  31. package/dist/OrthoFinderMsaAdapter/configSchema.d.ts +12 -0
  32. package/dist/OrthoFinderMsaAdapter/configSchema.js +13 -0
  33. package/dist/OrthoFinderMsaAdapter/index.d.ts +2 -0
  34. package/dist/OrthoFinderMsaAdapter/index.js +14 -0
  35. package/dist/OrthoFinderMsaAdapter/parseResults.d.ts +31 -0
  36. package/dist/OrthoFinderMsaAdapter/parseResults.js +81 -0
  37. package/dist/OrthoFinderMsaAdapter/parseResults.test.d.ts +1 -0
  38. package/dist/OrthoFinderMsaAdapter/parseResults.test.js +47 -0
  39. package/dist/chunks/BgzipFastaMsaAdapter-Y3LYQRQL.js +6 -0
  40. package/dist/chunks/BgzipFastaMsaAdapter-Y3LYQRQL.js.map +7 -0
  41. package/dist/chunks/LaunchMsaViewDialog-CZR76VIV.js +3 -0
  42. package/dist/chunks/LaunchMsaViewDialog-CZR76VIV.js.map +7 -0
  43. package/dist/chunks/MsaViewPanel-6ETJVX27.js +2 -0
  44. package/dist/chunks/{MsaViewPanel-UWT4FWLE.js.map → MsaViewPanel-6ETJVX27.js.map} +1 -1
  45. package/dist/chunks/OrthoFinderMsaAdapter-BAVVTZHO.js +2 -0
  46. package/dist/chunks/OrthoFinderMsaAdapter-BAVVTZHO.js.map +7 -0
  47. package/dist/chunks/chunk-3G2F2Y7O.js +2 -0
  48. package/dist/chunks/chunk-3G2F2Y7O.js.map +7 -0
  49. package/dist/chunks/chunk-CRESEEY4.js +2 -0
  50. package/dist/chunks/chunk-CRESEEY4.js.map +7 -0
  51. package/dist/chunks/chunk-ET6J3BQX.js +3 -0
  52. package/dist/chunks/chunk-ET6J3BQX.js.map +7 -0
  53. package/dist/chunks/{chunk-AO3YMCKS.js → chunk-TEQX6FF3.js} +6 -6
  54. package/dist/chunks/{chunk-AO3YMCKS.js.map → chunk-TEQX6FF3.js.map} +1 -1
  55. package/dist/chunks/chunk-WE35XW3J.js +3 -0
  56. package/dist/chunks/chunk-WE35XW3J.js.map +7 -0
  57. package/dist/chunks/chunk-YN7VVNZR.js +2 -0
  58. package/dist/chunks/chunk-YN7VVNZR.js.map +7 -0
  59. package/dist/chunks/dist-NZDQ46PF.js +2 -0
  60. package/dist/chunks/model-XMH3QSZV.js +11 -0
  61. package/dist/chunks/model-XMH3QSZV.js.map +7 -0
  62. package/dist/index.js +2 -0
  63. package/dist/jbrowse-plugin-msaview.esm.js +2 -2
  64. package/dist/jbrowse-plugin-msaview.esm.js.map +4 -4
  65. package/dist/utils/msaIndex.d.ts +14 -0
  66. package/dist/utils/msaIndex.js +13 -0
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
  70. package/src/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.ts +74 -3
  71. package/src/BgzipFastaMsaAdapter/configSchema.ts +6 -0
  72. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +31 -21
  73. package/src/LaunchMsaView/components/PreLoadedMSA/fetchMSAData.ts +19 -7
  74. package/src/LaunchMsaView/components/TranscriptSelector.tsx +4 -7
  75. package/src/LaunchMsaView/components/pickSelectedId.test.ts +24 -0
  76. package/src/LaunchMsaView/components/useTranscriptSelection.ts +13 -14
  77. package/src/LaunchMsaView/detectQueryRow.test.ts +11 -0
  78. package/src/LaunchMsaView/detectQueryRow.ts +49 -33
  79. package/src/LaunchMsaView/featureMatchesId.test.ts +97 -0
  80. package/src/LaunchMsaView/useQueryRowName.ts +7 -3
  81. package/src/LaunchMsaView/util.ts +52 -0
  82. package/src/MsaViewPanel/displayedRefName.ts +30 -0
  83. package/src/MsaViewPanel/fetchIndexedMsa.ts +15 -26
  84. package/src/MsaViewPanel/handleMsaClick.test.ts +75 -0
  85. package/src/MsaViewPanel/model.ts +17 -11
  86. package/src/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.ts +124 -0
  87. package/src/OrthoFinderMsaAdapter/configSchema.ts +19 -0
  88. package/src/OrthoFinderMsaAdapter/index.ts +19 -0
  89. package/src/OrthoFinderMsaAdapter/parseResults.test.ts +88 -0
  90. package/src/OrthoFinderMsaAdapter/parseResults.ts +97 -0
  91. package/src/index.ts +2 -0
  92. package/src/utils/msaIndex.ts +32 -0
  93. package/src/version.ts +1 -1
  94. package/dist/chunks/BgzipFastaMsaAdapter-LXM6BNRP.js +0 -4
  95. package/dist/chunks/BgzipFastaMsaAdapter-LXM6BNRP.js.map +0 -7
  96. package/dist/chunks/LaunchMsaViewDialog-CVUQSMRL.js +0 -5
  97. package/dist/chunks/LaunchMsaViewDialog-CVUQSMRL.js.map +0 -7
  98. package/dist/chunks/MsaViewPanel-UWT4FWLE.js +0 -2
  99. package/dist/chunks/chunk-LXN7NIHB.js +0 -2
  100. package/dist/chunks/chunk-LXN7NIHB.js.map +0 -7
  101. package/dist/chunks/chunk-YRZUKVPZ.js +0 -3
  102. package/dist/chunks/chunk-YRZUKVPZ.js.map +0 -7
  103. package/dist/chunks/dist-2VELCAWU.js +0 -2
  104. package/dist/chunks/model-DAIL6BNQ.js +0 -12
  105. package/dist/chunks/model-DAIL6BNQ.js.map +0 -7
  106. /package/dist/chunks/{dist-2VELCAWU.js.map → dist-NZDQ46PF.js.map} +0 -0
@@ -1,12 +1,30 @@
1
1
  import { BaseAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
+ import type { MsaIndexEntry } from '../utils/msaIndex';
2
3
  import type { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
4
+ import type { FileLocation } from '@jbrowse/core/util';
3
5
  export default class BgzipFastaMsaAdapter extends BaseAdapter {
4
6
  configureP: Promise<BaseFeatureDataAdapter> | undefined;
5
7
  msaRowsP: Promise<Map<string, string[]>> | undefined;
8
+ msaIndexP: Promise<Map<string, MsaIndexEntry> | undefined> | undefined;
6
9
  configurePre(): Promise<BaseFeatureDataAdapter<import("@jbrowse/core/configuration").AnyConfigurationModel>>;
7
10
  configure(): Promise<BaseFeatureDataAdapter<import("@jbrowse/core/configuration").AnyConfigurationModel>>;
11
+ readMsaIndex(location: FileLocation): Promise<Map<string, {
12
+ name: string;
13
+ offset: number;
14
+ length: number;
15
+ }>>;
16
+ loadMsaIndex(): Promise<Map<string, {
17
+ name: string;
18
+ offset: number;
19
+ length: number;
20
+ }> | undefined>;
21
+ getMsaIndex(): Promise<Map<string, MsaIndexEntry> | undefined>;
8
22
  groupRows(): Promise<Map<string, string[]>>;
9
23
  getMSARows(): Promise<Map<string, string[]>>;
10
24
  getMSAList(): Promise<string[]>;
11
- getMSA(id: string): Promise<import("@jbrowse/core/util").Feature[]>;
25
+ readIndexedMSA(index: Map<string, MsaIndexEntry>, id: string): Promise<string>;
26
+ readRowsMSA(id: string): Promise<string>;
27
+ getMSA(id: string): Promise<{
28
+ msa: string;
29
+ }>;
12
30
  }
@@ -1,6 +1,8 @@
1
1
  import { readConfObject } from '@jbrowse/core/configuration';
2
2
  import { BaseAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
3
+ import { openLocation } from '@jbrowse/core/util/io';
3
4
  import { firstValueFrom, toArray } from 'rxjs';
5
+ import { parseMsaIndex, readMsaBlock } from '../utils/msaIndex';
4
6
  export default class BgzipFastaMsaAdapter extends BaseAdapter {
5
7
  async configurePre() {
6
8
  const getSubAdapter = this.getSubAdapter;
@@ -22,6 +24,29 @@ export default class BgzipFastaMsaAdapter extends BaseAdapter {
22
24
  });
23
25
  return this.configureP;
24
26
  }
27
+ async readMsaIndex(location) {
28
+ const text = await openLocation(location, this.pluginManager).readFile('utf8');
29
+ return new Map(parseMsaIndex(text).map(e => [e.name, e]));
30
+ }
31
+ // an `.idx` beside the FASTA is used when present, as the host finds the
32
+ // `.fai` and `.gzi`; only one named in the config is required to exist
33
+ async loadMsaIndex() {
34
+ const configured = this.getConf('msaIndexLocation');
35
+ const adapter = await this.configure();
36
+ const beside = withSuffix(adapter.getConf('fastaLocation'), '.idx');
37
+ return 'uri' in configured && !configured.uri
38
+ ? beside
39
+ ? this.readMsaIndex(beside).catch(() => undefined)
40
+ : undefined
41
+ : this.readMsaIndex(configured);
42
+ }
43
+ getMsaIndex() {
44
+ this.msaIndexP ??= this.loadMsaIndex().catch((e) => {
45
+ this.msaIndexP = undefined;
46
+ throw e;
47
+ });
48
+ return this.msaIndexP;
49
+ }
25
50
  async groupRows() {
26
51
  const adapter = await this.configure();
27
52
  const separator = new RegExp(this.getConf('msaRegex'));
@@ -46,12 +71,23 @@ export default class BgzipFastaMsaAdapter extends BaseAdapter {
46
71
  return this.msaRowsP;
47
72
  }
48
73
  async getMSAList() {
49
- return [...(await this.getMSARows()).keys()];
74
+ return [...((await this.getMsaIndex()) ?? (await this.getMSARows())).keys()];
50
75
  }
51
- async getMSA(id) {
76
+ async readIndexedMSA(index, id) {
77
+ const adapter = await this.configure();
78
+ const entry = index.get(id);
79
+ return entry
80
+ ? readMsaBlock({
81
+ filehandle: openLocation(adapter.getConf('fastaLocation'), this.pluginManager),
82
+ gziFilehandle: openLocation(adapter.getConf('gziLocation'), this.pluginManager),
83
+ entry,
84
+ })
85
+ : '';
86
+ }
87
+ async readRowsMSA(id) {
52
88
  const adapter = await this.configure();
53
89
  const rows = (await this.getMSARows()).get(id) ?? [];
54
- return firstValueFrom(adapter
90
+ const features = await firstValueFrom(adapter
55
91
  .getFeaturesInMultipleRegions(rows.map(refName => ({
56
92
  refName,
57
93
  start: 0,
@@ -59,5 +95,21 @@ export default class BgzipFastaMsaAdapter extends BaseAdapter {
59
95
  assemblyName: '',
60
96
  })))
61
97
  .pipe(toArray()));
98
+ return features.map(f => `>${f.get('refName')}\n${f.get('seq')}`).join('\n');
62
99
  }
100
+ async getMSA(id) {
101
+ const index = await this.getMsaIndex();
102
+ return {
103
+ msa: await (index
104
+ ? this.readIndexedMSA(index, id)
105
+ : this.readRowsMSA(id)),
106
+ };
107
+ }
108
+ }
109
+ function withSuffix(location, suffix) {
110
+ return 'uri' in location
111
+ ? { ...location, uri: `${location.uri}${suffix}` }
112
+ : 'localPath' in location
113
+ ? { ...location, localPath: `${location.localPath}${suffix}` }
114
+ : undefined;
63
115
  }
@@ -5,10 +5,20 @@ export default function configSchemaF(pluginManager: PluginManager): import("@jb
5
5
  } & {
6
6
  defaultValue: string;
7
7
  };
8
+ msaIndexLocation: {
9
+ type: "fileLocation";
10
+ } & {
11
+ defaultValue: unknown;
12
+ };
8
13
  } | {
9
14
  readonly msaRegex: {
10
15
  type: "string";
11
16
  } & {
12
17
  defaultValue: string;
13
18
  };
19
+ readonly msaIndexLocation: {
20
+ type: "fileLocation";
21
+ } & {
22
+ defaultValue: unknown;
23
+ };
14
24
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<any, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
@@ -6,6 +6,11 @@ export default function configSchemaF(pluginManager) {
6
6
  type: 'string',
7
7
  defaultValue: '_',
8
8
  },
9
+ msaIndexLocation: {
10
+ type: 'fileLocation',
11
+ description: '`.idx` of name, offset and length per alignment, which lists the set and reads each alignment as one block in place of the per-row .fai. Found beside the FASTA when unset',
12
+ defaultValue: { uri: '', locationType: 'UriLocation' },
13
+ },
9
14
  }, {
10
15
  baseConfiguration: base.configSchema,
11
16
  });
@@ -7,7 +7,7 @@ import { makeStyles } from 'tss-react/mui';
7
7
  import TextField2 from '../../../components/TextField2';
8
8
  import { useFetch } from '../../../utils/useFetch';
9
9
  import { resolveQueryRowName, useQueryRowName } from '../../useQueryRowName';
10
- import { featureMatchesId, getGeneDisplayName, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
10
+ import { findDatasetId, getGeneDisplayName, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
11
11
  import LaunchPanelContent from '../LaunchPanelContent';
12
12
  import QueryRowSelector from '../QueryRowSelector';
13
13
  import SequenceStatusMessage from '../SequenceStatus';
@@ -28,43 +28,53 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
28
28
  const { classes } = useStyles();
29
29
  const { pluginManager } = getEnv(model);
30
30
  const { assemblyNames } = view;
31
+ const assemblyName = assemblyNames[0];
31
32
  const { launchError, submit } = useLaunchSubmit(handleClose);
32
33
  const datasets = readMsaDatasets(session.jbrowse);
33
34
  const [selectedDatasetId, setSelectedDatasetId] = useState(datasets?.[0]?.datasetId);
34
35
  const selectedDataset = datasets?.find(d => d.datasetId === selectedDatasetId);
35
- const { data: msaList, isLoading: msaListLoading, error: msaListFetchError, } = useFetch(selectedDataset ? `${selectedDataset.datasetId}-msa-list` : null, () => fetchMSAList({ config: selectedDataset.adapter, pluginManager }));
36
+ const { data: msaList, isLoading: msaListLoading, error: msaListFetchError, } = useFetch(selectedDataset
37
+ ? `${selectedDataset.datasetId}-${assemblyName}-msa-list`
38
+ : null, () => fetchMSAList({
39
+ config: selectedDataset.adapter,
40
+ pluginManager,
41
+ assemblyName,
42
+ }));
43
+ const msaIds = useMemo(() => msaList && new Set(msaList), [msaList]);
36
44
  const transcriptSelection = useTranscriptSelection({
37
45
  feature,
38
46
  view,
39
- validIds: msaList,
47
+ hasData: msaIds
48
+ ? transcript => !!findDatasetId(transcript, feature, msaIds)
49
+ : undefined,
40
50
  preferredTranscriptId,
41
51
  });
42
52
  const { selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
43
53
  // the dataset's name for this transcript, which a track is free not to use
44
54
  // as the feature's own id
45
- const msaId = selectedTranscript
46
- ? msaList?.find(id => featureMatchesId(selectedTranscript, id))
55
+ const msaId = selectedTranscript && msaIds
56
+ ? findDatasetId(selectedTranscript, feature, msaIds)
47
57
  : undefined;
48
58
  const { data: msaData, isLoading: msaDataLoading, error: msaDataFetchError, } = useFetch(msaId && selectedDataset
49
- ? `${selectedDataset.datasetId}-${msaId}-msa`
59
+ ? `${selectedDataset.datasetId}-${assemblyName}-${msaId}-msa`
50
60
  : null, () => fetchMSA({
51
61
  msaId: msaId,
62
+ assemblyName,
52
63
  config: selectedDataset.adapter,
53
64
  pluginManager,
54
65
  }));
55
- const msaText = useMemo(() => msaData?.map(r => `>${r.get('refName')}\n${r.get('seq')}`).join('\n') ??
56
- '', [msaData]);
66
+ const msaText = msaData?.msa ?? '';
57
67
  // The dataset's row for this transcript used to be assumed -- the launch
58
68
  // named `<transcriptId>_<assembly>` and hoped the file agreed. Nothing
59
69
  // checked, and a name the alignment does not carry fails silently: the view
60
70
  // opens, renders, and never navigates. The row is found by sequence instead,
61
71
  // the same way the Manual tab finds it.
62
- const queryRow = useQueryRowName(msaText, proteinSequence);
72
+ const queryRow = useQueryRowName(msaText, proteinSequence, msaData?.queryRowName);
63
73
  // The name this panel used to launch with unconditionally. A dataset built to
64
74
  // that convention does carry the row, so it is worth falling back to when the
65
75
  // residues do not match closely enough to find it -- but only when the
66
76
  // alignment really has it, which is the check the old code never made.
67
- const querySeqName = resolveQueryRowName(queryRow, `${msaId ?? ''}_${assemblyNames[0] ?? ''}`);
77
+ const querySeqName = resolveQueryRowName(queryRow, `${msaId ?? ''}_${assemblyName ?? ''}`);
68
78
  const e = msaListFetchError ??
69
79
  msaDataFetchError ??
70
80
  transcriptSelection.error ??
@@ -81,11 +91,11 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
81
91
  React.createElement(SanitizedHTML, { html: selectedDataset.description }),
82
92
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
83
93
  msaText ? (React.createElement(QueryRowSelector, { ...queryRow, querySeqName: querySeqName })) : null)) : null)) : null),
84
- React.createElement(SubmitCancelActions, { model: model, hint: msaDataLoading ? (React.createElement(LoadingEllipses, { message: "Loading alignment" })) : !!msaData?.length && !querySeqName ? (React.createElement(Typography, { color: "textSecondary", variant: "body2" }, "no row matches this transcript")) : (React.createElement(SequenceStatusMessage, { status: sequenceStatus })),
94
+ React.createElement(SubmitCancelActions, { model: model, hint: msaDataLoading ? (React.createElement(LoadingEllipses, { message: "Loading alignment" })) : !!msaText && !querySeqName ? (React.createElement(Typography, { color: "textSecondary", variant: "body2" }, "no row matches this transcript")) : (React.createElement(SequenceStatusMessage, { status: sequenceStatus })),
85
95
  // launching without a query row opens a view that renders and then
86
96
  // never navigates, which reads as a broken feature rather than a
87
97
  // dataset that does not cover this gene
88
- submitDisabled: !selectedTranscript || !msaData?.length || !querySeqName, onSubmit: placement => {
98
+ submitDisabled: !selectedTranscript || !msaText || !querySeqName, onSubmit: placement => {
89
99
  if (selectedTranscript && msaText) {
90
100
  submit(() => launchConnectedView({
91
101
  view,
@@ -100,7 +110,7 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
100
110
  colorSchemeName: 'percent_identity_dynamic',
101
111
  querySeqName,
102
112
  querySeqOffset: queryRow.querySeqOffset,
103
- data: { msa: msaText },
113
+ data: { msa: msaText, tree: msaData?.tree },
104
114
  }));
105
115
  }
106
116
  }, onCancel: handleClose })));
@@ -1,12 +1,19 @@
1
1
  import type PluginManager from '@jbrowse/core/PluginManager';
2
2
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
3
- import type { Feature } from '@jbrowse/core/util';
4
- export declare function fetchMSAList({ config, pluginManager, }: {
3
+ export interface DatasetMsa {
4
+ msa: string;
5
+ tree?: string;
6
+ /** the row the dataset names as the protein's own, preferred over a row that matches it equally */
7
+ queryRowName?: string;
8
+ }
9
+ export declare function fetchMSAList({ config, pluginManager, assemblyName, }: {
5
10
  config: AnyConfigurationModel;
6
11
  pluginManager: PluginManager;
12
+ assemblyName?: string;
7
13
  }): Promise<string[]>;
8
- export declare function fetchMSA({ config, pluginManager, msaId, }: {
14
+ export declare function fetchMSA({ config, pluginManager, msaId, assemblyName, }: {
9
15
  config: AnyConfigurationModel;
10
16
  pluginManager: PluginManager;
11
17
  msaId: string;
12
- }): Promise<Feature[]>;
18
+ assemblyName?: string;
19
+ }): Promise<DatasetMsa>;
@@ -3,11 +3,11 @@ async function getMsaAdapter(pluginManager, config) {
3
3
  const result = await getAdapter(pluginManager, 'msa', config);
4
4
  return result.dataAdapter;
5
5
  }
6
- export async function fetchMSAList({ config, pluginManager, }) {
6
+ export async function fetchMSAList({ config, pluginManager, assemblyName, }) {
7
7
  const adapter = await getMsaAdapter(pluginManager, config);
8
- return adapter.getMSAList();
8
+ return adapter.getMSAList(assemblyName);
9
9
  }
10
- export async function fetchMSA({ config, pluginManager, msaId, }) {
10
+ export async function fetchMSA({ config, pluginManager, msaId, assemblyName, }) {
11
11
  const adapter = await getMsaAdapter(pluginManager, config);
12
- return adapter.getMSA(msaId);
12
+ return adapter.getMSA(msaId, assemblyName);
13
13
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { SequenceStatus } from './SequenceStatus';
3
3
  import type { Feature } from '@jbrowse/core/util';
4
- declare const TranscriptSelector: ({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus, validIds, }: {
4
+ declare const TranscriptSelector: ({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus, hasData, }: {
5
5
  feature: Feature;
6
6
  options: Feature[];
7
7
  selectedId: string;
@@ -9,6 +9,6 @@ declare const TranscriptSelector: ({ feature, options, selectedId, selectedTrans
9
9
  setSelectedId: (transcriptId: string) => void;
10
10
  proteinSequence: string | undefined;
11
11
  sequenceStatus?: SequenceStatus;
12
- validIds?: string[];
12
+ hasData?: (transcript: Feature) => boolean;
13
13
  }) => React.JSX.Element;
14
14
  export default TranscriptSelector;
@@ -3,7 +3,7 @@ import { Button, MenuItem, TextField } from '@mui/material';
3
3
  import { observer } from 'mobx-react';
4
4
  import { makeStyles } from 'tss-react/mui';
5
5
  import ReadOnlyTextField2 from '../../components/ReadOnlyTextField2';
6
- import { featureMatchesId, getGeneDisplayName, getId, getTranscriptDisplayName, getTranscriptLength, } from '../util';
6
+ import { getGeneDisplayName, getId, getTranscriptDisplayName, getTranscriptLength, } from '../util';
7
7
  const useStyles = makeStyles()({
8
8
  flex: {
9
9
  display: 'flex',
@@ -16,7 +16,7 @@ const useStyles = makeStyles()({
16
16
  marginLeft: 20,
17
17
  },
18
18
  });
19
- const TranscriptSelector = observer(function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus = 'ready', validIds, }) {
19
+ const TranscriptSelector = observer(function TranscriptSelector({ feature, options, selectedId, selectedTranscript, setSelectedId, proteinSequence, sequenceStatus = 'ready', hasData, }) {
20
20
  const { classes } = useStyles();
21
21
  const [showSequence, setShowSequence] = useState(false);
22
22
  return (React.createElement(React.Fragment, null,
@@ -30,9 +30,7 @@ const TranscriptSelector = observer(function TranscriptSelector({ feature, optio
30
30
  helperText: "the query row, so the alignment stays linked to the genome view", select: true, className: classes.minWidth, value: selectedId, onChange: event => {
31
31
  setSelectedId(event.target.value);
32
32
  } }, options.map(val => {
33
- const inSet = validIds
34
- ? validIds.some(id => featureMatchesId(val, id))
35
- : true;
33
+ const inSet = hasData ? hasData(val) : true;
36
34
  const { len, mod } = getTranscriptLength(val);
37
35
  return (React.createElement(MenuItem, { value: getId(val), key: val.id(), disabled: !inSet },
38
36
  getTranscriptDisplayName(val),
@@ -41,7 +39,7 @@ const TranscriptSelector = observer(function TranscriptSelector({ feature, optio
41
39
  " aa)",
42
40
  ' ',
43
41
  mod ? ` (possible fragment)` : '',
44
- validIds ? (inSet ? ' (has data)' : ' (no data)') : ''));
42
+ hasData ? (inSet ? ' (has data)' : ' (no data)') : ''));
45
43
  })),
46
44
  React.createElement("div", { className: classes.centered },
47
45
  React.createElement(Button, { variant: "contained", color: "primary", onClick: () => {
@@ -0,0 +1,17 @@
1
+ import { SimpleFeature } from '@jbrowse/core/util';
2
+ import { expect, test } from 'vitest';
3
+ import { pickSelectedId } from './useTranscriptSelection';
4
+ const transcript = (id) => new SimpleFeature({ uniqueId: id, refName: '2L', start: 0, end: 10 });
5
+ const options = ['t1', 't2', 't3'].map(transcript);
6
+ test('with no hasData the selection always stands', () => {
7
+ expect(pickSelectedId('t2', options, undefined)).toBe('t2');
8
+ });
9
+ test('a selection with data stands', () => {
10
+ expect(pickSelectedId('t2', options, () => true)).toBe('t2');
11
+ });
12
+ test('a selection without data moves to the first option that has some', () => {
13
+ expect(pickSelectedId('t1', options, t => t.id() !== 't1')).toBe('t2');
14
+ });
15
+ test('with no option holding data the selection stands', () => {
16
+ expect(pickSelectedId('t3', options, () => false)).toBe('t3');
17
+ });
@@ -1,11 +1,13 @@
1
1
  import type { SequenceStatus } from './SequenceStatus';
2
2
  import type { Feature } from '@jbrowse/core/util';
3
- export declare function useTranscriptSelection({ feature, view, validIds, preferredTranscriptId, }: {
3
+ export declare function pickSelectedId(currentId: string, options: Feature[], hasData: ((transcript: Feature) => boolean) | undefined): string;
4
+ export declare function useTranscriptSelection({ feature, view, hasData, preferredTranscriptId, }: {
4
5
  feature: Feature;
5
6
  view: {
6
7
  assemblyNames?: string[];
7
8
  } | undefined;
8
- validIds?: string[];
9
+ /** which transcripts the panel can offer, when it can only offer some */
10
+ hasData?: (transcript: Feature) => boolean;
9
11
  /**
10
12
  * the isoform the user right-clicked, when the dialog opened on its gene.
11
13
  * Ignored when this gene does not carry it, so a stale id falls back to the
@@ -21,5 +23,5 @@ export declare function useTranscriptSelection({ feature, view, validIds, prefer
21
23
  error: unknown;
22
24
  isLoading: boolean;
23
25
  sequenceStatus: SequenceStatus;
24
- validIds: string[] | undefined;
26
+ hasData: ((transcript: Feature) => boolean) | undefined;
25
27
  };
@@ -1,19 +1,18 @@
1
1
  import { useMemo, useState } from 'react';
2
2
  import { featureMatchesId, getId, getSortedTranscriptFeatures } from '../util';
3
3
  import { useFeatureSequence } from './useFeatureSequence';
4
- // Keep the current selection if it's valid for the given validIds, otherwise
5
- // fall back to the first valid option (or the current id if none qualify). With
6
- // no validIds constraint the current selection always stands.
7
- function pickSelectedId(currentId, options, validIds) {
8
- if (!validIds?.length) {
4
+ // Keep the current selection if it has data, otherwise fall back to the first
5
+ // option that does (or the current id if none do). With no hasData constraint
6
+ // the current selection always stands.
7
+ export function pickSelectedId(currentId, options, hasData) {
8
+ if (!hasData) {
9
9
  return currentId;
10
10
  }
11
- const isValid = (opt) => validIds.some(id => featureMatchesId(opt, id));
12
11
  const current = options.find(opt => getId(opt) === currentId);
13
- if (current && isValid(current)) {
12
+ if (current && hasData(current)) {
14
13
  return currentId;
15
14
  }
16
- const firstValid = options.find(isValid);
15
+ const firstValid = options.find(hasData);
17
16
  return firstValid ? getId(firstValid) : currentId;
18
17
  }
19
18
  /**
@@ -31,11 +30,11 @@ function sequenceStatus({ error, isLoading, proteinSequence, }) {
31
30
  }
32
31
  return proteinSequence ? 'ready' : 'missing';
33
32
  }
34
- export function useTranscriptSelection({ feature, view, validIds, preferredTranscriptId, }) {
33
+ export function useTranscriptSelection({ feature, view, hasData, preferredTranscriptId, }) {
35
34
  const options = useMemo(() => getSortedTranscriptFeatures(feature), [feature]);
36
35
  const [selectedId, setSelectedId] = useState(() => getId(options.find(opt => featureMatchesId(opt, preferredTranscriptId ?? '')) ??
37
36
  options[0]));
38
- const validatedSelectedId = pickSelectedId(selectedId, options, validIds);
37
+ const validatedSelectedId = pickSelectedId(selectedId, options, hasData);
39
38
  const selectedTranscript = options.find(val => getId(val) === validatedSelectedId);
40
39
  const { proteinSequence, error, isLoading } = useFeatureSequence({
41
40
  view,
@@ -50,6 +49,6 @@ export function useTranscriptSelection({ feature, view, validIds, preferredTrans
50
49
  error,
51
50
  isLoading,
52
51
  sequenceStatus: sequenceStatus({ error, isLoading, proteinSequence }),
53
- validIds,
52
+ hasData,
54
53
  };
55
54
  }
@@ -44,4 +44,4 @@ export interface MsaQueryRow {
44
44
  * wanted on every keystroke in the paste box, and asking separately parsed a
45
45
  * few-hundred-row alignment twice per character.
46
46
  */
47
- export declare function findQueryRow(msaText: string, proteinSequence: string): MsaQueryRow;
47
+ export declare function findQueryRow(msaText: string, proteinSequence: string, preferredName?: string): MsaQueryRow;
@@ -41,7 +41,7 @@ const PARTIAL_COVERAGE_FLOOR = 0.5;
41
41
  * wanted on every keystroke in the paste box, and asking separately parsed a
42
42
  * few-hundred-row alignment twice per character.
43
43
  */
44
- export function findQueryRow(msaText, proteinSequence) {
44
+ export function findQueryRow(msaText, proteinSequence, preferredName) {
45
45
  if (!msaText.trim()) {
46
46
  return { names: [] };
47
47
  }
@@ -56,42 +56,54 @@ export function findQueryRow(msaText, proteinSequence) {
56
56
  }
57
57
  const names = parsed.getNames();
58
58
  const query = normalize(proteinSequence);
59
- return { names, match: query ? bestMatch(parsed, names, query) : undefined };
59
+ return {
60
+ names,
61
+ match: query ? bestMatch(parsed, names, query, preferredName) : undefined,
62
+ };
60
63
  }
61
- function bestMatch(parsed, names, query) {
62
- const candidates = [];
63
- for (const name of names) {
64
- const row = normalize(getUngappedSequence(parsed.getRow(name)));
65
- if (!row) {
66
- continue;
67
- }
68
- if (row === query) {
69
- // nothing beats an exact match, and a second one would be a duplicate row
70
- return { name, quality: 'exact', identity: 1, offset: 0 };
71
- }
64
+ function scoreRow(parsed, name, query) {
65
+ const row = normalize(getUngappedSequence(parsed.getRow(name)));
66
+ if (!row) {
67
+ return undefined;
68
+ }
69
+ else if (row === query) {
70
+ return { name, quality: 'exact', identity: 1, offset: 0 };
71
+ }
72
+ else {
72
73
  // BLAST reports the aligned region, so the row is often the query trimmed
73
74
  // at one or both ends rather than the whole protein
74
75
  const contained = query.indexOf(row);
75
76
  const containing = row.indexOf(query);
76
77
  if (contained !== -1 || containing !== -1) {
77
78
  const coverage = Math.min(row.length, query.length) / Math.max(row.length, query.length);
78
- if (coverage >= PARTIAL_COVERAGE_FLOOR) {
79
- candidates.push({
79
+ return coverage >= PARTIAL_COVERAGE_FLOOR
80
+ ? {
80
81
  name,
81
82
  quality: 'partial',
82
83
  identity: coverage,
83
84
  offset: contained === -1 ? -containing : contained,
84
- });
85
- }
86
- continue;
85
+ }
86
+ : undefined;
87
87
  }
88
- const identity = identityOverOverlap(row, query);
89
- if (identity >= SIMILARITY_FLOOR) {
90
- candidates.push({ name, quality: 'similar', identity, offset: 0 });
88
+ else {
89
+ const identity = identityOverOverlap(row, query);
90
+ return identity >= SIMILARITY_FLOOR
91
+ ? { name, quality: 'similar', identity, offset: 0 }
92
+ : undefined;
91
93
  }
92
94
  }
93
- // an exact match returns above, so only these two can be here
94
- const order = ['partial', 'similar'];
95
- return candidates.sort((a, b) => order.indexOf(a.quality) - order.indexOf(b.quality) ||
96
- b.identity - a.identity)[0];
95
+ }
96
+ const qualityOrder = ['exact', 'partial', 'similar'];
97
+ /**
98
+ * The row matching the protein best. A paralog with an identical protein
99
+ * matches exactly as well as the query does, so a row the dataset itself names
100
+ * as the protein's own wins any tie.
101
+ */
102
+ function bestMatch(parsed, names, query, preferredName) {
103
+ return names
104
+ .map(name => scoreRow(parsed, name, query))
105
+ .filter(match => match !== undefined)
106
+ .sort((a, b) => qualityOrder.indexOf(a.quality) - qualityOrder.indexOf(b.quality) ||
107
+ b.identity - a.identity ||
108
+ Number(b.name === preferredName) - Number(a.name === preferredName))[0];
97
109
  }
@@ -93,3 +93,12 @@ describe('findQueryRow row names', () => {
93
93
  expect(findQueryRow('', protein).names).toEqual([]);
94
94
  });
95
95
  });
96
+ test("an identical paralog loses the tie to the row the dataset names as the protein's own", () => {
97
+ const msa = '>sp_H4copy\nMSGRGKGG\n>sp_His4r\nMSGRGKGG\n>sp_other\nMSGRGKAA\n';
98
+ expect(findQueryRow(msa, 'MSGRGKGG').match?.name).toBe('sp_H4copy');
99
+ expect(findQueryRow(msa, 'MSGRGKGG', 'sp_His4r').match?.name).toBe('sp_His4r');
100
+ });
101
+ test('a named row that matches worse than another does not win', () => {
102
+ const msa = '>sp_exact\nMSGRGKGG\n>sp_named\nMSGRGKAA\n';
103
+ expect(findQueryRow(msa, 'MSGRGKGG', 'sp_named').match?.name).toBe('sp_exact');
104
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { SimpleFeature } from '@jbrowse/core/util';
2
+ import { expect, test } from 'vitest';
3
+ import { featureMatchesId, findDatasetId } from './util';
4
+ const transcript = new SimpleFeature({
5
+ uniqueId: 'u1',
6
+ refName: 'chr17',
7
+ start: 0,
8
+ end: 10,
9
+ id: 'ENST00000269305.9',
10
+ transcript_id: 'ENST00000269305.9',
11
+ gene_name: 'TP53',
12
+ });
13
+ test('a transcript matches a dataset keyed by transcript id', () => {
14
+ expect(featureMatchesId(transcript, 'ENST00000269305.9')).toBe(true);
15
+ });
16
+ test('a transcript matches a dataset keyed by its gene symbol', () => {
17
+ expect(featureMatchesId(transcript, 'TP53')).toBe(true);
18
+ expect(featureMatchesId(transcript, 'TP63')).toBe(false);
19
+ });
20
+ // the shape of NCBI's GFF3, as the OrthoFinder synteny tutorial's flies have it
21
+ const ncbiTranscript = new SimpleFeature({
22
+ uniqueId: 'u2',
23
+ refName: '2L',
24
+ start: 9839,
25
+ end: 18570,
26
+ id: 'rna-NM_078715.4',
27
+ name: 'NM_078715.4',
28
+ subfeatures: [
29
+ {
30
+ uniqueId: 'u3',
31
+ refName: '2L',
32
+ start: 9900,
33
+ end: 10000,
34
+ type: 'CDS',
35
+ protein_id: 'NP_523439.2',
36
+ },
37
+ ],
38
+ });
39
+ const ncbiGene = new SimpleFeature({
40
+ uniqueId: 'u4',
41
+ refName: '2L',
42
+ start: 9839,
43
+ end: 21376,
44
+ id: 'gene-Dmel_CG2671',
45
+ name: 'l(2)gl',
46
+ });
47
+ test("a transcript matches a proteome keyed by its CDS's protein_id", () => {
48
+ expect(featureMatchesId(ncbiTranscript, 'NP_523439.2')).toBe(true);
49
+ });
50
+ test('a set keyed by gene id matches through the gene, without its gene- prefix', () => {
51
+ expect(findDatasetId(ncbiTranscript, ncbiGene, new Set(['Dmel_CG2671']))).toBe('Dmel_CG2671');
52
+ const ensemblGene = new SimpleFeature({
53
+ uniqueId: 'u5',
54
+ refName: '17',
55
+ start: 0,
56
+ end: 10,
57
+ id: 'gene:ENSG00000141510',
58
+ });
59
+ expect(findDatasetId(transcript, ensemblGene, new Set(['ENSG00000141510']))).toBe('ENSG00000141510');
60
+ });
61
+ test("the transcript's own entry wins over its gene's", () => {
62
+ expect(findDatasetId(ncbiTranscript, ncbiGene, new Set(['Dmel_CG2671', 'NM_078715.4']))).toBe('NM_078715.4');
63
+ expect(findDatasetId(ncbiTranscript, ncbiGene, new Set(['other']))).toBe(undefined);
64
+ });
65
+ test('a gene named by symbol matches the name OrthoFinder rewrote', () => {
66
+ const gene = new SimpleFeature({
67
+ uniqueId: 'u6',
68
+ refName: '3R',
69
+ start: 0,
70
+ end: 10,
71
+ id: 'gene-E(spl)m2-BFM',
72
+ });
73
+ expect(findDatasetId(ncbiTranscript, gene, new Set(['E_spl_m2-BFM']))).toBe('E_spl_m2-BFM');
74
+ });
@@ -18,7 +18,7 @@ export declare function resolveQueryRowName(queryRow: {
18
18
  querySeqName: string;
19
19
  names: string[];
20
20
  }, fallback: string): string;
21
- export declare function useQueryRowName(msaText: string, proteinSequence: string): {
21
+ export declare function useQueryRowName(msaText: string, proteinSequence: string, preferredName?: string): {
22
22
  detected: import("./detectQueryRow").QueryRowMatch | undefined;
23
23
  names: string[];
24
24
  querySeqName: string;