@jbrowse/plugin-linear-genome-view 5.0.0-beta.17 → 5.0.0-beta.18

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.
@@ -37,7 +37,9 @@ import type PluginManager from '@jbrowse/core/PluginManager';
37
37
  * type: 'Gff3TabixAdapter',
38
38
  * uri: 'https://example.com/genes.sorted.gff.gz',
39
39
  * },
40
- * displayDefaults: { color: "jexl:feature.type=='CDS'?'red':'blue'" },
40
+ * displayDefaults: {
41
+ * encoding: { color: { value: "jexl:feature.type=='CDS'?'red':'blue'" } },
42
+ * },
41
43
  * }
42
44
  * ```
43
45
  */
@@ -38,7 +38,9 @@ import { createBaseTrackConfig } from '@jbrowse/core/pluggableElementTypes';
38
38
  * type: 'Gff3TabixAdapter',
39
39
  * uri: 'https://example.com/genes.sorted.gff.gz',
40
40
  * },
41
- * displayDefaults: { color: "jexl:feature.type=='CDS'?'red':'blue'" },
41
+ * displayDefaults: {
42
+ * encoding: { color: { value: "jexl:feature.type=='CDS'?'red':'blue'" } },
43
+ * },
42
44
  * }
43
45
  * ```
44
46
  */
@@ -2,8 +2,10 @@ import { getSession, isSessionModelWithWidgets, resolveNamedRegions, } from '@jb
2
2
  import { coerceHighlight } from '@jbrowse/core/util/highlights';
3
3
  import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
4
4
  import { normalizeTrackInit, warmTrackDisplayGeneric, } from '@jbrowse/core/util/tracks';
5
+ import { requestedDisplayType } from '@jbrowse/launch-args/displaySettings';
5
6
  import { addDisposer } from '@jbrowse/mobx-state-tree';
6
7
  import { autorun, untracked, when } from 'mobx';
8
+ import { launchTrackInit } from '../TrackModifiers/index.js';
7
9
  import { SearchResultsNotFoundError } from '../searchUtils.js';
8
10
  // `init` is a frozen blob that often comes from hand-authored JSON, where a
9
11
  // lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
