@jbrowse/display-ui 5.0.0-beta.8 → 5.0.0-beta.9

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 (49) hide show
  1. package/README.md +24 -14
  2. package/esm/AxisCaption.d.ts +24 -0
  3. package/esm/AxisCaption.js +57 -0
  4. package/esm/AxisGutter.d.ts +3 -3
  5. package/esm/AxisGutter.js +6 -16
  6. package/esm/FloatingLegend.d.ts +0 -1
  7. package/esm/FloatingLegend.js +16 -8
  8. package/esm/ScoreDomainCaption.d.ts +4 -3
  9. package/esm/ScoreDomainCaption.js +6 -5
  10. package/esm/ScoreRules.js +1 -1
  11. package/esm/YScaleBar.js +2 -1
  12. package/esm/YScaleBarOverlay.js +2 -3
  13. package/esm/axisPlacement.d.ts +23 -4
  14. package/esm/axisPlacement.js +42 -7
  15. package/esm/bottomRightCorner.d.ts +3 -4
  16. package/esm/bottomRightCorner.js +3 -4
  17. package/esm/chromeOverlayContext.d.ts +1 -1
  18. package/esm/chromeOverlayContext.js +4 -5
  19. package/esm/chromeOverlays.d.ts +4 -2
  20. package/esm/embed/index.d.ts +70 -0
  21. package/esm/embed/index.js +69 -0
  22. package/esm/embed/legend.d.ts +8 -0
  23. package/esm/embed/legend.js +35 -0
  24. package/esm/embed/location.d.ts +17 -0
  25. package/esm/embed/location.js +65 -0
  26. package/esm/embed/provider.d.ts +11 -0
  27. package/esm/embed/provider.js +6 -0
  28. package/esm/embed/regions.d.ts +53 -0
  29. package/esm/embed/regions.js +95 -0
  30. package/esm/floatingLegendTop.d.ts +1 -0
  31. package/esm/floatingLegendTop.js +2 -0
  32. package/esm/index.d.ts +8 -6
  33. package/esm/index.js +7 -5
  34. package/esm/plainChromeOverlays.d.ts +1 -1
  35. package/esm/plainChromeOverlays.js +5 -5
  36. package/esm/tooltip/Tooltip.d.ts +3 -3
  37. package/esm/tooltip/Tooltip.js +3 -3
  38. package/esm/trackControl/trackControlContext.d.ts +1 -1
  39. package/esm/trackControl/trackControlContext.js +6 -6
  40. package/esm/trackOverlay/TrackOverlaySlot.d.ts +2 -2
  41. package/esm/trackOverlay/TrackOverlaySlot.js +2 -2
  42. package/esm/valueScale.d.ts +22 -2
  43. package/esm/yAxisConstants.d.ts +6 -0
  44. package/esm/yAxisConstants.js +14 -0
  45. package/esm/yScaleTicks.d.ts +2 -1
  46. package/esm/yScaleTicks.js +2 -1
  47. package/package.json +7 -4
  48. package/esm/formatScore.d.ts +0 -1
  49. package/esm/formatScore.js +0 -14
package/README.md CHANGED
@@ -36,7 +36,7 @@ above them.
36
36
 
37
37
  | | |
38
38
  | ------------------------------------------- | --------------------------------------------------------------------------------------- |
39
- | `DisplayChromeOverlays` | the five `displayPhase` states, as a component set, with the model shape each is handed |
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). That is the _reason_ to make it structural rather than to leave
93
- it — nothing on either side is aware of the constraint, so the first display to
94
- want both would find the bug, and it presents as a status message that silently
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 five `displayPhase` states drawn with no UI toolkit: no theme provider to
212
- mount, no emotion in the host page, and nothing that reads as a stray Material
213
- widget inside someone else's design system.
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. This is the same trigger pattern MUI's own
292
- `Tooltip` uses, which is what the rest of JBrowse's chrome is built on; the
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 silently wrong in every other, and the failure — chrome painted under a mask
370
- — is invisible until someone looks at the right zoom.
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
@@ -451,7 +449,10 @@ the `[min, max]` caption and no axis, as a scale whose bands are too short for
451
449
  one does. `side` and `left` are which of the band's edges the display's own
