@jbrowse/display-ui 5.0.0-beta.1 → 5.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +53 -13
  2. package/esm/AxisCaption.d.ts +24 -0
  3. package/esm/AxisCaption.js +57 -0
  4. package/esm/AxisGutter.d.ts +12 -0
  5. package/esm/AxisGutter.js +15 -0
  6. package/esm/CrossHatches.d.ts +12 -0
  7. package/esm/CrossHatches.js +31 -0
  8. package/esm/FloatingLegend.d.ts +4 -6
  9. package/esm/FloatingLegend.js +96 -43
  10. package/esm/FloatingSvgOverlay.d.ts +5 -0
  11. package/esm/FloatingSvgOverlay.js +25 -0
  12. package/esm/FloatingText.d.ts +50 -0
  13. package/esm/FloatingText.js +48 -0
  14. package/esm/PointerLayer.d.ts +12 -18
  15. package/esm/PointerLayer.js +14 -20
  16. package/esm/ScoreDomainCaption.d.ts +15 -0
  17. package/esm/ScoreDomainCaption.js +23 -0
  18. package/esm/ScoreRules.d.ts +21 -0
  19. package/esm/ScoreRules.js +46 -0
  20. package/esm/YScaleBar.d.ts +6 -0
  21. package/esm/YScaleBar.js +47 -0
  22. package/esm/YScaleBarOverlay.d.ts +11 -0
  23. package/esm/YScaleBarOverlay.js +20 -0
  24. package/esm/axisPlacement.d.ts +50 -0
  25. package/esm/axisPlacement.js +80 -0
  26. package/esm/bottomRightCorner.d.ts +3 -4
  27. package/esm/bottomRightCorner.js +3 -4
  28. package/esm/chromeOverlayContext.d.ts +1 -1
  29. package/esm/chromeOverlayContext.js +4 -5
  30. package/esm/chromeOverlays.d.ts +4 -2
  31. package/esm/cullOverlappingLabels.d.ts +14 -0
  32. package/esm/cullOverlappingLabels.js +31 -0
  33. package/esm/embed/index.d.ts +70 -0
  34. package/esm/embed/index.js +72 -0
  35. package/esm/embed/legend.d.ts +10 -0
  36. package/esm/embed/legend.js +40 -0
  37. package/esm/embed/location.d.ts +17 -0
  38. package/esm/embed/location.js +65 -0
  39. package/esm/embed/provider.d.ts +11 -0
  40. package/esm/embed/provider.js +6 -0
  41. package/esm/embed/regions.d.ts +53 -0
  42. package/esm/embed/regions.js +95 -0
  43. package/esm/floatingLegendTop.d.ts +2 -0
  44. package/esm/floatingLegendTop.js +3 -0
  45. package/esm/index.d.ts +19 -0
  46. package/esm/index.js +24 -0
  47. package/esm/plainChromeOverlays.d.ts +1 -1
  48. package/esm/plainChromeOverlays.js +6 -6
  49. package/esm/tooltip/Tooltip.d.ts +3 -3
  50. package/esm/tooltip/Tooltip.js +3 -3
  51. package/esm/trackControl/trackControlContext.d.ts +1 -1
  52. package/esm/trackControl/trackControlContext.js +6 -6
  53. package/esm/trackOverlay/TrackOverlaySlot.d.ts +2 -2
  54. package/esm/trackOverlay/TrackOverlaySlot.js +2 -2
  55. package/esm/valueScale.d.ts +72 -0
  56. package/esm/valueScale.js +1 -0
  57. package/esm/yAxisConstants.d.ts +7 -0
  58. package/esm/yAxisConstants.js +19 -0
  59. package/esm/yScaleTicks.d.ts +49 -0
  60. package/esm/yScaleTicks.js +47 -0
  61. package/package.json +12 -7