@@ -127,8 +129,7 @@ async function navigateInit(self, session, init) {
127
129
  // display's state model first when it is registered lazily
128
130
  async function showInitTracks(self, init) {
129
131
  for (const t of asArray(init.tracks)) {
130
- const { trackId, trackSnapshot, displaySnapshot } = normalizeTrackInit(t);
131
- await self.launchTrack(trackId, trackSnapshot, displaySnapshot);
132
+ await launchTrackInit(self, t);
132
133
  }
133
134
  }
134
135
  /**
@@ -219,8 +220,9 @@ function setupInitWarmup(self) {
219
220
  }
220
221
  }
221
222
  for (const t of tracks) {
222
- const { trackId, displaySnapshot } = normalizeTrackInit(t);
223
- warmTrackDisplayGeneric(self, trackId, displaySnapshot, assembly);
223
+ const { trackId, displaySnapshot, modifiers } = normalizeTrackInit(t);
224
+ const type = requestedDisplayType(modifiers);
225
+ warmTrackDisplayGeneric(self, trackId, type ? { ...displaySnapshot, type } : displaySnapshot, assembly);
224
226
  }
225
227
  });
226
228
  }, { name: 'LGVInitWarmup' }));
@@ -102,7 +102,9 @@ const LinearGenomeViewContainer = observer(function LinearGenomeViewContainer({
102
102
  .flatMap(t => t.displays)
103
103
  .map(d => d.hoveredFeature)
104
104
  .find(Boolean);
105
- session.setHovered({ hoverPosition, hoverFeature });
105
+ // `viewId` lets a view paired with one linear view ignore another
106
+ // showing the same locus
107
+ session.setHovered({ hoverPosition, hoverFeature, viewId: model.id });
106
108
  }, children: [_jsx(NavigationAnnouncer, { model: model }), _jsxs("div", { ref: headerRef, "data-testid": `lgv-header-${model.id}`, className: classes.header, style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(Header, { model: model }), hideHeader ? _jsx(MiniControls, { model: model }) : null] }), _jsx("div", { ref: tracksRef, children: model.scalebarOnly ? (_jsx(Rubberband, { model: model })) : (_jsx(TracksContainer, { model: model, children: !tracks.length ? (_jsx(NoTracksActiveButton, { model: model })) : (_jsxs(_Fragment, { children: [pinnedTracks.length ? (_jsx(Paper, { elevation: 2, "data-testid": "pinned-tracks", className: classes.pinnedTracks, style: {
107
109
  top: stickyTops.pinnedTracks,
108
110
  // the scroll port this sticks in, not the window: a
@@ -22,7 +22,7 @@ export type SearchBoxPrefs = ReturnType<typeof useSearchBoxPrefs>;
22
22
  /**
23
23
  * The menu rows that drive those prefs. "Stacked" rather than "Vertical": the
24
24
  * two headers name the same state and had drifted apart on it once already.
25
- * A caller spreading the rows into a "Show..." submenu passes `'Search boxes'`
25
+ * A caller spreading the rows into a "Show" submenu passes `'Search boxes'`
26
26
  * as the toggle's label.
27
27
  */
28
28
  export declare function searchBoxMenuItems({ showSearchBoxes, setShowSearchBoxes, sideBySide, setSideBySide, }: SearchBoxPrefs, toggleLabel?: string): MenuItem[];
@@ -22,7 +22,7 @@ export function useSearchBoxPrefs(prefix, numViews) {
22
22
  /**
23
23
  * The menu rows that drive those prefs. "Stacked" rather than "Vertical": the
24
24
  * two headers name the same state and had drifted apart on it once already.
25
- * A caller spreading the rows into a "Show..." submenu passes `'Search boxes'`
25
+ * A caller spreading the rows into a "Show" submenu passes `'Search boxes'`
26
26
  * as the toggle's label.
27
27
  */
28
28
  export function searchBoxMenuItems({ showSearchBoxes, setShowSearchBoxes, sideBySide, setSideBySide, }, toggleLabel = 'Show search boxes') {
@@ -1,4 +1,4 @@
1
- import type { BpOffset, ExportSvgOptions, HighlightType, InitState, NavLocation, ViewTrackLabelMode, VolatileGuide } from './types.ts';
1
+ import type { BpOffset, ExportSvgOptions, HighlightInput, HighlightType, InitState, NavLocation, ViewTrackLabelMode, VolatileGuide } from './types.ts';
2
2
  import type { RegionEdit } from './util.ts';
3
3
  import type PluginManager from '@jbrowse/core/PluginManager';
4
4
  import type BaseResult from '@jbrowse/core/TextSearch/BaseResults';
@@ -20,18 +20,24 @@ export interface SearchPickerRow {
20
20
  trackName: string;
21
21
  }
22
22
  /**
23
- * The hits of a name search that point at more than one place, waiting for the
24
- * user to choose. A host draws them however it likes: the Material view shows
25
- * a dialog, and `SearchPicker` in `@jbrowse/display-ui/embed` a plain list.
23
+ * A name search's hits as a host lists them: what `view.search` returns, and
24
+ * what the picker holds.
26
25
  */
27
- export interface SearchPickerState {
26
+ export interface SearchHits {
28
27
  query: string;
29
28
  assemblyName: string;
30
- /** one row per place, hits in a track the view already shows first */
29
+ /** one row per hit, hits in a track the view already shows first */
31
30
  rows: SearchPickerRow[];
32
- /** navigates to the row's place, reports a failure and closes the picker */
31
+ /** lands on the row's place and reports a failure */
33
32
  pick: (id: string) => Promise<unknown>;
34
33
  }
34
+ /**
35
+ * The hits of a name search that point at more than one place, waiting for the
36
+ * user to choose; its `pick` also closes the picker. A host draws them however
37
+ * it likes: the Material view shows a dialog, and `SearchPicker` in
38
+ * `@jbrowse/display-ui/embed` a plain list.
39
+ */
40
+ export type SearchPickerState = SearchHits;
35
41
  export type { PaddingSpan } from './util.ts';
36
42
  export interface HighlightRegion {
37
43
  assemblyName?: string;
@@ -172,6 +178,13 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
172
178
  * #volatile
173
179
  */
174
180
  coarseBpPerPx: number;
181
+ /**
182
+ * #volatile
183
+ * the scroll offset the coarse blocks were laid out at, in px at
184
+ * `coarseBpPerPx`: the settled viewport's left edge, which the first
185
+ * coarse block is not while the view is scrolled before the first region
186
+ */
187
+ coarseOffsetPx: number;
175
188
  /**
176
189
  * #volatile
177
190
  */
@@ -967,6 +980,15 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
967
980
  * there and shows the track its index came from
968
981
  */
969
982
  setSearchResults(searchResults: BaseResult[], searchQuery: string, assemblyName: string, onPick?: (result: BaseResult) => Promise<unknown>): void;
983
+ /**
984
+ * #action
985
+ * Search the view's assembly by name, for a host listing the hits
986
+ * itself. A row's `pick` lands as the search box does: it navigates
987
+ * there and shows the track the hit's index came from.
988
+ */
989
+ search(query: string, opts?: {
990
+ signal?: AbortSignal;
991
+ }): Promise<SearchHits>;
970
992
  /**
971
993
  * #action
972
994
  */
@@ -988,6 +1010,22 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
988
1010
  * loaded: loads it, then shows
989
1011
  */
990
1012
  launchTrack(trackId: string, initialSnapshot?: any, displayInitialSnapshot?: any, inlineConf?: Record<string, unknown>): Promise<any>;
1013
+ /**
1014
+ * #action
1015
+ * Replace the highlights `highlights` reads, each written as the launch
1016
+ * spec's `highlight` writes one: a locstring, or an object whose
1017
+ * `assemblyName` defaults to the view's. Throws on one that names no
1018
+ * region, writing none.
1019
+ */
1020
+ setHighlights(highlights: readonly (string | HighlightInput)[]): void;
1021
+ /**
1022
+ * #action
1023
+ * Show files from the user's disk as session tracks: one per data file,
1024
+ * beside the index among them that names it, its type guessed from its
1025
+ * name. Adds nothing when one file has no known type. Resolves the new
1026
+ * tracks' ids.
1027
+ */
1028
+ showFiles(files: readonly File[]): Promise<string[]>;
991
1029
  } & {
992
1030
  /**
993
1031
  * #method
@@ -1911,6 +1949,13 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
1911
1949
  * #volatile
1912
1950
  */
1913
1951
  coarseBpPerPx: number;
1952
+ /**
1953
+ * #volatile
1954
+ * the scroll offset the coarse blocks were laid out at, in px at
1955
+ * `coarseBpPerPx`: the settled viewport's left edge, which the first
1956
+ * coarse block is not while the view is scrolled before the first region
1957
+ */
1958
+ coarseOffsetPx: number;
1914
1959
  /**
1915
1960
  * #volatile
1916
1961
  */
@@ -2706,6 +2751,15 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
2706
2751
  * there and shows the track its index came from
2707
2752
  */
2708
2753
  setSearchResults(searchResults: BaseResult[], searchQuery: string, assemblyName: string, onPick?: (result: BaseResult) => Promise<unknown>): void;
2754
+ /**
2755
+ * #action
2756
+ * Search the view's assembly by name, for a host listing the hits
2757
+ * itself. A row's `pick` lands as the search box does: it navigates
2758
+ * there and shows the track the hit's index came from.
2759
+ */
2760
+ search(query: string, opts?: {
2761
+ signal?: AbortSignal;
2762
+ }): Promise<SearchHits>;
2709
2763
  /**
2710
2764
  * #action
2711
2765
  */
@@ -2727,6 +2781,22 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
2727
2781
  * loaded: loads it, then shows
2728
2782
  */
2729
2783
  launchTrack(trackId: string, initialSnapshot?: any, displayInitialSnapshot?: any, inlineConf?: Record<string, unknown>): Promise<any>;
2784
+ /**
2785
+ * #action
2786
+ * Replace the highlights `highlights` reads, each written as the launch
2787
+ * spec's `highlight` writes one: a locstring, or an object whose
2788
+ * `assemblyName` defaults to the view's. Throws on one that names no
2789
+ * region, writing none.
2790
+ */
2791
+ setHighlights(highlights: readonly (string | HighlightInput)[]): void;
2792
+ /**
2793
+ * #action
2794
+ * Show files from the user's disk as session tracks: one per data file,
2795
+ * beside the index among them that names it, its type guessed from its
2796
+ * name. Adds nothing when one file has no known type. Resolves the new
2797
+ * tracks' ids.
2798
+ */
2799
+ showFiles(files: readonly File[]): Promise<string[]>;
2730
2800
  } & {
2731
2801
  /**
2732
2802
  * #method
@@ -8,6 +8,7 @@ import { scaleBandHeights } from '@jbrowse/core/util/bandHeight';
8
8
  import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
9
9
  import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
10
10
  import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
11
+ import { coerceHighlight } from '@jbrowse/core/util/highlights';
11
12
  import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
12
13
  import { ElementId } from '@jbrowse/core/util/types/mst';
13
14
  import { highlightsOnAssemblies } from '@jbrowse/core/util/viewHighlights';
@@ -17,7 +18,7 @@ import { MIN_DISPLAY_HEIGHT } from '@jbrowse/display-kit/const';
17
18
  import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
18
19
  import { cast, getParent, getSnapshot, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
19
20
  import { observable, when } from 'mobx';
20
- import { handleSelectedRegion, isOpenInView, navToOption, } from '../searchUtils.js';
21
+ import { fetchResults, handleSelectedRegion, isOpenInView, navToOption, } from '../searchUtils.js';
21
22
  import { doAfterAttach } from './afterAttach.js';
22
23
  import { linearGenomeViewAxisConfigSchema } from './axisConfigSchema.js';
23
24
  import { getCytobands, shouldSwapTracks } from './components/util.js';
@@ -27,7 +28,43 @@ import { setupKeyboardHandler } from './keyboardHandler.js';
27
28
  import { lgvLaunchKeys } from './launchKeys.js';
28
29
  import { buildHighlightsSubMenuItems, buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
29
30
  import { sharedScaleContainerOf } from './sharedScaleContainer.js';
31
+ import { showFiles } from './showFiles.js';
30
32
  import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getGridlineTicks, getPaddingSpans, getScalebarLabels, getScalebarRefNameLabels, regionsOrientation, setDisplayedRegionsKeepingCenter, } from './util.js';
33
+ function searchHitsOf(model, results, query, assemblyName, land) {
34
+ const session = getSession(model);
35
+ const assembly = session.assemblyManager.get(assemblyName);
36
+ const ordered = [...results].sort((a, b) => Number(isOpenInView(b, model)) - Number(isOpenInView(a, model)));
37
+ return {
38
+ query,
39
+ assemblyName,
40
+ rows: ordered.map(result => {
41
+ const location = result.getLocation() ?? '';
42
+ const trackId = result.getTrackId();
43
+ return {
44
+ id: result.getId(),
45
+ label: result.getLabel(),
46
+ location: assembly && location
47
+ ? canonicalLocString(location, assembly)
48
+ : location,
49
+ trackName: trackId ? (session.getTrackById(trackId)?.name ?? '') : '',
50
+ };
51
+ }),
52
+ pick: async (id) => {
53
+ const picked = ordered.find(result => result.getId() === id);
54
+ try {
55
+ if (picked) {
56
+ await land(picked);
57
+ }
58
+ }
59
+ catch (e) {
60
+ console.error(e);
61
+ if (isAlive(model)) {
62
+ getNotificationSink(model).notifyError(`${e}`, e);
63
+ }
64
+ }
65
+ },
66
+ };
67
+ }
31
68
  /**
32
69
  * Calculate the offsetPx needed to center content within a viewport.
33
70
  * Returns a negative offset when content is smaller than viewport (padding on left).
@@ -388,6 +425,13 @@ export function stateModelFactory(pluginManager) {
388
425
  // displays that lay out against it; `setWidth` seeds the true one at
389
426
  // the first measure.
390
427
  coarseBpPerPx: 1,
428
+ /**
429
+ * #volatile
430
+ * the scroll offset the coarse blocks were laid out at, in px at
431
+ * `coarseBpPerPx`: the settled viewport's left edge, which the first
432
+ * coarse block is not while the view is scrolled before the first region
433
+ */
434
+ coarseOffsetPx: 0,
391
435
  /**
392
436
  * #volatile
393
437
  */
@@ -1139,6 +1183,7 @@ export function stateModelFactory(pluginManager) {
1139
1183
  // The debounced coarse autorun is 500ms out; a restored snapshot's
1140
1184
  // consumers would lay out and fetch at 1 bp/px until then.
1141
1185
  self.coarseBpPerPx = self.bpPerPx;
1186
+ self.coarseOffsetPx = self.offsetPx;
1142
1187
  }
1143
1188
  },
1144
1189
  /**
@@ -1264,40 +1309,11 @@ export function stateModelFactory(pluginManager) {
1264
1309
  model: self,
1265
1310
  assemblyName,
1266
1311
  })) {
1267
- const session = getSession(self);
1268
- const assembly = session.assemblyManager.get(assemblyName);
1269
- const ordered = [...searchResults].sort((a, b) => Number(isOpenInView(b, self)) -
1270
- Number(isOpenInView(a, self)));
1312
+ const hits = searchHitsOf(self, searchResults, searchQuery, assemblyName, onPick);
1271
1313
  const picker = {
1272
- query: searchQuery,
1273
- assemblyName,
1274
- rows: ordered.map(result => {
1275
- const location = result.getLocation() ?? '';
1276
- const trackId = result.getTrackId();
1277
- return {
1278
- id: result.getId(),
1279
- label: result.getLabel(),
1280
- location: assembly && location
1281
- ? canonicalLocString(location, assembly)
1282
- : location,
1283
- trackName: trackId
1284
- ? (session.getTrackById(trackId)?.name ?? '')
1285
- : '',
1286
- };
1287
- }),
1314
+ ...hits,
1288
1315
  pick: async (id) => {
1289
- const picked = ordered.find(result => result.getId() === id);
1290
- try {
1291
- if (picked) {
1292
- await onPick(picked);
1293
- }
1294
- }
1295
- catch (e) {
1296
- console.error(e);
1297
- if (isAlive(self)) {
1298
- getNotificationSink(self).notifyError(`${e}`, e);
1299
- }
1300
- }
1316
+ await hits.pick(id);
1301
1317
  if (isAlive(self) && self.searchPicker === picker) {
1302
1318
  self.closeSearchPicker();
1303
1319
  }
@@ -1305,6 +1321,31 @@ export function stateModelFactory(pluginManager) {
1305
1321
  };
1306
1322
  self.searchPicker = picker;
1307
1323
  },
1324
+ /**
1325
+ * #action
1326
+ * Search the view's assembly by name, for a host listing the hits
1327
+ * itself. A row's `pick` lands as the search box does: it navigates
1328
+ * there and shows the track the hit's index came from.
1329
+ */
1330
+ async search(query, opts) {
1331
+ const assemblyName = self.assemblyNames[0] ?? self.launchAssemblyName;
1332
+ if (!assemblyName) {
1333
+ throw new Error('the view has no assembly to search yet');
1334
+ }
1335
+ const { assemblyManager, textSearchManager } = getSession(self);
1336
+ const results = await fetchResults({
1337
+ queryString: query,
1338
+ assemblyName,
1339
+ textSearchManager,
1340
+ assembly: await assemblyManager.waitForAssembly(assemblyName),
1341
+ signal: opts?.signal,
1342
+ });
1343
+ return searchHitsOf(self, results, query, assemblyName, result => navToOption({
1344
+ option: result,
1345
+ model: self,
1346
+ assemblyName,
1347
+ }));
1348
+ },
1308
1349
  /**
1309
1350
  * #action
1310
1351
  */
@@ -1337,6 +1378,34 @@ export function stateModelFactory(pluginManager) {
1337
1378
  async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
1338
1379
  return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
1339
1380
  },
1381
+ /**
1382
+ * #action
1383
+ * Replace the highlights `highlights` reads, each written as the launch
1384
+ * spec's `highlight` writes one: a locstring, or an object whose
1385
+ * `assemblyName` defaults to the view's. Throws on one that names no
1386
+ * region, writing none.
1387
+ */
1388
+ setHighlights(highlights) {
1389
+ const session = getSession(self);
1390
+ const assemblyName = self.assemblyNames[0] ?? self.launchAssemblyName ?? '';
1391
+ session.setHighlights(highlights.map(h => {
1392
+ const highlight = coerceHighlight(h, assemblyName, (ref, asm) => session.assemblyManager.isValidRefName(ref, asm ?? assemblyName));
1393
+ if (!highlight) {
1394
+ throw new Error(`not a region to highlight: ${JSON.stringify(h)}`);
1395
+ }
1396
+ return highlight;
1397
+ }));
1398
+ },
1399
+ /**
1400
+ * #action
1401
+ * Show files from the user's disk as session tracks: one per data file,
1402
+ * beside the index among them that names it, its type guessed from its
1403
+ * name. Adds nothing when one file has no known type. Resolves the new
1404
+ * tracks' ids.
1405
+ */
1406
+ async showFiles(files) {
1407
+ return showFiles(self, files);
1408
+ },
1340
1409
  }))
1341
1410
  .views(self => ({
1342
1411
  /**
@@ -2167,6 +2236,9 @@ export function stateModelFactory(pluginManager) {
2167
2236
  self.coarseTotalBp = blocks.totalBp;
2168
2237
  self.coarseBpPerPx = bpPerPx;
2169
2238
  }
2239
+ // on every call, since a scroll through the padding before the first
2240
+ // region moves the viewport and leaves the blocks the same
2241
+ self.coarseOffsetPx = self.offsetPx;
2170
2242
  },
2171
2243
  }))
2172
2244
  .actions(self => ({
@@ -15,6 +15,18 @@ export interface RExportTrack {
15
15
  heightMode?: unknown;
16
16
  height?: unknown;
17
17
  }
18
+ /**
19
+ * Each display ggjbrowse reads, by the reader that reads it, and the adapters
20
+ * it opens (the slot holding the file and the extension the reader knows it
21
+ * by).
22
+ */
23
+ export declare const READERS: Record<string, {
24
+ call: string;
25
+ adapters: Record<string, {
26
+ slot?: string;
27
+ ext?: RegExp;
28
+ }>;
29
+ }>;
18
30
  /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
19
31
  export declare const GGJBROWSE_VERSION = "0.10.0";
20
32
  /**
@@ -35,6 +47,15 @@ export declare function rValue(value: unknown): string;
35
47
  * `file://` URL as its path, or a `localPath`.
36
48
  */
37
49
  export declare function locationPath(location: unknown): string | undefined;
50
+ /**
51
+ * Each sequence adapter `jbrowse_assembly()` builds from a path, by the
52
+ * extension ggjbrowse reads it off, and the index each takes beside it.
53
+ */
54
+ export declare const SEQUENCE_ADAPTERS: Record<string, {
55
+ slot: string;
56
+ ext: RegExp;
57
+ beside: Record<string, string>;
58
+ }>;
38
59
  /**
39
60
  * The view's assembly as the R value every reader takes: `jbrowse_assembly()`
40
61
  * of its sequence and alias files where ggjbrowse builds the same config from
@@ -5,7 +5,7 @@ import { resolveUri } from '@jbrowse/core/util';
5
5
  * it opens (the slot holding the file and the extension the reader knows it
6
6
  * by).
7
7
  */
8
- const READERS = {
8
+ export const READERS = {
9
9
  LinearBasicDisplay: {
10
10
  call: 'jbrowse_features',
11
11
  adapters: {
@@ -216,7 +216,7 @@ function fileArgument(adapters, adapter) {
216
216
  * Each sequence adapter `jbrowse_assembly()` builds from a path, by the
217
217
  * extension ggjbrowse reads it off, and the index each takes beside it.
218
218
  */
219
- const SEQUENCE_ADAPTERS = {
219
+ export const SEQUENCE_ADAPTERS = {
220
220
  TwoBitAdapter: { slot: 'twoBitLocation', ext: /\.2bit$/, beside: {} },
221
221
  BgzipFastaAdapter: {
222
222
  slot: 'fastaLocation',
@@ -330,7 +330,7 @@ function trackNames(tracks) {
330
330
  }
331
331
  /** What the figure cannot show of a track whose settings ggjbrowse reads. */
332
332
  function settingNotes(track) {
333
- const color = track.display.color;
333
+ const { color } = track.display.encoding ?? {};
334
334
  return [
335
335
  ...(color?.field === 'mateRefName'
336
336
  ? [
@@ -1,17 +1,7 @@
1
- import { getParent, hasParent, isAlive } from '@jbrowse/mobx-state-tree';
1
+ import { findParent } from '@jbrowse/mobx-state-tree';
2
2
  export function isSharedScaleContainer(thing) {
3
3
  return typeof thing === 'object' && thing !== null && 'sharedFit' in thing;
4
4
  }
5
- // Walks rather than using findParentThatIs because having no such container is
6
- // the ordinary standalone case here, not the error that helper reports.
7
5
  export function sharedScaleContainerOf(node) {
8
- let current = node;
9
- let found;
10
- while (!found && hasParent(current) && isAlive(current)) {
11
- current = getParent(current);
12
- if (isSharedScaleContainer(current)) {
13
- found = current;
14
- }
15
- }
16
- return found;
6
+ return findParent(node, isSharedScaleContainer);
17
7
  }
@@ -0,0 +1,2 @@
1
+ import type { LinearGenomeViewModel } from './model.ts';
2
+ export declare function showFiles(view: LinearGenomeViewModel, files: readonly File[]): Promise<string[]>;
@@ -0,0 +1,19 @@
1
+ import { fileToLocation, getEnv, getSession, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
2
+ import { pairIndexFiles } from '@jbrowse/core/util/indexCandidates';
3
+ import { guessTrackConfForLocation } from '@jbrowse/core/util/tracks';
4
+ // Every type is guessed before any track is added, so a selection holding one
5
+ // file of no known type adds nothing rather than half of itself.
6
+ export async function showFiles(view, files) {
7
+ const session = getSession(view);
8
+ if (!isSessionWithAddSessionTrack(session)) {
9
+ throw new Error('this session does not take new tracks');
10
+ }
11
+ const { pluginManager } = getEnv(view);
12
+ const assemblyName = view.assemblyNames[0] ?? view.launchAssemblyName;
13
+ const confs = pairIndexFiles(files).map(({ data, index }) => guessTrackConfForLocation(fileToLocation(data), index && fileToLocation(index), pluginManager, assemblyName));
14
+ for (const conf of confs) {
15
+ session.addSessionTrackConf(conf);
16
+ }
17
+ await Promise.all(confs.map(conf => view.launchTrack(conf.trackId)));
18
+ return confs.map(conf => conf.trackId);
19
+ }
@@ -0,0 +1,80 @@
1
+ import type { FieldPredicate } from '@jbrowse/core/util/fieldPredicate';
2
+ import type { TrackInit } from '@jbrowse/core/util/tracks';
3
+ import type { DisplaySettings } from '@jbrowse/launch-args/displaySettings';
4
+ import type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree';
5
+ /** The linear view a command line's tracks open in, as these helpers read it. */
6
+ export interface ModifierView {
7
+ centerLineInfo?: {
8
+ refName?: string;
9
+ offset: number;
10
+ start: number;
11
+ end: number;
12
+ reversed?: boolean;
13
+ oob?: boolean;
14
+ };
15
+ launchTrack: (trackId: string, trackSnapshot?: object, displaySnapshot?: object) => Promise<{
16
+ displays: unknown[];
17
+ } | undefined>;
18
+ showTrack: (trackId: string, trackSnapshot?: object, displaySnapshot?: object) => unknown;
19
+ tracks: {
20
+ configuration: {
21
+ trackId: string;
22
+ };
23
+ displays: unknown[];
24
+ activeDisplay: {
25
+ configuration: Record<string, unknown>;
26
+ };
27
+ }[];
28
+ }
29
+ /**
30
+ * `displayTypeOf` for a track the session holds, undefined for one it does
31
+ * not; `displayType` is the display the caller opens it as when no
32
+ * `display:` modifier names one.
33
+ */
34
+ export declare function openedDisplayType(session: {
35
+ getTrackById: (trackId: string) => unknown;
36
+ }, trackId: string, modifiers: string[], displayType?: string): string | undefined;
37
+ /**
38
+ * Write the `filter` of `trackId`'s `settings` onto its display open in
39
+ * `view`, by field over the configured filter, as the track menu would.
40
+ * Returns the display's filter as written, which reopens the track as drawn,
41
+ * or undefined when the settings write no filter.
42
+ */
43
+ export declare function writeFilter(view: Pick<ModifierView, 'tracks' | 'showTrack'>, trackId: string, settings: DisplaySettings): FieldPredicate[] | undefined;
44
+ /**
45
+ * @deprecated `writeFilter`, which every display category takes. This one
46
+ * writes an alignments `display`'s filter only.
47
+ */
48
+ export declare function writeReadFilter(display: unknown, displayType: string | undefined, settings: DisplaySettings): FieldPredicate[] | undefined;
49
+ interface LaunchOptions {
50
+ /** the track entry's own display settings, which the modifiers write over */
51
+ displaySnapshot?: Record<string, unknown>;
52
+ trackSnapshot?: Record<string, unknown>;
53
+ /** changes the settings after the center-line sort resolves, before the open */
54
+ adjust?: (settings: DisplaySettings) => void;
55
+ warn?: (message: string) => void;
56
+ }
57
+ /**
58
+ * Open `trackId` in `view` with its command-line modifiers applied, and return
59
+ * the session-spec track entry that reopens it as drawn, or undefined when the
60
+ * open fails, which `launchTrack` reports to the session.
61
+ */
62
+ export declare function launchTrackWithModifiers(view: ModifierView, trackId: string, displayType: string | undefined, modifiers: string[], { displaySnapshot, trackSnapshot, adjust, warn, }?: LaunchOptions): Promise<{
63
+ trackId: string;
64
+ filter?: FieldPredicate[] | undefined;
65
+ } | undefined>;
66
+ /**
67
+ * Open a session spec's track entry in a navigated `view`, its `modifiers`
68
+ * written over its display settings. When a modifier or the open fails, the
69
+ * session's snackbar says why and the track opens with its own settings.
70
+ */
71
+ export declare function launchTrackInit(view: ModifierView & IAnyStateTreeNode, t: TrackInit): Promise<void>;
72
+ /**
73
+ * Write a track's `memberWrites` onto what its display in `view` already has,
74
+ * after every other modifier, so a member write keeps the rest of the setting
75
+ * whatever order the command line gives them in. A display replaces a color
76
+ * or facet object whole, which is why these cannot ride in on the launch
77
+ * snapshot.
78
+ */
79
+ export declare function writeMembers(view: Pick<ModifierView, 'tracks' | 'showTrack'>, trackId: string, members: string[]): Record<string, unknown>;
80
+ export {};
@@ -0,0 +1,212 @@
1
+ import { mergeConfSettings } from '@jbrowse/core/configuration';
2
+ import { getEnv, getSession } from '@jbrowse/core/util';
3
+ import { basePaintedAt } from '@jbrowse/core/util/Base1DUtils';
4
+ import { predicatesOf, testsSomething, withFields, } from '@jbrowse/core/util/fieldPredicate';
5
+ import { normalizeTrackInit, trackDisplayType } from '@jbrowse/core/util/tracks';
6
+ import { buildDisplaySnapshot, displayCategory, memberPatch, memberWrites, mergeSettings, requestedDisplayType, } from '@jbrowse/launch-args/displaySettings';
7
+ import { getSnapshot, getType, isArrayType, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
8
+ /**
9
+ * The display a linear view opens `conf` as, read off the display picker so it
10
+ * follows `display:` and the adapter rather than a list of track types.
11
+ */
12
+ function displayTypeOf(pluginManager, conf, modifiers, displayType) {
13
+ return trackDisplayType(pluginManager, conf, 'LinearGenomeView', requestedDisplayType(modifiers) ?? displayType);
14
+ }
15
+ /**
16
+ * `displayTypeOf` for a track the session holds, undefined for one it does
17
+ * not; `displayType` is the display the caller opens it as when no
18
+ * `display:` modifier names one.
19
+ */
20
+ export function openedDisplayType(session, trackId, modifiers, displayType) {
21
+ const conf = session.getTrackById(trackId);
22
+ return conf
23
+ ? displayTypeOf(getEnv(session).pluginManager, (isStateTreeNode(conf) ? getSnapshot(conf) : conf), modifiers, displayType)
24
+ : undefined;
25
+ }
26
+ const consoleWarn = (message) => {
27
+ console.warn(`Warning: ${message}`);
28
+ };
29
+ function modifierLaunch(displayType, modifiers, warn = consoleWarn) {
30
+ return {
31
+ displayType,
32
+ settings: buildDisplaySnapshot(displayType, modifiers, warn),
33
+ members: memberWrites(displayType, modifiers),
34
+ };
35
+ }
36
+ // the entries grouped by field, as `setFilterFields` takes them: a field whose
37
+ // entries are all bare is `[]`, which clears it, defaults included
38
+ function filterPatch(entries) {
39
+ const patch = {};
40
+ for (const p of entries) {
41
+ patch[p.field] = [
42
+ ...(patch[p.field] ?? []),
43
+ ...(testsSomething(p) ? [p] : []),
44
+ ];
45
+ }
46
+ return patch;
47
+ }
48
+ // the display snapshot a track opens with. `filter` stays out of it:
49
+ // `writeFilter` writes it once the track is open, since the list in a
50
+ // snapshot would replace the configured one
51
+ function launchSnapshot({ settings }) {
52
+ const { snap, displayType } = settings;
53
+ const { filter: _filter, ...unfiltered } = snap;
54
+ return {
55
+ ...unfiltered,
56
+ ...(displayType ? { type: displayType } : {}),
57
+ };
58
+ }
59
+ /**
60
+ * Write the `filter` of `trackId`'s `settings` onto its display open in
61
+ * `view`, by field over the configured filter, as the track menu would.
62
+ * Returns the display's filter as written, which reopens the track as drawn,
63
+ * or undefined when the settings write no filter.
64
+ */
65
+ export function writeFilter(view, trackId, settings) {
66
+ const { filter } = settings.snap;
67
+ const track = view.tracks.find(t => t.configuration.trackId === trackId);
68
+ if (!filter || !track) {
69
+ return undefined;
70
+ }
71
+ const patch = filterPatch(filter);
72
+ const display = track.displays[0];
73
+ if (display?.setFilterFields) {
74
+ display.setFilterFields(patch);
75
+ return display.writtenFilter;
76
+ }
77
+ const written = withFields(predicatesOf(track.activeDisplay.configuration.filter), patch);
78
+ view.showTrack(trackId, {}, { filter: written });
79
+ return written;
80
+ }
81
+ /**
82
+ * @deprecated `writeFilter`, which every display category takes. This one
83
+ * writes an alignments `display`'s filter only.
84
+ */
85
+ export function writeReadFilter(display, displayType, settings) {
86
+ const { filter } = settings.snap;
87
+ if (displayCategory(displayType) !== 'alignments' || !filter) {
88
+ return undefined;
89
+ }
90
+ const filterDisplay = display;
91
+ filterDisplay?.setFilterFields?.(filterPatch(filter));
92
+ return filterDisplay?.writtenFilter;
93
+ }
94
+ // the pivot is the base under the view's center, which a view has only once
95
+ // it shows a region
96
+ function centerSort(view, trackId, field, warn) {
97
+ const center = view.centerLineInfo;
98
+ if (center && !center.oob) {
99
+ return {
100
+ field,
101
+ pos: basePaintedAt(center, center.offset),
102
+ refName: center.refName ?? '',
103
+ };
104
+ }
105
+ else {
106
+ warn(`sort:${field} on "${trackId}" ignored — the view has no center position to sort at (pass --loc)`);
107
+ return undefined;
108
+ }
109
+ }
110
+ // The modifiers' settings written over the entry's own as a settings bag
111
+ // writes them (`mergeConfSettings`): a namespace such as `pileup` member by
112
+ // member, a bare value lifted first, and a channel such as `encoding.color`
113
+ // whole, so a `color:` modifier is not mixed into the entry's color by field.
114
+ // Plain JSON where no registered display type says which is which.
115
+ function overEntry(view, displayType, entry, over) {
116
+ const pluginManager = isStateTreeNode(view)
117
+ ? getEnv(view).pluginManager
118
+ : undefined;
119
+ return displayType && pluginManager?.displayTypes.has(displayType)
120
+ ? mergeConfSettings(pluginManager.getDisplayType(displayType).configSchema, entry, over)
121
+ : mergeSettings(structuredClone(entry), over);
122
+ }
123
+ /**
124
+ * Open `trackId` in `view` with its command-line modifiers applied, and return
125
+ * the session-spec track entry that reopens it as drawn, or undefined when the
126
+ * open fails, which `launchTrack` reports to the session.
127
+ */
128
+ export async function launchTrackWithModifiers(view, trackId, displayType, modifiers, { displaySnapshot = {}, trackSnapshot = {}, adjust, warn = consoleWarn, } = {}) {
129
+ const launch = modifierLaunch(displayType, modifiers, warn);
130
+ const { snap, sort } = launch.settings;
131
+ const at = sort && centerSort(view, trackId, sort.field, warn);
132
+ if (at) {
133
+ snap.sort = at;
134
+ }
135
+ adjust?.(launch.settings);
136
+ const launched = overEntry(view, displayType, displaySnapshot, launchSnapshot(launch));
137
+ const opened = await view.launchTrack(trackId, trackSnapshot, launched);
138
+ if (!opened) {
139
+ return undefined;
140
+ }
141
+ const drawnFilter = writeFilter(view, trackId, launch.settings);
142
+ const members = writeMembers(view, trackId, launch.members);
143
+ return {
144
+ trackId,
145
+ ...launched,
146
+ ...(drawnFilter ? { filter: drawnFilter } : {}),
147
+ ...members,
148
+ };
149
+ }
150
+ /**
151
+ * Open a session spec's track entry in a navigated `view`, its `modifiers`
152
+ * written over its display settings. When a modifier or the open fails, the
153
+ * session's snackbar says why and the track opens with its own settings.
154
+ */
155
+ export async function launchTrackInit(view, t) {
156
+ const { trackId, trackSnapshot, displaySnapshot, modifiers } = normalizeTrackInit(t);
157
+ if (modifiers.length === 0) {
158
+ await view.launchTrack(trackId, trackSnapshot, displaySnapshot);
159
+ return;
160
+ }
161
+ const session = getSession(view);
162
+ const { type } = displaySnapshot;
163
+ const notes = [];
164
+ try {
165
+ await launchTrackWithModifiers(view, trackId, openedDisplayType(session, trackId, modifiers, typeof type === 'string' ? type : undefined), modifiers, {
166
+ trackSnapshot,
167
+ displaySnapshot,
168
+ warn: message => notes.push(message),
169
+ });
170
+ }
171
+ catch (e) {
172
+ console.error(e);
173
+ session.notifyError(`"${trackId}" ${modifiers.join(' ')}: ${e}`, e);
174
+ }
175
+ // launchTrack reports its own failure, such as a display:
176
+ // the track cannot open as
177
+ if (!view.tracks.some(t => t.configuration.trackId === trackId)) {
178
+ await view.launchTrack(trackId, trackSnapshot, displaySnapshot);
179
+ }
180
+ if (notes.length) {
181
+ session.notify(notes.join('; '), 'warning');
182
+ }
183
+ }
184
+ /**
185
+ * Write a track's `memberWrites` onto what its display in `view` already has,
186
+ * after every other modifier, so a member write keeps the rest of the setting
187
+ * whatever order the command line gives them in. A display replaces a color
188
+ * or facet object whole, which is why these cannot ride in on the launch
189
+ * snapshot.
190
+ */
191
+ export function writeMembers(view, trackId, members) {
192
+ if (members.length === 0) {
193
+ return {};
194
+ }
195
+ const track = view.tracks.find(t => t.configuration.trackId === trackId);
196
+ if (!track) {
197
+ throw new Error(`"${trackId}" is not open, so ${members.join(' ')} has nothing to write to`);
198
+ }
199
+ const { configuration } = track.activeDisplay;
200
+ const configured = getSnapshot(configuration);
201
+ const patch = memberPatch(members, root => {
202
+ // a list at a default the snapshot strips, Manhattan's marks, still seeds
203
+ // the write, so `marks.0.mark=bar` edits the mark drawn
204
+ const member = configuration[root];
205
+ return (configured[root] ??
206
+ (isStateTreeNode(member) && isArrayType(getType(member))
207
+ ? getSnapshot(member)
208
+ : undefined));
209
+ });
210
+ view.showTrack(trackId, {}, patch);
211
+ return patch;
212
+ }
package/esm/index.d.ts CHANGED
@@ -29,12 +29,14 @@ export { searchBoxMenuItems, useSearchBoxPrefs, } from './LinearGenomeView/compo
29
29
  export type { SearchBoxPrefs } from './LinearGenomeView/components/useSearchBoxPrefs.ts';
30
30
  export { normalizeTrackInit } from '@jbrowse/core/util/tracks';
31
31
  export { applyInitHighlights } from './LinearGenomeView/afterAttach.ts';
32
+ export { launchTrackInit, launchTrackWithModifiers, openedDisplayType, writeFilter, writeMembers, writeReadFilter, } from './TrackModifiers/index.ts';
33
+ export type { ModifierView } from './TrackModifiers/index.ts';
32
34
  export { MultiLevelRubberband, multiLevelRowClickMenuItems, multiLevelRowMenuItems, rowLabels, } from './MultiLevelRubberband/index.ts';
33
35
  export { fetchResults, SearchResultsNotFoundError } from './searchUtils.ts';
34
36
  export type { LaunchLinearGenomeViewArgs } from './LaunchLinearGenomeView/index.ts';
35
37
  export type { BpOffset, ExportSvgOptions, HighlightInput, HighlightType, InitState, LinearGenomeViewLaunchProps, NavLocation, TrackLabelMode, ViewTrackLabelMode, VolatileGuide, } from './LinearGenomeView/types.ts';
36
38
  export type { ScalebarRefNameLabel } from './LinearGenomeView/util.ts';
37
- export type { HighlightRegion, SearchPickerRow, SearchPickerState, } from './LinearGenomeView/model.ts';
39
+ export type { HighlightRegion, SearchHits, SearchPickerRow, SearchPickerState, } from './LinearGenomeView/model.ts';
38
40
  export type { RegionsOrientation } from './LinearGenomeView/util.ts';
39
41
  export type { RegionEdit } from './LinearGenomeView/util.ts';
40
42
  export { makeTicks } from './LinearGenomeView/util.ts';
package/esm/index.js CHANGED
@@ -71,6 +71,7 @@ export { default as HeaderSearchBoxRow } from './LinearGenomeView/components/Hea
71
71
  export { searchBoxMenuItems, useSearchBoxPrefs, } from './LinearGenomeView/components/useSearchBoxPrefs.js';
72
72
  export { normalizeTrackInit } from '@jbrowse/core/util/tracks';
73
73
  export { applyInitHighlights } from './LinearGenomeView/afterAttach.js';
74
+ export { launchTrackInit, launchTrackWithModifiers, openedDisplayType, writeFilter, writeMembers, writeReadFilter, } from './TrackModifiers/index.js';
74
75
  export { MultiLevelRubberband, multiLevelRowClickMenuItems, multiLevelRowMenuItems, rowLabels, } from './MultiLevelRubberband/index.js';
75
76
  export { fetchResults, SearchResultsNotFoundError } from './searchUtils.js';
76
77
  // The scalebar's grid, published because it is a contract other views draw
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-linear-genome-view",
3
- "version": "5.0.0-beta.17",
3
+ "version": "5.0.0-beta.18",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 linear genome view",
6
6
  "keywords": [
@@ -21,15 +21,16 @@
21
21
  "esm"
22
22
  ],
23
23
  "dependencies": {
24
- "@jbrowse/mobx-state-tree": "^6.9.0",
24
+ "@jbrowse/mobx-state-tree": "^6.10.0",
25
25
  "@mui/icons-material": "^9.5.0",
26
26
  "@mui/material": "^9.5.0",
27
- "mobx": "^7.0.6",
27
+ "mobx": "^7.1.1",
28
28
  "mobx-react": "^10.0.2",
29
- "@jbrowse/core": "^5.0.0-beta.17",
30
- "@jbrowse/display-ui": "^5.0.0-beta.17",
31
- "@jbrowse/render-core": "^5.0.0-beta.17",
32
- "@jbrowse/display-kit": "^5.0.0-beta.17"
29
+ "@jbrowse/display-ui": "^5.0.0-beta.18",
30
+ "@jbrowse/display-kit": "^5.0.0-beta.18",
31
+ "@jbrowse/launch-args": "^5.0.0-beta.18",
32
+ "@jbrowse/render-core": "^5.0.0-beta.18",
33
+ "@jbrowse/core": "^5.0.0-beta.18"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "react": ">=19.0.0",
@@ -40,7 +41,7 @@
40
41
  },
41
42
  "sideEffects": false,
42
43
  "devDependencies": {
43
- "@jbrowse/display-test-utils": "^5.0.0-beta.17"
44
+ "@jbrowse/display-test-utils": "^5.0.0-beta.18"
44
45
  },
45
46
  "scripts": {
46
47
  "prebuild": "pnpm clean",