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

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 (45) hide show
  1. package/esm/LaunchLinearGenomeView/index.js +2 -13
  2. package/esm/LinearGenomeView/afterAttach.js +16 -18
  3. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +1 -1
  4. package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +1 -1
  5. package/esm/LinearGenomeView/components/PaddingBlocks.js +11 -1
  6. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +37 -3
  7. package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +1 -1
  8. package/esm/LinearGenomeView/components/TrackContainer.js +46 -4
  9. package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
  10. package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -2
  11. package/esm/LinearGenomeView/components/TrackLabelMenu.js +3 -2
  12. package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +2 -1
  13. package/esm/LinearGenomeView/components/TrackRenderingContainer.js +5 -15
  14. package/esm/LinearGenomeView/components/ZoomButton.js +6 -1
  15. package/esm/LinearGenomeView/components/ZoomTransform.js +4 -0
  16. package/esm/LinearGenomeView/components/searchModes.d.ts +1 -1
  17. package/esm/LinearGenomeView/components/searchModes.js +1 -1
  18. package/esm/LinearGenomeView/consts.js +1 -1
  19. package/esm/LinearGenomeView/containingLgv.d.ts +15 -0
  20. package/esm/LinearGenomeView/containingLgv.js +16 -0
  21. package/esm/LinearGenomeView/index.d.ts +1 -0
  22. package/esm/LinearGenomeView/index.js +8 -1
  23. package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
  24. package/esm/LinearGenomeView/launchKeys.js +23 -0
  25. package/esm/LinearGenomeView/lazyDialogs.d.ts +1 -1
  26. package/esm/LinearGenomeView/linkedViewSync.d.ts +1 -1
  27. package/esm/LinearGenomeView/linkedViewSync.js +6 -0
  28. package/esm/LinearGenomeView/model.d.ts +1779 -41
  29. package/esm/LinearGenomeView/model.js +192 -96
  30. package/esm/LinearGenomeView/svgcomponents/figureContract.js +14 -14
  31. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
  32. package/esm/LinearGenomeView/types.d.ts +1 -1
  33. package/esm/LinearGenomeView/util.d.ts +31 -0
  34. package/esm/LinearGenomeView/util.js +48 -0
  35. package/esm/displayKitTests/perRegionTestEnv.d.ts +145 -55
  36. package/esm/displayKitTests/perRegionTestEnv.js +26 -25
  37. package/esm/index.d.ts +2 -9
  38. package/esm/index.js +2 -3
  39. package/esm/searchUtils.d.ts +6 -3
  40. package/esm/searchUtils.js +37 -11
  41. package/package.json +8 -8
  42. package/esm/LinearGenomeView/FloatingSvgOverlay.d.ts +0 -5
  43. package/esm/LinearGenomeView/FloatingSvgOverlay.js +0 -25
  44. package/esm/LinearGenomeView/initKeys.d.ts +0 -13
  45. package/esm/LinearGenomeView/initKeys.js +0 -104
@@ -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
  }
@@ -5,7 +5,7 @@ export default function ExportSvgDialog({ model, handleClose, }: {
5
5
  exportSvg(opts: BaseExportSvgOptions & {
6
6
  trackLabels: TrackLabelMode;
7
7
  showGridlines: boolean;
8
- }): Promise<void>;
8
+ }): Promise<unknown>;
9
9
  };
10
10
  handleClose: () => void;
11
11
  }): import("react").JSX.Element;
@@ -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;
@@ -5,7 +5,7 @@ import { assembleLocString, assembleLocStrings } from '@jbrowse/core/util';
5
5
  import { copyText } from '@jbrowse/core/util/copyText';
6
6
  import { makeStyles } from '@jbrowse/core/util/tss-react';
7
7
  import { observer } from 'mobx-react';
