@jbrowse/plugin-linear-genome-view 5.0.0-beta.15 → 5.0.0-beta.17
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/axisConfigSchema.d.ts +18 -0
- package/esm/LinearGenomeView/axisConfigSchema.js +22 -0
- package/esm/LinearGenomeView/components/Highlight.js +21 -5
- package/esm/LinearGenomeView/components/HighlightBand.js +18 -13
- package/esm/LinearGenomeView/components/HighlightChip.js +9 -4
- package/esm/LinearGenomeView/components/HighlightLabelDialog.d.ts +6 -0
- package/esm/LinearGenomeView/components/HighlightLabelDialog.js +18 -0
- package/esm/LinearGenomeView/components/SVGHighlightBand.js +3 -3
- package/esm/LinearGenomeView/components/TracksContainer.js +2 -2
- package/esm/LinearGenomeView/components/useSideScroll.d.ts +3 -2
- package/esm/LinearGenomeView/components/useSideScroll.js +13 -8
- package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
- package/esm/LinearGenomeView/launchKeys.js +6 -5
- package/esm/LinearGenomeView/menuItems.js +4 -4
- package/esm/LinearGenomeView/model.d.ts +113 -101
- package/esm/LinearGenomeView/model.js +88 -83
- package/esm/LinearGenomeView/rExport/rScript.js +1 -0
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
- package/esm/index.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #config LinearGenomeViewAxis
|
|
3
|
+
* #category view
|
|
4
|
+
* The linear genome view's `axis`: the ruler along its top, whose ticks
|
|
5
|
+
* `grid` carries down through the tracks.
|
|
6
|
+
*
|
|
7
|
+
* #example
|
|
8
|
+
* ```js
|
|
9
|
+
* { type: 'LinearGenomeView', axis: { grid: false } }
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare const linearGenomeViewAxisConfigSchema: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
|
|
13
|
+
readonly grid: {
|
|
14
|
+
type: "boolean";
|
|
15
|
+
} & {
|
|
16
|
+
defaultValue: boolean;
|
|
17
|
+
};
|
|
18
|
+
}, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConfigurationSchema } from '@jbrowse/core/configuration';
|
|
2
|
+
/**
|
|
3
|
+
* #config LinearGenomeViewAxis
|
|
4
|
+
* #category view
|
|
5
|
+
* The linear genome view's `axis`: the ruler along its top, whose ticks
|
|
6
|
+
* `grid` carries down through the tracks.
|
|
7
|
+
*
|
|
8
|
+
* #example
|
|
9
|
+
* ```js
|
|
10
|
+
* { type: 'LinearGenomeView', axis: { grid: false } }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const linearGenomeViewAxisConfigSchema = ConfigurationSchema('LinearGenomeViewAxis', {
|
|
14
|
+
/**
|
|
15
|
+
* #slot
|
|
16
|
+
*/
|
|
17
|
+
grid: {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
defaultValue: true,
|
|
20
|
+
description: "carry the ruler's ticks down through the tracks",
|
|
21
|
+
},
|
|
22
|
+
}, { closed: true });
|
|
@@ -2,14 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useStyleTheme } from '@jbrowse/core/ui/PaletteContext';
|
|
3
3
|
import { getSession } from '@jbrowse/core/util';
|
|
4
4
|
import CloseIcon from '@mui/icons-material/Close';
|
|
5
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
5
6
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
6
7
|
import { observer } from 'mobx-react';
|
|
7
8
|
import HighlightBand from './HighlightBand.js';
|
|
8
9
|
import HighlightChip from './HighlightChip.js';
|
|
10
|
+
import HighlightLabelDialog from './HighlightLabelDialog.js';
|
|
9
11
|
import { getHighlightColor } from './util.js';
|
|
10
|
-
// A band narrower than the link-icon chip clips it into a smudge, so below this
|
|
11
|
-
// width the chip is dropped (the band itself still draws).
|
|
12
|
-
const CHIP_MIN_WIDTH = 24;
|
|
13
12
|
const Highlight = observer(function Highlight({ model, highlight, }) {
|
|
14
13
|
const theme = useStyleTheme();
|
|
15
14
|
const coords = model.getHighlightCoords(highlight);
|
|
@@ -17,7 +16,24 @@ const Highlight = observer(function Highlight({ model, highlight, }) {
|
|
|
17
16
|
const label = getSession(model).highlightLabelsVisible
|
|
18
17
|
? highlight.label
|
|
19
18
|
: undefined;
|
|
20
|
-
return coords ? (_jsx(HighlightBand, { coords: coords, background: bandColor.toRgbString(), label: label, gaps: model.nonGenomicTrackBands(), children:
|
|
19
|
+
return coords ? (_jsx(HighlightBand, { coords: coords, background: bandColor.toRgbString(), label: label, gaps: model.nonGenomicTrackBands(), children: _jsx(HighlightChip, { color: bandColor, label: label, tooltip: highlight.label ?? 'Highlighted region', menuItems: [
|
|
20
|
+
{
|
|
21
|
+
label: 'Edit label...',
|
|
22
|
+
icon: EditIcon,
|
|
23
|
+
onClick: () => {
|
|
24
|
+
const session = getSession(model);
|
|
25
|
+
session.queueDialog(handleClose => [
|
|
26
|
+
HighlightLabelDialog,
|
|
27
|
+
{
|
|
28
|
+
label: highlight.label,
|
|
29
|
+
onSubmit: (label) => {
|
|
30
|
+
session.updateHighlight(highlight, { label });
|
|
31
|
+
},
|
|
32
|
+
handleClose,
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
21
37
|
{
|
|
22
38
|
label: 'Dismiss highlight',
|
|
23
39
|
icon: CloseIcon,
|
|
@@ -33,6 +49,6 @@ const Highlight = observer(function Highlight({ model, highlight, }) {
|
|
|
33
49
|
},
|
|
34
50
|
},
|
|
35
51
|
...model.highlightMenuItems(highlight),
|
|
36
|
-
] })
|
|
52
|
+
] }) })) : null;
|
|
37
53
|
});
|
|
38
54
|
export default Highlight;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
3
3
|
import { Typography } from '@mui/material';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
@@ -16,10 +16,15 @@ const useStyles = makeStyles()(theme => ({
|
|
|
16
16
|
zIndex: 1,
|
|
17
17
|
pointerEvents: 'none',
|
|
18
18
|
},
|
|
19
|
+
// a sibling of the band, so a label wider than a narrow band runs past its
|
|
20
|
+
// right edge. No `top`: its static position is the band's top edge, where
|
|
21
|
+
// the first track's TrackLabel (zIndex 200) would otherwise cover it
|
|
19
22
|
overlay: {
|
|
20
23
|
position: 'absolute',
|
|
21
|
-
top: 0,
|
|
22
24
|
left: 3,
|
|
25
|
+
zIndex: 201,
|
|
26
|
+
pointerEvents: 'none',
|
|
27
|
+
whiteSpace: 'nowrap',
|
|
23
28
|
},
|
|
24
29
|
label: {
|
|
25
30
|
color: theme.palette.text.primary,
|
|
@@ -35,20 +40,20 @@ export function gapMask(gaps) {
|
|
|
35
40
|
return `linear-gradient(to bottom, ${stops.join(', ')})`;
|
|
36
41
|
}
|
|
37
42
|
// OverviewHighlight stays separate (different bpToPx API). A non-interactive
|
|
38
|
-
// colored band with an
|
|
39
|
-
//
|
|
40
|
-
//
|
|
43
|
+
// colored band with an optional top label; when `children` (the highlight
|
|
44
|
+
// chip) is passed it renders that instead of the plain label. Visibility is
|
|
45
|
+
// toggled from the view menu
|
|
41
46
|
const HighlightBand = observer(function HighlightBand({ coords, background, label, gaps, children, }) {
|
|
42
47
|
const { classes } = useStyles();
|
|
43
48
|
const content = children ??
|
|
44
49
|
(label ? (_jsx(Typography, { variant: "caption", className: classes.label, noWrap: true, children: label })) : null);
|
|
45
|
-
return (_jsx("div", { "data-testid": "highlight-band", className: classes.highlight, style: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { "data-testid": "highlight-band", className: classes.highlight, style: {
|
|
51
|
+
transform: `translateX(${coords.left}px)`,
|
|
52
|
+
width: coords.width,
|
|
53
|
+
background,
|
|
54
|
+
...(gaps?.length
|
|
55
|
+
? { maskImage: gapMask(gaps), WebkitMaskImage: gapMask(gaps) }
|
|
56
|
+
: undefined),
|
|
57
|
+
} }), content ? (_jsx("div", { className: classes.overlay, style: { transform: `translateX(${coords.left}px)` }, children: content })) : null] }));
|
|
53
58
|
});
|
|
54
59
|
export default HighlightBand;
|
|
@@ -3,13 +3,18 @@ import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
|
|
|
3
3
|
import LinkIcon from '@mui/icons-material/Link';
|
|
4
4
|
import { Box, Tooltip, Typography } from '@mui/material';
|
|
5
5
|
import { observer } from 'mobx-react';
|
|
6
|
-
// Interactive chip
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Interactive chip at a highlight band's top: a link icon tinted to the band
|
|
7
|
+
// color plus an optional inline label, wrapped in a context menu. Its own
|
|
8
|
+
// background keeps it legible over the first track's label, and it sets its own
|
|
9
|
+
// pointer-events since the band is click-through.
|
|
9
10
|
const HighlightChip = observer(function HighlightChip({ color, label, tooltip, menuItems, }) {
|
|
10
11
|
// hide the icon color when the band is fully transparent ("label-only"
|
|
11
12
|
// highlight); otherwise bump to 0.8 alpha so the chip stays legible
|
|
12
13
|
const chipAlpha = color.alpha() === 0 ? 0 : 0.8;
|
|
13
|
-
return (_jsx(Box, { sx: {
|
|
14
|
+
return (_jsx(Box, { sx: {
|
|
15
|
+
pointerEvents: 'auto',
|
|
16
|
+
bgcolor: 'background.paper',
|
|
17
|
+
borderRadius: 1,
|
|
18
|
+
}, children: _jsx(CascadingMenuButton, { "data-testid": "highlight-chip", menuItems: menuItems, children: _jsx(Tooltip, { title: tooltip, arrow: true, children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: [_jsx(LinkIcon, { fontSize: "small", sx: { color: color.alpha(chipAlpha).toRgbString() } }), label ? (_jsx(Typography, { variant: "caption", noWrap: true, children: label })) : null] }) }) }) }));
|
|
14
19
|
});
|
|
15
20
|
export default HighlightChip;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { SubmitDialog } from '@jbrowse/core/ui';
|
|
4
|
+
import { TextField } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
// Names a highlight. The label is written on the band and in every view that
|
|
7
|
+
// draws the session's highlights, such as an alignment's column or a
|
|
8
|
+
// structure's residue; an empty label leaves the highlight unnamed.
|
|
9
|
+
const HighlightLabelDialog = observer(function HighlightLabelDialog({ label, onSubmit, handleClose, }) {
|
|
10
|
+
const [val, setVal] = useState(label ?? '');
|
|
11
|
+
return (_jsx(SubmitDialog, { open: true, maxWidth: "xs", fullWidth: true, title: "Edit highlight label", onCancel: handleClose, onSubmit: () => {
|
|
12
|
+
onSubmit(val.trim() || undefined);
|
|
13
|
+
handleClose();
|
|
14
|
+
}, children: _jsx(TextField, { label: "Label", autoFocus: true, fullWidth: true, variant: "outlined", value: val, onChange: event => {
|
|
15
|
+
setVal(event.target.value);
|
|
16
|
+
} }) }));
|
|
17
|
+
});
|
|
18
|
+
export default HighlightLabelDialog;
|
|
@@ -1,8 +1,8 @@
|
|
|
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
3
|
// One full-height SVG highlight band with an optional label at the top,
|
|
4
|
-
// mirroring the on-screen HighlightBand div
|
|
5
|
-
//
|
|
4
|
+
// mirroring the on-screen HighlightBand div: a label wider than its band runs
|
|
5
|
+
// past the band's right edge.
|
|
6
6
|
export default function SVGHighlightBand({ coords, height, color, label, labelColor, }) {
|
|
7
|
-
return (_jsxs(_Fragment, { children: [_jsx("rect", { x: coords.left, y: 0, width: coords.width, height: height, ...getFillProps(color) }), label
|
|
7
|
+
return (_jsxs(_Fragment, { children: [_jsx("rect", { x: coords.left, y: 0, width: coords.width, height: height, ...getFillProps(color) }), label ? (_jsx("text", { x: coords.left + 3, y: 2, fontSize: 11, dominantBaseline: "hanging", ...getFillProps(labelColor), children: label })) : null] }));
|
|
8
8
|
}
|
|
@@ -40,13 +40,13 @@ const TracksContainer = observer(function TracksContainer({ children, model, })
|
|
|
40
40
|
const { classes } = useStyles();
|
|
41
41
|
const { pluginManager } = getEnv(model);
|
|
42
42
|
const sideScroll = useSideScroll(model);
|
|
43
|
-
const { showGridlines, showCenterLine } = model;
|
|
43
|
+
const { axisGrid: showGridlines, showCenterLine } = model;
|
|
44
44
|
const ref = useRef(null);
|
|
45
45
|
// shift-drag range select over the whole tracks area. This is intentionally
|
|
46
46
|
// a separate range-select instance from the one inside <Rubberband> (the
|
|
47
47
|
// scalebar control); don't dedupe them — they cover different regions.
|
|
48
48
|
const range = useRangeSelect(ref, model, true);
|
|
49
|
-
return (_jsxs("div", { ref: ref, "data-testid": "tracksContainer", className: classes.tracksContainer,
|
|
49
|
+
return (_jsxs("div", { ref: ref, "data-testid": "tracksContainer", className: classes.tracksContainer, ...sideScroll, onMouseDown: range.mouseDown, onMouseMove: event => {
|
|
50
50
|
range.mouseMove(event);
|
|
51
51
|
}, onMouseLeave: () => {
|
|
52
52
|
range.mouseOut();
|
|
@@ -15,6 +15,7 @@ import type React from 'react';
|
|
|
15
15
|
* effect, so a release that lands before React commits still finds them.
|
|
16
16
|
*/
|
|
17
17
|
export declare function useSideScroll(model: LinearGenomeViewModel): {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
onPointerDownCapture: (event: React.PointerEvent) => void;
|
|
19
|
+
onPointerDown: (event: React.PointerEvent) => void;
|
|
20
|
+
onClickCapture: (event: React.MouseEvent) => void;
|
|
20
21
|
};
|
|
@@ -10,7 +10,7 @@ const PAN_CLICK_THRESHOLD_PX = 4;
|
|
|
10
10
|
// as `data-gesture-owner`. `data-pan-dragging` is present from press to
|
|
11
11
|
// release; `data-pan-moved` is set once the press travels past the threshold
|
|
12
12
|
// or becomes a pinch, and cleared by the next press, so it still answers for
|
|
13
|
-
// the click that follows the release: `
|
|
13
|
+
// the click that follows the release: `onClickCapture` swallows that click
|
|
14
14
|
// before any track sees it.
|
|
15
15
|
const PAN_DRAGGING_ATTR = 'data-pan-dragging';
|
|
16
16
|
const PAN_MOVED_ATTR = 'data-pan-moved';
|
|
@@ -36,16 +36,21 @@ export function useSideScroll(model) {
|
|
|
36
36
|
useEffect(() => () => {
|
|
37
37
|
gestureRef.current?.cancel();
|
|
38
38
|
}, []);
|
|
39
|
-
|
|
39
|
+
// The marker outlives the gesture that set it, so every press clears it, in
|
|
40
|
+
// the capture phase: a track that keeps its own press and stops it bubbling
|
|
41
|
+
// (multiway's lane slide, maf's drag select) would otherwise leave the last
|
|
42
|
+
// pan's marker standing to swallow its click.
|
|
43
|
+
function onPointerDownCapture(event) {
|
|
44
|
+
if (!gestureRef.current) {
|
|
45
|
+
event.currentTarget.removeAttribute(PAN_MOVED_ATTR);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function onPointerDown(event) {
|
|
40
49
|
const host = event.currentTarget;
|
|
41
50
|
if (gestureRef.current) {
|
|
42
51
|
gestureRef.current.join(event);
|
|
43
52
|
return;
|
|
44
53
|
}
|
|
45
|
-
// Cleared for EVERY press, ahead of the returns below: the marker outlives
|
|
46
|
-
// the gesture that set it, and a stale true is what the next reader of
|
|
47
|
-
// this DOM contract would inherit.
|
|
48
|
-
host.removeAttribute(PAN_MOVED_ATTR);
|
|
49
54
|
if (event.shiftKey || event.button !== 0 || !event.isPrimary) {
|
|
50
55
|
return;
|
|
51
56
|
}
|
|
@@ -165,10 +170,10 @@ export function useSideScroll(model) {
|
|
|
165
170
|
// The browser fires `click` after a release on whatever the press and the
|
|
166
171
|
// release share, which over a track is its canvas, so a pan that ended on a
|
|
167
172
|
// gene would open that gene's details.
|
|
168
|
-
function
|
|
173
|
+
function onClickCapture(event) {
|
|
169
174
|
if (event.currentTarget.hasAttribute(PAN_MOVED_ATTR)) {
|
|
170
175
|
event.stopPropagation();
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
|
-
return {
|
|
178
|
+
return { onPointerDownCapture, onPointerDown, onClickCapture };
|
|
174
179
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { InitState } from './types.ts';
|
|
2
|
-
export declare const lgvLaunchKeys: import("@jbrowse/core/util/withLaunchInput").LaunchKeyRegistration<InitState, "bpPerPx" | "offsetPx" | "showCytobandsSetting">;
|
|
2
|
+
export declare const lgvLaunchKeys: import("@jbrowse/core/util/withLaunchInput").LaunchKeyRegistration<InitState, "bpPerPx" | "colorByCDS" | "offsetPx" | "showCytobandsSetting">;
|
|
@@ -4,10 +4,11 @@ import { defineLaunchKeys } from '@jbrowse/core/util/withLaunchInput';
|
|
|
4
4
|
// key that partitions as a typo.
|
|
5
5
|
//
|
|
6
6
|
// `bpPerPx`/`offsetPx` are v4's viewport spelling from before it was stored as
|
|
7
|
-
// a window,
|
|
8
|
-
// property, so the partition would read them
|
|
9
|
-
// preProcessSnapshot still converts them, so a URL or
|
|
10
|
-
// goes on working. Share links carry all
|
|
7
|
+
// a window, `showCytobandsSetting` its cytoband toggle and `colorByCDS` its
|
|
8
|
+
// frame coloring. None is a declared property, so the partition would read them
|
|
9
|
+
// as typos; the model's own preProcessSnapshot still converts them, so a URL or
|
|
10
|
+
// saved spec naming them goes on working. Share links carry all four, so they
|
|
11
|
+
// stay.
|
|
11
12
|
//
|
|
12
13
|
// #launchKeys LinearGenomeView — the URL parameters page renders this list
|
|
13
14
|
// rather than restating it; each of these keys has its own `¶m=` section
|
|
@@ -23,5 +24,5 @@ export const lgvLaunchKeys = defineLaunchKeys()({
|
|
|
23
24
|
tracks: { kind: 'trackEntries' },
|
|
24
25
|
highlight: { kind: 'launch' },
|
|
25
26
|
}, {
|
|
26
|
-
passThrough: ['bpPerPx', 'offsetPx', 'showCytobandsSetting'],
|
|
27
|
+
passThrough: ['bpPerPx', 'offsetPx', 'showCytobandsSetting', 'colorByCDS'],
|
|
27
28
|
});
|
|
@@ -229,9 +229,9 @@ export function buildMenuItems(self) {
|
|
|
229
229
|
{
|
|
230
230
|
label: 'Guidelines',
|
|
231
231
|
type: 'checkbox',
|
|
232
|
-
checked: self.
|
|
232
|
+
checked: self.axisGrid,
|
|
233
233
|
onClick: () => {
|
|
234
|
-
self.
|
|
234
|
+
self.setAxisGrid(!self.axisGrid);
|
|
235
235
|
},
|
|
236
236
|
},
|
|
237
237
|
{
|
|
@@ -254,9 +254,9 @@ export function buildMenuItems(self) {
|
|
|
254
254
|
{
|
|
255
255
|
label: 'CDS reading frame colors',
|
|
256
256
|
type: 'checkbox',
|
|
257
|
-
checked: self.
|
|
257
|
+
checked: self.colorByFrame,
|
|
258
258
|
onClick: () => {
|
|
259
|
-
self.
|
|
259
|
+
self.setColorByFrame(!self.colorByFrame);
|
|
260
260
|
},
|
|
261
261
|
},
|
|
262
262
|
{
|