@jbrowse/plugin-linear-genome-view 5.0.0-beta.1 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/esm/LaunchLinearGenomeView/index.js +2 -13
  2. package/esm/LinearGenomeView/afterAttach.js +16 -18
  3. package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +1 -1
  4. package/esm/LinearGenomeView/components/PaddingBlocks.js +11 -1
  5. package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +1 -1
  6. package/esm/LinearGenomeView/components/ZoomButton.js +6 -1
  7. package/esm/LinearGenomeView/components/ZoomTransform.js +4 -0
  8. package/esm/LinearGenomeView/components/searchModes.d.ts +1 -1
  9. package/esm/LinearGenomeView/components/searchModes.js +1 -1
  10. package/esm/LinearGenomeView/index.d.ts +1 -0
  11. package/esm/LinearGenomeView/index.js +8 -1
  12. package/esm/LinearGenomeView/initKeys.d.ts +0 -1
  13. package/esm/LinearGenomeView/initKeys.js +9 -45
  14. package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
  15. package/esm/LinearGenomeView/launchKeys.js +23 -0
  16. package/esm/LinearGenomeView/lazyDialogs.d.ts +1 -1
  17. package/esm/LinearGenomeView/linkedViewSync.d.ts +1 -1
  18. package/esm/LinearGenomeView/linkedViewSync.js +6 -0
  19. package/esm/LinearGenomeView/model.d.ts +1734 -22
  20. package/esm/LinearGenomeView/model.js +121 -57
  21. package/esm/LinearGenomeView/svgcomponents/figureContract.js +14 -14
  22. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
  23. package/esm/LinearGenomeView/types.d.ts +1 -1
  24. package/esm/displayKitTests/perRegionTestEnv.d.ts +51 -28
  25. package/esm/displayKitTests/perRegionTestEnv.js +0 -3
  26. package/esm/index.d.ts +14 -2
  27. package/esm/index.js +1 -2
  28. package/esm/searchUtils.d.ts +6 -3
  29. package/esm/searchUtils.js +37 -11
  30. package/package.json +8 -8
  31. package/esm/LinearGenomeView/FloatingSvgOverlay.d.ts +0 -5
  32. package/esm/LinearGenomeView/FloatingSvgOverlay.js +0 -25
@@ -1,22 +1,11 @@
1
- import { linearGenomeViewPropKeys, partitionLaunchKeys, warnUnknownLaunchKeys, } from '../LinearGenomeView/initKeys.js';
2
1
  export default function LaunchLinearGenomeViewF(pluginManager) {
3
2
  /** #extensionPoint LaunchView-LinearGenomeView | async | Programmatically launch a linear genome view */
4
3
  pluginManager.addToExtensionPoint('LaunchView-LinearGenomeView', args => {
5
- const { session, id, ...spec } = args;
4
+ const { session, ...spec } = args;
6
5
  if (!spec.assembly) {
7
6
  throw new Error('No assembly provided when launching linear genome view');
8
7
  }
9
- // Resolution keys go into the one-shot `init` blob afterAttach applies and
10
- // discards; plain props go straight onto the view snapshot, where MST
11
- // restores them natively (and, except for the purely localStorage-backed
12
- // showCenterLine, persist on save). Anything in neither set is a typo — MST
13
- // drops unknown snapshot keys and `init` is a frozen blob, so nothing
14
- // downstream would notice.
15
- const { init, viewProps, unknown } = partitionLaunchKeys(spec, linearGenomeViewPropKeys(pluginManager));
16
- warnUnknownLaunchKeys('LaunchView-LinearGenomeView', unknown);
17
- // A provided id is passed top-level so MST's optional identifier honors it
18
- // (undefined falls back to an auto-generated id).
19
- session.addView('LinearGenomeView', { id, ...viewProps, init });
8
+ session.addView('LinearGenomeView', spec);
20
9
  return args;
21
10
  });
22
11
  }
