jbrowse-plugin-msaview 5.16.0 → 5.16.1

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 (94) 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 +11 -9
  6. package/dist/LaunchMsaView/components/PreLoadedMSA/fetchMSAData.d.ts +5 -2
  7. package/dist/LaunchMsaView/components/TranscriptSelector.d.ts +2 -2
  8. package/dist/LaunchMsaView/components/TranscriptSelector.js +4 -6
  9. package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +4 -3
  10. package/dist/LaunchMsaView/components/useTranscriptSelection.js +10 -11
  11. package/dist/LaunchMsaView/featureMatchesId.test.d.ts +1 -0
  12. package/dist/LaunchMsaView/featureMatchesId.test.js +74 -0
  13. package/dist/LaunchMsaView/util.d.ts +2 -0
  14. package/dist/LaunchMsaView/util.js +38 -0
  15. package/dist/MsaViewPanel/displayedRefName.d.ts +19 -0
  16. package/dist/MsaViewPanel/displayedRefName.js +16 -0
  17. package/dist/MsaViewPanel/fetchIndexedMsa.js +10 -23
  18. package/dist/MsaViewPanel/handleMsaClick.test.d.ts +1 -0
  19. package/dist/MsaViewPanel/handleMsaClick.test.js +59 -0
  20. package/dist/MsaViewPanel/model.js +13 -11
  21. package/dist/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.d.ts +19 -0
  22. package/dist/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.js +61 -0
  23. package/dist/OrthoFinderMsaAdapter/configSchema.d.ts +12 -0
  24. package/dist/OrthoFinderMsaAdapter/configSchema.js +13 -0
  25. package/dist/OrthoFinderMsaAdapter/index.d.ts +2 -0
  26. package/dist/OrthoFinderMsaAdapter/index.js +14 -0
  27. package/dist/OrthoFinderMsaAdapter/parseResults.d.ts +16 -0
  28. package/dist/OrthoFinderMsaAdapter/parseResults.js +63 -0
  29. package/dist/OrthoFinderMsaAdapter/parseResults.test.d.ts +1 -0
  30. package/dist/OrthoFinderMsaAdapter/parseResults.test.js +29 -0
  31. package/dist/chunks/BgzipFastaMsaAdapter-Y3LYQRQL.js +6 -0
  32. package/dist/chunks/BgzipFastaMsaAdapter-Y3LYQRQL.js.map +7 -0
  33. package/dist/chunks/LaunchMsaViewDialog-H23R3EDJ.js +3 -0
  34. package/dist/chunks/LaunchMsaViewDialog-H23R3EDJ.js.map +7 -0
  35. package/dist/chunks/MsaViewPanel-6ETJVX27.js +2 -0
  36. package/dist/chunks/{MsaViewPanel-UWT4FWLE.js.map → MsaViewPanel-6ETJVX27.js.map} +1 -1
  37. package/dist/chunks/OrthoFinderMsaAdapter-I366HWAA.js +2 -0
  38. package/dist/chunks/OrthoFinderMsaAdapter-I366HWAA.js.map +7 -0
  39. package/dist/chunks/chunk-3G2F2Y7O.js +2 -0
  40. package/dist/chunks/chunk-3G2F2Y7O.js.map +7 -0
  41. package/dist/chunks/chunk-CRESEEY4.js +2 -0
  42. package/dist/chunks/chunk-CRESEEY4.js.map +7 -0
  43. package/dist/chunks/chunk-ET6J3BQX.js +3 -0
  44. package/dist/chunks/chunk-ET6J3BQX.js.map +7 -0
  45. package/dist/chunks/{chunk-AO3YMCKS.js → chunk-TEQX6FF3.js} +6 -6
  46. package/dist/chunks/{chunk-AO3YMCKS.js.map → chunk-TEQX6FF3.js.map} +1 -1
  47. package/dist/chunks/chunk-WE35XW3J.js +3 -0
  48. package/dist/chunks/chunk-WE35XW3J.js.map +7 -0
  49. package/dist/chunks/chunk-YN7VVNZR.js +2 -0
  50. package/dist/chunks/chunk-YN7VVNZR.js.map +7 -0
  51. package/dist/chunks/dist-NZDQ46PF.js +2 -0
  52. package/dist/chunks/model-XMH3QSZV.js +11 -0
  53. package/dist/chunks/model-XMH3QSZV.js.map +7 -0
  54. package/dist/index.js +2 -0
  55. package/dist/jbrowse-plugin-msaview.esm.js +2 -2
  56. package/dist/jbrowse-plugin-msaview.esm.js.map +4 -4
  57. package/dist/utils/msaIndex.d.ts +14 -0
  58. package/dist/utils/msaIndex.js +13 -0
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/package.json +1 -1
  62. package/src/BgzipFastaMsaAdapter/BgzipFastaMsaAdapter.ts +74 -3
  63. package/src/BgzipFastaMsaAdapter/configSchema.ts +6 -0
  64. package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +13 -16
  65. package/src/LaunchMsaView/components/PreLoadedMSA/fetchMSAData.ts +8 -4
  66. package/src/LaunchMsaView/components/TranscriptSelector.tsx +4 -7
  67. package/src/LaunchMsaView/components/useTranscriptSelection.ts +12 -13
  68. package/src/LaunchMsaView/featureMatchesId.test.ts +97 -0
  69. package/src/LaunchMsaView/util.ts +52 -0
  70. package/src/MsaViewPanel/displayedRefName.ts +30 -0
  71. package/src/MsaViewPanel/fetchIndexedMsa.ts +15 -26
  72. package/src/MsaViewPanel/handleMsaClick.test.ts +75 -0
  73. package/src/MsaViewPanel/model.ts +17 -11
  74. package/src/OrthoFinderMsaAdapter/OrthoFinderMsaAdapter.ts +87 -0
  75. package/src/OrthoFinderMsaAdapter/configSchema.ts +19 -0
  76. package/src/OrthoFinderMsaAdapter/index.ts +19 -0
  77. package/src/OrthoFinderMsaAdapter/parseResults.test.ts +61 -0
  78. package/src/OrthoFinderMsaAdapter/parseResults.ts +69 -0
  79. package/src/index.ts +2 -0
  80. package/src/utils/msaIndex.ts +32 -0
  81. package/src/version.ts +1 -1
  82. package/dist/chunks/BgzipFastaMsaAdapter-LXM6BNRP.js +0 -4
  83. package/dist/chunks/BgzipFastaMsaAdapter-LXM6BNRP.js.map +0 -7
  84. package/dist/chunks/LaunchMsaViewDialog-CVUQSMRL.js +0 -5
  85. package/dist/chunks/LaunchMsaViewDialog-CVUQSMRL.js.map +0 -7
  86. package/dist/chunks/MsaViewPanel-UWT4FWLE.js +0 -2
  87. package/dist/chunks/chunk-LXN7NIHB.js +0 -2
  88. package/dist/chunks/chunk-LXN7NIHB.js.map +0 -7
  89. package/dist/chunks/chunk-YRZUKVPZ.js +0 -3
  90. package/dist/chunks/chunk-YRZUKVPZ.js.map +0 -7
  91. package/dist/chunks/dist-2VELCAWU.js +0 -2
  92. package/dist/chunks/model-DAIL6BNQ.js +0 -12
  93. package/dist/chunks/model-DAIL6BNQ.js.map +0 -7
  94. /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';