@@ -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 — guides, a tooltip, a
5
- * highlighted column — rendered off the chrome's `mouseTracker` in a leaf of
6
- * its own, so a mousemove re-renders this and nothing else. Read in the
7
- * component that mounts `DisplayChrome` instead, the position would re-render
8
- * the chrome, its status container and every overlay under it on each move
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
- * `rowsTopOffset` answers whether the pointer is over the rows rather than a
12
- * band stacked above them, for a display that gates its crosshair to the rows:
13
- * a guide drawn while the pointer is over a variant lane or a connector zone
14
- * names a row the pointer is not on. The tooltip is usually not gated with it,
15
- * which is why the child decides. `children` is also handed an absent state, so
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 function PointerLayer({ mouseTracker, rowsTopOffset, children, }: {
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
- }): import("react").JSX.Element;
20
+ }) => ReactNode;
@@ -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 — guides, a tooltip, a
7
- * highlighted column — rendered off the chrome's `mouseTracker` in a leaf of
8
- * its own, so a mousemove re-renders this and nothing else. Read in the
9
- * component that mounts `DisplayChrome` instead, the position would re-render
10
- * the chrome, its status container and every overlay under it on each move
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
- * `rowsTopOffset` answers whether the pointer is over the rows rather than a
14
- * band stacked above them, for a display that gates its crosshair to the rows:
15
- * a guide drawn while the pointer is over a variant lane or a connector zone
16
- * names a row the pointer is not on. The tooltip is usually not gated with it,
17
- * which is why the child decides. `children` is also handed an absent state, so
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 _jsx(_Fragment, { children: children(state, !!state && state.y > rowsTopOffset) });
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
+ }
@@ -0,0 +1,6 @@
1
+ import type { YScaleTicks } from './yScaleTicks.ts';
2
+ export default function YScaleBar({ ticks, orientation, bandHeight, }: {
3
+ ticks: YScaleTicks | undefined;
4
+ orientation?: 'left' | 'right';
5
+ bandHeight?: number;
6
+ }): import("react").JSX.Element | null;
@@ -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,50 @@
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
+ /**
33
+ * Px the export's left margin holds for these axes: the widest left-side axis
34
+ * that draws, 0 when none does. A sidebar the export parks left of the figure
35
+ * sits past this strip so the scalebars stay beside the plot.
36
+ */
37
+ export declare function leftAxisGutterWidth(axes: readonly Pick<YAxis, 'side' | 'ticks' | 'caption' | 'height' | 'bandTops'>[]): number;
38
+ /** How far in from the plot's right edge a right-side gutter ends in an export. */
39
+ export declare const AXIS_RIGHT_INSET_PX = 4;
40
+ /**
41
+ * Left edge of the gutter an axis is drawn in. A right-side axis takes the
42
+ * gutter inside the plot's right edge, less `rightInset` (the vertical
43
+ * scrollbar a display may mount on screen; a margin in an export). A left-side
44
+ * axis takes the gutter at the display's left edge, past whatever panel the
45
+ * scale's `left` reserves — except in an export, whose margin is the gutter,
46
+ * so an axis nothing pushes right sits in the margin with its numbers outside
47
+ * the plot and its spine on the content edge, or past it by what
48
+ * `axisGutterWidth` grew.
49
+ */
50
+ export declare function axisGutterLeft(axis: Pick<YAxis, 'side' | 'left' | 'ticks' | 'caption'>, width: number, rightInset: number, exportContentLeft?: number): number;
@@ -0,0 +1,80 @@
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
+ /**
51
+ * Px the export's left margin holds for these axes: the widest left-side axis
52
+ * that draws, 0 when none does. A sidebar the export parks left of the figure
53
+ * sits past this strip so the scalebars stay beside the plot.
54
+ */
55
+ export function leftAxisGutterWidth(axes) {
56
+ return Math.max(0, ...axes
57
+ .filter(axis => axis.side !== 'right' && axisDrawn(axis))
58
+ .map(axisGutterWidth));
59
+ }
60
+ /** How far in from the plot's right edge a right-side gutter ends in an export. */
61
+ export const AXIS_RIGHT_INSET_PX = 4;
62
+ /**
63
+ * Left edge of the gutter an axis is drawn in. A right-side axis takes the
64
+ * gutter inside the plot's right edge, less `rightInset` (the vertical
65
+ * scrollbar a display may mount on screen; a margin in an export). A left-side
66
+ * axis takes the gutter at the display's left edge, past whatever panel the
67
+ * scale's `left` reserves — except in an export, whose margin is the gutter,
68
+ * so an axis nothing pushes right sits in the margin with its numbers outside
69
+ * the plot and its spine on the content edge, or past it by what
70
+ * `axisGutterWidth` grew.
71
+ */
72
+ export function axisGutterLeft(axis, width, rightInset, exportContentLeft) {
73
+ if (axis.side === 'right') {
74
+ return width - rightInset - axisGutterWidth(axis);
75
+ }
76
+ const left = axis.left ?? 0;
77
+ return exportContentLeft !== undefined && left === 0
78
+ ? exportContentLeft - AXIS_GUTTER_WIDTH_PX
79
+ : left;
80
+ }
@@ -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). That is the *reason* to make it structural rather
26
- * than to leave it — nothing on either side is aware of the constraint, so the
27
- * first display to want both would find the bug, and it presents as a status
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
@@ -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). That is the *reason* to make it structural rather
27
- * than to leave it — nothing on either side is aware of the constraint, so the
28
- * first display to want both would find the bug, and it presents as a status
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, which is what keeps this module free of one.
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** — that is deliberate. A plain default would silently degrade
22
- * every display that renders outside a provider (unit tests, SVG export,
23
- * breakpoint-split-view's overlayUtils), and that degradation is invisible: the
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, which is what keeps this module free of one.
39
+ * supplies the default, so this module binds none.
41
40
  */
42
41
  export function useChromeOverlayOverride() {
43
42
  return use(DisplayChromeOverlayContext);
@@ -54,8 +54,10 @@ export interface DisplayChromeOverlays {
54
54
  visible: boolean;
55
55
  }>;
56
56
  /**
57
- * The loading scrim. `visible` is `displayPhase === 'loading'`; `immediate`
58
- * asks it to skip its anti-flash delay because nothing is painted yet.
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;