@@ -2,18 +2,9 @@ import { getSession, isSessionModelWithWidgets, localStorageSetItem, resolveName
2
2
  import { coerceHighlight } from '@jbrowse/core/util/highlights';
3
3
  import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
4
4
  import { normalizeTrackInit } from '@jbrowse/core/util/tracks';
5
- import { addDisposer, getPropertyMembers } from '@jbrowse/mobx-state-tree';
5
+ import { addDisposer } from '@jbrowse/mobx-state-tree';
6
6
  import { autorun, when } from 'mobx';
7
7
  import { SearchResultsNotFoundError } from '../searchUtils.js';
8
- import { partitionLaunchKeys, warnUnknownLaunchKeys } from './initKeys.js';
9
- function warnInitKeyProblems(self, init) {
10
- const { viewProps, unknown } = partitionLaunchKeys(init, new Set(Object.keys(getPropertyMembers(self).properties)));
11
- const viewPropKeys = Object.keys(viewProps);
12
- warnUnknownLaunchKeys('LinearGenomeView init', unknown);
13
- if (viewPropKeys.length) {
14
- console.warn(`LinearGenomeView init ignored view prop(s): ${viewPropKeys.join(', ')} — set these on the view alongside init, not inside it`);
15
- }
16
- }
17
8
  // `init` is a frozen blob that often comes from hand-authored JSON, where a
18
9
  // lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
19
10
  // looping over it directly walked its characters — one track opened per letter.
@@ -103,7 +94,11 @@ async function navigateInit(self, session, init) {
103
94
  // navToLocString waits for the assembly itself, and this autorun only
104
95
  // runs once `initialized` confirms init.assembly has loaded regions, so
105
96
  // no explicit waitForAssembly is needed here
106
- await self.navToLocString(init.loc, init.assembly, init.grow);
97
+ // a spec that listed its tracks gets those tracks; one that only named
98
+ // a gene gets the track the gene was found in, as the search box would
99
+ await self.navToLocString(init.loc, init.assembly, init.grow, {
100
+ showHitTrack: asArray(init.tracks).length === 0,
101
+ });
107
102
  }
108
103
  else if (init.displayedRegionNames?.length) {
109
104
  // an explicit region list is a navigation request just like `loc`, so it
@@ -134,12 +129,13 @@ async function navigateInit(self, session, init) {
134
129
  }
135
130
  }
136
131
  }
