@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.
- 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 +96 -43
- 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 +50 -0
- package/esm/axisPlacement.js +80 -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 +12 -7
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ above them.
|
|
|
36
36
|
|
|
37
37
|
| | |
|
|
38
38
|
| ------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
39
|
-
| `DisplayChromeOverlays` | the
|
|
39
|
+
| `DisplayChromeOverlays` | the `displayPhase` states, as a five-component set, with the model shape each is handed |
|
|
40
40
|
| `DisplayChromeOverlayProvider` | redirects those states for JBrowse's own displays |
|
|
41
41
|
| `TrackControlComponent` | one shape for every ambient bottom-right control, icons named rather than passed |
|
|
42
42
|
| `TrackControlProvider` | redirects those |
|
|
@@ -89,10 +89,9 @@ and the status text disappearing under them.
|
|
|
89
89
|
It has never been reachable: the two displays that render the control row
|
|
90
90
|
(alignments, canvas) are not among the four that report a `ready`-phase status
|
|
91
91
|
(clustering, on multi-wiggle / multi-row features / maf / the multi-sample
|
|
92
|
-
variant pair).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
never appears.
|
|
92
|
+
variant pair). The layout is structural anyway, because neither side checks for
|
|
93
|
+
the constraint, so the first display to want both would hit the bug as a status
|
|
94
|
+
message that never appears.
|
|
96
95
|
|
|
97
96
|
`BottomRightIndicators` already described itself as "the single anchor point for
|
|
98
97
|
every bottom-right overlay ... so they lay out as one row instead of each
|
|
@@ -208,9 +207,9 @@ question only exists for an MST node.
|
|
|
208
207
|
|
|
209
208
|
### plainChromeOverlays
|
|
210
209
|
|
|
211
|
-
The
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
The `displayPhase` states drawn with no UI toolkit: no theme provider to mount,
|
|
211
|
+
no emotion in the host page, and nothing that reads as a stray Material widget
|
|
212
|
+
inside someone else's design system.
|
|
214
213
|
|
|
215
214
|
`DisplayUIProvider` installs this by default, so mount that rather than naming
|
|
216
215
|
this — reach for it directly only to wrap a state or to build a context value by
|
|
@@ -288,9 +287,8 @@ toolkit, so the package's no-Material-UI guarantee holds.
|
|
|
288
287
|
Takes a single element child and clones it rather than wrapping it: the controls
|
|
289
288
|
that want a tooltip are absolutely positioned inside a legend or sit in a flex
|
|
290
289
|
row, where an extra `<span>` moves them. The child keeps its own handlers —
|
|
291
|
-
these compose on top of them.
|
|
292
|
-
|
|
293
|
-
toolkit is what differs here, not the shape.
|
|
290
|
+
these compose on top of them. MUI's `Tooltip`, which the rest of JBrowse's
|
|
291
|
+
chrome is built on, uses the same trigger pattern; only the toolkit differs.
|
|
294
292
|
|
|
295
293
|
```tsx
|
|
296
294
|
<Tooltip title="Hide legend">
|
|
@@ -366,8 +364,8 @@ under them with nothing to say so.
|
|
|
366
364
|
this component: JBrowse's own track container passes 100, which is positioned
|
|
367
365
|
above `PaddingBlocks` and below `TrackLabel` at 200, and means nothing to a host
|
|
368
366
|
whose masks sit at 2. A default would be a number that is right in one layout
|
|
369
|
-
and
|
|
370
|
-
|
|
367
|
+
and wrong in every other, and the failure — chrome painted under a mask — goes
|
|
368
|
+
unseen until someone looks at the right zoom.
|
|
371
369
|
|
|
372
370
|
The node takes no pointer events, so it does not eat the canvas's. Chrome that
|
|
373
371
|
wants them takes them back on its own positioned box; it also carries
|
|
@@ -430,4 +428,46 @@ carries position only.
|
|
|
430
428
|
|
|
431
429
|
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/trackControl/useTrackControlMenu.tsx)
|
|
432
430
|
|
|
431
|
+
### ValueScale
|
|
432
|
+
|
|
433
|
+
A value scale a display places its y through, declared so the chrome can derive
|
|
434
|
+
the axis from it — the score axis's counterpart to a colour scale.
|
|
435
|
+
|
|
436
|
+
`domain` is the resolved `[min, max]`; `height` the band it rules and `offset`
|
|
437
|
+
the inset of the plot box inside that band (`axisPlotBox(height, offset)`);
|
|
438
|
+
`minimalTicks` and `symlogConstant` how the ticks are chosen. `ticks` is the
|
|
439
|
+
display's own ladder where the band's arithmetic is its own (the coverage band's
|
|
440
|
+
octaves against the shader's box, the read cloud's decades against the arc
|
|
441
|
+
geometry); `ScoreScaleMixin` derives one through `computeYTicks` otherwise.
|
|
442
|
+
|
|
443
|
+
`bandTops` is the screen y of each band the scale rules — one at 0 unless the
|
|
444
|
+
display stacks the same plot: the multi-wiggle's rows, a grouped alignments
|
|
445
|
+
track's coverage band per group, each projected through the display's own
|
|
446
|
+
scroll; the chrome drops the ones off screen. `[]` is a scale the display maps
|
|
447
|
+
to colour rather than to y (density rows each in their own colour), which gets
|
|
448
|
+
the `[min, max]` caption and no axis, as a scale whose bands are too short for
|
|
449
|
+
one does. `side` and `left` are which of the band's edges the display's own
|
|
450
|
+
panels leave clear for a gutter: `right` where a group label chip takes the
|
|
451
|
+
left, `left: n` where a dendrogram takes the first `n` px. `caption` is what the
|
|
452
|
+
scale measures (`TLEN`), as a colour scale's `field` is; the chrome draws it
|
|
453
|
+
once for the scale, beside the bands on screen, however many it rules. `rules`
|
|
454
|
+
are the reference lines the scale declares and `grid` a line at every tick, both
|
|
455
|
+
drawn across every band.
|
|
456
|
+
|
|
457
|
+
A member describes the scale or the band it rules, never the axis — not an
|
|
458
|
+
orientation, a form, a gutter width or a font. The chrome reads a member to keep
|
|
459
|
+
off what the display put in the band, never to choose between two drawings of it
|
|
460
|
+
(ADR-109).
|
|
461
|
+
|
|
462
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/valueScale.ts)
|
|
463
|
+
|
|
464
|
+
### ValueScaleRule
|
|
465
|
+
|
|
466
|
+
One reference line a value scale declares, at a value in the scale's own units.
|
|
467
|
+
`label` is free text and carries no meaning the chrome assigns: over a coverage
|
|
468
|
+
plot "2 copies" is a claim only the author can make, since no ploidy can be
|
|
469
|
+
assumed.
|
|
470
|
+
|
|
471
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/valueScale.ts)
|
|
472
|
+
|
|
433
473
|
<!-- API_DOCS_END -->
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { YAxis } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A scale's caption, drawn once however many bands the scale rules: rotated
|
|
4
|
+
* along the outer edge of the gutter column, the origin at the column's top
|
|
5
|
+
* left, centred on the bands given as nearly as the display's `height` holds
|
|
6
|
+
* it whole, and cut to that height where it is longer. The on-screen overlay
|
|
7
|
+
* and the export both draw through this, so the two cannot place a caption
|
|
8
|
+
* differently.
|
|
9
|
+
*/
|
|
10
|
+
export default function AxisCaption({ axis, bandTops, height, }: {
|
|
11
|
+
axis: YAxis;
|
|
12
|
+
bandTops: number[];
|
|
13
|
+
height: number;
|
|
14
|
+
}): import("react").JSX.Element | null;
|
|
15
|
+
/**
|
|
16
|
+
* The on-screen caption of one scale: an `<svg>` over the gutter column, the
|
|
17
|
+
* height of the display, with `AxisCaption` inside.
|
|
18
|
+
*/
|
|
19
|
+
export declare function AxisCaptionOverlay({ axis, bandTops, width, height, }: {
|
|
20
|
+
axis: YAxis;
|
|
21
|
+
bandTops: number[];
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { VERTICAL_SCROLLBAR_CLEARANCE } from '@jbrowse/core/ui/VerticalScrollbar';
|
|
4
|
+
import { measureText } from '@jbrowse/core/util/measureText';
|
|
5
|
+
import { axisCaptionY, axisGutterLeft, axisGutterWidth, } from './axisPlacement.js';
|
|
6
|
+
import { AXIS_FONT_PX, CAPTION_INSET_PX } from './yAxisConstants.js';
|
|
7
|
+
// The caption and its length, cut to an ellipsis where it is longer than
|
|
8
|
+
// `room`, as a Vega-Lite axis title is cut at its `titleLimit`. The ellipsis
|
|
9
|
+
// is one em.
|
|
10
|
+
function fitCaption(caption, room) {
|
|
11
|
+
const whole = measureText(caption, AXIS_FONT_PX);
|
|
12
|
+
if (whole <= room) {
|
|
13
|
+
return { text: caption, length: whole };
|
|
14
|
+
}
|
|
15
|
+
for (let end = caption.length - 1; end > 0; end--) {
|
|
16
|
+
const length = measureText(caption.slice(0, end), AXIS_FONT_PX) + AXIS_FONT_PX;
|
|
17
|
+
if (length <= room) {
|
|
18
|
+
return { text: `${caption.slice(0, end)}…`, length };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A scale's caption, drawn once however many bands the scale rules: rotated
|
|
25
|
+
* along the outer edge of the gutter column, the origin at the column's top
|
|
26
|
+
* left, centred on the bands given as nearly as the display's `height` holds
|
|
27
|
+
* it whole, and cut to that height where it is longer. The on-screen overlay
|
|
28
|
+
* and the export both draw through this, so the two cannot place a caption
|
|
29
|
+
* differently.
|
|
30
|
+
*/
|
|
31
|
+
export default function AxisCaption({ axis, bandTops, height, }) {
|
|
32
|
+
const palette = usePalette();
|
|
33
|
+
const x = axis.side === 'right'
|
|
34
|
+
? axisGutterWidth(axis) - CAPTION_INSET_PX
|
|
35
|
+
: CAPTION_INSET_PX;
|
|
36
|
+
const caption = fitCaption(axis.caption ?? '', height);
|
|
37
|
+
if (!caption) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const half = caption.length / 2;
|
|
41
|
+
const y = Math.min(Math.max(axisCaptionY(axis, bandTops), half), height - half);
|
|
42
|
+
return (_jsx("text", { x: x, y: y, fontSize: AXIS_FONT_PX, textAnchor: "middle", fill: palette.text.primary, stroke: palette.background.default, strokeWidth: 2.5, paintOrder: "stroke", transform: `rotate(-90, ${x}, ${y})`, children: caption.text }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The on-screen caption of one scale: an `<svg>` over the gutter column, the
|
|
46
|
+
* height of the display, with `AxisCaption` inside.
|
|
47
|
+
*/
|
|
48
|
+
export function AxisCaptionOverlay({ axis, bandTops, width, height, }) {
|
|
49
|
+
return (_jsx("svg", { style: {
|
|
50
|
+
position: 'absolute',
|
|
51
|
+
top: 0,
|
|
52
|
+
left: axisGutterLeft(axis, width, VERTICAL_SCROLLBAR_CLEARANCE),
|
|
53
|
+
pointerEvents: 'none',
|
|
54
|
+
height,
|
|
55
|
+
width: axisGutterWidth(axis),
|
|
56
|
+
}, children: _jsx(AxisCaption, { axis: axis, bandTops: bandTops, height: height }) }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { YAxis } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* One band's axis drawn inside its gutter, the origin at the gutter's left
|
|
4
|
+
* edge: the spine on the gutter's inner edge — its right for a left-side
|
|
5
|
+
* axis, its left for a right-side one — with the numbers growing outward. The
|
|
6
|
+
* on-screen overlay and the export both draw through this, so the two cannot
|
|
7
|
+
* place a spine differently. The scale's caption is `AxisCaption`'s, drawn once
|
|
8
|
+
* for the scale and not per band.
|
|
9
|
+
*/
|
|
10
|
+
export default function AxisGutter({ axis }: {
|
|
11
|
+
axis: YAxis;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import YScaleBar from './YScaleBar.js';
|
|
3
|
+
import { axisGutterWidth } from './axisPlacement.js';
|
|
4
|
+
/**
|
|
5
|
+
* One band's axis drawn inside its gutter, the origin at the gutter's left
|
|
6
|
+
* edge: the spine on the gutter's inner edge — its right for a left-side
|
|
7
|
+
* axis, its left for a right-side one — with the numbers growing outward. The
|
|
8
|
+
* on-screen overlay and the export both draw through this, so the two cannot
|
|
9
|
+
* place a spine differently. The scale's caption is `AxisCaption`'s, drawn once
|
|
10
|
+
* for the scale and not per band.
|
|
11
|
+
*/
|
|
12
|
+
export default function AxisGutter({ axis }) {
|
|
13
|
+
const right = axis.side === 'right';
|
|
14
|
+
return (_jsx("g", { transform: `translate(${right ? 0 : axisGutterWidth(axis)} 0)`, children: _jsx(YScaleBar, { ticks: axis.ticks, orientation: right ? 'right' : 'left', bandHeight: axis.height }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { YScaleTicks } from './yScaleTicks.ts';
|
|
2
|
+
export declare function CrossHatchLines({ ticks, width, offsetY, }: {
|
|
3
|
+
ticks: YScaleTicks;
|
|
4
|
+
width: number;
|
|
5
|
+
offsetY?: number;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export default function CrossHatches({ ticks, width, height, bandTops, }: {
|
|
8
|
+
ticks: YScaleTicks;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
bandTops: number[];
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { clampStrokeInsideAxis } from './yScaleTicks.js';
|
|
3
|
+
// Bare <line> set for the Y-scale tick guide lines, no wrapping <svg>. Shared by
|
|
4
|
+
// the absolutely-positioned on-screen CrossHatches overlay and the flat SVG
|
|
5
|
+
// export, so the two can't drift. `offsetY` is the band's top.
|
|
6
|
+
export function CrossHatchLines({ ticks, width, offsetY = 0, }) {
|
|
7
|
+
return (_jsx(_Fragment, { children: ticks.items.map(({ value, y }) => {
|
|
8
|
+
// Same clamp the axis ticks take, so a guide line and the tick it
|
|
9
|
+
// belongs to can't disagree at the bottom edge — where, in multi-wiggle,
|
|
10
|
+
// "one pixel lower" is the next sample's row. Only that edge moves; the
|
|
11
|
+
// guide lines don't crispen to a half pixel the way the axis does.
|
|
12
|
+
const lineY = offsetY + clampStrokeInsideAxis(y, ticks.yBottom);
|
|
13
|
+
return (_jsx("line", { x1: 0, x2: width, y1: lineY, y2: lineY, stroke: "rgb(200,200,200)",
|
|
14
|
+
// a separate stroke-opacity attribute (not baked into an rgba()
|
|
15
|
+
// string) survives the SVG export, whose renderToStaticMarkup strips
|
|
16
|
+
// rgba() alpha
|
|
17
|
+
strokeOpacity: 0.8, strokeWidth: 1 }, `${value}-${y}`));
|
|
18
|
+
}) }));
|
|
19
|
+
}
|
|
20
|
+
// Horizontal guide lines at each Y-scale tick, once per band the scale rules.
|
|
21
|
+
// Pointer-events disabled so the underlying canvas still receives mouse events.
|
|
22
|
+
export default function CrossHatches({ ticks, width, height, bandTops, }) {
|
|
23
|
+
return (_jsx("svg", { style: {
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
top: 0,
|
|
26
|
+
left: 0,
|
|
27
|
+
pointerEvents: 'none',
|
|
28
|
+
height,
|
|
29
|
+
width,
|
|
30
|
+
}, children: bandTops.map(top => (_jsx(CrossHatchLines, { ticks: ticks, width: width, offsetY: top }, top))) }));
|
|
31
|
+
}
|
package/esm/FloatingLegend.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import type { LegendItem, LegendSection } from '@jbrowse/core/ui/legendSpec';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
items?: LegendItem[];
|
|
6
|
-
sections?: LegendSection[];
|
|
2
|
+
export type { LegendGradient, LegendItem, LegendSection, LegendSpec, LegendSwatch, } from '@jbrowse/core/ui/legendSpec';
|
|
3
|
+
declare const FloatingLegend: ({ sections, title, onDismiss, onDismissSection, onItemClick, maxItems, maxWidth, top, right, }: {
|
|
4
|
+
sections: readonly LegendSection[];
|
|
7
5
|
title?: string;
|
|
8
|
-
children?: ReactNode;
|
|
9
6
|
onDismiss?: () => void;
|
|
10
7
|
onDismissSection?: (id: string) => void;
|
|
11
8
|
onItemClick?: (item: LegendItem, section: LegendSection) => void;
|
|
12
9
|
maxItems?: number;
|
|
13
10
|
maxWidth?: number;
|
|
14
11
|
top?: number;
|
|
12
|
+
right?: number;
|
|
15
13
|
}) => import("react").JSX.Element | null;
|
|
16
14
|
export default FloatingLegend;
|
package/esm/FloatingLegend.js
CHANGED
|
@@ -8,12 +8,12 @@ import { LegendSwatchGlyph } from '@jbrowse/core/ui/LegendSwatchGlyph';
|
|
|
8
8
|
import { legendSwatches, nonEmptyLegendSections, } from '@jbrowse/core/ui/legendSpec';
|
|
9
9
|
import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
|
|
10
10
|
import { observer } from 'mobx-react';
|
|
11
|
+
import { FLOATING_LEGEND_RIGHT_PX, FLOATING_LEGEND_TOP_PX, } from './floatingLegendTop.js';
|
|
11
12
|
import Tooltip from './tooltip/Tooltip.js';
|
|
12
13
|
import { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
|
|
13
14
|
const useStyles = makeStyles()(theme => ({
|
|
14
15
|
legend: {
|
|
15
16
|
position: 'absolute',
|
|
16
|
-
right: 10,
|
|
17
17
|
background: theme.palette.background.paper,
|
|
18
18
|
// A hairline edge, because `background.paper` on a track whose canvas is
|
|
19
19
|
// also paper-white is no edge at all: the key read as loose text floating
|
|
@@ -25,6 +25,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
25
25
|
border: `1px solid ${theme.palette.divider}`,
|
|
26
26
|
borderRadius: 3,
|
|
27
27
|
padding: 3,
|
|
28
|
+
// Two keys on one short display ran past its bottom edge and were cut
|
|
29
|
+
// there; the box scrolls inside the display instead.
|
|
30
|
+
overflowY: 'auto',
|
|
28
31
|
fontSize: 10,
|
|
29
32
|
zIndex: 100,
|
|
30
33
|
// the default; `maxWidth` overrides it per display
|
|
@@ -34,20 +37,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
34
37
|
// rendered inline, where events already pass through)
|
|
35
38
|
pointerEvents: 'auto',
|
|
36
39
|
},
|
|
37
|
-
// A plain <button
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// This component is rendered by canvas, alignments, variants and multi-wiggle
|
|
41
|
-
// displays *directly*, behind neither bring-your-own seam, so a Material
|
|
42
|
-
// widget here reaches an embedder who has opted out of both — and the
|
|
43
|
-
// build-your-own site's zero-MUI census, which no page currently trips only
|
|
44
|
-
// because no page turns on a colorBy that raises a legend. Same call
|
|
45
|
-
// `BaseTooltip` made in 2026-08: what it needed was colors, and colors have a
|
|
46
|
-
// toolkit-free home already. See DISPLAYCHROME.md, "a third seam was
|
|
47
|
-
// considered for the tooltip and rejected".
|
|
48
|
-
//
|
|
49
|
-
// `×` is what `SvgColorLegend` already draws for this control, so the
|
|
50
|
-
// exported legend and the on-screen one now agree.
|
|
40
|
+
// A plain <button> with a `×` glyph, not MUI: displays render this legend
|
|
41
|
+
// directly, behind neither bring-your-own seam. See DISPLAYCHROME.md, "The
|
|
42
|
+
// bring-your-own seams".
|
|
51
43
|
closeButton: {
|
|
52
44
|
position: 'absolute',
|
|
53
45
|
top: 0,
|
|
@@ -87,6 +79,13 @@ const useStyles = makeStyles()(theme => ({
|
|
|
87
79
|
overflow: 'hidden',
|
|
88
80
|
textOverflow: 'ellipsis',
|
|
89
81
|
},
|
|
82
|
+
// Wraps where every other line ellipsizes: a note is a sentence, and half of
|
|
83
|
+
// one says nothing.
|
|
84
|
+
sectionNote: {
|
|
85
|
+
color: theme.palette.text.secondary,
|
|
86
|
+
whiteSpace: 'normal',
|
|
87
|
+
marginBottom: 2,
|
|
88
|
+
},
|
|
90
89
|
sectionClose: {
|
|
91
90
|
font: 'inherit',
|
|
92
91
|
lineHeight: 1,
|
|
@@ -134,6 +133,28 @@ const useStyles = makeStyles()(theme => ({
|
|
|
134
133
|
overflow: 'hidden',
|
|
135
134
|
textOverflow: 'ellipsis',
|
|
136
135
|
},
|
|
136
|
+
hiddenItem: {
|
|
137
|
+
opacity: 0.4,
|
|
138
|
+
textDecoration: 'line-through',
|
|
139
|
+
},
|
|
140
|
+
gradientRow: {
|
|
141
|
+
marginBottom: 1,
|
|
142
|
+
},
|
|
143
|
+
gradientBar: {
|
|
144
|
+
width: GRADIENT_BAR_WIDTH,
|
|
145
|
+
height: 10,
|
|
146
|
+
borderRadius: 2,
|
|
147
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
148
|
+
// clip the gradient to the padding box so it doesn't paint under the
|
|
149
|
+
// translucent divider border — otherwise the border composites over the
|
|
150
|
+
// gradient's end and the edge renders a dark sliver
|
|
151
|
+
backgroundClip: 'padding-box',
|
|
152
|
+
},
|
|
153
|
+
gradientLabels: {
|
|
154
|
+
display: 'flex',
|
|
155
|
+
justifyContent: 'space-between',
|
|
156
|
+
width: GRADIENT_BAR_WIDTH,
|
|
157
|
+
},
|
|
137
158
|
// was `<Link component="button" underline="hover">`, which is the same
|
|
138
159
|
// element with Material's typography on it — see `closeButton` above for why
|
|
139
160
|
// that mattered
|
|
@@ -146,7 +167,7 @@ const useStyles = makeStyles()(theme => ({
|
|
|
146
167
|
border: 0,
|
|
147
168
|
background: 'none',
|
|
148
169
|
textAlign: 'left',
|
|
149
|
-
color: theme.palette.
|
|
170
|
+
color: theme.palette.accent,
|
|
150
171
|
cursor: 'pointer',
|
|
151
172
|
'&:hover': {
|
|
152
173
|
textDecoration: 'underline',
|
|
@@ -155,56 +176,88 @@ const useStyles = makeStyles()(theme => ({
|
|
|
155
176
|
}));
|
|
156
177
|
const DEFAULT_MAX_ITEMS = 12;
|
|
157
178
|
const DEFAULT_MAX_WIDTH = 200;
|
|
158
|
-
|
|
159
|
-
//
|
|
179
|
+
const GRADIENT_BAR_WIDTH = 100;
|
|
180
|
+
// Side of one swatch box.
|
|
160
181
|
const SWATCH = 12;
|
|
182
|
+
// A ramp row: the caption, the bar and the domain ends. The bar is a CSS
|
|
183
|
+
// gradient over the same stops the export's <linearGradient> takes, so the
|
|
184
|
+
// two draw one ramp. Stops are flattened over the paper because a translucent
|
|
185
|
+
// first stop (Hi-C's juicebox fade) rasterizes to a dark sliver at the bar's
|
|
186
|
+
// left edge.
|
|
187
|
+
function gradientCss({ stops }, paper) {
|
|
188
|
+
const list = stops.map(({ color, opacity = 1, offset }) => {
|
|
189
|
+
const flat = opacity >= 1
|
|
190
|
+
? color
|
|
191
|
+
: `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, ${paper})`;
|
|
192
|
+
return `${flat} ${offset * 100}%`;
|
|
193
|
+
});
|
|
194
|
+
return `linear-gradient(to right, ${list.join(', ')})`;
|
|
195
|
+
}
|
|
196
|
+
function GradientRow({ label, gradient, }) {
|
|
197
|
+
const { classes, theme } = useStyles();
|
|
198
|
+
return (_jsxs("div", { className: classes.gradientRow, children: [label ? _jsx("div", { className: classes.label, children: label }) : null, _jsx("div", { className: classes.gradientBar,
|
|
199
|
+
// backgroundImage (longhand), not the `background` shorthand: the
|
|
200
|
+
// shorthand resets background-clip back to border-box, undoing the
|
|
201
|
+
// padding-box clip
|
|
202
|
+
style: {
|
|
203
|
+
backgroundImage: gradientCss(gradient, theme.palette.background.paper),
|
|
204
|
+
} }), _jsxs("div", { className: classes.gradientLabels, children: [_jsx("span", { children: gradient.minLabel }), _jsx("span", { children: gradient.maxLabel })] })] }));
|
|
205
|
+
}
|
|
161
206
|
// One list of swatches with its own independent collapse state, so each section
|
|
162
207
|
// in a multi-section legend expands/collapses on its own.
|
|
163
208
|
const LegendItemList = observer(function LegendItemList({ items, maxItems, onItemClick, }) {
|
|
164
209
|
const { classes } = useStyles();
|
|
165
210
|
const [expanded, setExpanded] = useState(false);
|
|
166
|
-
|
|
211
|
+
// A toggle revealing one row costs the row it saves.
|
|
212
|
+
const collapsible = items.length > maxItems + 1;
|
|
167
213
|
const shown = collapsible && !expanded ? items.slice(0, maxItems) : items;
|
|
168
214
|
const hiddenCount = items.length - maxItems;
|
|
169
|
-
// Every row reserves the widest row's swatch column,
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
// replaces used to.
|
|
173
|
-
const columns = Math.max(1, ...shown.map(i => legendSwatches(i).length));
|
|
215
|
+
// Every row reserves the widest row's swatch column, collapsed or not, so
|
|
216
|
+
// labels stay in one vertical line and do not shift on expanding.
|
|
217
|
+
const columns = Math.max(1, ...items.map(i => legendSwatches(i).length));
|
|
174
218
|
return (_jsxs(_Fragment, { children: [shown.map((item, i) => {
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
if (item.gradient) {
|
|
220
|
+
return (_jsx(GradientRow
|
|
221
|
+
// eslint-disable-next-line @eslint-react/no-array-index-key
|
|
222
|
+
, { label: item.label, gradient: item.gradient }, `${item.label}-${i}`));
|
|
223
|
+
}
|
|
224
|
+
const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: classes.swatches, style: { minWidth: columns * SWATCH + (columns - 1) * 2 }, children: legendSwatches(item).map(swatch => (_jsx("svg", { width: SWATCH, height: SWATCH, children: _jsx(LegendSwatchGlyph, { swatch: swatch, size: SWATCH }) }, `${swatch.shape}-${swatch.color}`))) }), _jsx("span", { className: classes.label, children: item.label })] }));
|
|
225
|
+
const itemClass = cx(classes.item, item.hidden && classes.hiddenItem);
|
|
226
|
+
return onItemClick ? (_jsx("button", { type: "button", className: cx(itemClass, classes.itemButton), title: "Show only these rows", onClick: () => {
|
|
177
227
|
onItemClick(item);
|
|
178
|
-
}, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className:
|
|
228
|
+
}, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className: itemClass, children: row }, `${item.label}-${i}`));
|
|
179
229
|
}), collapsible ? (_jsx("button", { type: "button", className: classes.toggle, onClick: () => {
|
|
180
230
|
setExpanded(!expanded);
|
|
181
231
|
}, children: expanded ? 'Show less' : `Show ${hiddenCount} more…` })) : null] }));
|
|
182
232
|
});
|
|
183
|
-
// Floating color-key overlay
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// is a heading for the whole box, shown regardless of section count.
|
|
233
|
+
// Floating color-key overlay: one panel per section (e.g. genotype colors vs.
|
|
234
|
+
// sample-grouping colors on the multi-sample variant display). Section titles
|
|
235
|
+
// and per-section close buttons only appear when there is more than one
|
|
236
|
+
// section, so a single-scheme legend is just its rows. `title` is a heading
|
|
237
|
+
// for the whole box, shown regardless of section count.
|
|
189
238
|
//
|
|
190
|
-
// `onItemClick` makes the rows
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
const FloatingLegend = observer(function FloatingLegend({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top = 10, children, }) {
|
|
239
|
+
// `onItemClick` makes the rows of a section declaring `focusesRows` act —
|
|
240
|
+
// clicking an entry focuses the rows it names. A section coloring features or
|
|
241
|
+
// cells stays inert text beside it.
|
|
242
|
+
const FloatingLegend = observer(function FloatingLegend({ sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top = FLOATING_LEGEND_TOP_PX, right = FLOATING_LEGEND_RIGHT_PX, }) {
|
|
195
243
|
const { classes } = useStyles();
|
|
196
|
-
const nonEmpty = nonEmptyLegendSections(
|
|
197
|
-
if (nonEmpty.length === 0
|
|
244
|
+
const nonEmpty = nonEmptyLegendSections(sections);
|
|
245
|
+
if (nonEmpty.length === 0) {
|
|
198
246
|
return null;
|
|
199
247
|
}
|
|
200
248
|
const multiSection = nonEmpty.length > 1;
|
|
201
249
|
// portal above the inter-region padding masks so the key isn't buried at
|
|
202
250
|
// whole-genome / multi-region scale (see TrackOverlayPortal)
|
|
203
|
-
return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: {
|
|
251
|
+
return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: {
|
|
252
|
+
top,
|
|
253
|
+
right,
|
|
254
|
+
maxWidth,
|
|
255
|
+
maxHeight: `calc(100% - ${top + 4}px)`,
|
|
256
|
+
}, "data-testid": "floating-legend", "data-gesture-owner": "true", children: [onDismiss ? (_jsx(Tooltip, { title: "Hide legend", children: _jsx("button", { type: "button", className: classes.closeButton, "aria-label": "Hide legend", onClick: () => {
|
|
204
257
|
onDismiss();
|
|
205
|
-
}, children: "\u00D7" }) })) : null, title ? _jsx("div", { className: classes.topTitle, children: title }) : null,
|
|
258
|
+
}, children: "\u00D7" }) })) : null, title ? _jsx("div", { className: classes.topTitle, children: title }) : null, nonEmpty.map(section => (_jsxs("div", { className: classes.section, children: [multiSection && section.title ? (_jsxs("div", { className: classes.sectionHeader, children: [_jsx("span", { className: classes.sectionTitle, children: section.title }), onDismissSection ? (_jsx(Tooltip, { title: `Hide ${section.title}`, children: _jsx("button", { type: "button", className: classes.sectionClose, "aria-label": `Hide ${section.title}`, onClick: () => {
|
|
206
259
|
onDismissSection(section.id);
|
|
207
|
-
}, children: "\u00D7" }) })) : null] })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick
|
|
260
|
+
}, children: "\u00D7" }) })) : null] })) : null, section.note ? (_jsx("div", { className: classes.sectionNote, children: section.note })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick && section.focusesRows
|
|
208
261
|
? item => {
|
|
209
262
|
onItemClick(item, section);
|
|
210
263
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { observer } from 'mobx-react';
|
|
3
|
+
import { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
|
|
4
|
+
// Lifts an SVG legend / chrome above the inter-region padding masks: a
|
|
5
|
+
// full-track, pointer-events:none <svg> portaled into the TrackContainer overlay
|
|
6
|
+
// node (see TrackOverlayPortal). The SVG counterpart to the HTML overlays
|
|
7
|
+
// (FloatingLegend, HicOverlayPanel) — used where the same legend
|
|
8
|
+
// markup is also drawn by an SVG-export path, so it must stay <svg>.
|
|
9
|
+
//
|
|
10
|
+
// Children draw in <svg> user space; the wrapping <svg> stays pointer-events:none
|
|
11
|
+
// so it doesn't eat canvas events, so any interactive sub-content (a dismiss
|
|
12
|
+
// "×", clickable swatches) must set pointer-events:auto on its own element.
|
|
13
|
+
// fallbackInline={false}: outside a TrackContainer it renders nothing, matching
|
|
14
|
+
// callers whose export path draws its own inline legend.
|
|
15
|
+
export const FloatingSvgOverlay = observer(function FloatingSvgOverlay({ width, height, children, }) {
|
|
16
|
+
return (_jsx(TrackOverlayPortal, { fallbackInline: false, children: _jsx("svg", { style: {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: 0,
|
|
19
|
+
left: 0,
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
pointerEvents: 'none',
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
}, children: children }) }));
|
|
25
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CSSProperties, ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Four offset shadows in the surface colour: a halo under a line of DOM text
|
|
4
|
+
* that needs no measured stroke and reads the same over a mark and over the
|
|
5
|
+
* background.
|
|
6
|
+
*/
|
|
7
|
+
export declare function textHalo(color: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Where a line box's glyph baseline sits below its top, as a fraction of the
|
|
10
|
+
* font size. A DOM label is positioned by its top and an exported `<text>` by
|
|
11
|
+
* its baseline, and this is what puts the two on one line.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TEXT_BASELINE_RATIO = 0.84;
|
|
14
|
+
/**
|
|
15
|
+
* One line of text floated over a display, at the px `x` and `y` of its line
|
|
16
|
+
* box's top-left corner: absolutely positioned, one line high, never wrapped.
|
|
17
|
+
* The canvas feature labels and the mark display's text marks emit through
|
|
18
|
+
* this, so the typography of a floated label is one declaration; what places
|
|
19
|
+
* each label is its display's own rule. `halo` draws {@link textHalo} under
|
|
20
|
+
* the glyphs in that colour. Everything else a caller passes reaches the div:
|
|
21
|
+
* a class for the pointer and the cursor, the data attributes a delegated
|
|
22
|
+
* handler reads, a `title`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function FloatingText({ x, y, color, fontSize, halo, style, children, ...rest }: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
color: string;
|
|
28
|
+
fontSize: number;
|
|
29
|
+
halo?: string;
|
|
30
|
+
style?: CSSProperties;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
} & Omit<ComponentPropsWithoutRef<'div'>, 'style' | 'color' | 'children'>): import("react").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* The same line in an SVG export: two `<text>`s at the baseline `y`, a stroke
|
|
35
|
+
* in the surface colour under the glyphs, since SVG paints stroke over fill
|
|
36
|
+
* and a thick stroke beneath a second copy is how a halo is drawn there. Both
|
|
37
|
+
* carry the size and family explicitly, because a `<text>` with neither takes
|
|
38
|
+
* SVG's 16px default in a saved file.
|
|
39
|
+
*/
|
|
40
|
+
export declare function SvgHaloText({ x, y, fill, halo, haloWidth, fontSize, fontFamily, anchor, children, }: {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
fill: string;
|
|
44
|
+
halo: string;
|
|
45
|
+
haloWidth: number | string;
|
|
46
|
+
fontSize: number;
|
|
47
|
+
fontFamily: string;
|
|
48
|
+
anchor?: 'start' | 'middle' | 'end';
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Four offset shadows in the surface colour: a halo under a line of DOM text
|
|
4
|
+
* that needs no measured stroke and reads the same over a mark and over the
|
|
5
|
+
* background.
|
|
6
|
+
*/
|
|
7
|
+
export function textHalo(color) {
|
|
8
|
+
return `-1px -1px 0 ${color}, 1px -1px 0 ${color}, -1px 1px 0 ${color}, 1px 1px 0 ${color}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Where a line box's glyph baseline sits below its top, as a fraction of the
|
|
12
|
+
* font size. A DOM label is positioned by its top and an exported `<text>` by
|
|
13
|
+
* its baseline, and this is what puts the two on one line.
|
|
14
|
+
*/
|
|
15
|
+
export const TEXT_BASELINE_RATIO = 0.84;
|
|
16
|
+
/**
|
|
17
|
+
* One line of text floated over a display, at the px `x` and `y` of its line
|
|
18
|
+
* box's top-left corner: absolutely positioned, one line high, never wrapped.
|
|
19
|
+
* The canvas feature labels and the mark display's text marks emit through
|
|
20
|
+
* this, so the typography of a floated label is one declaration; what places
|
|
21
|
+
* each label is its display's own rule. `halo` draws {@link textHalo} under
|
|
22
|
+
* the glyphs in that colour. Everything else a caller passes reaches the div:
|
|
23
|
+
* a class for the pointer and the cursor, the data attributes a delegated
|
|
24
|
+
* handler reads, a `title`.
|
|
25
|
+
*/
|
|
26
|
+
export function FloatingText({ x, y, color, fontSize, halo, style, children, ...rest }) {
|
|
27
|
+
return (_jsx("div", { ...rest, style: {
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
lineHeight: 1,
|
|
30
|
+
whiteSpace: 'nowrap',
|
|
31
|
+
color,
|
|
32
|
+
fontSize,
|
|
33
|
+
transform: `translate(${x}px, ${y}px)`,
|
|
34
|
+
...(halo ? { textShadow: textHalo(halo) } : {}),
|
|
35
|
+
...style,
|
|
36
|
+
}, children: children }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The same line in an SVG export: two `<text>`s at the baseline `y`, a stroke
|
|
40
|
+
* in the surface colour under the glyphs, since SVG paints stroke over fill
|
|
41
|
+
* and a thick stroke beneath a second copy is how a halo is drawn there. Both
|
|
42
|
+
* carry the size and family explicitly, because a `<text>` with neither takes
|
|
43
|
+
* SVG's 16px default in a saved file.
|
|
44
|
+
*/
|
|
45
|
+
export function SvgHaloText({ x, y, fill, halo, haloWidth, fontSize, fontFamily, anchor = 'start', children, }) {
|
|
46
|
+
const attrs = { x, y, fontSize, fontFamily, textAnchor: anchor };
|
|
47
|
+
return (_jsxs(_Fragment, { children: [_jsx("text", { ...attrs, stroke: halo, strokeWidth: haloWidth, strokeLinejoin: "round", children: children }), _jsx("text", { ...attrs, fill: fill, children: children })] }));
|
|
48
|
+
}
|