452
450
  panels leave clear for a gutter: `right` where a group label chip takes the
453
451
  left, `left: n` where a dendrogram takes the first `n` px. `caption` is what the
454
- scale measures (`TLEN`), as a colour scale's `field` is.
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, which the chrome draws across every
455
+ band.
455
456
 
456
457
  A member describes the scale or the band it rules, never the axis — not an
457
458
  orientation, a form, a gutter width or a font. The chrome reads a member to keep
@@ -460,4 +461,13 @@ off what the display put in the band, never to choose between two drawings of it
460
461
 
461
462
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/valueScale.ts)
462
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
+
463
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
+ }
@@ -2,10 +2,10 @@ import type { YAxis } from './valueScale.ts';
2
2
  /**
3
3
  * One band's axis drawn inside its gutter, the origin at the gutter's left
4
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, and
6
- * the caption, where the scale has one, rotated along the outer edge. The
5
+ * axis, its left for a right-side one — with the numbers growing outward. The
7
6
  * on-screen overlay and the export both draw through this, so the two cannot
8
- * place a spine differently.
7
+ * place a spine differently. The scale's caption is `AxisCaption`'s, drawn once
8
+ * for the scale and not per band.
9
9
  */
10
10
  export default function AxisGutter({ axis }: {
11
11
  axis: YAxis;
package/esm/AxisGutter.js CHANGED
@@ -1,25 +1,15 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { usePalette } from '@jbrowse/core/ui/PaletteContext';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import YScaleBar from './YScaleBar.js';
4
- import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
5
- // Half the rotated caption's own 10px height plus a px of margin: how far in
6
- // from the gutter's outer edge it sits with its glyphs still inside.
7
- const CAPTION_INSET_PX = 8;
3
+ import { axisGutterWidth } from './axisPlacement.js';
8
4
  /**
9
5
  * One band's axis drawn inside its gutter, the origin at the gutter's left
10
6
  * edge: the spine on the gutter's inner edge — its right for a left-side
11
- * axis, its left for a right-side one — with the numbers growing outward, and
12
- * the caption, where the scale has one, rotated along the outer edge. The
7
+ * axis, its left for a right-side one — with the numbers growing outward. The
13
8
  * on-screen overlay and the export both draw through this, so the two cannot
14
- * place a spine differently.
9
+ * place a spine differently. The scale's caption is `AxisCaption`'s, drawn once
10
+ * for the scale and not per band.
15
11
  */
16
12
  export default function AxisGutter({ axis }) {
17
- const palette = usePalette();
18
13
  const right = axis.side === 'right';
19
- const { ticks, caption, height } = axis;
20
- const captionX = right
21
- ? AXIS_GUTTER_WIDTH_PX - CAPTION_INSET_PX
22
- : CAPTION_INSET_PX;
23
- const midY = (ticks.yTop + ticks.yBottom) / 2;
24
- return (_jsxs(_Fragment, { children: [_jsx("g", { transform: `translate(${right ? 0 : AXIS_GUTTER_WIDTH_PX} 0)`, children: _jsx(YScaleBar, { ticks: ticks, orientation: right ? 'right' : 'left', bandHeight: height }) }), caption ? (_jsx("text", { x: captionX, y: midY, fontSize: 10, textAnchor: "middle", fill: palette.text.primary, stroke: palette.background.default, strokeWidth: 2.5, paintOrder: "stroke", transform: `rotate(-90, ${captionX}, ${midY})`, children: caption })) : null] }));
14
+ return (_jsx("g", { transform: `translate(${right ? 0 : axisGutterWidth(axis)} 0)`, children: _jsx(YScaleBar, { ticks: axis.ticks, orientation: right ? 'right' : 'left', bandHeight: axis.height }) }));
25
15
  }
@@ -1,5 +1,4 @@
1
1
  import type { LegendItem, LegendSection } from '@jbrowse/core/ui/legendSpec';
2
- export declare const FLOATING_LEGEND_TOP_PX = 10;
3
2
  export type { LegendGradient, LegendItem, LegendSection, LegendSpec, LegendSwatch, } from '@jbrowse/core/ui/legendSpec';
4
3
  declare const FloatingLegend: ({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems, maxWidth, top, }: {
5
4
  items?: LegendItem[];
@@ -8,10 +8,9 @@ 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_TOP_PX } from './floatingLegendTop.js';
11
12
  import Tooltip from './tooltip/Tooltip.js';
12
13
  import { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
13
- // The key's own inset from the display's top-right corner.
14
- export const FLOATING_LEGEND_TOP_PX = 10;
15
14
  const useStyles = makeStyles()(theme => ({
16
15
  legend: {
17
16
  position: 'absolute',
@@ -27,6 +26,9 @@ const useStyles = makeStyles()(theme => ({
27
26
  border: `1px solid ${theme.palette.divider}`,
28
27
  borderRadius: 3,
29
28
  padding: 3,
29
+ // Two keys on one short display ran past its bottom edge and were cut
30
+ // there; the box scrolls inside the display instead.
31
+ overflowY: 'auto',
30
32
  fontSize: 10,
31
33
  zIndex: 100,
32
34
  // the default; `maxWidth` overrides it per display
@@ -89,6 +91,13 @@ const useStyles = makeStyles()(theme => ({
89
91
  overflow: 'hidden',
90
92
  textOverflow: 'ellipsis',
91
93
  },
94
+ // Wraps where every other line ellipsizes: a note is a sentence, and half of
95
+ // one says nothing.
96
+ sectionNote: {
97
+ color: theme.palette.text.secondary,
98
+ whiteSpace: 'normal',
99
+ marginBottom: 2,
100
+ },
92
101
  sectionClose: {
93
102
  font: 'inherit',
94
103
  lineHeight: 1,
@@ -236,10 +245,9 @@ const LegendItemList = observer(function LegendItemList({ items, maxItems, onIte
236
245
  // is more than one section, so a single-scheme legend looks unchanged. `title`
237
246
  // is a heading for the whole box, shown regardless of section count.
238
247
  //
239
- // `onItemClick` makes the rows act — for a key whose entries name groups of
240
- // rows, clicking one focuses those rows. It is handed the section too, so a
241
- // display with several vocabularies can act on the one that names rows and
242
- // leave the others inert.
248
+ // `onItemClick` makes the rows of a section declaring `focusesRows` act —
249
+ // clicking an entry focuses the rows it names. A section coloring features or
250
+ // cells stays inert text beside it.
243
251
  const FloatingLegend = observer(function FloatingLegend({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top = FLOATING_LEGEND_TOP_PX, }) {
244
252
  const { classes } = useStyles();
245
253
  const nonEmpty = nonEmptyLegendSections({ items, sections });
@@ -249,11 +257,11 @@ const FloatingLegend = observer(function FloatingLegend({ items, sections, title
249
257
  const multiSection = nonEmpty.length > 1;
250
258
  // portal above the inter-region padding masks so the key isn't buried at
251
259
  // whole-genome / multi-region scale (see TrackOverlayPortal)
252
- return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: { top, maxWidth }, "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: () => {
260
+ return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: { top, maxWidth, maxHeight: `calc(100% - ${top + 4}px)` }, "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: () => {
253
261
  onDismiss();
254
262
  }, 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: () => {
255
263
  onDismissSection(section.id);
256
- }, children: "\u00D7" }) })) : null] })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick
264
+ }, 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
257
265
  ? item => {
258
266
  onItemClick(item, section);
259
267
  }
@@ -1,14 +1,15 @@
1
- export declare const SCORE_CAPTION_HEIGHT = 16;
2
1
  /**
3
2
  * The one-line `[min, max]` caption that stands in for a y axis where none can
4
3
  * be drawn — a scale whose bands are too short for tick labels, and a density
5
4
  * track whose rows each carry their own colour, so no single ramp describes
6
5
  * them. An axis stand-in rather than a key, which is why it is not a colour
7
6
  * scale and draws whether or not the legend does. Right-aligned to
8
- * `canvasWidth` at the top of the display.
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
9
  */
10
- export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, }: {
10
+ export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, caption, }: {
11
11
  domain: [number, number];
12
12
  scaleType: string;
13
13
  canvasWidth: number;
14
+ caption?: string;
14
15
  }): import("react").JSX.Element;
@@ -1,21 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
3
  import { measureLegendText } from '@jbrowse/core/ui/measureLegendText';
4
+ import { formatScore } from '@jbrowse/core/util/numericUtils';
4
5
  import { stripAlpha } from '@jbrowse/core/util/svgColorProps';
5
- import { formatScore } from './formatScore.js';
6
- export const SCORE_CAPTION_HEIGHT = 16;
6
+ import { SCORE_CAPTION_HEIGHT } from './yAxisConstants.js';
7
7
  /**
8
8
  * The one-line `[min, max]` caption that stands in for a y axis where none can
9
9
  * be drawn — a scale whose bands are too short for tick labels, and a density
10
10
  * track whose rows each carry their own colour, so no single ramp describes
11
11
  * them. An axis stand-in rather than a key, which is why it is not a colour
12
12
  * scale and draws whether or not the legend does. Right-aligned to
13
- * `canvasWidth` at the top of the display.
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.
14
15
  */
15
- export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, }) {
16
+ export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, caption, }) {
16
17
  const palette = usePalette();
17
18
  const suffix = scaleType === 'log' ? ' (log)' : scaleType === 'symlog' ? ' (symlog)' : '';
18
- const text = `[${formatScore(domain[0])}, ${formatScore(domain[1])}]${suffix}`;
19
+ const text = `${caption ? `${caption} ` : ''}[${formatScore(domain[0])}, ${formatScore(domain[1])}]${suffix}`;
19
20
  const len = measureLegendText(text, 12);
20
21
  const xpos = Math.max(0, canvasWidth - len - 10);
21
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 })] }));
package/esm/ScoreRules.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
- const DEFAULT_RULE_COLOR = 'rgb(120,120,120)';
3
+ import { DEFAULT_RULE_COLOR } from './yAxisConstants.js';
4
4
  // Bare marks, no wrapping <svg>, so the on-screen overlay and the SVG export
5
5
  // draw the same elements from the same y. Same split as CrossHatchLines.
6
6
  export function ScoreRuleLines({ marks, width, offsetY = 0, }) {
package/esm/YScaleBar.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
+ import { AXIS_FONT_PX, TICK_LABEL_X_PX } from './yAxisConstants.js';
3
4
  import { clampStrokeInsideAxis } from './yScaleTicks.js';
4
5
  // Not an `observer`, and deliberately: there is nothing here to observe. Every
5
6
  // caller already reads `model.ticks` inside its own observer and hands the
@@ -42,5 +43,5 @@ export default function YScaleBar({ ticks, orientation, bandHeight, }) {
42
43
  const inset = Math.min(Math.max(sy, LABEL_HALF_HEIGHT_PX), bandHeight - LABEL_HALF_HEIGHT_PX);
43
44
  return inset === sy ? undefined : inset - sy;
44
45
  };
45
- return (_jsxs("g", { fontSize: 10, 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 * 9, children: label ?? value })] }, `${value}-${y}`)))] }));
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}`)))] }));
46
47
  }
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { VERTICAL_SCROLLBAR_CLEARANCE } from '@jbrowse/core/ui/VerticalScrollbar';
3
3
  import AxisGutter from './AxisGutter.js';
4
- import { axisGutterLeft } from './axisPlacement.js';
5
- import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
4
+ import { axisGutterLeft, axisGutterWidth } from './axisPlacement.js';
6
5
  /**
7
6
  * The on-screen axis of one band: an `<svg>` the width of the gutter,
8
7
  * positioned absolutely over the band at `top`, with `AxisGutter` inside. A
@@ -15,7 +14,7 @@ export default function YScaleBarOverlay({ axis, top, width, }) {
15
14
  left: axisGutterLeft(axis, width, VERTICAL_SCROLLBAR_CLEARANCE),
16
15
  pointerEvents: 'none',
17
16
  height: axis.height,
18
- width: AXIS_GUTTER_WIDTH_PX,
17
+ width: axisGutterWidth(axis),
19
18
  overflow: 'visible',
20
19
  }, children: _jsx(AxisGutter, { axis: axis }) }));
21
20
  }
@@ -1,4 +1,4 @@
1
- import type { ValueScale } from './valueScale.ts';
1
+ import type { ValueScale, YAxis } from './valueScale.ts';
2
2
  /**
3
3
  * Below this band height a full `YScaleBar`'s tick labels overlap each other,
4
4
  * so the chrome draws one `ScoreDomainCaption` for the scale instead of an
@@ -6,11 +6,29 @@ import type { ValueScale } from './valueScale.ts';
6
6
  * makes 30 the number.
7
7
  */
8
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;
9
14
  /**
10
15
  * Whether a scale gets an axis at all: it rules at least one band, and the
11
16
  * band has room for tick labels. A scale that fails either is captioned.
12
17
  */
13
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;
14
32
  /** How far in from the plot's right edge a right-side gutter ends in an export. */
15
33
  export declare const AXIS_RIGHT_INSET_PX = 4;
16
34
  /**
@@ -19,7 +37,8 @@ export declare const AXIS_RIGHT_INSET_PX = 4;
19
37
  * scrollbar a display may mount on screen; a margin in an export). A left-side
20
38
  * axis takes the gutter at the display's left edge, past whatever panel the
21
39
  * scale's `left` reserves — except in an export, whose margin is the gutter,
22
- * so an axis nothing pushes right sits in the margin with its spine on the
23
- * content edge and its numbers outside the plot.
40
+ * so an axis nothing pushes right sits in the margin with its numbers outside
41
+ * the plot and its spine on the content edge, or past it by what
42
+ * `axisGutterWidth` grew.
24
43
  */
25
- export declare function axisGutterLeft(scale: Pick<ValueScale, 'side' | 'left'>, width: number, rightInset: number, exportContentLeft?: number): number;
44
+ export declare function axisGutterLeft(axis: Pick<YAxis, 'side' | 'left' | 'ticks' | 'caption'>, width: number, rightInset: number, exportContentLeft?: number): number;
@@ -1,3 +1,5 @@
1
+ import { measureText } from '@jbrowse/core/util/measureText';
2
+ import { AXIS_FONT_PX, CAPTION_BAND_PX, TICK_LABEL_X_PX, } from './yAxisConstants.js';
1
3
  import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
2
4
  /**
3
5
  * Below this band height a full `YScaleBar`'s tick labels overlap each other,
@@ -6,12 +8,44 @@ import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
6
8
  * makes 30 the number.
7
9
  */
8
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
+ }
9
18
  /**
10
19
  * Whether a scale gets an axis at all: it rules at least one band, and the
11
20
  * band has room for tick labels. A scale that fails either is captioned.
12
21
  */
13
22
  export function axisDrawn(scale) {
14
- return ((scale.bandTops?.length ?? 1) > 0 && scale.height >= COMPACT_AXIS_HEIGHT);
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));
15
49
  }
16
50
  /** How far in from the plot's right edge a right-side gutter ends in an export. */
17
51
  export const AXIS_RIGHT_INSET_PX = 4;
@@ -21,14 +55,15 @@ export const AXIS_RIGHT_INSET_PX = 4;
21
55
  * scrollbar a display may mount on screen; a margin in an export). A left-side
22
56
  * axis takes the gutter at the display's left edge, past whatever panel the
23
57
  * scale's `left` reserves — except in an export, whose margin is the gutter,
24
- * so an axis nothing pushes right sits in the margin with its spine on the
25
- * content edge and its numbers outside the plot.
58
+ * so an axis nothing pushes right sits in the margin with its numbers outside
59
+ * the plot and its spine on the content edge, or past it by what
60
+ * `axisGutterWidth` grew.
26
61
  */
27
- export function axisGutterLeft(scale, width, rightInset, exportContentLeft) {
28
- if (scale.side === 'right') {
29
- return width - rightInset - AXIS_GUTTER_WIDTH_PX;
62
+ export function axisGutterLeft(axis, width, rightInset, exportContentLeft) {
63
+ if (axis.side === 'right') {
64
+ return width - rightInset - axisGutterWidth(axis);
30
65
  }
31
- const left = scale.left ?? 0;
66
+ const left = axis.left ?? 0;
32
67
  return exportContentLeft !== undefined && left === 0
33
68
  ? exportContentLeft - AXIS_GUTTER_WIDTH_PX
34
69
  : left;
@@ -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