@jbrowse/display-ui 4.3.0 → 5.0.0-beta.10
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/README.md +53 -13
- package/esm/AxisCaption.d.ts +24 -0
- package/esm/AxisCaption.js +57 -0
- package/esm/AxisGutter.d.ts +12 -0
- package/esm/AxisGutter.js +15 -0
- package/esm/CrossHatches.d.ts +12 -0
- package/esm/CrossHatches.js +31 -0
- package/esm/FloatingLegend.d.ts +4 -6
- package/esm/FloatingLegend.js +93 -29
- package/esm/FloatingSvgOverlay.d.ts +5 -0
- package/esm/FloatingSvgOverlay.js +25 -0
- package/esm/FloatingText.d.ts +50 -0
- package/esm/FloatingText.js +48 -0
- package/esm/PointerLayer.d.ts +12 -18
- package/esm/PointerLayer.js +14 -20
- package/esm/ScoreDomainCaption.d.ts +15 -0
- package/esm/ScoreDomainCaption.js +23 -0
- package/esm/ScoreRules.d.ts +21 -0
- package/esm/ScoreRules.js +46 -0
- package/esm/YScaleBar.d.ts +6 -0
- package/esm/YScaleBar.js +47 -0
- package/esm/YScaleBarOverlay.d.ts +11 -0
- package/esm/YScaleBarOverlay.js +20 -0
- package/esm/axisPlacement.d.ts +44 -0
- package/esm/axisPlacement.js +70 -0
- package/esm/bottomRightCorner.d.ts +3 -4
- package/esm/bottomRightCorner.js +3 -4
- package/esm/chromeOverlayContext.d.ts +1 -1
- package/esm/chromeOverlayContext.js +4 -5
- package/esm/chromeOverlays.d.ts +4 -2
- package/esm/cullOverlappingLabels.d.ts +14 -0
- package/esm/cullOverlappingLabels.js +31 -0
- package/esm/embed/index.d.ts +70 -0
- package/esm/embed/index.js +72 -0
- package/esm/embed/legend.d.ts +10 -0
- package/esm/embed/legend.js +40 -0
- package/esm/embed/location.d.ts +17 -0
- package/esm/embed/location.js +65 -0
- package/esm/embed/provider.d.ts +11 -0
- package/esm/embed/provider.js +6 -0
- package/esm/embed/regions.d.ts +53 -0
- package/esm/embed/regions.js +95 -0
- package/esm/floatingLegendTop.d.ts +2 -0
- package/esm/floatingLegendTop.js +3 -0
- package/esm/index.d.ts +19 -0
- package/esm/index.js +24 -0
- package/esm/plainChromeOverlays.d.ts +1 -1
- package/esm/plainChromeOverlays.js +6 -6
- package/esm/tooltip/Tooltip.d.ts +3 -3
- package/esm/tooltip/Tooltip.js +3 -3
- package/esm/trackControl/trackControlContext.d.ts +1 -1
- package/esm/trackControl/trackControlContext.js +6 -6
- package/esm/trackOverlay/TrackOverlaySlot.d.ts +2 -2
- package/esm/trackOverlay/TrackOverlaySlot.js +2 -2
- package/esm/valueScale.d.ts +72 -0
- package/esm/valueScale.js +1 -0
- package/esm/yAxisConstants.d.ts +7 -0
- package/esm/yAxisConstants.js +19 -0
- package/esm/yScaleTicks.d.ts +49 -0
- package/esm/yScaleTicks.js +47 -0
- package/package.json +11 -7
package/esm/PointerLayer.d.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
import type { MouseState, MouseTracker } from '@jbrowse/core/ui/useMouseTracking';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
/**
|
|
4
|
-
* The part of a display that follows the pointer
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (see `useMouseTracking`).
|
|
4
|
+
* The part of a display that follows the pointer, in a leaf of its own so a
|
|
5
|
+
* mousemove re-renders this and not the chrome, its status container and every
|
|
6
|
+
* overlay under it (see `useMouseTracking`). An observer, so a model read in
|
|
7
|
+
* `children` is tracked here too rather than freezing under a stationary
|
|
8
|
+
* cursor.
|
|
10
9
|
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* a layer that has to render while nothing is hovered — the matrix display's
|
|
17
|
-
* connector lines, with no column highlighted — can.
|
|
18
|
-
*
|
|
19
|
-
* Four displays used to write this component out, with the same paragraph over
|
|
20
|
-
* each.
|
|
10
|
+
* `inRows` is whether the pointer is past `rowsTopOffset`, the bands a display
|
|
11
|
+
* stacks above its rows — a crosshair drawn over one names a row the pointer is
|
|
12
|
+
* not on, and `>=` because the rows begin AT the offset. A tooltip usually
|
|
13
|
+
* wants it ungated, which is why the child decides, and `children` is handed an
|
|
14
|
+
* absent state so a layer that draws with nothing hovered still can.
|
|
21
15
|
*/
|
|
22
|
-
export declare
|
|
16
|
+
export declare const PointerLayer: ({ mouseTracker, rowsTopOffset, children, }: {
|
|
23
17
|
mouseTracker: MouseTracker;
|
|
24
18
|
rowsTopOffset?: number;
|
|
25
19
|
children: (state: MouseState | undefined, inRows: boolean) => ReactNode;
|
|
26
|
-
})
|
|
20
|
+
}) => ReactNode;
|
package/esm/PointerLayer.js
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
1
|
// deep subpath, never the `@jbrowse/core/ui` barrel: this package reaches no
|
|
3
2
|
// UI toolkit, and the barrel pulls MUI in
|
|
4
3
|
import { useMouseState } from '@jbrowse/core/ui/useMouseTracking';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
5
5
|
/**
|
|
6
|
-
* The part of a display that follows the pointer
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (see `useMouseTracking`).
|
|
6
|
+
* The part of a display that follows the pointer, in a leaf of its own so a
|
|
7
|
+
* mousemove re-renders this and not the chrome, its status container and every
|
|
8
|
+
* overlay under it (see `useMouseTracking`). An observer, so a model read in
|
|
9
|
+
* `children` is tracked here too rather than freezing under a stationary
|
|
10
|
+
* cursor.
|
|
12
11
|
*
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* a layer that has to render while nothing is hovered — the matrix display's
|
|
19
|
-
* connector lines, with no column highlighted — can.
|
|
20
|
-
*
|
|
21
|
-
* Four displays used to write this component out, with the same paragraph over
|
|
22
|
-
* each.
|
|
12
|
+
* `inRows` is whether the pointer is past `rowsTopOffset`, the bands a display
|
|
13
|
+
* stacks above its rows — a crosshair drawn over one names a row the pointer is
|
|
14
|
+
* not on, and `>=` because the rows begin AT the offset. A tooltip usually
|
|
15
|
+
* wants it ungated, which is why the child decides, and `children` is handed an
|
|
16
|
+
* absent state so a layer that draws with nothing hovered still can.
|
|
23
17
|
*/
|
|
24
|
-
export function PointerLayer({ mouseTracker, rowsTopOffset = 0, children, }) {
|
|
18
|
+
export const PointerLayer = observer(function PointerLayer({ mouseTracker, rowsTopOffset = 0, children, }) {
|
|
25
19
|
const state = useMouseState(mouseTracker);
|
|
26
|
-
return
|
|
27
|
-
}
|
|
20
|
+
return children(state, !!state && state.y >= rowsTopOffset);
|
|
21
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line `[min, max]` caption that stands in for a y axis where none can
|
|
3
|
+
* be drawn — a scale whose bands are too short for tick labels, and a density
|
|
4
|
+
* track whose rows each carry their own colour, so no single ramp describes
|
|
5
|
+
* them. An axis stand-in rather than a key, which is why it is not a colour
|
|
6
|
+
* scale and draws whether or not the legend does. Right-aligned to
|
|
7
|
+
* `canvasWidth` at the top of the display, led by the scale's `caption` where
|
|
8
|
+
* it has one, which has no gutter to sit in here.
|
|
9
|
+
*/
|
|
10
|
+
export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, caption, }: {
|
|
11
|
+
domain: [number, number];
|
|
12
|
+
scaleType: string;
|
|
13
|
+
canvasWidth: number;
|
|
14
|
+
caption?: string;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { measureLegendText } from '@jbrowse/core/ui/measureLegendText';
|
|
4
|
+
import { formatScore } from '@jbrowse/core/util/numericUtils';
|
|
5
|
+
import { stripAlpha } from '@jbrowse/core/util/svgColorProps';
|
|
6
|
+
import { SCORE_CAPTION_HEIGHT } from './yAxisConstants.js';
|
|
7
|
+
/**
|
|
8
|
+
* The one-line `[min, max]` caption that stands in for a y axis where none can
|
|
9
|
+
* be drawn — a scale whose bands are too short for tick labels, and a density
|
|
10
|
+
* track whose rows each carry their own colour, so no single ramp describes
|
|
11
|
+
* them. An axis stand-in rather than a key, which is why it is not a colour
|
|
12
|
+
* scale and draws whether or not the legend does. Right-aligned to
|
|
13
|
+
* `canvasWidth` at the top of the display, led by the scale's `caption` where
|
|
14
|
+
* it has one, which has no gutter to sit in here.
|
|
15
|
+
*/
|
|
16
|
+
export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, caption, }) {
|
|
17
|
+
const palette = usePalette();
|
|
18
|
+
const suffix = scaleType === 'log' ? ' (log)' : scaleType === 'symlog' ? ' (symlog)' : '';
|
|
19
|
+
const text = `${caption ? `${caption} ` : ''}[${formatScore(domain[0])}, ${formatScore(domain[1])}]${suffix}`;
|
|
20
|
+
const len = measureLegendText(text, 12);
|
|
21
|
+
const xpos = Math.max(0, canvasWidth - len - 10);
|
|
22
|
+
return (_jsxs("g", { children: [_jsx("rect", { x: xpos - 3, y: 0, width: len + 6, height: SCORE_CAPTION_HEIGHT, fill: stripAlpha(palette.background.paper), fillOpacity: 0.8 }), _jsx("text", { y: 12, x: xpos, fontSize: 12, fill: palette.text.primary, children: text })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One horizontal rule across a score plot at a chosen value, placed in the
|
|
3
|
+
* plot's own pixel space; `label` is free text the reader chose.
|
|
4
|
+
*/
|
|
5
|
+
export interface ScoreRuleMark {
|
|
6
|
+
value: number;
|
|
7
|
+
y: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function ScoreRuleLines({ marks, width, offsetY, }: {
|
|
12
|
+
marks: ScoreRuleMark[];
|
|
13
|
+
width: number;
|
|
14
|
+
offsetY?: number;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export default function ScoreRules({ marks, width, height, bandTops, }: {
|
|
17
|
+
marks: ScoreRuleMark[];
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
bandTops: number[];
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { DEFAULT_RULE_COLOR } from './yAxisConstants.js';
|
|
4
|
+
const LABEL_FONT_SIZE = 10;
|
|
5
|
+
const LABEL_GAP = 2;
|
|
6
|
+
// Above its line, unless the band's top edge leaves the label no room: a rule
|
|
7
|
+
// at the top of the axis, like a pinned maximum, captions itself below.
|
|
8
|
+
function labelY(y, offsetY) {
|
|
9
|
+
return y >= LABEL_FONT_SIZE + LABEL_GAP
|
|
10
|
+
? offsetY + y - LABEL_GAP
|
|
11
|
+
: offsetY + y + LABEL_FONT_SIZE + LABEL_GAP;
|
|
12
|
+
}
|
|
13
|
+
// Bare marks, no wrapping <svg>, so the on-screen overlay and the SVG export
|
|
14
|
+
// draw the same elements from the same y. Same split as CrossHatchLines.
|
|
15
|
+
export function ScoreRuleLines({ marks, width, offsetY = 0, }) {
|
|
16
|
+
// A caption sits over the plot, not beside it, so it needs the halo the tick
|
|
17
|
+
// labels next to it already draw (YScaleBar) — without one a rule crossing
|
|
18
|
+
// filled bars captions itself in grey on the fill and cannot be read. From
|
|
19
|
+
// the palette rather than a hardcoded white, which inverts into a glare in
|
|
20
|
+
// dark mode.
|
|
21
|
+
const haloColor = usePalette().background.default;
|
|
22
|
+
return (_jsx(_Fragment, { children: marks.map(({ value, label, color, y }) => {
|
|
23
|
+
const lineY = offsetY + y;
|
|
24
|
+
const stroke = color ?? DEFAULT_RULE_COLOR;
|
|
25
|
+
return (_jsxs("g", { children: [_jsx("line", { x1: 0, x2: width, y1: lineY, y2: lineY, stroke: stroke,
|
|
26
|
+
// separate attribute rather than an rgba() string: the SVG
|
|
27
|
+
// export's renderToStaticMarkup strips rgba() alpha
|
|
28
|
+
strokeOpacity: 0.9, strokeWidth: 1, strokeDasharray: "4 3" }), label ? (_jsx("text", { x: width - 4, y: labelY(y, offsetY), textAnchor: "end", fontSize: LABEL_FONT_SIZE,
|
|
29
|
+
// full alpha, unlike the line: 0.9 is there to let the rule
|
|
30
|
+
// recede into the plot, and a caption that has to be read wants
|
|
31
|
+
// the opposite
|
|
32
|
+
fill: stroke, stroke: haloColor, strokeWidth: 2.5, paintOrder: "stroke", children: label })) : null] }, `${value}-${y}`));
|
|
33
|
+
}) }));
|
|
34
|
+
}
|
|
35
|
+
// Once per band the scale rules, like CrossHatches; pointer-events disabled so
|
|
36
|
+
// the canvas underneath still gets mouse events.
|
|
37
|
+
export default function ScoreRules({ marks, width, height, bandTops, }) {
|
|
38
|
+
return (_jsx("svg", { style: {
|
|
39
|
+
position: 'absolute',
|
|
40
|
+
top: 0,
|
|
41
|
+
left: 0,
|
|
42
|
+
pointerEvents: 'none',
|
|
43
|
+
height,
|
|
44
|
+
width,
|
|
45
|
+
}, children: bandTops.map(top => (_jsx(ScoreRuleLines, { marks: marks, width: width, offsetY: top }, top))) }));
|
|
46
|
+
}
|
package/esm/YScaleBar.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { AXIS_FONT_PX, TICK_LABEL_X_PX } from './yAxisConstants.js';
|
|
4
|
+
import { clampStrokeInsideAxis } from './yScaleTicks.js';
|
|
5
|
+
// Not an `observer`, and deliberately: there is nothing here to observe. Every
|
|
6
|
+
// caller already reads `model.ticks` inside its own observer and hands the
|
|
7
|
+
// resolved plain object down, and `usePalette` is a React context, not MobX.
|
|
8
|
+
// Wrapping it anyway allocated a Reaction per instance — multi-wiggle renders
|
|
9
|
+
// one per sample row, on screen as well as in export — and, per CLAUDE.md, put
|
|
10
|
+
// the component out of `babel-plugin-react-compiler`'s reach, so it got neither
|
|
11
|
+
// the MobX tracking it doesn't need nor the memoization it does. Its sibling
|
|
12
|
+
// `CrossHatchLines`, consuming the same ticks, is already plain.
|
|
13
|
+
// Half the vertical space a 10px label needs, halo included: digits reach about
|
|
14
|
+
// 3.6px above the centered baseline and the 2.5px-wide background stroke another
|
|
15
|
+
// 1.25px past that.
|
|
16
|
+
const LABEL_HALF_HEIGHT_PX = 5;
|
|
17
|
+
export default function YScaleBar({ ticks, orientation, bandHeight, }) {
|
|
18
|
+
const palette = usePalette();
|
|
19
|
+
if (!ticks) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const { items, yTop, yBottom } = ticks;
|
|
23
|
+
const bg = palette.background.default;
|
|
24
|
+
const fg = palette.text.primary;
|
|
25
|
+
const isLeft = orientation !== 'right';
|
|
26
|
+
const k = isLeft ? -1 : 1;
|
|
27
|
+
const tickLength = 6;
|
|
28
|
+
// Crispen to `y + 0.5` so each 1px stroke fills one pixel instead of
|
|
29
|
+
// straddling two, then clamp, which only bites at the bottom edge: there the
|
|
30
|
+
// stroke goes on the last pixel inside the box rather than the first one
|
|
31
|
+
// below it.
|
|
32
|
+
const strokeY = (y) => clampStrokeInsideAxis(y + 0.5, yBottom);
|
|
33
|
+
// The tick keeps the group transform; only the text moves off it, and only
|
|
34
|
+
// under `bandHeight` within half a label of an end. A tick is where the data
|
|
35
|
+
// is, so it never moves — the label is then drawn against the tick rather than
|
|
36
|
+
// across it, which is what any axis does at the edge of its own frame.
|
|
37
|
+
// `undefined` rather than 0 for the labels that don't move, so the markup this
|
|
38
|
+
// emits is unchanged wherever the inset doesn't apply.
|
|
39
|
+
const labelDy = (sy) => {
|
|
40
|
+
if (bandHeight === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const inset = Math.min(Math.max(sy, LABEL_HALF_HEIGHT_PX), bandHeight - LABEL_HALF_HEIGHT_PX);
|
|
44
|
+
return inset === sy ? undefined : inset - sy;
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs("g", { fontSize: AXIS_FONT_PX, textAnchor: isLeft ? 'end' : 'start', stroke: fg, strokeWidth: 1, children: [_jsx("path", { fill: "none", d: `M${k * tickLength} ${strokeY(yTop)}H0.5V${strokeY(yBottom)}H${k * tickLength}` }), items.map(({ value, y, label }) => (_jsxs("g", { transform: `translate(0,${strokeY(y)})`, children: [_jsx("line", { x2: k * tickLength }), _jsx("text", { stroke: bg, strokeWidth: 2.5, paintOrder: "stroke", fill: fg, y: labelDy(strokeY(y)), dy: "0.32em", x: k * TICK_LABEL_X_PX, children: label ?? value })] }, `${value}-${y}`)))] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { YAxis } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The on-screen axis of one band: an `<svg>` the width of the gutter,
|
|
4
|
+
* positioned absolutely over the band at `top`, with `AxisGutter` inside. A
|
|
5
|
+
* right-side gutter clears the vertical scrollbar a display may mount.
|
|
6
|
+
*/
|
|
7
|
+
export default function YScaleBarOverlay({ axis, top, width, }: {
|
|
8
|
+
axis: YAxis;
|
|
9
|
+
top: number;
|
|
10
|
+
width: number;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { VERTICAL_SCROLLBAR_CLEARANCE } from '@jbrowse/core/ui/VerticalScrollbar';
|
|
3
|
+
import AxisGutter from './AxisGutter.js';
|
|
4
|
+
import { axisGutterLeft, axisGutterWidth } from './axisPlacement.js';
|
|
5
|
+
/**
|
|
6
|
+
* The on-screen axis of one band: an `<svg>` the width of the gutter,
|
|
7
|
+
* positioned absolutely over the band at `top`, with `AxisGutter` inside. A
|
|
8
|
+
* right-side gutter clears the vertical scrollbar a display may mount.
|
|
9
|
+
*/
|
|
10
|
+
export default function YScaleBarOverlay({ axis, top, width, }) {
|
|
11
|
+
return (_jsx("svg", { style: {
|
|
12
|
+
position: 'absolute',
|
|
13
|
+
top,
|
|
14
|
+
left: axisGutterLeft(axis, width, VERTICAL_SCROLLBAR_CLEARANCE),
|
|
15
|
+
pointerEvents: 'none',
|
|
16
|
+
height: axis.height,
|
|
17
|
+
width: axisGutterWidth(axis),
|
|
18
|
+
overflow: 'visible',
|
|
19
|
+
}, children: _jsx(AxisGutter, { axis: axis }) }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ValueScale, YAxis } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Below this band height a full `YScaleBar`'s tick labels overlap each other,
|
|
4
|
+
* so the chrome draws one `ScoreDomainCaption` for the scale instead of an
|
|
5
|
+
* axis per band. Its own module because `YScaleBar`'s label geometry is what
|
|
6
|
+
* makes 30 the number.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMPACT_AXIS_HEIGHT = 30;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a scale places anything by y, which is whether its rules draw: it
|
|
11
|
+
* rules at least one band, where `[]` is a scale mapped to colour instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rulesABand(scale: Pick<ValueScale, 'bandTops'>): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Whether a scale gets an axis at all: it rules at least one band, and the
|
|
16
|
+
* band has room for tick labels. A scale that fails either is captioned.
|
|
17
|
+
*/
|
|
18
|
+
export declare function axisDrawn(scale: Pick<ValueScale, 'height' | 'bandTops'>): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The y a scale's one caption centres on: halfway down the extent of the bands
|
|
21
|
+
* given, from the first's top tick to the last's bottom. One band gives the
|
|
22
|
+
* middle of its own axis.
|
|
23
|
+
*/
|
|
24
|
+
export declare function axisCaptionY(axis: Pick<YAxis, 'ticks'>, bandTops: number[]): number;
|
|
25
|
+
/**
|
|
26
|
+
* How wide an axis's gutter is: the ordinary width, grown inward over the plot
|
|
27
|
+
* where a caption and the widest tick label cannot share it, so neither is
|
|
28
|
+
* drawn over the other. The caption keeps the outer edge and the labels stay
|
|
29
|
+
* beside the spine.
|
|
30
|
+
*/
|
|
31
|
+
export declare function axisGutterWidth(axis: Pick<YAxis, 'ticks' | 'caption'>): number;
|
|
32
|
+
/** How far in from the plot's right edge a right-side gutter ends in an export. */
|
|
33
|
+
export declare const AXIS_RIGHT_INSET_PX = 4;
|
|
34
|
+
/**
|
|
35
|
+
* Left edge of the gutter an axis is drawn in. A right-side axis takes the
|
|
36
|
+
* gutter inside the plot's right edge, less `rightInset` (the vertical
|
|
37
|
+
* scrollbar a display may mount on screen; a margin in an export). A left-side
|
|
38
|
+
* axis takes the gutter at the display's left edge, past whatever panel the
|
|
39
|
+
* scale's `left` reserves — except in an export, whose margin is the gutter,
|
|
40
|
+
* so an axis nothing pushes right sits in the margin with its numbers outside
|
|
41
|
+
* the plot and its spine on the content edge, or past it by what
|
|
42
|
+
* `axisGutterWidth` grew.
|
|
43
|
+
*/
|
|
44
|
+
export declare function axisGutterLeft(axis: Pick<YAxis, 'side' | 'left' | 'ticks' | 'caption'>, width: number, rightInset: number, exportContentLeft?: number): number;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { measureText } from '@jbrowse/core/util/measureText';
|
|
2
|
+
import { AXIS_FONT_PX, CAPTION_BAND_PX, TICK_LABEL_X_PX, } from './yAxisConstants.js';
|
|
3
|
+
import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
|
|
4
|
+
/**
|
|
5
|
+
* Below this band height a full `YScaleBar`'s tick labels overlap each other,
|
|
6
|
+
* so the chrome draws one `ScoreDomainCaption` for the scale instead of an
|
|
7
|
+
* axis per band. Its own module because `YScaleBar`'s label geometry is what
|
|
8
|
+
* makes 30 the number.
|
|
9
|
+
*/
|
|
10
|
+
export const COMPACT_AXIS_HEIGHT = 30;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a scale places anything by y, which is whether its rules draw: it
|
|
13
|
+
* rules at least one band, where `[]` is a scale mapped to colour instead.
|
|
14
|
+
*/
|
|
15
|
+
export function rulesABand(scale) {
|
|
16
|
+
return (scale.bandTops?.length ?? 1) > 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a scale gets an axis at all: it rules at least one band, and the
|
|
20
|
+
* band has room for tick labels. A scale that fails either is captioned.
|
|
21
|
+
*/
|
|
22
|
+
export function axisDrawn(scale) {
|
|
23
|
+
return rulesABand(scale) && scale.height >= COMPACT_AXIS_HEIGHT;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The y a scale's one caption centres on: halfway down the extent of the bands
|
|
27
|
+
* given, from the first's top tick to the last's bottom. One band gives the
|
|
28
|
+
* middle of its own axis.
|
|
29
|
+
*/
|
|
30
|
+
export function axisCaptionY(axis, bandTops) {
|
|
31
|
+
const { yTop, yBottom } = axis.ticks;
|
|
32
|
+
return (Math.min(...bandTops) + yTop + Math.max(...bandTops) + yBottom) / 2;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* How wide an axis's gutter is: the ordinary width, grown inward over the plot
|
|
36
|
+
* where a caption and the widest tick label cannot share it, so neither is
|
|
37
|
+
* drawn over the other. The caption keeps the outer edge and the labels stay
|
|
38
|
+
* beside the spine.
|
|
39
|
+
*/
|
|
40
|
+
export function axisGutterWidth(axis) {
|
|
41
|
+
if (!axis.caption) {
|
|
42
|
+
return AXIS_GUTTER_WIDTH_PX;
|
|
43
|
+
}
|
|
44
|
+
let widest = 0;
|
|
45
|
+
for (const { value, label } of axis.ticks.items) {
|
|
46
|
+
widest = Math.max(widest, measureText(label ?? value, AXIS_FONT_PX));
|
|
47
|
+
}
|
|
48
|
+
return Math.max(AXIS_GUTTER_WIDTH_PX, Math.ceil(CAPTION_BAND_PX + widest + TICK_LABEL_X_PX));
|
|
49
|
+
}
|
|
50
|
+
/** How far in from the plot's right edge a right-side gutter ends in an export. */
|
|
51
|
+
export const AXIS_RIGHT_INSET_PX = 4;
|
|
52
|
+
/**
|
|
53
|
+
* Left edge of the gutter an axis is drawn in. A right-side axis takes the
|
|
54
|
+
* gutter inside the plot's right edge, less `rightInset` (the vertical
|
|
55
|
+
* scrollbar a display may mount on screen; a margin in an export). A left-side
|
|
56
|
+
* axis takes the gutter at the display's left edge, past whatever panel the
|
|
57
|
+
* scale's `left` reserves — except in an export, whose margin is the gutter,
|
|
58
|
+
* so an axis nothing pushes right sits in the margin with its numbers outside
|
|
59
|
+
* the plot and its spine on the content edge, or past it by what
|
|
60
|
+
* `axisGutterWidth` grew.
|
|
61
|
+
*/
|
|
62
|
+
export function axisGutterLeft(axis, width, rightInset, exportContentLeft) {
|
|
63
|
+
if (axis.side === 'right') {
|
|
64
|
+
return width - rightInset - axisGutterWidth(axis);
|
|
65
|
+
}
|
|
66
|
+
const left = axis.left ?? 0;
|
|
67
|
+
return exportContentLeft !== undefined && left === 0
|
|
68
|
+
? exportContentLeft - AXIS_GUTTER_WIDTH_PX
|
|
69
|
+
: left;
|
|
70
|
+
}
|
|
@@ -22,10 +22,9 @@
|
|
|
22
22
|
* It has never been reachable: the two displays that render the control row
|
|
23
23
|
* (alignments, canvas) are not among the four that report a `ready`-phase
|
|
24
24
|
* status (clustering, on multi-wiggle / multi-row features / maf / the
|
|
25
|
-
* multi-sample variant pair).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* message that silently never appears.
|
|
25
|
+
* multi-sample variant pair). The layout is structural anyway, because neither
|
|
26
|
+
* side checks for the constraint, so the first display to want both would hit
|
|
27
|
+
* the bug as a status message that never appears.
|
|
29
28
|
*
|
|
30
29
|
* `BottomRightIndicators` already described itself as "the single anchor point
|
|
31
30
|
* for every bottom-right overlay ... so they lay out as one row instead of each
|
package/esm/bottomRightCorner.js
CHANGED
|
@@ -23,10 +23,9 @@ import { createContext } from 'react';
|
|
|
23
23
|
* It has never been reachable: the two displays that render the control row
|
|
24
24
|
* (alignments, canvas) are not among the four that report a `ready`-phase
|
|
25
25
|
* status (clustering, on multi-wiggle / multi-row features / maf / the
|
|
26
|
-
* multi-sample variant pair).
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* message that silently never appears.
|
|
26
|
+
* multi-sample variant pair). The layout is structural anyway, because neither
|
|
27
|
+
* side checks for the constraint, so the first display to want both would hit
|
|
28
|
+
* the bug as a status message that never appears.
|
|
30
29
|
*
|
|
31
30
|
* `BottomRightIndicators` already described itself as "the single anchor point
|
|
32
31
|
* for every bottom-right overlay ... so they lay out as one row instead of each
|
|
@@ -2,6 +2,6 @@ import type { DisplayChromeOverlays } from './chromeOverlays.ts';
|
|
|
2
2
|
export declare const DisplayChromeOverlayProvider: import("react").Provider<DisplayChromeOverlays | undefined>;
|
|
3
3
|
/**
|
|
4
4
|
* The set a provider installed, or `undefined` for "nobody asked". The caller
|
|
5
|
-
* supplies the default,
|
|
5
|
+
* supplies the default, so this module binds none.
|
|
6
6
|
*/
|
|
7
7
|
export declare function useChromeOverlayOverride(): DisplayChromeOverlays | undefined;
|
|
@@ -18,10 +18,9 @@ import { createContext, use } from 'react';
|
|
|
18
18
|
// `muiFreeSeam.test.ts` fails naming the import trail if that stops being true.
|
|
19
19
|
/**
|
|
20
20
|
* Runtime override for the overlay set. **Defaults to undefined, not to a
|
|
21
|
-
* component set
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* display still works, it just stops looking like JBrowse. Undefined means
|
|
21
|
+
* component set**. A plain default would degrade every display that renders
|
|
22
|
+
* outside a provider (unit tests, SVG export, breakpoint-split-view's
|
|
23
|
+
* overlayUtils): the display would still work, but stop looking like JBrowse. Undefined means
|
|
25
24
|
* "nobody asked", so `DisplayChrome` keeps its MUI set and nothing changes.
|
|
26
25
|
*
|
|
27
26
|
* This is the escape hatch for embedders who want JBrowse's *own* displays
|
|
@@ -37,7 +36,7 @@ const DisplayChromeOverlayContext = createContext(undefined);
|
|
|
37
36
|
export const DisplayChromeOverlayProvider = DisplayChromeOverlayContext.Provider;
|
|
38
37
|
/**
|
|
39
38
|
* The set a provider installed, or `undefined` for "nobody asked". The caller
|
|
40
|
-
* supplies the default,
|
|
39
|
+
* supplies the default, so this module binds none.
|
|
41
40
|
*/
|
|
42
41
|
export function useChromeOverlayOverride() {
|
|
43
42
|
return use(DisplayChromeOverlayContext);
|
package/esm/chromeOverlays.d.ts
CHANGED
|
@@ -54,8 +54,10 @@ export interface DisplayChromeOverlays {
|
|
|
54
54
|
visible: boolean;
|
|
55
55
|
}>;
|
|
56
56
|
/**
|
|
57
|
-
* The loading scrim. `visible` is `displayPhase
|
|
58
|
-
*
|
|
57
|
+
* The loading scrim. `visible` is `displayPhase` `loading` or `canceled`,
|
|
58
|
+
* the second drawn from `model.fetchCanceled` with Retry in place of Cancel;
|
|
59
|
+
* `immediate` asks it to skip its anti-flash delay because nothing is painted
|
|
60
|
+
* yet.
|
|
59
61
|
*
|
|
60
62
|
* Mounted unconditionally, so it must handle `visible === false` itself — and
|
|
61
63
|
* that is load-bearing rather than a style choice: the anti-flash delay is
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LabelBox {
|
|
2
|
+
left: number;
|
|
3
|
+
right: number;
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The labels to keep, left to right: one survives only where the plot holds its
|
|
9
|
+
* glyphs whole and no kept label's halo meets them, as ggplot2's
|
|
10
|
+
* `check_overlap` leaves them. A sweep over the kept labels still reaching the
|
|
11
|
+
* current left edge, so a screen of labels costs a sort and a short walk each.
|
|
12
|
+
* Sorts `candidates` in place; `tieBreak` orders two starting at the same x.
|
|
13
|
+
*/
|
|
14
|
+
export declare function cullOverlappingLabels<T extends LabelBox>(candidates: T[], width: number, height: number, haloPx: number, tieBreak?: (a: T, b: T) => number): T[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The labels to keep, left to right: one survives only where the plot holds its
|
|
3
|
+
* glyphs whole and no kept label's halo meets them, as ggplot2's
|
|
4
|
+
* `check_overlap` leaves them. A sweep over the kept labels still reaching the
|
|
5
|
+
* current left edge, so a screen of labels costs a sort and a short walk each.
|
|
6
|
+
* Sorts `candidates` in place; `tieBreak` orders two starting at the same x.
|
|
7
|
+
*/
|
|
8
|
+
export function cullOverlappingLabels(candidates, width, height, haloPx, tieBreak = () => 0) {
|
|
9
|
+
candidates.sort((a, b) => a.left - b.left || tieBreak(a, b));
|
|
10
|
+
const gap = 2 * haloPx;
|
|
11
|
+
const kept = [];
|
|
12
|
+
const active = [];
|
|
13
|
+
for (const c of candidates) {
|
|
14
|
+
if (c.left < 0 || c.right > width || c.top < 0 || c.bottom > height) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
let n = 0;
|
|
18
|
+
for (const a of active) {
|
|
19
|
+
if (a.right + gap > c.left) {
|
|
20
|
+
active[n++] = a;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
active.length = n;
|
|
24
|
+
if (active.some(a => a.top - gap < c.bottom && a.bottom + gap > c.top)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
active.push(c);
|
|
28
|
+
kept.push(c);
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { PanZoomView } from '@jbrowse/core/util/usePanZoom';
|
|
2
|
+
import type { ViewStatus as ViewStatusValue } from '@jbrowse/core/util/viewStatus';
|
|
3
|
+
import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
4
|
+
import type React from 'react';
|
|
5
|
+
export { Legend } from './legend.tsx';
|
|
6
|
+
export { LocationBox, useLocationBox } from './location.tsx';
|
|
7
|
+
export { EmbedProvider } from './provider.tsx';
|
|
8
|
+
export { Highlights, RegionSeams, Scalebar } from './regions.tsx';
|
|
9
|
+
export interface EmbedDisplay {
|
|
10
|
+
height: number;
|
|
11
|
+
RenderingComponent: React.ComponentType<{
|
|
12
|
+
model: EmbedDisplay;
|
|
13
|
+
onHorizontalScroll?: (distance: number) => void;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface EmbedView extends PanZoomView, IStateTreeNode {
|
|
17
|
+
status: ViewStatusValue;
|
|
18
|
+
tracks: {
|
|
19
|
+
configuration: {
|
|
20
|
+
trackId: string;
|
|
21
|
+
};
|
|
22
|
+
}[];
|
|
23
|
+
getTrack: (trackId: string) => {
|
|
24
|
+
activeDisplay: EmbedDisplay;
|
|
25
|
+
} | undefined;
|
|
26
|
+
setWidth: (width: number) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare const Track: ({ view, trackId, }: {
|
|
29
|
+
view: EmbedView;
|
|
30
|
+
trackId: string;
|
|
31
|
+
}) => React.JSX.Element | null;
|
|
32
|
+
export interface ResizableDisplay {
|
|
33
|
+
setResizing: (resizing: boolean) => void;
|
|
34
|
+
resizeHeight: (distance: number) => unknown;
|
|
35
|
+
}
|
|
36
|
+
export declare const ResizeHandle: ({ view, trackId, style, }: {
|
|
37
|
+
view: {
|
|
38
|
+
getTrack: (trackId: string) => {
|
|
39
|
+
activeDisplay: ResizableDisplay;
|
|
40
|
+
} | undefined;
|
|
41
|
+
};
|
|
42
|
+
trackId: string;
|
|
43
|
+
style?: React.CSSProperties;
|
|
44
|
+
}) => React.JSX.Element | null;
|
|
45
|
+
export declare const TrackToggle: ({ view, trackId, style, children, }: {
|
|
46
|
+
view: {
|
|
47
|
+
tracks: {
|
|
48
|
+
configuration: {
|
|
49
|
+
trackId: string;
|
|
50
|
+
};
|
|
51
|
+
}[];
|
|
52
|
+
launchToggleTrack: (trackId: string) => Promise<unknown>;
|
|
53
|
+
};
|
|
54
|
+
trackId: string;
|
|
55
|
+
style?: React.CSSProperties;
|
|
56
|
+
children?: React.ReactNode;
|
|
57
|
+
}) => React.JSX.Element;
|
|
58
|
+
export declare const ViewStatus: ({ view, style, }: {
|
|
59
|
+
view: {
|
|
60
|
+
status: ViewStatusValue;
|
|
61
|
+
};
|
|
62
|
+
style?: React.CSSProperties;
|
|
63
|
+
}) => React.JSX.Element | null;
|
|
64
|
+
export declare const TrackStack: ({ view, trackIds, renderTrack, style, children, }: {
|
|
65
|
+
view: EmbedView;
|
|
66
|
+
trackIds?: string[];
|
|
67
|
+
renderTrack?: (trackId: string) => React.ReactNode;
|
|
68
|
+
style?: React.CSSProperties;
|
|
69
|
+
children?: React.ReactNode;
|
|
70
|
+
}) => React.JSX.Element;
|