@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.
- package/esm/LaunchLinearGenomeView/index.js +2 -13
- package/esm/LinearGenomeView/afterAttach.js +16 -18
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +1 -1
- package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +1 -1
- package/esm/LinearGenomeView/components/PaddingBlocks.js +11 -1
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +37 -3
- package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +1 -1
- package/esm/LinearGenomeView/components/TrackContainer.js +46 -4
- package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +3 -2
- package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +2 -1
- package/esm/LinearGenomeView/components/TrackRenderingContainer.js +5 -15
- package/esm/LinearGenomeView/components/ZoomButton.js +6 -1
- package/esm/LinearGenomeView/components/ZoomTransform.js +4 -0
- package/esm/LinearGenomeView/components/searchModes.d.ts +1 -1
- package/esm/LinearGenomeView/components/searchModes.js +1 -1
- package/esm/LinearGenomeView/consts.js +1 -1
- package/esm/LinearGenomeView/containingLgv.d.ts +15 -0
- package/esm/LinearGenomeView/containingLgv.js +16 -0
- package/esm/LinearGenomeView/index.d.ts +1 -0
- package/esm/LinearGenomeView/index.js +8 -1
- package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
- package/esm/LinearGenomeView/launchKeys.js +23 -0
- package/esm/LinearGenomeView/lazyDialogs.d.ts +1 -1
- package/esm/LinearGenomeView/linkedViewSync.d.ts +1 -1
- package/esm/LinearGenomeView/linkedViewSync.js +6 -0
- package/esm/LinearGenomeView/model.d.ts +1779 -41
- package/esm/LinearGenomeView/model.js +192 -96
- package/esm/LinearGenomeView/svgcomponents/figureContract.js +14 -14
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
- package/esm/LinearGenomeView/types.d.ts +1 -1
- package/esm/LinearGenomeView/util.d.ts +31 -0
- package/esm/LinearGenomeView/util.js +48 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +145 -55
- package/esm/displayKitTests/perRegionTestEnv.js +26 -25
- package/esm/index.d.ts +2 -9
- package/esm/index.js +2 -3
- package/esm/searchUtils.d.ts +6 -3
- package/esm/searchUtils.js +37 -11
- package/package.json +8 -8
- package/esm/LinearGenomeView/FloatingSvgOverlay.d.ts +0 -5
- package/esm/LinearGenomeView/FloatingSvgOverlay.js +0 -25
- package/esm/LinearGenomeView/initKeys.d.ts +0 -13
- 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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
138
|
-
// (unresolved id, bad config, etc) as its own snackbar
|
|
139
|
-
|
|
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.
|
|
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
|
|
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<
|
|
8
|
+
}): Promise<unknown>;
|
|
9
9
|
};
|
|
10
10
|
handleClose: () => void;
|
|
11
11
|
}): import("react").JSX.Element;
|
|
@@ -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:
|
|
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
|
|
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
|
|
207
|
+
setDisplayedRegionsKeepingCenter(model, withRegionRemoved(displayedRegions, idx));
|
|
174
208
|
},
|
|
175
209
|
},
|
|
176
210
|
]
|
|
@@ -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
|
|
20
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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
|
|
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 {
|
|
81
|
+
const { showTrackOutlines } = model;
|
|
82
82
|
const trackId = track.trackId;
|
|
83
83
|
const minimized = track.minimized;
|
|
84
|
-
|
|
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,
|
|
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
|
|
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
|
|
@@ -14,7 +14,7 @@ export function addReferenceScanTrack(model, args) {
|
|
|
14
14
|
type: 'FeatureTrack',
|
|
15
15
|
adapter: args.adapter,
|
|
16
16
|
});
|
|
17
|
-
model.
|
|
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
|
|
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
|
|
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,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 `¶m=` 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
|
-
|
|
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;
|