8
- import { REF_NAME_LABEL_FONT_SIZE, regionMoveActions, withRegionMoved, withRegionRemoved, withRegionReversed, } from '../util.js';
8
+ import { REF_NAME_LABEL_FONT_SIZE, regionMoveActions, regionRunBounds, setDisplayedRegionsKeepingCenter, withRegionMoved, withRegionRemoved, withRegionReversed, } from '../util.js';
9
9
  const useStyles = makeStyles()(theme => ({
10
10
  refLabel: {
11
11
  // the width the fit test in getScalebarRefNameLabels measures against
@@ -104,6 +104,16 @@ const RefNameMenu = observer(function RefNameMenu({ model, menuState, onClose, }
104
104
  const locString = oneRegion
105
105
  ? assembleLocString(displayedRegions[idx])
106
106
  : assembleLocStrings(labeled);
107
+ // The region this label's blocks were cut out of. Taken over the WHOLE run
108
+ // rather than `labeled`, which the viewport clips: a collapsed-introns view
109
+ // zoomed into two exons would otherwise offer a span that is not the gene's.
110
+ const { first: runFirst, last: runLast } = regionRunBounds(displayedRegions, idx, lastIdx);
111
+ const run = displayedRegions.slice(runFirst, runLast + 1);
112
+ const wholeSpan = {
113
+ ...run[0],
114
+ start: Math.min(...run.map(r => r.start)),
115
+ end: Math.max(...run.map(r => r.end)),
116
+ };
107
117
  return (_jsx(Menu, { anchorEl: menuState.anchorEl, open: true, onClose: onClose, onMenuItemClick: callback => {
108
118
  callback();
109
119
  onClose();
@@ -126,7 +136,31 @@ const RefNameMenu = observer(function RefNameMenu({ model, menuState, onClose, }
126
136
  ? 'Show only this region'
127
137
  : `Show only ${refName}`,
128
138
  onClick: () => {
129
- model.setDisplayedRegions(labeled);
139
+ setDisplayedRegionsKeepingCenter(model, labeled);
140
+ },
141
+ },
142
+ ]
143
+ : []),
144
+ ...(runLast > runFirst
145
+ ? [
146
+ {
147
+ // Names its subject the way the two rows above it do; the span
148
+ // it lands on is not spelled into the label, which would move
149
+ // under the reader as they navigate.
150
+ label: `Show the whole span of ${refName}`,
151
+ // Focused rather than centre-kept: the rejoined region is one
152
+ // the old viewport never saw, so there is no centre to hold,
153
+ // and showing the span is the whole point of the row.
154
+ onClick: () => {
155
+ model.setDisplayedRegions([
156
+ ...displayedRegions.slice(0, runFirst),
157
+ wholeSpan,
158
+ ...displayedRegions.slice(runLast + 1),
159
+ ]);
160
+ model.moveTo({ index: runFirst, offset: 0 }, {
161
+ index: runFirst,
162
+ offset: wholeSpan.end - wholeSpan.start,
163
+ });
130
164
  },
131
165
  },
132
166
  ]
@@ -170,7 +204,7 @@ const RefNameMenu = observer(function RefNameMenu({ model, menuState, onClose, }
170
204
  {
171
205
  label: 'Remove this region from view',
172
206
  onClick: () => {
173
- model.setDisplayedRegions(withRegionRemoved(displayedRegions, idx));
207
+ setDisplayedRegionsKeepingCenter(model, withRegionRemoved(displayedRegions, idx));
174
208
  },
175
209
  },
176
210
  ]
@@ -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;
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
2
3
  import { ErrorBanner, ResizeHandle } from '@jbrowse/core/ui';
3
4
  import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
4
5
  import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
5
6
  import { TrackOverlaySlot } from '@jbrowse/display-ui';
7
+ import { isAlive } from '@jbrowse/mobx-state-tree';
6
8
  import { Paper } from '@mui/material';
7
9
  import { observer } from 'mobx-react';
8
10
  import { RESIZE_HANDLE_HEIGHT, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from '../consts.js';
@@ -16,8 +18,8 @@ const useStyles = makeStyles()({
16
18
  // described in TrackRenderingContainer. flow-root keeps the block formatting
17
19
  // context that `overflow: hidden` used to establish as a side effect (neither
18
20
  // `clip` nor no overflow does), so child margins still can't collapse through
19
- // and shift the track — which the breakpoint split view would pick up, since
20
- // its connector overlay measures trackRefs' getBoundingClientRect().top.
21
+ // and shift the track — which would put the pixels off the model's offset
22
+ // arithmetic, which the breakpoint split view draws its connectors from.
21
23
  root: {
22
24
  marginTop: TRACK_TOP_GAP,
23
25
  position: 'relative',
@@ -63,6 +65,45 @@ const useStyles = makeStyles()({
63
65
  position: 'absolute',
64
66
  },
65
67
  });
68
+ // Measures the in-flow label band, the distance from the Paper's content top to
69
+ // the rendering container, into the model. Observes the Paper: it grows and
70
+ // shrinks with the label, the label setting, and the display, and each of those
71
+ // can move the container. Published after a requestAnimationFrame like
72
+ // useWidthSetter, and cleared on unmount so a removed track leaves no band.
73
+ function useTrackLabelBand(model, trackId) {
74
+ const [paper, setPaper] = useState(null);
75
+ const [container, setContainer] = useState(null);
76
+ useEffect(() => {
77
+ if (paper && container && 'ResizeObserver' in window) {
78
+ let token;
79
+ const publish = () => {
80
+ token = requestAnimationFrame(() => {
81
+ if (isAlive(model)) {
82
+ const paperRect = paper.getBoundingClientRect();
83
+ const band = container.getBoundingClientRect().top -
84
+ paperRect.top -
85
+ paper.clientTop;
86
+ model.setTrackLabelBand(trackId, band);
87
+ }
88
+ });
89
+ };
90
+ publish();
91
+ const observer = new ResizeObserver(publish);
92
+ observer.observe(paper);
93
+ return () => {
94
+ observer.disconnect();
95
+ if (token !== undefined) {
96
+ cancelAnimationFrame(token);
97
+ }
98
+ if (isAlive(model)) {
99
+ model.setTrackLabelBand(trackId, 0);
100
+ }
101
+ };
102
+ }
103
+ return undefined;
104
+ }, [paper, container, model, trackId]);
105
+ return { setPaper, setContainer };
106
+ }
66
107
  const TrackContainer = observer(function TrackContainer({ model, track, }) {
67
108
  const { classes } = useStyles();
68
109
  // an LGV track always holds at least one linear display (activeDisplay =
@@ -79,9 +120,10 @@ const TrackContainer = observer(function TrackContainer({ model, track, }) {
79
120
  const trackLabelStyle = model.effectiveTrackLabels !== 'overlapping' || display.prefersOffset
80
121
  ? classes.trackLabelOffset
81
122
  : classes.trackLabelOverlap;
82
- return (_jsxs(Paper, { className: cx(classes.root, track.pinned ? null : classes.unpinnedTrack), variant: showTrackOutlines ? 'outlined' : undefined, elevation: showTrackOutlines ? undefined : 0, onDragOver: event => {
123
+ const { setPaper, setContainer } = useTrackLabelBand(model, track.trackId);
124
+ return (_jsxs(Paper, { ref: setPaper, className: cx(classes.root, track.pinned ? null : classes.unpinnedTrack), variant: showTrackOutlines ? 'outlined' : undefined, elevation: showTrackOutlines ? undefined : 0, onDragOver: event => {
83
125
  model.onTrackDragOver(track.id, event.clientY);
84
- }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, children: _jsx(ErrorBoundary, { resetKeys: [display.id], FallbackComponent: e => (_jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary })), children: _jsx(TrackRenderingContainer, { model: model, track: track }) }) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, onDrag: distance => display.resizeHeight(distance),
126
+ }, children: [track.pinned ? _jsx(Gridlines, { model: model, offset: outlineOffset }) : null, model.effectiveTrackLabels !== 'hidden' ? (_jsx(TrackLabel, { track: track, className: trackLabelStyle })) : null, _jsx(TrackOverlaySlot, { zIndex: 100, overlayStyle: { left: -outlineOffset }, children: _jsx(ErrorBoundary, { resetKeys: [display.id], FallbackComponent: e => (_jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary })), children: _jsx(TrackRenderingContainer, { model: model, track: track, ref: setContainer }) }) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, onDrag: distance => display.resizeHeight(distance),
85
127
  // the drag's shortcut: grow the track by whatever it is scrolled over,
86
128
  // so a pileup showing three rows of forty opens to all forty
87
129
  // Optional: only a display that scrolls its own content has anything
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { getContainingView } from '@jbrowse/core/util';
3
2
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
3
  import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
5
4
  import ArrowRightIcon from '@mui/icons-material/ArrowRight';
6
5
  import CloseIcon from '@mui/icons-material/Close';
7
6
  import { IconButton } from '@mui/material';
8
7
  import { observer } from 'mobx-react';
8
+ import { containingLgv } from '../../LinearGenomeView/containingLgv.js';
9
9
  const useStyles = makeStyles()({
10
10
  iconButton: {
11
11
  padding: 0,
@@ -21,7 +21,7 @@ const useStyles = makeStyles()({
21
21
  // write it.
22
22
  export const TrackLabelCloseButton = observer(function TrackLabelCloseButton({ track, }) {
23
23
  const { classes } = useStyles();
24
- const view = getContainingView(track);
24
+ const view = containingLgv(track);
25
25
  return (_jsx(IconButton, { onClick: () => view.hideTrack(track.trackId), className: classes.iconButton, title: "close this track", "data-testid": `track-close-${track.trackId}`, children: _jsx(CloseIcon, { fontSize: "small" }) }));
26
26
  });
27
27
  export const TrackLabelMinimizeButton = observer(function TrackLabelMinimizeButton({ track }) {
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { getContainingView } from '@jbrowse/core/util';
3
2
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
3
  import DragIcon from '@mui/icons-material/DragIndicator';
5
4
  import { observer } from 'mobx-react';
5
+ import { containingLgv } from '../../LinearGenomeView/containingLgv.js';
6
6
  const useStyles = makeStyles()({
7
7
  dragHandle: {
8
8
  cursor: 'grab',
@@ -15,7 +15,7 @@ const useStyles = makeStyles()({
15
15
  });
16
16
  const TrackLabelDragHandle = observer(function TrackLabelDragHandle({ track, }) {
17
17
  const { classes } = useStyles();
18
- const view = getContainingView(track);
18
+ const view = containingLgv(track);
19
19
  const trackId = track.trackId;
20
20
  return (_jsx("span", { draggable: true, className: classes.dragHandle, onDragStart: event => {
21
21
  const target = event.currentTarget;
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
3
- import { getContainingView, getSession } from '@jbrowse/core/util';
3
+ import { getSession } from '@jbrowse/core/util';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import LowPriorityIcon from '@mui/icons-material/LowPriority';
6
6
  import MoreVertIcon from '@mui/icons-material/MoreVert';
7
7
  import { observer } from 'mobx-react';
8
+ import { containingLgv } from '../../LinearGenomeView/containingLgv.js';
8
9
  import { getTrackOrderSubMenu } from './trackLabelMenuItems.js';
9
10
  // IconButton's default 5px padding made this the tallest thing in the label
10
11
  // (27px), so it, not the text or the other controls, set the label height and
@@ -17,7 +18,7 @@ const useStyles = makeStyles()({
17
18
  });
18
19
  const TrackLabelMenu = observer(function TrackLabelMenu({ track, }) {
19
20
  const { classes } = useStyles();
20
- const view = getContainingView(track);
21
+ const view = containingLgv(track);
21
22
  const session = getSession(track);
22
23
  function getMenuItems() {
23
24
  const orderSubMenu = getTrackOrderSubMenu({ view, track });
@@ -1,8 +1,9 @@
1
1
  import type { LinearGenomeViewModel } from '../index.ts';
2
2
  import type { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models';
3
3
  type LGV = LinearGenomeViewModel;
4
- declare const TrackRenderingContainer: ({ model, track, }: {
4
+ declare const TrackRenderingContainer: ({ model, track, ref, }: {
5
5
  model: LGV;
6
6
  track: BaseTrackModel;
7
+ ref?: React.Ref<HTMLDivElement>;
7
8
  }) => import("react").JSX.Element;
8
9
  export default TrackRenderingContainer;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, useCallback } from 'react';
2
+ import { Suspense } from 'react';
3
3
  import { LoadingOverlay } from '@jbrowse/core/ui';
4
4
  import { getSession } from '@jbrowse/core/util';
5
5
  import { getTrackName } from '@jbrowse/core/util/tracks';
@@ -72,28 +72,18 @@ const TrackDisplayRegion = observer(function TrackDisplayRegion({ model, track,
72
72
  const loc = model.coarseVisibleLocStrings;
73
73
  return (_jsx("div", { ...divProps, role: "figure", "aria-label": loc ? `${trackName} track, showing ${loc}` : `${trackName} track`, children: children }));
74
74
  });
75
- const TrackRenderingContainer = observer(function TrackRenderingContainer({ model, track, }) {
75
+ const TrackRenderingContainer = observer(function TrackRenderingContainer({ model, track, ref, }) {
76
76
  const { classes } = useStyles();
77
77
  // an LGV track always holds at least one linear display (activeDisplay =
78
78
  // displays[0]); narrow to the linear shape for height/RenderingComponent
79
79
  const display = track.activeDisplay;
80
80
  const { height, RenderingComponent, DisplayBlurb } = display;
81
- const { trackRefs, showTrackOutlines } = model;
81
+ const { showTrackOutlines } = model;
82
82
  const trackId = track.trackId;
83
83
  const minimized = track.minimized;
84
- // callback ref keeps trackRefs in sync as the rendering div mounts/unmounts
85
- // (e.g. on minimize/restore), unlike a useEffect that misses the toggle
86
- const setRef = useCallback((el) => {
87
- if (el) {
88
- trackRefs[trackId] = el;
89
- }
90
- else {
91
- delete trackRefs[trackId];
92
- }
93
- }, [trackRefs, trackId]);
94
- return (_jsx("div", { className: classes.trackRenderingContainer, style: {
84
+ return (_jsx("div", { ref: ref, className: classes.trackRenderingContainer, style: {
95
85
  height: minimized ? MINIMIZED_TRACK_HEIGHT : height,
96
- }, "data-testid": `trackRenderingContainer-${model.id}-${trackId}`, children: !minimized ? (_jsxs(_Fragment, { children: [_jsx(TrackDisplayRegion, { model: model, track: track, ref: setRef, className: classes.renderingComponentContainer, style: { left: showTrackOutlines ? -1 : 0 }, children: _jsx(Suspense, { fallback: _jsx(LoadingOverlay, { isVisible: true, immediate: true }), children: _jsx(RenderingComponent, { model: display, onHorizontalScroll: model.horizontalScroll }) }) }), DisplayBlurb ? (_jsx("div", { style: {
86
+ }, "data-testid": `trackRenderingContainer-${model.id}-${trackId}`, children: !minimized ? (_jsxs(_Fragment, { children: [_jsx(TrackDisplayRegion, { model: model, track: track, className: classes.renderingComponentContainer, style: { left: showTrackOutlines ? -1 : 0 }, children: _jsx(Suspense, { fallback: _jsx(LoadingOverlay, { isVisible: true, immediate: true }), children: _jsx(RenderingComponent, { model: display, onHorizontalScroll: model.horizontalScroll }) }) }), DisplayBlurb ? (_jsx("div", { style: {
97
87
  position: 'absolute',
98
88
  left: 0,
99
89
  top: display.height - 20,
@@ -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');
@@ -14,7 +14,7 @@ export const SVG_SCALEBAR_CAP = 5;
14
14
  // TrackContainer lays its Paper out from these same three and nothing else —
15
15
  // the previous spelling had the model on a 3 while the CSS rendered 2 + 1 + 4,
16
16
  // which put every model-derived track offset 5px per track above the pixels.
17
- // The track *label* is deliberately not among them; see `trackChromeHeight`.
17
+ // The track *label* is not among them: it is measured, see `trackLabelBand`.
18
18
  export const RESIZE_HANDLE_HEIGHT = 4;
19
19
  export const TRACK_TOP_GAP = 2;
20
20
  export const TRACK_OUTLINE_BORDER = 1;
@@ -0,0 +1,15 @@
1
+ import type { LinearGenomeViewModel } from './model.ts';
2
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
3
+ /**
4
+ * The containing view as the linear genome view itself, for a display that
5
+ * needs `pxToBp`, `showTrack`, `navTo` or the view's own chrome settings.
6
+ *
7
+ * The twin of display-kit's `containingHost`, and deliberately not the same
8
+ * function: `RegionHost` is the region contract display-kit reads, and widening
9
+ * it to carry the LGV's own surface is how the display layer would come to
10
+ * depend on the view plugin. A display that wants the view names the view.
11
+ *
12
+ * Components and structural helpers taking duck-typed model shapes keep calling
13
+ * `getContainingView` — the same carve-out `containingHost` documents.
14
+ */
15
+ export declare function containingLgv(self: IStateTreeNode): LinearGenomeViewModel;
@@ -0,0 +1,16 @@
1
+ import { getContainingView } from '@jbrowse/core/util';
2
+ /**
3
+ * The containing view as the linear genome view itself, for a display that
4
+ * needs `pxToBp`, `showTrack`, `navTo` or the view's own chrome settings.
5
+ *
6
+ * The twin of display-kit's `containingHost`, and deliberately not the same
7
+ * function: `RegionHost` is the region contract display-kit reads, and widening
8
+ * it to carry the LGV's own surface is how the display layer would come to
9
+ * depend on the view plugin. A display that wants the view names the view.
10
+ *
11
+ * Components and structural helpers taking duck-typed model shapes keep calling
12
+ * `getContainingView` — the same carve-out `containingHost` documents.
13
+ */
14
+ export function containingLgv(self) {
15
+ return getContainingView(self);
16
+ }
@@ -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';
@@ -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;