@@ -33,17 +33,20 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
33
33
  const [selectedDatasetId, setSelectedDatasetId] = useState(datasets?.[0]?.datasetId);
34
34
  const selectedDataset = datasets?.find(d => d.datasetId === selectedDatasetId);
35
35
  const { data: msaList, isLoading: msaListLoading, error: msaListFetchError, } = useFetch(selectedDataset ? `${selectedDataset.datasetId}-msa-list` : null, () => fetchMSAList({ config: selectedDataset.adapter, pluginManager }));
36
+ const msaIds = useMemo(() => msaList && new Set(msaList), [msaList]);
36
37
  const transcriptSelection = useTranscriptSelection({
37
38
  feature,
38
39
  view,
39
- validIds: msaList,
40
+ hasData: msaIds
41
+ ? transcript => !!findDatasetId(transcript, feature, msaIds)
42
+ : undefined,
40
43
  preferredTranscriptId,
41
44
  });
42
45
  const { selectedTranscript, proteinSequence, sequenceStatus } = transcriptSelection;
43
46
  // the dataset's name for this transcript, which a track is free not to use
44
47
  // as the feature's own id
45
- const msaId = selectedTranscript
46
- ? msaList?.find(id => featureMatchesId(selectedTranscript, id))
48
+ const msaId = selectedTranscript && msaIds
49
+ ? findDatasetId(selectedTranscript, feature, msaIds)
47
50
  : undefined;
