jbrowse-plugin-msaview 3.7.0 → 3.7.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.
@@ -19,7 +19,7 @@
19
19
  // pantherOrthologs.ts produce, so the launch, the labels and the aligner do not
20
20
  // know which source ran. `protein` is the UniProt accession, which is what the
21
21
  // CDD overlay reads through efetch (Swiss-Prot rows only; see the PANTHER note
22
- // in DEVELOPERS.md).
22
+ // in docs/alignments-from-a-gene.md).
23
23
  import { handleFetch, jsonfetch } from './fetch';
24
24
  import { cleanGeneCandidate, dedupeLabels, defaultMaxSpecies, } from './ncbiOrthologs';
25
25
  const UNIPROT = 'https://rest.uniprot.org';
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.7.0";
1
+ export declare const version = "3.7.1";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '3.7.0';
1
+ export const version = '3.7.1';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.7.0",
2
+ "version": "3.7.1",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -46,7 +46,7 @@
46
46
  "jsdom": "^30.1.0",
47
47
  "mobx": "^7.0.3",
48
48
  "mobx-react": "^10.0.2",
49
- "msa-parsers": "^8.1.0",
49
+ "msa-parsers": "^8.2.0",
50
50
  "oxfmt": "^0.68.0",
51
51
  "oxlint": "^1.83.0",
52
52
  "oxlint-tsgolint": "^7.0.2001",
@@ -56,7 +56,7 @@
56
56
  "puppeteer": "^25.11.0",
57
57
  "react": "^19.3.0",
58
58
  "react-dom": "^19.3.0",
59
- "react-msaview": "^8.1.0",
59
+ "react-msaview": "^8.2.0",
60
60
  "rimraf": "^6.1.3",
61
61
  "rxjs": "^7.8.2",
62
62
  "serve": "^14.2.6",
@@ -87,10 +87,11 @@
87
87
  "test:setup:version": "node scripts/test-versions.mjs setup",
88
88
  "test:versions": "node scripts/test-versions.mjs run",
89
89
  "test:version": "node scripts/test-versions.mjs run",
90
+ "readme-figure": "node scripts/readme-figure.mjs",
90
91
  "host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
91
92
  "check-ci": "node scripts/require-green-ci.mjs",
92
93
  "preversion": "pnpm check-ci && pnpm check-host-dep-pins && pnpm lint && pnpm build && pnpm check-host-externals && pnpm host-compat",
93
- "version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add src/version.ts CHANGELOG.md",
94
+ "version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && (pnpm readme-figure || echo 'WARNING: README figure not refreshed, see above') && git add src/version.ts CHANGELOG.md img/1.png",
94
95
  "postversion": "git push --follow-tags"
95
96
  }
96
97
  }
@@ -88,3 +88,51 @@ test('one field set is enough to need init', () => {
88
88
  querySeqName: 'QUERY',
89
89
  })
90
90
  })
