@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/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
|
|
@@ -87,6 +90,13 @@ const useStyles = makeStyles()(theme => ({
|
|
|
87
90
|
overflow: 'hidden',
|
|
88
91
|
textOverflow: 'ellipsis',
|
|
89
92
|
},
|
|
93
|
+
// Wraps where every other line ellipsizes: a note is a sentence, and half of
|
|
94
|
+
// one says nothing.
|
|
95
|
+
sectionNote: {
|
|
96
|
+
color: theme.palette.text.secondary,
|
|
97
|
+
whiteSpace: 'normal',
|
|
98
|
+
marginBottom: 2,
|
|
99
|
+
},
|
|
90
100
|
sectionClose: {
|
|
91
101
|
font: 'inherit',
|
|
92
102
|
lineHeight: 1,
|
|
@@ -134,6 +144,28 @@ const useStyles = makeStyles()(theme => ({
|
|
|
134
144
|
overflow: 'hidden',
|
|
135
145
|
textOverflow: 'ellipsis',
|
|
136
146
|
},
|
|
147
|
+
hiddenItem: {
|
|
148
|
+
opacity: 0.4,
|
|
149
|
+
textDecoration: 'line-through',
|
|
150
|
+
},
|
|
151
|
+
gradientRow: {
|
|
152
|
+
marginBottom: 1,
|
|
153
|
+
},
|
|
154
|
+
gradientBar: {
|
|
155
|
+
width: GRADIENT_BAR_WIDTH,
|
|
156
|
+
height: 10,
|
|
157
|
+
borderRadius: 2,
|
|
158
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
159
|
+
// clip the gradient to the padding box so it doesn't paint under the
|
|
160
|
+
// translucent divider border — otherwise the border composites over the
|
|
161
|
+
// gradient's end and the edge renders a dark sliver
|
|
162
|
+
backgroundClip: 'padding-box',
|
|
163
|
+
},
|
|
164
|
+
gradientLabels: {
|
|
165
|
+
display: 'flex',
|
|
166
|
+
justifyContent: 'space-between',
|
|
167
|
+
width: GRADIENT_BAR_WIDTH,
|
|
168
|
+
},
|
|
137
169
|
// was `<Link component="button" underline="hover">`, which is the same
|
|
138
170
|
// element with Material's typography on it — see `closeButton` above for why
|
|
139
171
|
// that mattered
|
|
@@ -146,7 +178,7 @@ const useStyles = makeStyles()(theme => ({
|
|
|
146
178
|
border: 0,
|
|
147
179
|
background: 'none',
|
|
148
180
|
textAlign: 'left',
|
|
149
|
-
color: theme.palette.
|
|
181
|
+
color: theme.palette.accent,
|
|
150
182
|
cursor: 'pointer',
|
|
151
183
|
'&:hover': {
|
|
152
184
|
textDecoration: 'underline',
|
|
@@ -155,56 +187,88 @@ const useStyles = makeStyles()(theme => ({
|
|
|
155
187
|
}));
|
|
156
188
|
const DEFAULT_MAX_ITEMS = 12;
|
|
157
189
|
const DEFAULT_MAX_WIDTH = 200;
|
|
158
|
-
|
|
159
|
-
//
|
|
190
|
+
const GRADIENT_BAR_WIDTH = 100;
|
|
191
|
+
// Side of one swatch box.
|
|
160
192
|
const SWATCH = 12;
|
|
193
|
+
// A ramp row: the caption, the bar and the domain ends. The bar is a CSS
|
|
194
|
+
// gradient over the same stops the export's <linearGradient> takes, so the
|
|
195
|
+
// two draw one ramp. Stops are flattened over the paper because a translucent
|
|
196
|
+
// first stop (Hi-C's juicebox fade) rasterizes to a dark sliver at the bar's
|
|
197
|
+
// left edge.
|
|
198
|
+
function gradientCss({ stops }, paper) {
|
|
199
|
+
const list = stops.map(({ color, opacity = 1, offset }) => {
|
|
200
|
+
const flat = opacity >= 1
|
|
201
|
+
? color
|
|
202
|
+
: `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, ${paper})`;
|
|
203
|
+
return `${flat} ${offset * 100}%`;
|
|
204
|
+
});
|
|
205
|
+
return `linear-gradient(to right, ${list.join(', ')})`;
|
|
206
|
+
}
|
|
207
|
+
function GradientRow({ label, gradient, }) {
|
|
208
|
+
const { classes, theme } = useStyles();
|
|
209
|
+
return (_jsxs("div", { className: classes.gradientRow, children: [label ? _jsx("div", { className: classes.label, children: label }) : null, _jsx("div", { className: classes.gradientBar,
|
|
210
|
+
// backgroundImage (longhand), not the `background` shorthand: the
|
|
211
|
+
// shorthand resets background-clip back to border-box, undoing the
|
|
212
|
+
// padding-box clip
|
|
213
|
+
style: {
|
|
214
|
+
backgroundImage: gradientCss(gradient, theme.palette.background.paper),
|
|
215
|
+
} }), _jsxs("div", { className: classes.gradientLabels, children: [_jsx("span", { children: gradient.minLabel }), _jsx("span", { children: gradient.maxLabel })] })] }));
|
|
216
|
+
}
|
|
161
217
|
// One list of swatches with its own independent collapse state, so each section
|
|
162
218
|
// in a multi-section legend expands/collapses on its own.
|
|
163
219
|
const LegendItemList = observer(function LegendItemList({ items, maxItems, onItemClick, }) {
|
|
164
220
|
const { classes } = useStyles();
|
|
165
221
|
const [expanded, setExpanded] = useState(false);
|
|
166
|
-
|
|
222
|
+
// A toggle revealing one row costs the row it saves.
|
|
223
|
+
const collapsible = items.length > maxItems + 1;
|
|
167
224
|
const shown = collapsible && !expanded ? items.slice(0, maxItems) : items;
|
|
168
225
|
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));
|
|
226
|
+
// Every row reserves the widest row's swatch column, collapsed or not, so
|
|
227
|
+
// labels stay in one vertical line and do not shift on expanding.
|
|
228
|
+
const columns = Math.max(1, ...items.map(i => legendSwatches(i).length));
|
|
174
229
|
return (_jsxs(_Fragment, { children: [shown.map((item, i) => {
|
|
175
|
-
|
|
176
|
-
|
|
230
|
+
if (item.gradient) {
|
|
231
|
+
return (_jsx(GradientRow
|
|
232
|
+
// eslint-disable-next-line @eslint-react/no-array-index-key
|
|
233
|
+
, { label: item.label, gradient: item.gradient }, `${item.label}-${i}`));
|
|
234
|
+
}
|
|
235
|
+
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 })] }));
|
|
236
|
+
const itemClass = cx(classes.item, item.hidden && classes.hiddenItem);
|
|
237
|
+
return onItemClick ? (_jsx("button", { type: "button", className: cx(itemClass, classes.itemButton), title: "Show only these rows", onClick: () => {
|
|
177
238
|
onItemClick(item);
|
|
178
|
-
}, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className:
|
|
239
|
+
}, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className: itemClass, children: row }, `${item.label}-${i}`));
|
|
179
240
|
}), collapsible ? (_jsx("button", { type: "button", className: classes.toggle, onClick: () => {
|
|
180
241
|
setExpanded(!expanded);
|
|
181
242
|
}, children: expanded ? 'Show less' : `Show ${hiddenCount} more…` })) : null] }));
|
|
182
243
|
});
|
|
183
|
-
// Floating color-key overlay
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// is a heading for the whole box, shown regardless of section count.
|
|
244
|
+
// Floating color-key overlay: one panel per section (e.g. genotype colors vs.
|
|
245
|
+
// sample-grouping colors on the multi-sample variant display). Section titles
|
|
246
|
+
// and per-section close buttons only appear when there is more than one
|
|
247
|
+
// section, so a single-scheme legend is just its rows. `title` is a heading
|
|
248
|
+
// for the whole box, shown regardless of section count.
|
|
189
249
|
//
|
|
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, }) {
|
|
250
|
+
// `onItemClick` makes the rows of a section declaring `focusesRows` act —
|
|
251
|
+
// clicking an entry focuses the rows it names. A section coloring features or
|
|
252
|
+
// cells stays inert text beside it.
|
|
253
|
+
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
254
|
const { classes } = useStyles();
|
|
196
|
-
const nonEmpty = nonEmptyLegendSections(
|
|
197
|
-
if (nonEmpty.length === 0
|
|
255
|
+
const nonEmpty = nonEmptyLegendSections(sections);
|
|
256
|
+
if (nonEmpty.length === 0) {
|
|
198
257
|
return null;
|
|
199
258
|
}
|
|
200
259
|
const multiSection = nonEmpty.length > 1;
|
|
201
260
|
// portal above the inter-region padding masks so the key isn't buried at
|
|
202
261
|
// whole-genome / multi-region scale (see TrackOverlayPortal)
|
|
203
|
-
return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: {
|
|
262
|
+
return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: {
|
|
263
|
+
top,
|
|
264
|
+
right,
|
|
265
|
+
maxWidth,
|
|
266
|
+
maxHeight: `calc(100% - ${top + 4}px)`,
|
|
267
|
+
}, "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
268
|
onDismiss();
|
|
205
|
-
}, children: "\u00D7" }) })) : null, title ? _jsx("div", { className: classes.topTitle, children: title }) : null,
|
|
269
|
+
}, 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
270
|
onDismissSection(section.id);
|
|
207
|
-
}, children: "\u00D7" }) })) : null] })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick
|
|
271
|
+
}, 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
272
|
? item => {
|
|
209
273
|
onItemClick(item, section);
|
|
210
274
|
}
|
|
@@ -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
|
+
}
|