48
51
  const { data: msaData, isLoading: msaDataLoading, error: msaDataFetchError, } = useFetch(msaId && selectedDataset
49
52
  ? `${selectedDataset.datasetId}-${msaId}-msa`
@@ -52,8 +55,7 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
52
55
  config: selectedDataset.adapter,
53
56
  pluginManager,
54
57
  }));
55
- const msaText = useMemo(() => msaData?.map(r => `>${r.get('refName')}\n${r.get('seq')}`).join('\n') ??
56
- '', [msaData]);
58
+ const msaText = msaData?.msa ?? '';
57
59
  // The dataset's row for this transcript used to be assumed -- the launch
58
60
  // named `<transcriptId>_<assembly>` and hoped the file agreed. Nothing
59
61
  // checked, and a name the alignment does not carry fails silently: the view
@@ -81,11 +83,11 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
81
83
  React.createElement(SanitizedHTML, { html: selectedDataset.description }),
82
84
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
83
85
  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 })),
86
+ 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
87
  // launching without a query row opens a view that renders and then
86
88
  // never navigates, which reads as a broken feature rather than a
87
89
  // dataset that does not cover this gene
88
- submitDisabled: !selectedTranscript || !msaData?.length || !querySeqName, onSubmit: placement => {
90
+ submitDisabled: !selectedTranscript || !msaText || !querySeqName, onSubmit: placement => {
89
91
  if (selectedTranscript && msaText) {
90
92
  submit(() => launchConnectedView({
91
93
  view,
@@ -100,7 +102,7 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, preferred
100
102
  colorSchemeName: 'percent_identity_dynamic',
101
103
  querySeqName,
102
104
  querySeqOffset: queryRow.querySeqOffset,
103
- data: { msa: msaText },
105
+ data: { msa: msaText, tree: msaData?.tree },
104
106
  }));
105
107
  }
106
108
  }, onCancel: handleClose })));