91
+
92
+ test('msa and tree urls reach the plugin sources, query the query row', () => {
93
+ const snapshot = launch({
94
+ msa: 'https://example.com/p53.afa',
95
+ tree: 'https://example.com/p53.nh',
96
+ query: 'Human',
97
+ })
98
+ expect(snapshot.init).toMatchObject({
99
+ msaUrl: 'https://example.com/p53.afa',
100
+ querySeqName: 'Human',
101
+ })
102
+ expect(snapshot.treeFilehandle).toEqual({
103
+ uri: 'https://example.com/p53.nh',
104
+ locationType: 'UriLocation',
105
+ })
106
+ expect(snapshot.relativeTo).toBe('Human')
107
+ expect('msa' in snapshot || 'query' in snapshot).toBe(false)
108
+ })
109
+
110
+ test('inline msa and newick text become inline data', () => {
111
+ const snapshot = launch({ msa: '>a\nMEEP\n>b\nMEEP', tree: '(a,b);' })
112
+ expect(snapshot.data).toEqual({ msa: '>a\nMEEP\n>b\nMEEP', tree: '(a,b);' })
113
+ expect('init' in snapshot).toBe(false)
114
+ })
115
+
116
+ test('highlights, region and column tracks expand onto the query row', () => {
117
+ const snapshot = launch({
118
+ msa: 'https://example.com/p53.afa',
119
+ query: 'Human',
120
+ highlights: ['102-292 DNA-binding', 175],
121
+ region: '170-290',
122
+ columnTracks: [{ name: 'ClinVar', start: 3, values: [2, 1] }],
123
+ })
124
+ expect(snapshot.highlights).toEqual([
125
+ { row: 'Human', start: 102, end: 292, label: 'DNA-binding' },
126
+ { row: 'Human', start: 175, end: 175, label: '{residue}{position}' },
127
+ ])
128
+ expect(snapshot.region).toEqual({ row: 'Human', start: 170, end: 290 })
129
+ expect(snapshot.columnTracks).toEqual([
130
+ {
131
+ id: 'clinvar',
132
+ name: 'ClinVar',
133
+ kind: 'bar',
134
+ row: 'Human',
135
+ values: [0, 0, 2, 1],
136
+ },
137
+ ])
138
+ })
@@ -1,3 +1,5 @@
1
+ import { expandSpec } from 'react-msaview'
2
+
1
3
  import { launchMsaView } from '../utils/launchMsaView'
2
4
 
3
5
  import type { BlastParams, OrthologParams } from '../MsaViewPanel/model'
@@ -6,16 +8,25 @@ import type PluginManager from '@jbrowse/core/PluginManager'
6
8
  import type { AbstractSessionModel } from '@jbrowse/core/util'
7
9
  import type {
8
10
  Clade,
9
- ColumnTrackSpec,
11
+ ColumnTrackShorthand,
10
12
  Encoding,
11
- Highlight,
12
- Region,
13
+ HighlightShorthand,
14
+ RegionShorthand,
13
15
  ResidueMapping,
14
16
  RowPanelSpec,
15
17
  } from 'react-msaview'
16
18
 