137
- // showTrack funnels through showTrackGeneric, which surfaces any failure
138
- // (unresolved id, bad config, etc) as its own snackbar
139
- function showInitTracks(self, init) {
132
+ // launchTrack funnels through showTrackGeneric, which surfaces any failure
133
+ // (unresolved id, bad config, etc) as its own snackbar, and loads the picked
134
+ // display's state model first when it is registered lazily
135
+ async function showInitTracks(self, init) {
140
136
  for (const t of asArray(init.tracks)) {
141
137
  const { trackId, trackSnapshot, displaySnapshot } = normalizeTrackInit(t);
142
- self.showTrack(trackId, trackSnapshot, displaySnapshot);
138
+ await self.launchTrack(trackId, trackSnapshot, displaySnapshot);
143
139
  }
144
140
  }
145
141
  // backfill assemblyName on any session-authored highlights that omitted it so
@@ -182,14 +178,13 @@ export function applyInitHighlights(self, session, init) {
182
178
  }
183
179
  async function applyInit(self, init, { superseded }) {
184
180
  const session = getSession(self);
185
- warnInitKeyProblems(self, init);
186
181
  if (init.tracklist) {
187
182
  await openTracklist(self, session, superseded);
188
183
  }
189
184
  // `superseded`, not a bare isAlive: it subsumes the liveness check (the view
190
185
  // may have been removed while the drawer or the navigation resolved, and
191
186
  // reading or mutating a detached node throws) and also covers the case
192
- // isAlive misses — a newer setInit landed mid-apply, so the rest of *this*
187
+ // isAlive misses — a newer setLaunch landed mid-apply, so the rest of *this*
193
188
  // blob is stale. Finishing it anyway appends its tracks and highlights under
194
189
  // the one that replaced it, and addToHighlights pushes, so a re-launch of the
195
190
  // same spec (a StrictMode remount) doubles the bands. The drain loop applies
@@ -205,7 +200,10 @@ async function applyInit(self, init, { superseded }) {
205
200
  if (superseded()) {
206
201
  return;
207
202
  }
208
- showInitTracks(self, init);
203
+ await showInitTracks(self, init);
204
+ if (superseded()) {
205
+ return;
206
+ }
209
207
  if (init.nav !== undefined) {
210
208
  self.setHideHeader(!init.nav);
211
209
  }
@@ -3,5 +3,5 @@ type LGV = LinearGenomeViewModel;
3
3
  declare const PaddingBlocks: ({ model, offset, }: {
4
4
  model: LGV;
5
5
  offset?: number;
6
- }) => import("react").JSX.Element;
6
+ }) => import("react").JSX.Element | null;
7
7
  export default PaddingBlocks;
@@ -41,11 +41,21 @@ const PaddingBlocks = observer(function PaddingBlocks({ model, offset = 0, }) {
41
41
  // the geometry is `model.paddingSpans` -- shared with any host drawing this
42
42
  // itself, so a seam here and a seam in an embedder's own chrome can't be
43
43
  // computed two different ways. This file owns only what the spans look like.
44
+ const spans = model.paddingSpans;
45
+ // Nothing to mask, so no positioned container to hold it — the common case,
46
+ // since a view sitting inside one contig has no seam, elision or boundary.
47
+ // The wrapper is not free: `ZoomTransform` reads `staticBlocks`, so an empty
48
+ // one still re-rendered and rewrote its transform on every gesture frame, per
49
+ // track. The view returns one shared array here, so this bails without even
50
+ // re-rendering.
51
+ if (spans.length === 0) {
52
+ return null;
53
+ }
44
54
  const kindClass = {
45
55
  seam: classes.regionSeparator,
46
56
  elided: classes.elided,
47
57
  boundary: classes.boundaryBlock,
48
58
  };
49
- return (_jsx(ZoomTransform, { model: model, offset: offset, children: _jsx("div", { className: classes.absoluteFill, children: model.paddingSpans.map(({ key, x, width, kind }) => (_jsx("div", { className: cx(classes.block, kindClass[kind]), style: { transform: `translateX(${x}px)`, width } }, key))) }) }));
59
+ return (_jsx(ZoomTransform, { model: model, offset: offset, children: _jsx("div", { className: classes.absoluteFill, children: spans.map(({ x, width, kind }, i) => (_jsx("div", { className: cx(classes.block, kindClass[kind]), style: { transform: `translateX(${x}px)`, width } }, i))) }) }));
50
60
  });
51
61
  export default PaddingBlocks;
@@ -1,7 +1,7 @@
1
1
  declare const SequenceSearchDialog: ({ model, handleClose, }: {
2
2
  model: {
3
3
  assemblyNames: string[];
4
- showTrack: (trackId: string) => void;
4
+ launchTrack: (trackId: string) => Promise<unknown>;
5
5
  };
6
6
  handleClose: () => void;
7
7
  }) => import("react").JSX.Element;
@@ -15,7 +15,12 @@ const ZoomButton = observer(function ZoomButton({ model, direction, small, }) {
15
15
  ? coarseBpPerPx >= maxBpPerPx - EPSILON
16
16
  : coarseBpPerPx <= minBpPerPx + EPSILON;
17
17
  const fontSize = small ? 'small' : undefined;
18
- return (_jsx(Tooltip, { title: out ? 'Zoom out 2x' : 'Zoom in 2x', children: _jsx("span", { children: _jsx(IconButton, { "data-testid": out ? 'zoom_out' : 'zoom_in', size: small ? 'small' : undefined, disabled: disabled, onClick: () => {
18
+ return (_jsx(Tooltip, { title: out ? 'Zoom out 2x' : 'Zoom in 2x', children: _jsx("span", { children: _jsx(IconButton
19
+ // on the button, not left to the Tooltip: its title lands on the
20
+ // <span> the disabled case needs, so the button itself reached the
21
+ // accessibility tree unnamed and nothing driving by name could aim at
22
+ // it
23
+ , { "aria-label": out ? 'Zoom out' : 'Zoom in', "data-testid": out ? 'zoom_out' : 'zoom_in', size: small ? 'small' : undefined, disabled: disabled, onClick: () => {
19
24
  model.zoom(out ? model.bpPerPx * 2 : model.bpPerPx / 2);
20
25
  }, children: out ? (_jsx(ZoomOut, { fontSize: fontSize })) : (_jsx(ZoomIn, { fontSize: fontSize })) }) }) }));
21
26
  });
@@ -13,6 +13,10 @@ const useStyles = makeStyles()({
13
13
  position: 'absolute',
14
14
  height: '100%',
15
15
  pointerEvents: 'none',
16
+ // its own compositor layer: a pan then moves the layer instead of
17
+ // re-rasterizing the root layer's tiles, which paid for the gridline paths
18
+ // on every frame
19
+ willChange: 'transform',
16
20
  },
17
21
  });
18
22
  // Shared zoom/scroll-transform wrapper for the gridline ticks and padding-block
@@ -1,6 +1,6 @@
1
1
  export interface SequenceSearchModel {
2
2
  assemblyNames: string[];
3
- showTrack: (trackId: string) => void;
3
+ launchTrack: (trackId: string) => Promise<unknown>;
4
4
  }
5
5
  export interface SequenceSearchModeProps {
6
6
  model: SequenceSearchModel;
@@ -14,7 +14,7 @@ export function addReferenceScanTrack(model, args) {
14
14
  type: 'FeatureTrack',
15
15
  adapter: args.adapter,
16
16
  });
17
- model.showTrack(args.trackId);
17
+ void model.launchTrack(args.trackId);
18
18
  }
19
19
  else {
20
20
  session.notify('This session does not support adding tracks', 'warning');
@@ -25,6 +25,7 @@ declare module '@jbrowse/core/PluginManager' {
25
25
  }
26
26
  export default function LinearGenomeViewF(pluginManager: PluginManager): void;
27
27
  export { stateModelFactory } from './model.ts';
28
+ export { lgvLaunchKeys } from './launchKeys.ts';
28
29
  export type { LinearGenomeViewModel, LinearGenomeViewStateModel, } from './model.ts';
29
30
  export type { BpOffset, ExportSvgOptions, HighlightType, InitState, LinearGenomeViewLaunchProps, NavLocation, TrackLabelMode, VolatileGuide, } from './types.ts';
30
31
  export { type SyncableViewAction, installLinkedViewSync, } from './linkedViewSync.ts';
@@ -1,17 +1,24 @@
1
1
  import { lazy } from 'react';
2
2
  import { ViewType } from '@jbrowse/core/pluggableElementTypes';
3
+ import { lgvLaunchKeys } from './launchKeys.js';
3
4
  import { stateModelFactory } from './model.js';
4
5
  export default function LinearGenomeViewF(pluginManager) {
5
6
  pluginManager.addViewType(() => {
7
+ // annotated against the registry rather than inferred, which is what
8
+ // makes a hand-written augmentation earn what `getViewType` promises
9
+ // its callers — see `ViewTypeRegistry`
10
+ const stateModel = stateModelFactory(pluginManager);
6
11
  return new ViewType({
7
12
  name: 'LinearGenomeView',
8
13
  displayName: 'Linear genome view',
9
- stateModel: stateModelFactory(pluginManager),
14
+ stateModel,
15
+ launchKeys: lgvLaunchKeys,
10
16
  ReactComponent: lazy(() => import('./components/LinearGenomeView.js')),
11
17
  });
12
18
  });
13
19
  }
14
20
  export { stateModelFactory } from './model.js';
21
+ export { lgvLaunchKeys } from './launchKeys.js';
15
22
  export { installLinkedViewSync, } from './linkedViewSync.js';
16
23
  export { default as HighlightBand } from './components/HighlightBand.js';
17
24
  export { default as HighlightChip } from './components/HighlightChip.js';
@@ -10,4 +10,3 @@ export declare function linearGenomeViewPropKeys(pluginManager: {
10
10
  };
11
11
  };
12
12
  }): ReadonlySet<string>;
13
- export declare function warnUnknownLaunchKeys(surface: string, unknown: object): void;
@@ -1,39 +1,13 @@
1
- // The init keys are derived from their interface so they can't drift: the
2
- // Record requires exactly one entry per key, so adding a field without listing
3
- // it here is a compile error rather than a key that silently warns as "unknown".
4
- //
5
- // There is no matching list for the plain view props, and there must not be:
6
- // they are whatever the state model declares, read off it by the caller and
7
- // passed in as `viewPropKeys`. A hand-written eight is what left `hideHeader`,
8
- // `showGridlines`, `showCytobands` and five others declared, menu-settable and
9
- // silently unreachable from any spec — the same failure LinearSyntenyView's
10
- // `drawLocationMarkers` had.
11
- //
12
- // #launchKeys LinearGenomeView — the URL parameters page renders this list
13
- // rather than restating it; each of these keys has its own `&param=` section
14
- // there, which is why no description is attached here.
15
- const knownInitKeyMap = {
16
- loc: true,
17
- grow: true,
18
- assembly: true,
19
- displayedRegionNames: true,
20
- tracks: true,
21
- tracklist: true,
22
- nav: true,
23
- highlight: true,
24
- };
25
- const knownInitKeys = new Set(Object.keys(knownInitKeyMap));
1
+ import { lgvLaunchKeys } from './launchKeys.js';
2
+ // The remaining v4 partition, still used by the synteny view to build its LGV
3
+ // rows. `lgvLaunchKeys` is the one declaration of what a launch key is, so the
4
+ // two cannot disagree while both exist.
5
+ const knownInitKeys = new Set(Object.keys(lgvLaunchKeys.keys));
26
6
  // Never settable from a spec, whatever the model declares: `type` picks the
27
- // view and `id` is passed top-level so MST's optional identifier honors it.
28
- const RESERVED = new Set(['id', 'type', 'init']);
29
- // The viewport keys from before it was stored as a window. They are no longer
30
- // declared properties, so the model's own property list does not contain them
31
- // and they would partition as typos — reported as unknown and dropped. They are
32
- // not typos: `preProcessSnapshot` still converts them, so they belong in the
33
- // snapshot bucket, and a URL or saved spec that names them goes on working.
34
- //
35
- // Deletable with `legacyBpPerPx` in the model, and for the same reason.
36
- const LEGACY_VIEWPORT_PROPS = new Set(['bpPerPx', 'offsetPx']);
7
+ // view, `id` is passed top-level so MST's optional identifier honors it, and
8
+ // `launch` is the blob the partition fills rather than something anyone writes.
9
+ const RESERVED = new Set(['id', 'type', 'launch']);
10
+ const LEGACY_VIEWPORT_PROPS = new Set(lgvLaunchKeys.passThrough);
37
11
  // Partition launch keys three ways, once, for every caller that needs to know
38
12
  // which is which:
39
13
  //
@@ -92,13 +66,3 @@ export function linearGenomeViewPropKeys(pluginManager) {
92
66
  propKeyCache.set(pluginManager, keys);
93
67
  return keys;
94
68
  }
95
- // Report the `unknown` bucket. Both callers say the same thing about the same
96
- // mistake and differ only in which surface they name, so the wording lives next
97
- // to the partition that produces the bucket rather than in two hand-written
98
- // copies that drift.
99
- export function warnUnknownLaunchKeys(surface, unknown) {
100
- const keys = Object.keys(unknown);
101
- if (keys.length) {
102
- console.warn(`${surface} ignored unknown key(s): ${keys.join(', ')}`);
103
- }
104
- }
@@ -0,0 +1,2 @@
1
+ import type { InitState } from './types.ts';
2
+ export declare const lgvLaunchKeys: import("@jbrowse/core/util/withLaunchInput").LaunchKeyRegistration<InitState, "bpPerPx" | "offsetPx">;
@@ -0,0 +1,23 @@
1
+ import { defineLaunchKeys } from '@jbrowse/core/util/withLaunchInput';
2
+ // The argument is `Record<keyof InitState, LaunchKeySpec>`, so a command this
3
+ // view interprets and nobody registered here is a compile error rather than a
4
+ // key that partitions as a typo.
5
+ //
6
+ // `bpPerPx`/`offsetPx` are the viewport spelling from before it was stored as a
7
+ // window. They are no longer declared properties, so the partition would read
8
+ // them as typos; the model's own preProcessSnapshot still converts them, so a
9
+ // URL or saved spec naming them goes on working. Deletable with `legacyBpPerPx`.
10
+ //
11
+ // #launchKeys LinearGenomeView — the URL parameters page renders this list
12
+ // rather than restating it; each of these keys has its own `&param=` section
13
+ // there, which is why no description is attached here.
14
+ export const lgvLaunchKeys = defineLaunchKeys()({
15
+ loc: { kind: 'launch' },
16
+ grow: { kind: 'launch' },
17
+ assembly: { kind: 'launch' },
18
+ displayedRegionNames: { kind: 'launch' },
19
+ tracklist: { kind: 'launch' },
20
+ nav: { kind: 'launch' },
21
+ tracks: { kind: 'trackEntries' },
22
+ highlight: { kind: 'highlightEntries' },
23
+ }, { passThrough: ['bpPerPx', 'offsetPx'] });
@@ -7,7 +7,7 @@ export declare const ReturnToImportFormDialog: import("react").LazyExoticCompone
7
7
  export declare const SequenceSearchDialog: import("react").LazyExoticComponent<({ model, handleClose, }: {
8
8
  model: {
9
9
  assemblyNames: string[];
10
- showTrack: (trackId: string) => void;
10
+ launchTrack: (trackId: string) => Promise<unknown>;
11
11
  };
12
12
  handleClose: () => void;
13
13
  }) => import("react").JSX.Element>;
@@ -6,7 +6,7 @@ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
6
6
  * arrays in each parent view) means the dispatch below stays exhaustive and a
7
7
  * typo can't silently produce a no-op sync.
8
8
  */
9
- export type SyncableViewAction = 'horizontalScroll' | 'zoomTo' | 'showTrack' | 'toggleTrack' | 'hideTrack' | 'setTrackLabels' | 'setShowCenterLine';
9
+ export type SyncableViewAction = 'horizontalScroll' | 'zoomTo' | 'showTrack' | 'toggleTrack' | 'launchTrack' | 'launchToggleTrack' | 'hideTrack' | 'setTrackLabels' | 'setShowCenterLine';
10
10
  interface LinkableViews {
11
11
  linkViews: boolean;
12
12
  views: LinearGenomeViewModel[];
@@ -13,6 +13,12 @@ function applyViewAction(view, name, args) {
13
13
  case 'toggleTrack':
14
14
  view.toggleTrack(args[0]);
15
15
  break;
16
+ case 'launchTrack':
17
+ void view.launchTrack(args[0]);
18
+ break;
19
+ case 'launchToggleTrack':
20
+ void view.launchToggleTrack(args[0]);
21
+ break;
16
22
  case 'hideTrack':
17
23
  view.hideTrack(args[0]);
18
24
  break;