@@ -1,6 +1,9 @@
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';
3
+ export interface DatasetMsa {
4
+ msa: string;
5
+ tree?: string;
6
+ }
4
7
  export declare function fetchMSAList({ config, pluginManager, }: {
5
8
  config: AnyConfigurationModel;
6
9
  pluginManager: PluginManager;
@@ -9,4 +12,4 @@ export declare function fetchMSA({ config, pluginManager, msaId, }: {
9
12
  config: AnyConfigurationModel;
10
13
  pluginManager: PluginManager;
11
14
  msaId: string;
12
- }): Promise<Feature[]>;
15
+ }): Promise<DatasetMsa>;
@@ -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: () => {
@@ -1,11 +1,12 @@
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 useTranscriptSelection({ feature, view, hasData, preferredTranscriptId, }: {
4
4
  feature: Feature;
5
5
  view: {
6
6
  assemblyNames?: string[];
7
7
  } | undefined;
8
- validIds?: string[];
8
+ /** which transcripts the panel can offer, when it can only offer some */
9
+ hasData?: (transcript: Feature) => boolean;
9
10
  /**
10
11
  * the isoform the user right-clicked, when the dialog opened on its gene.
11
12
  * Ignored when this gene does not carry it, so a stale id falls back to the
@@ -21,5 +22,5 @@ export declare function useTranscriptSelection({ feature, view, validIds, prefer
21
22
  error: unknown;
22
23
  isLoading: boolean;
23
24
  sequenceStatus: SequenceStatus;
24
- validIds: string[] | undefined;
25
+ hasData: ((transcript: Feature) => boolean) | undefined;
25
26
  };
@@ -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
+ 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
  }
@@ -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
+ });
@@ -8,6 +8,8 @@ export declare function getTranscriptLength(feature: Feature): {
8
8
  };
9
9
  export declare function getId(val?: Feature): string;
10
10
  export declare function featureMatchesId(feature: Feature, id: string): boolean;
11
+ /** the dataset entry for this transcript, else for the gene it belongs to */
12
+ export declare function findDatasetId(transcript: Feature, gene: Feature, ids: ReadonlySet<string>): string | undefined;
11
13
  export declare function getTranscriptDisplayName(val?: Feature): string;
12
14
  export declare function getGeneDisplayName(val?: Feature): string;
13
15
  export declare function getBlastViewTitle(feature: Feature, transcript: Feature): string;
@@ -39,6 +39,13 @@ export function getTranscriptLength(feature) {
39
39
  export function getId(val) {
40
40
  return val?.id() ?? '';
41
41
  }
42
+ // a proteome downloaded from NCBI or Ensembl names each protein by the CDS's
43
+ // protein_id rather than by its transcript
44
+ function getCdsProteinIds(transcript) {
45
+ return (transcript.get('subfeatures') ?? [])
46
+ .filter(f => f.get('type') === 'CDS')
47
+ .map(f => f.get('protein_id'));
48
+ }
42
49
  function getMatchableIds(val) {
43
50
  return val
44
51
  ? uniqueDefined([
@@ -46,12 +53,43 @@ function getMatchableIds(val) {
46
53
  val.get('name'),
47
54
  val.get('id'),
48
55
  val.get('transcript_id'),
56
+ val.get('gene_name'),
57
+ val.get('locus_tag'),
58
+ ...getCdsProteinIds(val),
49
59
  ])
50
60
  : [];
51
61
  }
52
62
  export function featureMatchesId(feature, id) {
53
63
  return getMatchableIds(feature).includes(id);
54
64
  }
65
+ const geneIdPrefix = /^gene[-:]/;
66
+ // A set keyed by gene, such as an OrthoFinder run over one protein per gene,
67
+ // names the gene by its GFF3 ID without NCBI's `gene-` or Ensembl's `gene:`
68
+ function getGeneMatchableIds(gene) {
69
+ const id = gene.get('id');
70
+ return uniqueDefined([
71
+ id,
72
+ id?.replace(geneIdPrefix, ''),
73
+ gene.get('gene_id'),
74
+ gene.get('locus_tag'),
75
+ ]);
76
+ }
77
+ // OrthoFinder writes every gene name with each character that Newick or a TSV
78
+ // cannot hold replaced by `_` (its utils/gene_ids.py clean_name), so NCBI's
79
+ // gene `E(spl)m2-BFM` is `E_spl_m2-BFM` in its results. It also replaces control
80
+ // characters, which a GFF3 attribute cannot carry unescaped.
81
+ const orthoFinderUnsafe = /[\s:;(),[\]='"]/g;
82
+ function findIn(candidates, ids) {
83
+ return (candidates.find(id => ids.has(id)) ??
84
+ candidates
85
+ .map(id => id.replace(orthoFinderUnsafe, '_'))
86
+ .find(id => ids.has(id)));
87
+ }
88
+ /** the dataset entry for this transcript, else for the gene it belongs to */
89
+ export function findDatasetId(transcript, gene, ids) {
90
+ return (findIn(getMatchableIds(transcript), ids) ??
91
+ findIn(getGeneMatchableIds(gene), ids));
92
+ }
55
93
  export function getTranscriptDisplayName(val) {
56
94
  return val ? joinDefined(' ', [val.get('name'), val.get('id')]) : '';
57
95
  }
@@ -0,0 +1,19 @@
1
+ interface Resolver {
2
+ initialized?: boolean;
3
+ getCanonicalRefName: (refName: string) => string | undefined;
4
+ }
5
+ /**
6
+ * The name a view's displayed regions use for `refName`'s chromosome. core's
7
+ * bpToPx, and so centerAt, matches region names exactly and does nothing when
8
+ * the feature says `chr1` and the view displays `1`. The raw name is kept when
9
+ * a region already carries it, and when no region resolves to the same
10
+ * chromosome, so a view this cannot place behaves as it did before.
11
+ */
12
+ export declare function displayedRefName({ displayedRegions, assembly, refName, }: {
13
+ displayedRegions: readonly {
14
+ refName: string;
15
+ }[];
16
+ assembly?: Resolver;
17
+ refName: string;
18
+ }): string;
19
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The name a view's displayed regions use for `refName`'s chromosome. core's
3
+ * bpToPx, and so centerAt, matches region names exactly and does nothing when
4
+ * the feature says `chr1` and the view displays `1`. The raw name is kept when
5
+ * a region already carries it, and when no region resolves to the same
6
+ * chromosome, so a view this cannot place behaves as it did before.
7
+ */
8
+ export function displayedRefName({ displayedRegions, assembly, refName, }) {
9
+ const canonical = (name) => assembly?.initialized ? assembly.getCanonicalRefName(name) : undefined;
10
+ const wanted = canonical(refName);
11
+ return displayedRegions.some(r => r.refName === refName) ||
12
+ wanted === undefined
13
+ ? refName
14
+ : (displayedRegions.find(r => canonical(r.refName) === wanted)?.refName ??
15
+ refName);
16
+ }
@@ -1,36 +1,23 @@
1
- import { BgzfFilehandle } from '@gmod/bgzf-filehandle';
2
1
  import { openLocation } from '@jbrowse/core/util/io';
2
+ import { parseMsaIndex, readMsaBlock } from '../utils/msaIndex';
3
3
  // Pull one transcript's whole multiple-alignment out of a single bgzip file
4
4
  // keyed by NAME. Given the `.fa.gz` uri, the bgzip index (`.gzi`) and name index
5
- // (`.idx`, a TSV `name<TAB>offset<TAB>length` of each block's uncompressed byte
6
- // offset + length) are found by suffix. We fetch the small `.idx` once, look up
7
- // the name, and random-read just that block — already valid FASTA
5
+ // (`.idx`) are found by suffix. We fetch the small `.idx` once, look up the
6
+ // name, and random-read just that block — already valid FASTA
8
7
  // (`>hg38\nSEQ\n>panTro4\nSEQ\n...`). One genome-scale alignment serves any gene
9
8
  // with no per-gene files or coordinates.
10
9
  export async function fetchIndexedMsa({ location, name, }) {
11
10
  const open = (uri) => openLocation({ uri, locationType: 'UriLocation' });
12
- const idxText = await open(`${location.uri}.idx`).readFile('utf8');
13
- const entry = lookup(idxText, name);
14
- if (entry) {
15
- const fh = new BgzfFilehandle({
11
+ const want = versionless(name);
12
+ const entry = parseMsaIndex(await open(`${location.uri}.idx`).readFile('utf8')).find(e => versionless(e.name) === want);
13
+ return entry
14
+ ? readMsaBlock({
16
15
  filehandle: open(location.uri),
17
16
  gziFilehandle: open(`${location.uri}.gzi`),
18
- });
19
- const bytes = await fh.read(entry.length, entry.offset);
20
- return new TextDecoder().decode(bytes).trim();
21
- }
22
- return undefined;
17
+ entry,
18
+ })
19
+ : undefined;
23
20
  }
24
21
  // ids are matched versionless, so version drift between the alignment build and
25
22
  // the live annotation never breaks the lookup
26
23
  const versionless = (s) => s.replace(/\.\d+$/, '');
27
- function lookup(idxText, name) {
28
- const want = versionless(name);
29
- for (const line of idxText.split('\n')) {
30
- const [id, offset, length] = line.split('\t');
31
- if (id && versionless(id) === want) {
32
- return { offset: Number(offset), length: Number(length) };
33
- }
34
- }
35
- return undefined;
36
- }
@@ -0,0 +1 @@
1
+ export {};