17
19
  interface LaunchMsaViewArgs {
18
20
  session: AbstractSessionModel
21
+ /**
22
+ * Shorthand, see react-msaview's docs/layers.md: `msa` and `tree` take a
23
+ * url or the text itself, and `query` names the row the spec is about, the
24
+ * `querySeqName` and the `row` of every highlight, region and column track
25
+ * that names none
26
+ */
27
+ msa?: string
28
+ tree?: string
29
+ query?: string
19
30
  data?: { msa: string; tree?: string }
20
31
  msaFileLocation?: { uri: string }
21
32
  msaIndexedLocation?: { uri: string }
@@ -54,15 +65,15 @@ interface LaunchMsaViewArgs {
54
65
  * for whole rows. Prefer this over `highlightColumns`: a residue range stays
55
66
  * put when `allowedGappyness` changes the visible columns.
56
67
  */
57
- highlights?: Highlight[]
68
+ highlights?: HighlightShorthand[]
58
69
  /** per-column tracks supplied as data, see react-msaview's docs/layers.md */
59
- columnTracks?: ColumnTrackSpec[]
70
+ columnTracks?: ColumnTrackShorthand[]
60
71
  /**
61
72
  * Where the view opens, in `highlights` coordinates: `{row, start, end}`
62
73
  * zooms onto residues of that row, `{start, end}` onto alignment columns.
63
74
  * Applied once the alignment has loaded, then dropped from the session.
64
75
  */
65
- region?: Region
76
+ region?: RegionShorthand
66
77
  /**
67
78
  * Passed through untouched as react-msaview snapshot properties; see its
68
79
  * docs/layers.md. A bare `{ uri }` is enough for the two filehandles.
@@ -121,6 +132,30 @@ interface LaunchMsaViewArgs {
121
132
  placement?: MsaViewPlacement
122
133
  }
123
134
 
135
+ /**
136
+ * `msa` and `tree` onto the plugin's own sources, so a url still goes through
137
+ * `init.msaUrl` and its format sniffing, and `query` onto `querySeqName`
138
+ */
139
+ function withLongSources({ msa, tree, ...args }: LaunchMsaViewArgs) {
140
+ const inlineMsa = msa?.includes('\n') ? msa : undefined
141
+ const inlineTree = tree?.trimStart().startsWith('(') ? tree : undefined
142
+ const data =
143
+ inlineMsa || inlineTree
144
+ ? {
145
+ ...args.data,
146
+ ...(inlineMsa ? { msa: inlineMsa } : {}),
147
+ ...(inlineTree ? { tree: inlineTree } : {}),
148
+ }
149
+ : args.data
150
+ return {
151
+ ...args,
152
+ data: data as LaunchMsaViewArgs['data'],
153
+ ...(msa && !inlineMsa ? { msaFileLocation: { uri: msa } } : {}),
154
+ ...(tree && !inlineTree ? { treeFileLocation: { uri: tree } } : {}),
155
+ querySeqName: args.querySeqName ?? args.query,
156
+ }
157
+ }
158
+
124
159
  export default function LaunchMsaViewExtensionPointF(
125
160
  pluginManager: PluginManager,
126
161
  ) {
@@ -129,6 +164,7 @@ export default function LaunchMsaViewExtensionPointF(
129
164
  (args: LaunchMsaViewArgs) => {
130
165
  const {
131
166
  session,
167
+ query,
132
168
  data,
133
169
  msaFileLocation,
134
170
  msaIndexedLocation,
@@ -137,7 +173,7 @@ export default function LaunchMsaViewExtensionPointF(
137
173
  querySeqName,
138
174
  searchParams,
139
175
  ...rest
140
- } = args
176
+ } = withLongSources(args)
141
177
 
142
178
  // `orthologParams` and `searchParams` name no alignment at all — the
143
179
  // view builds one at launch, which is the dialog's Orthologs and BLAST
@@ -169,7 +205,7 @@ export default function LaunchMsaViewExtensionPointF(
169
205
  querySeqName,
170
206
  }
171
207
  launchMsaView(session, {
172
- ...rest,
208
+ ...expandSpec({ ...rest, query }),
173
209
  ...(searchParams ? { blastParams: searchParams } : {}),
174
210
  data,
175
211
  ...(treeFileLocation
@@ -20,7 +20,6 @@ import {
20
20
  storeDataToIndexedDB,
21
21
  syncGenomeHoverToMsaColumn,
22
22
  } from './afterCreateAutoruns'
23
- import { applyRegion } from './applyRegion'
24
23
  import {
25
24
  msaCoordToGenomeCoord,
26
25
  msaCoordToGenomeRegions,
@@ -40,7 +39,6 @@ import type { TranscriptRef } from './util'
40
39
  import type { MenuItem } from '@jbrowse/core/ui'
41
40
  import type { Instance } from '@jbrowse/mobx-state-tree'
42
41
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
43
- import type { Region } from 'react-msaview'
44
42
 
45
43
  type LGV = LinearGenomeViewModel
46
44
 
@@ -246,14 +244,6 @@ export default function stateModelFactory() {
246
244
  * #property
247
245
  */
248
246
  mafRegion: types.frozen<MafRegion | undefined>(),
249
-
250
- /**
251
- * #property
252
- * where the view opens: 1-based residues of `row`, or alignment
253
- * columns without one. Zoomed onto once and then cleared, so a
254
- * reloaded session keeps the reader's own scroll.
255
- */
256
- region: types.frozen<Region | undefined>(),
257
247
  }),
258
248
  )
259
249
 
@@ -463,12 +453,6 @@ export default function stateModelFactory() {
463
453
  setMafRegion(arg?: MafRegion) {
464
454
  self.mafRegion = arg
465
455
  },
466
- /**
467
- * #action
468
- */
469
- setRegion(arg?: Region) {
470
- self.region = arg
471
- },
472
456
  /**
473
457
  * #action
474
458
  */
@@ -671,12 +655,6 @@ export default function stateModelFactory() {
671
655
  // so they're factories returning the autorun body rather than plain fns
672
656
  addDisposer(self, autorun(syncGenomeHoverToMsaColumn(self)))
673
657
  addDisposer(self, autorun(observeProteinHighlights(self)))
674
- addDisposer(
675
- self,
676
- autorun(() => {
677
- applyRegion(self)
678
- }),
679
- )
680
658
  },
681
659
  }))
682
660
  }
@@ -19,7 +19,7 @@
19
19
  // pantherOrthologs.ts produce, so the launch, the labels and the aligner do not
20
20
  // know which source ran. `protein` is the UniProt accession, which is what the
21
21
  // CDD overlay reads through efetch (Swiss-Prot rows only; see the PANTHER note
22
- // in DEVELOPERS.md).
22
+ // in docs/alignments-from-a-gene.md).
23
23
 
24
24
  import { handleFetch, jsonfetch } from './fetch'
25
25
  import {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '3.7.0'
1
+ export const version = '3.7.1'
@@ -1,23 +0,0 @@
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 {};
@@ -1,19 +0,0 @@
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
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,45 +0,0 @@
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
- });
@@ -1,55 +0,0 @@
1
- import { expect, test, vi } from 'vitest'
2
-
3
- import { applyRegion } from './applyRegion'
4
-
5
- import type { Region } from 'react-msaview'
6
-
7
- const REGION: Region = { row: 'Human', start: 245, end: 249 }
8
-
9
- function makeModel(over: Record<string, unknown> = {}) {
10
- const model = {
11
- region: REGION as Region | undefined,
12
- viewInitialized: true,
13
- numColumns: 400,
14
- treeFilehandle: undefined as unknown,
15
- data: {} as { tree?: string },
16
- zoomToRegion: vi.fn(),
17
- setRegion: vi.fn((arg?: Region) => {
18
- model.region = arg
19
- }),
20
- ...over,
21
- }
22
- return model
23
- }
24
-
25
- test('zooms onto the region once, then forgets it', () => {
26
- const model = makeModel()
27
- applyRegion(model)
28
- applyRegion(model)
29
- expect(model.zoomToRegion).toHaveBeenCalledTimes(1)
30
- expect(model.zoomToRegion).toHaveBeenCalledWith(REGION)
31
- expect(model.region).toBeUndefined()
32
- })
33
-
34
- test('waits for the view to have a width', () => {
35
- const model = makeModel({ viewInitialized: false })
36
- applyRegion(model)
37
- expect(model.zoomToRegion).not.toHaveBeenCalled()
38
- expect(model.region).toEqual(REGION)
39
- })
40
-
41
- test('waits for the alignment', () => {
42
- const model = makeModel({ numColumns: 0 })
43
- applyRegion(model)
44
- expect(model.zoomToRegion).not.toHaveBeenCalled()
45
- })
46
-
47
- test('waits for a tree file, whose collapsed clades move the columns', () => {
48
- const model = makeModel({ treeFilehandle: { uri: 'x.nh' } })
49
- applyRegion(model)
50
- expect(model.zoomToRegion).not.toHaveBeenCalled()
51
-
52
- model.data.tree = '(a,b);'
53
- applyRegion(model)
54
- expect(model.zoomToRegion).toHaveBeenCalledTimes(1)
55
- })
@@ -1,33 +0,0 @@
1
- import type { Region } from 'react-msaview'
2
-
3
- interface RegionModel {
4
- region?: Region
5
- viewInitialized: boolean
6
- numColumns: number
7
- treeFilehandle?: unknown
8
- data: { tree?: string }
9
- zoomToRegion: (region: Region) => void
10
- setRegion: (region?: Region) => void
11
- }
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 function applyRegion(self: RegionModel) {
23
- const { region } = self
24
- if (
25
- region &&
26
- self.viewInitialized &&
27
- self.numColumns > 0 &&
28
- !(self.treeFilehandle && !self.data.tree)
29
- ) {
30
- self.zoomToRegion(region)
31
- self.setRegion(undefined)
32
- }
33
- }