@jbrowse/plugin-linear-genome-view 5.0.0-beta.19 → 5.0.0-beta.20
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/LinearGenomeView/afterAttach.d.ts +17 -0
- package/esm/LinearGenomeView/afterAttach.js +71 -43
- package/esm/LinearGenomeView/components/CenterLine.js +6 -3
- package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
- package/esm/LinearGenomeView/components/ExportSvgDialog.js +12 -3
- package/esm/LinearGenomeView/components/Header.js +1 -1
- package/esm/LinearGenomeView/components/HeaderZoomControls.js +3 -2
- package/esm/LinearGenomeView/components/HighlightBand.js +7 -7
- package/esm/LinearGenomeView/components/ImportForm.js +3 -3
- package/esm/LinearGenomeView/components/MiniControls.js +1 -1
- package/esm/LinearGenomeView/components/OverviewRubberband.js +5 -3
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +7 -4
- package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
- package/esm/LinearGenomeView/components/SVGHighlightBand.js +15 -3
- package/esm/LinearGenomeView/components/SearchBox.js +3 -2
- package/esm/LinearGenomeView/components/SearchResultsTable.js +2 -2
- package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -3
- package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
- package/esm/LinearGenomeView/components/useRangeSelect.js +5 -2
- package/esm/LinearGenomeView/menuItems.js +5 -5
- package/esm/LinearGenomeView/model.d.ts +52 -40
- package/esm/LinearGenomeView/model.js +71 -50
- package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +4 -0
- package/esm/LinearGenomeView/rExport/exportLinearView.js +26 -6
- package/esm/LinearGenomeView/rExport/rScript.d.ts +19 -9
- package/esm/LinearGenomeView/rExport/rScript.js +91 -53
- package/esm/LinearGenomeView/showRegionsWithUndo.js +5 -3
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +2 -2
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +2 -2
- package/esm/LinearGenomeView/svgcomponents/SVGView.js +15 -1
- package/esm/LinearGenomeView/svgcomponents/util.d.ts +1 -0
- package/esm/LinearGenomeView/types.d.ts +6 -0
- package/esm/LinearGenomeView/types.js +0 -1
- package/esm/LinearGenomeView/util.js +9 -9
- package/esm/MultiLevelRubberband/useRangeSelect.js +14 -12
- package/esm/TrackModifiers/index.d.ts +4 -1
- package/esm/TrackModifiers/index.js +26 -7
- package/esm/displayKitTests/perRegionTestEnv.d.ts +2 -0
- package/esm/index.d.ts +2 -2
- package/esm/index.js +1 -1
- package/esm/searchUtils.d.ts +1 -3
- package/esm/searchUtils.js +33 -44
- package/esm/shared/useWindowDrag.js +11 -9
- package/package.json +7 -7
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { LinearGenomeViewModel } from './model.ts';
|
|
2
2
|
import type { InitState } from './types.ts';
|
|
3
3
|
import type { AbstractSessionModel, AssemblyHost, NotificationSink } from '@jbrowse/core/util';
|
|
4
|
+
/**
|
|
5
|
+
* Navigate a view to an init blob's `loc`, reporting a bad one as a warning
|
|
6
|
+
* (a gene that matched nothing) or an error rather than throwing. Exported
|
|
7
|
+
* because a LinearSyntenyView row is a LinearGenomeView that does not go
|
|
8
|
+
* through this file's init autorun, so it navigates each row itself.
|
|
9
|
+
*/
|
|
10
|
+
export declare function navToInitLoc(view: Pick<LinearGenomeViewModel, 'navToLocString'>, session: NotificationSink, init: Pick<InitState, 'assembly' | 'grow' | 'showHitTrack' | 'tracks'> & {
|
|
11
|
+
loc: string;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Open an init blob's `tracks`, one at a time, a bare string taken as a list
|
|
15
|
+
* of one. launchTrack funnels through showTrackGeneric, which surfaces any
|
|
16
|
+
* failure (unresolved id, bad config) as its own snackbar, and loads the
|
|
17
|
+
* picked display's state model first when it is registered lazily. Exported
|
|
18
|
+
* for the LinearSyntenyView rows, as `navToInitLoc` is.
|
|
19
|
+
*/
|
|
20
|
+
export declare function showInitTracks(self: LinearGenomeViewModel, init: Pick<InitState, 'tracks'>): Promise<void>;
|
|
4
21
|
/**
|
|
5
22
|
* Add an init blob's `highlight` entries to the session, coercing each
|
|
6
23
|
* locstring or wire-format object and reporting a bad one without taking out
|
|
@@ -2,10 +2,9 @@ import { getSession, isSessionModelWithWidgets, resolveNamedRegions, } from '@jb
|
|
|
2
2
|
import { coerceHighlight } from '@jbrowse/core/util/highlights';
|
|
3
3
|
import { installInitAutorun } from '@jbrowse/core/util/installInitAutorun';
|
|
4
4
|
import { normalizeTrackInit, warmTrackDisplayGeneric, } from '@jbrowse/core/util/tracks';
|
|
5
|
-
import { requestedDisplayType } from '@jbrowse/launch-args/
|
|
5
|
+
import { requestedDisplayType } from '@jbrowse/launch-args/displayAlias';
|
|
6
6
|
import { addDisposer } from '@jbrowse/mobx-state-tree';
|
|
7
7
|
import { autorun, untracked, when } from 'mobx';
|
|
8
|
-
import { launchTrackInit } from '../TrackModifiers/index.js';
|
|
9
8
|
import { SearchResultsNotFoundError } from '../searchUtils.js';
|
|
10
9
|
// `init` is a frozen blob that often comes from hand-authored JSON, where a
|
|
11
10
|
// lone entry gets written bare (`tracks: 'genes'`). A string is iterable, so
|
|
@@ -83,53 +82,82 @@ function showNamedRegions(self, session, assemblyName, names) {
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
|
-
|
|
85
|
+
function reportInitNavError(session, e) {
|
|
86
|
+
console.error(e);
|
|
87
|
+
if (e instanceof SearchResultsNotFoundError) {
|
|
88
|
+
// a gene name that matched nothing is a soft miss, not an app error
|
|
89
|
+
session.notify(e.message, 'warning');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
session.notifyError(`${e}`, e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Navigate a view to an init blob's `loc`, reporting a bad one as a warning
|
|
97
|
+
* (a gene that matched nothing) or an error rather than throwing. Exported
|
|
98
|
+
* because a LinearSyntenyView row is a LinearGenomeView that does not go
|
|
99
|
+
* through this file's init autorun, so it navigates each row itself.
|
|
100
|
+
*/
|
|
101
|
+
export async function navToInitLoc(view, session, init) {
|
|
87
102
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await self.navToLocString(init.loc, init.assembly, init.grow, {
|
|
95
|
-
showHitTrack: init.showHitTrack ?? asArray(init.tracks).length === 0,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
else if (init.displayedRegionNames?.length) {
|
|
99
|
-
// an explicit region list is a navigation request just like `loc`, so it
|
|
100
|
-
// applies even when regions already exist (URL params layered onto a
|
|
101
|
-
// defaultSession that already navigated)
|
|
102
|
-
//
|
|
103
|
-
// `?.length`, not the bare key: an empty array is truthy, so a spec
|
|
104
|
-
// writing `displayedRegionNames: []` took this path with nothing to name
|
|
105
|
-
// and got told its list of no names had matched no regions. That is why
|
|
106
|
-
// LinearSyntenyView's doSubmit omits the key rather than passing [].
|
|
107
|
-
showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
|
|
108
|
-
}
|
|
109
|
-
else if (!self.hasDisplayedRegions) {
|
|
110
|
-
// a highlight-only init (nothing to navigate to) must not clobber a
|
|
111
|
-
// defaultSession's existing navigation, so only auto-navigate when
|
|
112
|
-
// nothing is shown yet
|
|
113
|
-
self.showAllRegionsInAssembly(init.assembly);
|
|
114
|
-
}
|
|
103
|
+
// navToLocString waits for the assembly itself. A spec that listed its
|
|
104
|
+
// tracks gets those tracks; one that only named a gene gets the track the
|
|
105
|
+
// gene was found in, as the search box would
|
|
106
|
+
await view.navToLocString(init.loc, init.assembly, init.grow, {
|
|
107
|
+
showHitTrack: init.showHitTrack ?? asArray(init.tracks).length === 0,
|
|
108
|
+
});
|
|
115
109
|
}
|
|
116
110
|
catch (e) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
reportInitNavError(session, e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function navigateInit(self, session, init) {
|
|
115
|
+
if (init.loc) {
|
|
116
|
+
await navToInitLoc(self, session, { ...init, loc: init.loc });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
try {
|
|
120
|
+
navigateWithoutLoc(self, session, init);
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
session
|
|
122
|
+
catch (e) {
|
|
123
|
+
reportInitNavError(session, e);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
function navigateWithoutLoc(self, session, init) {
|
|
128
|
+
if (init.displayedRegionNames?.length) {
|
|
129
|
+
// an explicit region list is a navigation request just like `loc`, so it
|
|
130
|
+
// applies even when regions already exist (URL params layered onto a
|
|
131
|
+
// defaultSession that already navigated)
|
|
132
|
+
//
|
|
133
|
+
// `?.length`, not the bare key: an empty array is truthy, so a spec
|
|
134
|
+
// writing `displayedRegionNames: []` took this path with nothing to name
|
|
135
|
+
// and got told its list of no names had matched no regions. That is why
|
|
136
|
+
// LinearSyntenyView's doSubmit omits the key rather than passing [].
|
|
137
|
+
showNamedRegions(self, session, init.assembly, init.displayedRegionNames);
|
|
138
|
+
}
|
|
139
|
+
else if (!self.hasDisplayedRegions) {
|
|
140
|
+
// a highlight-only init (nothing to navigate to) must not clobber a
|
|
141
|
+
// defaultSession's existing navigation, so only auto-navigate when
|
|
142
|
+
// nothing is shown yet
|
|
143
|
+
self.showAllRegionsInAssembly(init.assembly);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Open an init blob's `tracks`, one at a time, a bare string taken as a list
|
|
148
|
+
* of one. launchTrack funnels through showTrackGeneric, which surfaces any
|
|
149
|
+
* failure (unresolved id, bad config) as its own snackbar, and loads the
|
|
150
|
+
* picked display's state model first when it is registered lazily. Exported
|
|
151
|
+
* for the LinearSyntenyView rows, as `navToInitLoc` is.
|
|
152
|
+
*/
|
|
153
|
+
export async function showInitTracks(self, init) {
|
|
154
|
+
const tracks = asArray(init.tracks);
|
|
155
|
+
if (tracks.length) {
|
|
156
|
+
// lazy, so the modifier and slot tables stay out of every page's first load
|
|
157
|
+
const { launchTrackInit } = await import('../TrackModifiers/index.js');
|
|
158
|
+
for (const t of tracks) {
|
|
159
|
+
await launchTrackInit(self, t);
|
|
160
|
+
}
|
|
133
161
|
}
|
|
134
162
|
}
|
|
135
163
|
/**
|
|
@@ -147,7 +175,7 @@ export function applyInitHighlights(session, init) {
|
|
|
147
175
|
// parseLocString throws on an unknown refName or a malformed locstring, and
|
|
148
176
|
// one bad entry must take out neither its siblings nor the rest of init
|
|
149
177
|
try {
|
|
150
|
-
const highlight = coerceHighlight(h, init.assembly, refName => session.assemblyManager.isValidRefName(refName, init.assembly));
|
|
178
|
+
const highlight = coerceHighlight(h, init.assembly, (refName, asm) => session.assemblyManager.isValidRefName(refName, asm ?? init.assembly));
|
|
151
179
|
if (highlight) {
|
|
152
180
|
session.addHighlight(highlight);
|
|
153
181
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { clamp } from '@jbrowse/core/util';
|
|
3
|
-
import { basePaintedAt } from '@jbrowse/core/util/Base1DUtils';
|
|
3
|
+
import { basePaintedAt, bpIntoPaintedBase, } from '@jbrowse/core/util/Base1DUtils';
|
|
4
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
6
6
|
import { IconButton, Tooltip } from '@mui/material';
|
|
@@ -36,9 +36,12 @@ const useStyles = makeStyles()(theme => ({
|
|
|
36
36
|
const CenterLine = observer(function CenterLine({ model }) {
|
|
37
37
|
const { bpPerPx, centerLineInfo, tracks, width } = model;
|
|
38
38
|
const { classes } = useStyles();
|
|
39
|
-
const
|
|
39
|
+
const left = centerLineInfo && bpPerPx < 1
|
|
40
|
+
? width / 2 -
|
|
41
|
+
bpIntoPaintedBase(centerLineInfo, centerLineInfo.offset) / bpPerPx
|
|
42
|
+
: width / 2;
|
|
40
43
|
return tracks.length ? (_jsx("div", { "data-testid": "centerline_container", className: classes.centerLineContainer, role: "presentation", style: {
|
|
41
|
-
transform: `translateX(${
|
|
44
|
+
transform: `translateX(${left}px)`,
|
|
42
45
|
width: Math.max(1 / bpPerPx, 1),
|
|
43
46
|
}, children: centerLineInfo && (_jsxs("div", { "data-testid": "centerline_text", className: classes.centerLineText, role: "presentation", style: {
|
|
44
47
|
transform: `translateX(${Math.max(1 / bpPerPx, 1) + 5}px)`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { getSession, isSessionModelWithWidgets } from '@jbrowse/core/util';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import HoverPositionHighlight from './HoverPositionHighlight.js';
|
|
5
5
|
// Draws a crosshair for every session widget "connected" to this view that is
|
|
@@ -9,8 +9,7 @@ import HoverPositionHighlight from './HoverPositionHighlight.js';
|
|
|
9
9
|
// hovered interval or undefined; the widget iteration and drawing are shared.
|
|
10
10
|
const ConnectedHoverHighlight = observer(function ConnectedHoverHighlight({ model, getPosition, }) {
|
|
11
11
|
const session = getSession(model);
|
|
12
|
-
|
|
13
|
-
return !widgets ? null : (_jsx(_Fragment, { children: [...widgets].flatMap(([id, widget]) => {
|
|
12
|
+
return !isSessionModelWithWidgets(session) ? null : (_jsx(_Fragment, { children: [...session.widgets].flatMap(([id, widget]) => {
|
|
14
13
|
const position = getPosition(widget, model.id);
|
|
15
14
|
return position
|
|
16
15
|
? [
|
|
@@ -5,6 +5,12 @@ import { MenuItem, TextField } from '@mui/material';
|
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
6
|
const MIN_WIDTH = 100;
|
|
7
7
|
const MAX_WIDTH = 10000;
|
|
8
|
+
const TRACK_LABEL_MODES = [
|
|
9
|
+
{ mode: 'offset', label: 'Offset' },
|
|
10
|
+
{ mode: 'overlapping', label: 'Overlapping' },
|
|
11
|
+
{ mode: 'left', label: 'Left' },
|
|
12
|
+
{ mode: 'hidden', label: 'Hidden' },
|
|
13
|
+
];
|
|
8
14
|
// Shared track-label + gridlines export dialog. Used by LGV, linear-synteny and
|
|
9
15
|
// breakpoint-split views (their lazyDialogs re-export this). Only a caller
|
|
10
16
|
// passing `chooseWidth` offers the width field, whose model lays itself out at
|
|
@@ -29,9 +35,12 @@ const ExportSvgDialog = observer(function ExportSvgDialog({ model, handleClose,
|
|
|
29
35
|
viewWidth: chooseWidth && widthValid ? typedWidth : undefined,
|
|
30
36
|
}), checkboxes: _jsx(LabeledCheckbox, { checked: showGridlines, onChange: val => {
|
|
31
37
|
setShowGridlines(val);
|
|
32
|
-
}, label: "Show gridlines" }), children: [
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
}, label: "Show gridlines" }), children: [_jsx(TextField, { select: true, label: "Track label positioning", variant: "outlined", style: { minWidth: 200 }, value: trackLabels, onChange: event => {
|
|
39
|
+
const picked = TRACK_LABEL_MODES.find(m => m.mode === event.target.value);
|
|
40
|
+
if (picked) {
|
|
41
|
+
setTrackLabels(picked.mode);
|
|
42
|
+
}
|
|
43
|
+
}, children: TRACK_LABEL_MODES.map(({ mode, label }) => (_jsx(MenuItem, { value: mode, children: label }, mode))) }), chooseWidth ? (_jsx(TextField, { label: "Width (px)", variant: "outlined", type: "number", value: widthText, error: !widthValid, helperText: widthValid
|
|
35
44
|
? 'Lays the tracks out as a window this wide would; text keeps its size'
|
|
36
45
|
: `A whole number from ${MIN_WIDTH} to ${MAX_WIDTH}`, slotProps: {
|
|
37
46
|
htmlInput: { min: MIN_WIDTH, max: MAX_WIDTH, step: 1 },
|
|
@@ -50,7 +50,7 @@ const Controls = observer(function Controls({ model, }) {
|
|
|
50
50
|
viewMenu,
|
|
51
51
|
touch,
|
|
52
52
|
});
|
|
53
|
-
return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [viewMenu ? (_jsx(CascadingMenuButton, { menuItems: () => model.menuItems(), "data-testid": "view_menu_icon", children: _jsx(MenuIcon, {}) })) : null, _jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), touch ? null : (_jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel })), _jsx("div", { className: classes.spacer }), touch ? null : (_jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing })), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
|
|
53
|
+
return (_jsxs("div", { className: classes.headerBar, ref: ref, children: [viewMenu ? (_jsx(CascadingMenuButton, { tooltip: "View menu", menuItems: () => model.menuItems(), "data-testid": "view_menu_icon", children: _jsx(MenuIcon, {}) })) : null, _jsx(HeaderTrackSelectorButton, { model: model, indent: fit.trackSelectorIndent }), touch ? null : (_jsx(ScrollZoomToggle, { model: model, iconOnly: !fit.scrollZoomLabel })), _jsx("div", { className: classes.spacer }), touch ? null : (_jsx(HeaderPanControls, { model: model, compact: !fit.panButtonSpacing })), _jsx(SearchBox, { model: model }), _jsx(HeaderClearHighlightButton, { highlighted: highlighted }), fit.regionWidth ? _jsx(HeaderRegionWidth, { model: model }) : null, _jsx(HeaderZoomControls, { model: model, showSlider: fit.zoomSlider }), _jsx("div", { className: classes.spacer })] }));
|
|
54
54
|
});
|
|
55
55
|
const LinearGenomeViewHeader = observer(function LinearGenomeViewHeader({ model, }) {
|
|
56
56
|
const { hideHeader, hideHeaderOverview } = model;
|
|
@@ -37,11 +37,12 @@ const ZoomSlider = observer(function ZoomSlider({ model, }) {
|
|
|
37
37
|
const { width, maxBpPerPx, minBpPerPx, bpPerPx } = model;
|
|
38
38
|
const [dragValue, setDragValue] = useState(null);
|
|
39
39
|
const value = dragValue ?? -Math.log2(bpPerPx) * 100;
|
|
40
|
+
const windowSize = (sliderValue) => `Window size: ${getBpDisplayStr(2 ** (-sliderValue / 100) * width)}`;
|
|
40
41
|
return (_jsx(SingleSlider, { size: "small", className: classes.slider, value: value, min: -Math.log2(maxBpPerPx) * 100, max: -Math.log2(minBpPerPx) * 100, onChangeCommitted: val => {
|
|
41
42
|
setDragValue(null);
|
|
42
43
|
model.cancelZoomAnimation();
|
|
43
44
|
model.zoomTo(2 ** (-val / 100));
|
|
44
|
-
}, valueLabelDisplay: "auto", valueLabelFormat: newValue =>
|
|
45
|
+
}, valueLabelDisplay: "auto", "aria-label": "Zoom level", valueLabelFormat: newValue => windowSize(newValue), getAriaValueText: newValue => windowSize(newValue), onChange: val => {
|
|
45
46
|
// Take over from any in-flight animated zoom as soon as the user grabs
|
|
46
47
|
// the thumb, so the view stops lurching underneath the drag.
|
|
47
48
|
if (dragValue === null) {
|
|
@@ -52,6 +53,6 @@ const ZoomSlider = observer(function ZoomSlider({ model, }) {
|
|
|
52
53
|
});
|
|
53
54
|
const HeaderZoomControls = observer(function HeaderZoomControls({ model, showSlider = true, }) {
|
|
54
55
|
const { classes } = useStyles();
|
|
55
|
-
return (_jsxs("div", { className: classes.container, children: [_jsx(ZoomButton, { model: model, direction: "out" }), showSlider ? _jsx(ZoomSlider, { model: model }) : null, _jsx(ZoomButton, { model: model, direction: "in" }), _jsx(CascadingMenuButton, { menuItems: () => zoomMenuItems(model), children: _jsx(MoreVert, {}) })] }));
|
|
56
|
+
return (_jsxs("div", { className: classes.container, children: [_jsx(ZoomButton, { model: model, direction: "out" }), showSlider ? _jsx(ZoomSlider, { model: model }) : null, _jsx(ZoomButton, { model: model, direction: "in" }), _jsx(CascadingMenuButton, { tooltip: "Zoom options", menuItems: () => zoomMenuItems(model), children: _jsx(MoreVert, {}) })] }));
|
|
56
57
|
});
|
|
57
58
|
export default HeaderZoomControls;
|
|
@@ -39,21 +39,21 @@ export function gapMask(gaps) {
|
|
|
39
39
|
]);
|
|
40
40
|
return `linear-gradient(to bottom, ${stops.join(', ')})`;
|
|
41
41
|
}
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// toggled from the view menu
|
|
42
|
+
// A colored band with an optional top label; when `children` (the highlight
|
|
43
|
+
// chip) is passed it renders that instead of the plain label. Clicks fall
|
|
44
|
+
// through it to the tracks, which is what keeps it apart from the hoverable
|
|
45
|
+
// OverviewHighlightBand. Visibility is toggled from the view menu
|
|
46
46
|
const HighlightBand = observer(function HighlightBand({ coords, background, label, gaps, children, }) {
|
|
47
47
|
const { classes } = useStyles();
|
|
48
|
+
const mask = gaps?.length ? gapMask(gaps) : undefined;
|
|
48
49
|
const content = children ??
|
|
49
50
|
(label ? (_jsx(Typography, { variant: "caption", className: classes.label, noWrap: true, children: label })) : null);
|
|
50
51
|
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "highlight-band", className: classes.highlight, style: {
|
|
51
52
|
transform: `translateX(${coords.left}px)`,
|
|
52
53
|
width: coords.width,
|
|
53
54
|
background,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
: undefined),
|
|
55
|
+
maskImage: mask,
|
|
56
|
+
WebkitMaskImage: mask,
|
|
57
57
|
} }), content ? (_jsx("div", { className: classes.overlay, style: { transform: `translateX(${coords.left}px)` }, children: content })) : null] }));
|
|
58
58
|
});
|
|
59
59
|
export default HighlightBand;
|
|
@@ -44,12 +44,12 @@ const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm(
|
|
|
44
44
|
model.setError(undefined);
|
|
45
45
|
if (selectedAsm) {
|
|
46
46
|
try {
|
|
47
|
-
await navigateToSelectedOption({
|
|
47
|
+
const moved = await navigateToSelectedOption({
|
|
48
48
|
option,
|
|
49
49
|
model,
|
|
50
50
|
assemblyName: selectedAsm,
|
|
51
51
|
});
|
|
52
|
-
if (record) {
|
|
52
|
+
if (record && moved) {
|
|
53
53
|
addRecentLocation(recentLocationOf(option));
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -61,7 +61,7 @@ const LinearGenomeViewImportForm = observer(function LinearGenomeViewImportForm(
|
|
|
61
61
|
const recentMenuItems = recentLocationsMenu({
|
|
62
62
|
recentLocations,
|
|
63
63
|
onNavigate: option => {
|
|
64
|
-
navigate({ option, record: true })
|
|
64
|
+
void navigate({ option, record: true });
|
|
65
65
|
},
|
|
66
66
|
onClear: clearRecentLocations,
|
|
67
67
|
});
|
|
@@ -33,7 +33,7 @@ const MiniControls = observer(function MiniControls({ model, }) {
|
|
|
33
33
|
? { top: model.scalebarHeight, transform: 'translateY(-100%)' }
|
|
34
34
|
: undefined, children: _jsxs(Paper, { className: focusedViewId === id ? classes.focusedBackground : undefined, children: [_jsx(Tooltip, { title: "Open track selector", children: _jsx(IconButton, { size: "small", onClick: () => {
|
|
35
35
|
model.activateTrackSelector();
|
|
36
|
-
}, children: _jsx(TrackSelectorIcon, { fontSize: "small" }) }) }), _jsx(CascadingMenuButton, { menuItems: () => model.menuItems(), children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsx(Tooltip, { title: scalebarOnly ? 'Show tracks' : 'Show ruler only', children: _jsx(IconButton, { size: "small", onClick: () => {
|
|
36
|
+
}, children: _jsx(TrackSelectorIcon, { fontSize: "small" }) }) }), _jsx(CascadingMenuButton, { tooltip: "View menu", menuItems: () => model.menuItems(), children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsx(Tooltip, { title: scalebarOnly ? 'Show tracks' : 'Show ruler only', children: _jsx(IconButton, { size: "small", onClick: () => {
|
|
37
37
|
model.setScalebarOnly(!scalebarOnly);
|
|
38
38
|
}, children: scalebarOnly ? (_jsx(AddIcon, { fontSize: "small" })) : (_jsx(RemoveIcon, { fontSize: "small" })) }) }), _jsx(ZoomButton, { model: model, direction: "out", small: true }), _jsx(ZoomButton, { model: model, direction: "in", small: true })] }) }));
|
|
39
39
|
});
|
|
@@ -52,9 +52,11 @@ const OverviewRubberband = observer(function OverviewRubberband({ model, overvie
|
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
function mouseDown(event) {
|
|
55
|
-
event.
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if (event.button === 0) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
setStartX(getRelativeX(event, controlsRef.current));
|
|
59
|
+
}
|
|
58
60
|
}
|
|
59
61
|
function mouseMove(event) {
|
|
60
62
|
setGuideX(getRelativeX(event, controlsRef.current));
|
|
@@ -40,7 +40,7 @@ function useScrollPortExcess(ref, model, enabled) {
|
|
|
40
40
|
}
|
|
41
41
|
const ResizeAllTracksHandle = observer(function ResizeAllTracksHandle({ model, }) {
|
|
42
42
|
const ref = useRef(null);
|
|
43
|
-
const drag = useRef({ from: new Map()
|
|
43
|
+
const drag = useRef({ from: new Map() });
|
|
44
44
|
const { isTopLevelView } = model;
|
|
45
45
|
useScrollPortExcess(ref, model, isTopLevelView);
|
|
46
46
|
return (_jsx("div", { ref: ref, style: isTopLevelView
|
|
@@ -54,12 +54,15 @@ const ResizeAllTracksHandle = observer(function ResizeAllTracksHandle({ model, }
|
|
|
54
54
|
position: 'absolute',
|
|
55
55
|
bottom: 0,
|
|
56
56
|
}, onDragStart: () => {
|
|
57
|
-
drag.current = { from: model.resizableTrackHeights
|
|
57
|
+
drag.current = { from: model.resizableTrackHeights };
|
|
58
58
|
for (const track of model.tracks) {
|
|
59
59
|
track.setResizing(true);
|
|
60
60
|
}
|
|
61
|
-
},
|
|
62
|
-
|
|
61
|
+
},
|
|
62
|
+
// no value, so the distance from the press, every track scaled from
|
|
63
|
+
// where it was
|
|
64
|
+
onChange: distance => {
|
|
65
|
+
model.resizeTracks(distance, drag.current.from);
|
|
63
66
|
}, onDragEnd: () => {
|
|
64
67
|
for (const track of model.tracks) {
|
|
65
68
|
track.setResizing(false);
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface BandGap {
|
|
2
|
+
top: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function spansAround(gaps: BandGap[], height: number): {
|
|
6
|
+
top: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}[];
|
|
9
|
+
export default function SVGHighlightBand({ coords, height, gaps, color, label, labelColor, }: {
|
|
2
10
|
coords: {
|
|
3
11
|
left: number;
|
|
4
12
|
width: number;
|
|
5
13
|
};
|
|
6
14
|
height: number;
|
|
15
|
+
gaps: BandGap[];
|
|
7
16
|
color: string;
|
|
8
17
|
label?: string;
|
|
9
18
|
labelColor: string;
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { getFillProps } from '@jbrowse/core/util';
|
|
3
|
+
// The stretches of a band `height` tall left once the sorted, disjoint `gaps`
|
|
4
|
+
// are cut out of it.
|
|
5
|
+
export function spansAround(gaps, height) {
|
|
6
|
+
const edges = [0, ...gaps.flatMap(g => [g.top, g.top + g.height]), height];
|
|
7
|
+
return edges.flatMap((top, i) => {
|
|
8
|
+
const bottom = edges[i + 1];
|
|
9
|
+
return i % 2 === 0 && bottom !== undefined && bottom > top
|
|
10
|
+
? [{ top, height: bottom - top }]
|
|
11
|
+
: [];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
3
14
|
// One full-height SVG highlight band with an optional label at the top,
|
|
4
15
|
// mirroring the on-screen HighlightBand div: a label wider than its band runs
|
|
5
|
-
// past the band's right edge
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
// past the band's right edge, and the band leaves out `gaps`, the tracks that
|
|
17
|
+
// do not draw in genomic coordinates.
|
|
18
|
+
export default function SVGHighlightBand({ coords, height, gaps, color, label, labelColor, }) {
|
|
19
|
+
return (_jsxs(_Fragment, { children: [spansAround(gaps, height).map(span => (_jsx("rect", { x: coords.left, y: span.top, width: coords.width, height: span.height, ...getFillProps(color) }, span.top))), label ? (_jsx("text", { x: coords.left + 3, y: 2, fontSize: 11, dominantBaseline: "hanging", ...getFillProps(labelColor), children: label })) : null] }));
|
|
8
20
|
}
|
|
@@ -39,8 +39,9 @@ const SearchBox = observer(function SearchBox({ model, showHelp = true, minWidth
|
|
|
39
39
|
const { recentLocations, addRecentLocation, clearRecentLocations } = useRecentLocations(assemblyName);
|
|
40
40
|
async function navigate(option) {
|
|
41
41
|
try {
|
|
42
|
-
await navigateToSelectedOption({ model, assemblyName, option })
|
|
43
|
-
|
|
42
|
+
if (await navigateToSelectedOption({ model, assemblyName, option })) {
|
|
43
|
+
addRecentLocation(recentLocationOf(option));
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
46
|
catch (e) {
|
|
46
47
|
notifySearchFailure(session, e);
|
|
@@ -15,7 +15,7 @@ export function filterRows(rows, text) {
|
|
|
15
15
|
const SearchResultsTable = observer(function SearchResultsTable({ picker, }) {
|
|
16
16
|
const [filter, setFilter] = useState('');
|
|
17
17
|
const rows = filterRows(picker.rows, filter);
|
|
18
|
-
return (_jsxs(_Fragment, { children: [picker.rows.length > FILTER_AT ? (_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", margin: "dense", placeholder: "Filter by name, location or track", value: filter, onChange: event => {
|
|
18
|
+
return (_jsxs(_Fragment, { children: [picker.rows.length > FILTER_AT ? (_jsx(TextField, { autoFocus: true, fullWidth: true, size: "small", margin: "dense", placeholder: "Filter by name, location or track", slotProps: { htmlInput: { 'aria-label': 'Filter search results' } }, value: filter, onChange: event => {
|
|
19
19
|
setFilter(event.target.value);
|
|
20
20
|
}, onKeyDown: event => {
|
|
21
21
|
if (event.key === 'Enter' && rows.length === 1) {
|
|
@@ -23,6 +23,6 @@ const SearchResultsTable = observer(function SearchResultsTable({ picker, }) {
|
|
|
23
23
|
}
|
|
24
24
|
}, helperText: filter ? `${rows.length} of ${picker.rows.length}` : undefined })) : null, _jsx(TableContainer, { component: Paper, children: _jsxs(Table, { children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: "Name" }), _jsx(TableCell, { align: "right", children: "Location" }), _jsx(TableCell, { align: "right", children: "Track" }), _jsx(TableCell, { align: "right" })] }) }), _jsx(TableBody, { children: rows.map(row => (_jsxs(TableRow, { children: [_jsx(TableCell, { component: "th", scope: "row", children: row.label }), _jsx(TableCell, { align: "right", children: row.location }), _jsx(TableCell, { align: "right", children: _jsx(SanitizedHTML, { html: row.trackName || 'N/A' }) }), _jsx(TableCell, { align: "right", children: _jsx(Button, { onClick: () => {
|
|
25
25
|
void picker.pick(row.id);
|
|
26
|
-
}, color: "primary", variant: "contained", children: "Go" }) })] }, row.id))) })] }) })] }));
|
|
26
|
+
}, color: "primary", variant: "contained", "aria-label": `Go to ${row.label} at ${row.location}`, children: "Go" }) })] }, row.id))) })] }) })] }));
|
|
27
27
|
});
|
|
28
28
|
export default SearchResultsTable;
|
|
@@ -134,7 +134,7 @@ const TrackContainer = observer(function TrackContainer({ model, track, }) {
|
|
|
134
134
|
const { setPaper, setContainer } = useTrackLabelBand(model, track.trackId);
|
|
135
135
|
return (_jsxs(Paper, { ref: setPaper, className: cx(classes.root, track.pinned ? null : classes.unpinnedTrack), variant: showTrackOutlines ? 'outlined' : undefined, elevation: showTrackOutlines ? undefined : 0, onDragOver: event => {
|
|
136
136
|
model.onTrackDragOver(track.id, event.clientY);
|
|
137
|
-
}, 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 }, labelBand: model.trackLabelBand(track), children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT },
|
|
137
|
+
}, 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 }, labelBand: model.trackLabelBand(track), children: _jsx(ErrorBoundary, { FallbackComponent: e => (_jsx("div", { className: classes.crashBanner, "data-gesture-owner": "true", children: _jsx(ErrorBanner, { error: e.error, onReset: e.resetErrorBoundary }) })), children: _jsx(TrackRenderingContainer, { model: model, track: track, display: display, ref: setContainer }) }, display.id) }), _jsx(PaddingBlocks, { model: model, offset: outlineOffset }), _jsx(ResizeHandle, { style: { height: RESIZE_HANDLE_HEIGHT }, value: display.height, onChange: height => display.resizeHeight(height - display.height),
|
|
138
138
|
// the drag's shortcut: grow the track by whatever it is scrolled over,
|
|
139
139
|
// so a pileup showing three rows of forty opens to all forty
|
|
140
140
|
// Optional: only a display that scrolls its own content has anything
|
|
@@ -18,9 +18,8 @@ const TrackLabelDragHandle = observer(function TrackLabelDragHandle({ track, })
|
|
|
18
18
|
const view = containingLgv(track);
|
|
19
19
|
const trackId = track.trackId;
|
|
20
20
|
return (_jsx("span", { draggable: true, className: classes.dragHandle, onDragStart: event => {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
const parent = target.parentNode;
|
|
21
|
+
const parent = event.currentTarget.parentElement;
|
|
22
|
+
if (parent) {
|
|
24
23
|
event.dataTransfer.setDragImage(parent, 20, 20);
|
|
25
24
|
view.setDraggingTrackId(track.id);
|
|
26
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getSequenceAdapterConfig } from '@jbrowse/core/assemblyManager/assembly';
|
|
2
|
-
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { assembleLocString, getSession } from '@jbrowse/core/util';
|
|
3
3
|
export async function fetchSequence(model, regions, opts = {}) {
|
|
4
4
|
const session = getSession(model);
|
|
5
5
|
const assemblyNames = new Set(regions.map(r => r.assemblyName));
|
|
@@ -25,10 +25,18 @@ export async function fetchSequence(model, regions, opts = {}) {
|
|
|
25
25
|
if (!adapterConfig) {
|
|
26
26
|
throw new Error(`assembly ${assemblyName} has no resolved configuration, so its sequence adapter cannot be read`);
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
// one call per region: a multi-region fetch merges its regions' streams in
|
|
29
|
+
// the order they finish, and a region the sequence lacks yields nothing,
|
|
30
|
+
// where the reader wants one entry per selected region, in selection order
|
|
31
|
+
return Promise.all(regions.map(async (region) => {
|
|
32
|
+
const [feature] = await rpcManager.call('getSequence', 'CoreGetFeatures', {
|
|
33
|
+
adapterConfig,
|
|
34
|
+
regions: [region],
|
|
35
|
+
...opts,
|
|
36
|
+
});
|
|
37
|
+
if (!feature) {
|
|
38
|
+
throw new Error(`no sequence found for ${assembleLocString(region)} in assembly ${assemblyName}`);
|
|
39
|
+
}
|
|
40
|
+
return feature;
|
|
41
|
+
}));
|
|
34
42
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
2
|
import { getRelativeX } from '@jbrowse/core/util/getRelativeX';
|
|
3
|
-
import { isFromPortal } from '@jbrowse/core/util/pressOwner';
|
|
3
|
+
import { isClaimedPress, isFromPortal } from '@jbrowse/core/util/pressOwner';
|
|
4
4
|
import { useWindowDrag } from '../../shared/useWindowDrag.js';
|
|
5
5
|
import { isOnScalebarRefLabel } from './util.js';
|
|
6
6
|
export function useRangeSelect(ref, model, shiftOnly) {
|
|
@@ -45,7 +45,10 @@ export function useRangeSelect(ref, model, shiftOnly) {
|
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
function mouseDown(event) {
|
|
48
|
-
if (
|
|
48
|
+
if (event.button !== 0 ||
|
|
49
|
+
(shiftOnly
|
|
50
|
+
? !event.shiftKey || isClaimedPress(event)
|
|
51
|
+
: isFromPortal(event))) {
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
// a press with the refName menu already open reopens it on the label under
|
|
@@ -320,7 +320,8 @@ export function buildRubberBandMenuItems(self, launchItems) {
|
|
|
320
320
|
// reversed region it names neither the base painted at the pixel nor the ends
|
|
321
321
|
// in ascending order, and a `leftRef === rightRef` test calls a selection
|
|
322
322
|
// crossing a collapsed intron one range when it is two.
|
|
323
|
-
const
|
|
323
|
+
const regions = self.getSelectedRegions(leftOffset, rightOffset);
|
|
324
|
+
const rangeString = assembleLocStrings(regions);
|
|
324
325
|
const launch = [
|
|
325
326
|
...(self.isTopLevelView
|
|
326
327
|
? [
|
|
@@ -348,7 +349,6 @@ export function buildRubberBandMenuItems(self, launchItems) {
|
|
|
348
349
|
label: 'Get sequence...',
|
|
349
350
|
icon: MenuOpenIcon,
|
|
350
351
|
onClick: () => {
|
|
351
|
-
const regions = self.getSelectedRegions(leftOffset, rightOffset);
|
|
352
352
|
getDialogHost(self).queueDialog(handleClose => [
|
|
353
353
|
GetSequenceDialog,
|
|
354
354
|
{
|
|
@@ -373,9 +373,9 @@ export function buildRubberBandMenuItems(self, launchItems) {
|
|
|
373
373
|
label: 'Highlight region',
|
|
374
374
|
icon: Highlighter,
|
|
375
375
|
onClick: () => {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
const session = getSession(self);
|
|
377
|
+
for (const region of regions) {
|
|
378
|
+
session.addHighlight(region);
|
|
379
379
|
}
|
|
380
380
|
},
|
|
381
381
|
},
|