@jbrowse/plugin-linear-genome-view 5.0.0-beta.10 → 5.0.0-beta.13

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.
@@ -234,9 +234,8 @@ export function setupInitAutorun(self) {
234
234
  name: 'LGVInit',
235
235
  // `init` is what makes hasSomethingToShow / awaitingInitNavigation report
236
236
  // "loading"
237
- // until navigation populates displayedRegions, so the alternative gate
238
- // (clearing `init` up front, the way SpreadsheetView does) would flash the
239
- // import form mid-load.
237
+ // until navigation populates displayedRegions, so clearing `init` up front
238
+ // would flash the import form mid-load.
240
239
  ready: () => self.initialized,
241
240
  // No pre-materialization phase to protect: the view is its own single row,
242
241
  // and `error` already derives a failed init assembly declaratively (via
@@ -13,8 +13,11 @@ const useStyles = makeStyles()(theme => ({
13
13
  display: 'flex',
14
14
  alignItems: 'center',
15
15
  gap: theme.spacing(0.5),
16
+ minWidth: 0,
16
17
  },
17
18
  }));
19
+ const MIN_BOX_WIDTH = 100;
20
+ const boxStyle = { margin: 0, minWidth: MIN_BOX_WIDTH };
18
21
  // One row's locate control in a header that stacks several linear genome views:
19
22
  // the compact search box, then the assembly name and the span on screen. The
20
23
  // comparative view and the breakpoint split view each draw one per row, and each
@@ -23,6 +26,6 @@ const useStyles = makeStyles()(theme => ({
23
26
  const HeaderSearchBoxes = observer(function HeaderSearchBoxes({ view, }) {
24
27
  const { classes } = useStyles();
25
28
  const { assemblyDisplayNames, coarseTotalBp } = view;
26
- return (_jsxs("span", { className: classes.searchBox, children: [_jsx(SearchBox, { model: view, showHelp: false, maxWidth: 250, minWidth: 100, style: { margin: 0 } }), _jsxs(Typography, { variant: "body2", color: "text.secondary", className: classes.bp, children: [assemblyDisplayNames.join(','), " ", getBpDisplayStr(coarseTotalBp)] })] }));
29
+ return (_jsxs("span", { className: classes.searchBox, children: [_jsx(SearchBox, { model: view, showHelp: false, maxWidth: 250, minWidth: MIN_BOX_WIDTH, style: boxStyle }), _jsxs(Typography, { variant: "body2", color: "text.secondary", className: classes.bp, children: [assemblyDisplayNames.join(','), " ", getBpDisplayStr(coarseTotalBp)] })] }));
27
30
  });
28
31
  export default HeaderSearchBoxes;
@@ -17,7 +17,7 @@ const Highlight = observer(function Highlight({ model, highlight, }) {
17
17
  const label = getSession(model).highlightLabelsVisible
18
18
  ? highlight.label
19
19
  : undefined;
20
- return coords ? (_jsx(HighlightBand, { coords: coords, background: bandColor.toRgbString(), label: label, children: coords.width >= CHIP_MIN_WIDTH ? (_jsx(HighlightChip, { color: bandColor, label: label, tooltip: highlight.label ?? 'Highlighted region', menuItems: [
20
+ return coords ? (_jsx(HighlightBand, { coords: coords, background: bandColor.toRgbString(), label: label, gaps: model.nonGenomicTrackBands(), children: coords.width >= CHIP_MIN_WIDTH ? (_jsx(HighlightChip, { color: bandColor, label: label, tooltip: highlight.label ?? 'Highlighted region', menuItems: [
21
21
  {
22
22
  label: 'Dismiss highlight',
23
23
  icon: CloseIcon,
@@ -1,9 +1,17 @@
1
- export default function HighlightBand({ coords, background, label, children, }: {
1
+ export declare function gapMask(gaps: {
2
+ top: number;
3
+ height: number;
4
+ }[]): string;
5
+ export default function HighlightBand({ coords, background, label, gaps, children, }: {
2
6
  coords: {
3
7
  left: number;
4
8
  width: number;
5
9
  };
6
10
  background: string;
7
11
  label?: string;
12
+ gaps?: {
13
+ top: number;
14
+ height: number;
15
+ }[];
8
16
  children?: React.ReactNode;
9
17
  }): import("react").JSX.Element;
@@ -24,11 +24,20 @@ const useStyles = makeStyles()(theme => ({
24
24
  color: theme.palette.text.primary,
25
25
  },
26
26
  }));
27
+ export function gapMask(gaps) {
28
+ const stops = gaps.flatMap(g => [
29
+ `#000 ${g.top}px`,
30
+ `transparent ${g.top}px`,
31
+ `transparent ${g.top + g.height}px`,
32
+ `#000 ${g.top + g.height}px`,
33
+ ]);
34
+ return `linear-gradient(to bottom, ${stops.join(', ')})`;
35
+ }
27
36
  // OverviewHighlight stays separate (different bpToPx API). A non-interactive
28
37
  // colored band with an
29
38
  // optional top label; when `children` (the highlight chip) is passed it renders
30
39
  // that instead of the plain label. Visibility is toggled from the view menu
31
- export default function HighlightBand({ coords, background, label, children, }) {
40
+ export default function HighlightBand({ coords, background, label, gaps, children, }) {
32
41
  const { classes } = useStyles();
33
42
  const content = children ??
34
43
  (label ? (_jsx(Typography, { variant: "caption", className: classes.label, noWrap: true, children: label })) : null);
@@ -36,5 +45,8 @@ export default function HighlightBand({ coords, background, label, children, })
36
45
  transform: `translateX(${coords.left}px)`,
37
46
  width: coords.width,
38
47
  background,
48
+ ...(gaps?.length
49
+ ? { maskImage: gapMask(gaps), WebkitMaskImage: gapMask(gaps) }
50
+ : undefined),
39
51
  }, children: coords.width > 3 && content ? (_jsx("div", { className: classes.overlay, children: content })) : null }));
40
52
  }
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy, useEffect, useRef } from 'react';
2
+ import { useEffect, useRef } from 'react';
3
3
  import { VIEW_HEADER_HEIGHT } from '@jbrowse/core/ui';
4
4
  import { getSession } from '@jbrowse/core/util';
5
5
  import { LGV_HEADER_HEIGHT_VAR, SCROLL_PORT_HEIGHT_VAR, VIEW_HEADER_HEIGHT_VAR, useChromeHeightVar, useFocusOnInteraction, } from '@jbrowse/core/util/hooks';
@@ -11,12 +11,11 @@ import { stickyChromeTops } from '../stickyChrome.js';
11
11
  import Header from './Header.js';
12
12
  import MiniControls from './MiniControls.js';
13
13
  import NavigationAnnouncer from './NavigationAnnouncer.js';
14
+ import NoTracksActiveButton from './NoTracksActiveButton.js';
14
15
  import ResizeAllTracksHandle from './ResizeAllTracksHandle.js';
15
16
  import Rubberband from './Rubberband.js';
16
17
  import TrackContainer from './TrackContainer.js';
17
18
  import TracksContainer from './TracksContainer.js';
18
- // lazies
19
- const NoTracksActiveButton = lazy(() => import('./NoTracksActiveButton.js'));
20
19
  const useStyles = makeStyles()(theme => ({
21
20
  header: {
22
21
  background: theme.palette.background.paper,
@@ -104,7 +103,7 @@ const LinearGenomeViewContainer = observer(function LinearGenomeViewContainer({
104
103
  .map(d => d.hoveredFeature)
105
104
  .find(Boolean);
106
105
  session.setHovered({ hoverPosition, hoverFeature });
107
- }, children: [_jsx(NavigationAnnouncer, { model: model }), _jsxs("div", { ref: headerRef, "data-testid": `lgv-header-${model.id}`, className: classes.header, style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(Header, { model: model }), hideHeader ? _jsx(MiniControls, { model: model }) : null] }), _jsx("div", { ref: tracksRef, children: model.scalebarOnly ? (_jsx(Rubberband, { model: model })) : (_jsx(TracksContainer, { model: model, children: !tracks.length ? (_jsx(Suspense, { fallback: null, children: _jsx(NoTracksActiveButton, { model: model }) })) : (_jsxs(_Fragment, { children: [pinnedTracks.length ? (_jsx(Paper, { elevation: 2, "data-testid": "pinned-tracks", className: classes.pinnedTracks, style: {
106
+ }, children: [_jsx(NavigationAnnouncer, { model: model }), _jsxs("div", { ref: headerRef, "data-testid": `lgv-header-${model.id}`, className: classes.header, style: { position: stickyViewHeaders ? 'sticky' : undefined }, children: [_jsx(Header, { model: model }), hideHeader ? _jsx(MiniControls, { model: model }) : null] }), _jsx("div", { ref: tracksRef, children: model.scalebarOnly ? (_jsx(Rubberband, { model: model })) : (_jsx(TracksContainer, { model: model, children: !tracks.length ? (_jsx(NoTracksActiveButton, { model: model })) : (_jsxs(_Fragment, { children: [pinnedTracks.length ? (_jsx(Paper, { elevation: 2, "data-testid": "pinned-tracks", className: classes.pinnedTracks, style: {
108
107
  top: stickyTops.pinnedTracks,
109
108
  // the scroll port this sticks in, not the window: it is
110
109
  // under the app bar in the classic stack, a dockview
@@ -14,19 +14,24 @@ const useStyles = makeStyles()(theme => ({
14
14
  background: {
15
15
  position: 'absolute',
16
16
  right: 0,
17
- background: theme.palette.background.paper,
18
17
  // needed when sticky header is off in lgv, e.g. in breakpoint split view
19
18
  zIndex: 2,
20
19
  },
21
20
  focusedBackground: {
22
- background: alpha(theme.palette.secondary.light, 0.2),
21
+ backgroundColor: alpha(theme.palette.secondary.light, 0.2),
23
22
  },
24
23
  }));
25
24
  const MiniControls = observer(function MiniControls({ model, }) {
26
25
  const { classes } = useStyles();
27
26
  const { id, scalebarOnly } = model;
28
27
  const { focusedViewId } = getSession(model);
29
- return (_jsx(Paper, { className: classes.background, children: _jsxs(Paper, { className: focusedViewId === id ? classes.focusedBackground : undefined, children: [_jsx(Tooltip, { title: "Open track selector", children: _jsx(IconButton, { size: "small", onClick: () => {
28
+ // A ruler-only row is shorter than these controls, so they rise from the
29
+ // ruler's bottom edge rather than hang from its top: a nested row always has
30
+ // chrome above it, while the last row of a stack has only the view's frame
31
+ // below.
32
+ return (_jsx(Paper, { "data-testid": "lgv-mini-controls", className: classes.background, style: scalebarOnly
33
+ ? { top: model.scalebarHeight, transform: 'translateY(-100%)' }
34
+ : undefined, children: _jsxs(Paper, { className: focusedViewId === id ? classes.focusedBackground : undefined, children: [_jsx(Tooltip, { title: "Open track selector", children: _jsx(IconButton, { size: "small", onClick: () => {
30
35
  model.activateTrackSelector();
31
36
  }, children: _jsx(TrackSelectorIcon, { fontSize: "small" }) }) }), _jsx(CascadingMenuButton, { menuItems: () => model.menuItems(), children: _jsx(MoreVertIcon, { fontSize: "small" }) }), _jsx(Tooltip, { title: scalebarOnly ? 'Show tracks' : 'Show ruler only', children: _jsx(IconButton, { size: "small", onClick: () => {
32
37
  model.setScalebarOnly(!scalebarOnly);
@@ -1,6 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { canonicalLocString } from '@jbrowse/core/TextSearch/places';
3
- import { readConfObject } from '@jbrowse/core/configuration';
4
3
  import { SanitizedHTML } from '@jbrowse/core/ui';
5
4
  import { getSession } from '@jbrowse/core/util';
6
5
  import { Button, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, } from '@mui/material';
@@ -12,7 +11,7 @@ const SearchResultsTable = observer(function SearchResultsTable({ searchResults,
12
11
  const assembly = assemblyManager.get(assemblyName);
13
12
  function getTrackName(trackId) {
14
13
  const conf = trackId !== undefined ? session.getTrackById(trackId) : undefined;
15
- return conf ? readConfObject(conf, 'name') : '';
14
+ return conf?.name ?? '';
16
15
  }
17
16
  // A location the assembly can't resolve is listed as-is: Go navigates
18
17
  // through navToOption, which reports its own failure
@@ -13,7 +13,10 @@ const useStyles = makeStyles()(theme => ({
13
13
  root: {
14
14
  // above breakpoint split view
15
15
  zIndex: 200,
16
- background: theme.palette.background.paper,
16
+ ...(theme.palette.mode === 'dark' && {
17
+ outline: `1px solid ${theme.palette.divider}`,
18
+ outlineOffset: -1,
19
+ }),
17
20
  display: 'inline-flex',
18
21
  alignItems: 'center',
19
22
  maxWidth: 'calc(100% - 16px)',
@@ -1698,6 +1698,15 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
1698
1698
  width: number;
1699
1699
  left: number;
1700
1700
  } | undefined;
1701
+ /**
1702
+ * #method
1703
+ * y ranges, from the top of the highlight bands, of tracks whose display
1704
+ * sets `drawsGenomicCoordinates` to false; the bands skip them
1705
+ */
1706
+ nonGenomicTrackBands(): {
1707
+ top: number;
1708
+ height: any;
1709
+ }[];
1701
1710
  /**
1702
1711
  * #method
1703
1712
  * like getHighlightCoords but laid out against the overview scalebar and
@@ -3403,6 +3412,15 @@ export declare function stateModelFactory(pluginManager: PluginManager): import(
3403
3412
  width: number;
3404
3413
  left: number;
3405
3414
  } | undefined;
3415
+ /**
3416
+ * #method
3417
+ * y ranges, from the top of the highlight bands, of tracks whose display
3418
+ * sets `drawsGenomicCoordinates` to false; the bands skip them
3419
+ */
3420
+ nonGenomicTrackBands(): {
3421
+ top: number;
3422
+ height: any;
3423
+ }[];
3406
3424
  /**
3407
3425
  * #method
3408
3426
  * like getHighlightCoords but laid out against the overview scalebar and
@@ -2657,6 +2657,21 @@ export function stateModelFactory(pluginManager) {
2657
2657
  getHighlightCoords(region) {
2658
2658
  return getLayoutHighlightCoords(self, canonicalHighlight(self, region));
2659
2659
  },
2660
+ /**
2661
+ * #method
2662
+ * y ranges, from the top of the highlight bands, of tracks whose display
2663
+ * sets `drawsGenomicCoordinates` to false; the bands skip them
2664
+ */
2665
+ nonGenomicTrackBands() {
2666
+ const y0 = self.headerHeight + self.scalebarHeight;
2667
+ return [...self.pinnedTracks, ...self.unpinnedTracks].flatMap(track => {
2668
+ const display = track.activeDisplay;
2669
+ const top = self.getTrackYOffset(track.configuration.trackId);
2670
+ return display.drawsGenomicCoordinates === false && top !== undefined
2671
+ ? [{ top: top - y0, height: self.trackHeight(track) }]
2672
+ : [];
2673
+ });
2674
+ },
2660
2675
  /**
2661
2676
  * #method
2662
2677
  * like getHighlightCoords but laid out against the overview scalebar and
@@ -34,7 +34,7 @@ export async function renderToSvg(model, opts) {
34
34
  // itself.
35
35
  const { tracks, displayResults, tracksHeight, legendWidth, skippedTracks } = await renderViewTracks({
36
36
  view: model,
37
- opts,
37
+ opts: { ...opts, fontSize },
38
38
  theme,
39
39
  textHeight,
40
40
  trackLabels,
@@ -3,17 +3,16 @@ import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
3
  import { stripAlpha } from '@jbrowse/core/util';
4
4
  import { TRACK_LABEL_GAP, insetLabelBaselineY, offsetLabelBaselineY, } from './util.js';
5
5
  // Where the label sits, per mode. 'left' right-aligns in the gutter
6
- // trackLabelLeftOffset reserved (hence the shared TRACK_LABEL_GAP); the other
7
- // modes hang off the leftmost visible content at `x`, either just above the
8
- // track body ('offset') or inset over it ('overlapping').
6
+ // trackLabelLeftOffset reserved (hence the shared TRACK_LABEL_GAP), beside the
7
+ // first row. The other modes hang off the leftmost visible content
8
+ // at `x`, either just above the track body ('offset') or inset over it
9
+ // ('overlapping').
9
10
  //
10
11
  // Every mode is placed on its alphabetic baseline (the SVG default, so no
11
- // dominantBaseline is emitted at all): 'offset' so its descenders land a known
12
- // distance above the track body — the band it sits in is `textHeight` tall, and
13
- // offsetLabelBaselineY owns that arithmetic — and 'left'/'overlapping' so their
14
- // ascenders stay inside the track box they align with. 'left' used to be pinned
15
- // at a hardcoded y=20 on a hanging baseline, which neither scaled with fontSize
16
- // nor stayed inside a track shorter than ~35px.
12
+ // dominantBaseline is emitted at all): 'offset' so its descenders land a known distance above the track body — the band is
13
+ // `textHeight` tall, and offsetLabelBaselineY owns that arithmetic — and
14
+ // 'left'/'overlapping' so their ascenders stay inside the track box they align
15
+ // with.
17
16
  function labelPosition({ trackLabels, trackLabelOffset, textHeight, fontSize, x, }) {
18
17
  if (trackLabels === 'left') {
19
18
  return {
@@ -17,6 +17,6 @@ export default function SVGTracks({ displayResults, model, textHeight, fontSize,
17
17
  const display = track.displays[0];
18
18
  const currentOffset = offsets[i];
19
19
  const textOffset = labelOffset(track, trackLabels, textHeight);
20
- return (_jsxs("g", { transform: `translate(0 ${currentOffset})`, children: [_jsx(SvgClipRect, { id: `track-clip-${svgNodeId(model)}-${conf.trackId}`, x: -leftBuffer, y: textOffset, width: model.width + trackLabelOffset + leftBuffer + legendWidth, height: display.height, children: _jsxs("g", { transform: `translate(${trackLabelOffset} ${textOffset})`, children: [result, exportsTooLargeNote(display) ? null : (_jsx(SVGRegionSeparators, { model: model, height: display.height }))] }) }), _jsx(SVGTrackLabel, { trackName: trackName, fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabelMode(track, trackLabels), trackLabelOffset: trackLabelOffset, x: x })] }, conf.trackId));
20
+ return (_jsxs("g", { transform: `translate(0 ${currentOffset})`, children: [_jsx(SvgClipRect, { id: `track-clip-${svgNodeId(model)}-${conf.trackId}`, x: -leftBuffer, y: textOffset, width: model.width + trackLabelOffset + leftBuffer + legendWidth, height: display.height, children: _jsxs("g", { transform: `translate(${trackLabelOffset} ${textOffset})`, children: [result, exportsTooLargeNote(display) ? null : (_jsx(SVGRegionSeparators, { model: model, height: display.height }))] }) }), _jsx(SVGTrackLabel, { trackName: trackName, fontSize: fontSize, textHeight: textHeight, trackLabels: trackLabelMode(track, trackLabels), trackLabelOffset: trackLabelOffset, x: x + trackLabelOffset })] }, conf.trackId));
21
21
  }) }));
22
22
  }
@@ -1,4 +1,5 @@
1
1
  import type { ExportSvgOptions, TrackLabelMode } from '../types.ts';
2
+ import type { LgvSvgExportable } from '@jbrowse/display-kit/renderDisplaySvg';
2
3
  import type { ExportSvgDisplayOptions } from '@jbrowse/display-kit/types';
3
4
  import type { ThemeOptions } from '@mui/material';
4
5
  import type { ReactNode } from 'react';
@@ -7,6 +8,7 @@ export interface SvgExportTrack {
7
8
  displays: {
8
9
  height: number;
9
10
  svgLegendWidth?: () => number;
11
+ svgSidebarWidth?: LgvSvgExportable['svgSidebarWidth'];
10
12
  /**
11
13
  * Optional, and the option is the point: SVG export is a substantial extra
12
14
  * implementation for a display type, and a third-party plugin that has not
@@ -125,7 +125,15 @@ const FrozenSvgFigure = memo(function FrozenSvgFigure({ view, snapshot, }) {
125
125
  * question about regions and is true before a track is instantiated (the first
126
126
  * figure then draws a header over nothing, and nothing else ever changes to
127
127
  * correct it); **their heights and minimized flags**, since the geometry is
128
- * derived from them; **the width**, since a resize can widen the canvas without
128
+ * derived from them; **their configs**, since a display's settings are slots on
129
+ * the track config and a shown track's snapshot spells that config as its id;
130
+ * **the legend sections a reader dismissed** and **the per-group state**
131
+ * (hidden sections, collapses, height overrides: `groupStateKey`), which are
132
+ * volatile and in no snapshot; **the reload counter**, since a Retry or a Force
133
+ * load leaves every snapshot as it was while the figure has an error or a
134
+ * too-large note to replace; **the view's coloring toggles** (`colorByCDS`,
135
+ * `showAminoAcids`), named one by one because the view's own snapshot moves
136
+ * every pan frame; **the width**, since a resize can widen the canvas without
129
137
  * moving the locus; **the theme**, since each display bakes colors into its own
130
138
  * bodies, so a figure from the other mode is not stale but unreadable; and **the
131
139
  * highlight bands**, which are drawn in the figure and are the one thing in it a
@@ -133,8 +141,10 @@ const FrozenSvgFigure = memo(function FrozenSvgFigure({ view, snapshot, }) {
133
141
  *
134
142
  * A plugin's own overlay, through `LinearGenomeView-HighlightSVGComponent`, is
135
143
  * state this cannot see, so a change to it shows up at the figure's next
136
- * redraw rather than at once. Subscribing from inside the figure is the thing
137
- * that must not happen — see the note at the top of the file.
144
+ * redraw rather than at once. So does a track's vertical scroll, which moves
145
+ * every wheel frame and has no settled copy to key on. Subscribing from inside
146
+ * the figure is the thing that must not happen — see the note at the top of
147
+ * the file.
138
148
  */
139
149
  function figureKey(view, themeName) {
140
150
  // A closed view is not an error, it is a host doing an ordinary thing in an
@@ -156,9 +166,19 @@ function figureKey(view, themeName) {
156
166
  view.coarseVisibleLocStrings,
157
167
  view.coarseBpPerPx,
158
168
  view.width,
159
- // the snapshot carries pinned, minimized and the display settings a body
160
- // bakes in; the effective height is a getter, so it is read beside it
161
- view.tracks.map(track => [getSnapshot(track), track.displays[0]?.height]),
169
+ view.tracks.map(track => {
170
+ const display = track.displays[0];
171
+ return [
172
+ getSnapshot(track),
173
+ getSnapshot(track.configuration),
174
+ display?.height,
175
+ display?.dismissedLegendSections,
176
+ display?.groupStateKey,
177
+ display?.reloadCounter,
178
+ ];
179
+ }),
180
+ view.colorByCDS,
181
+ view.showAminoAcids,
162
182
  session.getActiveThemeOptions?.(themeName),
163
183
  session.highlightsVisible ? view.highlights : false,
164
184
  session.highlightLabelsVisible,
@@ -1,21 +1,24 @@
1
1
  import type { TrackLabelMode } from '../types.ts';
2
2
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
3
3
  import type { TrackCatalog } from '@jbrowse/core/util';
4
+ import type { LgvSvgExportable } from '@jbrowse/display-kit/renderDisplaySvg';
5
+ import type { ExportTextStyle } from '@jbrowse/display-kit/types';
4
6
  import type { ReactNode } from 'react';
7
+ type SidebarDisplay = Pick<LgvSvgExportable, 'svgSidebarWidth'>;
5
8
  interface TrackHeights {
6
- displays: {
9
+ displays: ({
7
10
  height: number;
8
11
  prefersOffset?: boolean;
9
- }[];
12
+ } & SidebarDisplay)[];
10
13
  }
11
14
  export interface SvgDisplayResult {
12
15
  track: {
13
16
  configuration: AnyConfigurationModel;
14
- displays: {
17
+ displays: ({
15
18
  height: number;
16
19
  regionTooLarge?: boolean;
17
20
  drawsWhenTooLarge?: boolean;
18
- }[];
21
+ } & SidebarDisplay)[];
19
22
  };
20
23
  result: ReactNode;
21
24
  }
@@ -81,18 +84,25 @@ export declare function getRulerLayout(rulerHeight: number): {
81
84
  };
82
85
  export declare const refNameLabelBoxHeight: number;
83
86
  export declare const refNameLabelBaselineY: number;
84
- export declare function trackLabelMode(track: TrackHeights, trackLabels: TrackLabelMode): TrackLabelMode;
87
+ export declare function trackLabelMode(track: {
88
+ displays: ({
89
+ prefersOffset?: boolean;
90
+ } & SidebarDisplay)[];
91
+ }, trackLabels: TrackLabelMode): TrackLabelMode;
85
92
  export declare function labelOffset(track: TrackHeights, trackLabels: TrackLabelMode, textHeight: number): number;
93
+ interface SidebarTrack {
94
+ configuration: AnyConfigurationModel;
95
+ displays: SidebarDisplay[];
96
+ }
86
97
  export declare const TRACK_LABEL_GAP = 40;
87
98
  export declare function trackLabelLeftOffset({ tracks, trackLabels, fontSize, fontFamily, session, }: {
88
- tracks: {
89
- configuration: AnyConfigurationModel;
90
- }[];
99
+ tracks: SidebarTrack[];
91
100
  trackLabels: TrackLabelMode;
92
101
  fontSize: number;
93
102
  fontFamily?: string;
94
103
  session: TrackCatalog;
95
104
  }): number;
105
+ export declare function trackSidebarWidth(track: Pick<SidebarTrack, 'displays'>, text?: ExportTextStyle): number;
96
106
  export declare function trackBoxHeight(track: TrackHeights, trackLabels: TrackLabelMode, textHeight: number): number;
97
107
  export declare function trackBoxOffsets(tracks: TrackHeights[], trackLabels: TrackLabelMode, textHeight: number): number[];
98
108
  export declare function totalHeight(tracks: TrackHeights[], trackLabels: TrackLabelMode, textHeight: number): number;
@@ -179,12 +179,15 @@ export const refNameLabelBaselineY = labelBaselineFromTop(0, REF_NAME_LABEL_FONT
179
179
  // The mode one track's label is drawn in. 'overlapping' yields to 'offset' for a
180
180
  // display that `prefersOffset`, as TrackContainer does on screen: its left edge
181
181
  // carries content — an axis, a sample's genotypes — the label would print over.
182
+ // 'left' yields to 'offset' for a track whose sidebar holds the gutter beside
183
+ // its rows.
182
184
  export function trackLabelMode(track, trackLabels) {
183
- return trackLabels === 'overlapping' && track.displays[0]?.prefersOffset
185
+ return (trackLabels === 'overlapping' && track.displays[0]?.prefersOffset) ||
186
+ (trackLabels === 'left' && trackSidebarWidth(track) > 0)
184
187
  ? 'offset'
185
188
  : trackLabels;
186
189
  }
187
- // space the label pushes a track's body down by; only 'offset' mode does
190
+ // space an 'offset' label pushes a track's body down by
188
191
  export function labelOffset(track, trackLabels, textHeight) {
189
192
  return trackLabelMode(track, trackLabels) === 'offset' ? textHeight : 0;
190
193
  }
@@ -193,9 +196,10 @@ export function labelOffset(track, trackLabels, textHeight) {
193
196
  // so it and trackLabelLeftOffset must agree or the widest name overflows the
194
197
  // gutter it was measured for.
195
198
  export const TRACK_LABEL_GAP = 40;
196
- // Horizontal gutter reserved for 'left' track labels (0 in every other mode).
197
- // Takes an already-minimized-filtered track list, so the reserved width matches
198
- // the labels that actually get drawn.
199
+ // Horizontal gutter left of the track bodies: the widest of each track's
200
+ // sidebar and of each name drawn 'left', plus TRACK_LABEL_GAP. Takes an
201
+ // already-minimized-filtered track list, so the reserved width matches the
202
+ // labels that actually get drawn.
199
203
  //
200
204
  // `fontFamily` is the export's own font option, and it has to reach the ruler
201
205
  // here: the labels are drawn in it (wrapSvgExport puts it on the root <svg>,
@@ -212,11 +216,14 @@ export const TRACK_LABEL_GAP = 40;
212
216
  // `getScalebarRefNameLabels`, an on-screen model getter, so making them
213
217
  // family-aware means threading an export option through screen state.
214
218
  export function trackLabelLeftOffset({ tracks, trackLabels, fontSize, fontFamily, session, }) {
215
- // no labels means no gutter, and that includes having no tracks to label: the
216
- // gap alone would push the whole figure right of a margin nothing is drawn in
217
- return trackLabels === 'left' && tracks.length > 0
218
- ? max(tracks.map(t => measureText(svgTrackName(t, session), fontSize, fontFamily)), 0) + TRACK_LABEL_GAP
219
- : 0;
219
+ return max(tracks.map(t => Math.max(trackSidebarWidth(t, { fontSize, fontFamily }), trackLabelMode(t, trackLabels) === 'left'
220
+ ? measureText(svgTrackName(t, session), fontSize, fontFamily) +
221
+ TRACK_LABEL_GAP
222
+ : 0)), 0);
223
+ }
224
+ // Px of tree and row labels a track's display draws left of its body.
225
+ export function trackSidebarWidth(track, text) {
226
+ return track.displays[0]?.svgSidebarWidth?.(text) ?? 0;
220
227
  }
221
228
  // vertical box a single track occupies. Shared by totalHeight (sum) and
222
229
  // trackBoxOffsets (prefix-sum) so the two can't drift.
@@ -260,6 +260,16 @@ declare function makeStateModel(control: PerRegionTestControl, gate: GateOptIns,
260
260
  error: string;
261
261
  }[];
262
262
  };
263
+ } & {
264
+ readonly plotKeys: string[];
265
+ readonly plot: import("@jbrowse/core/configuration").Plot;
266
+ readonly plotExamples: readonly import("@jbrowse/core/configuration").PlotExample[];
267
+ readonly configDocsUrl: string;
268
+ liftPlot(draft: import("@jbrowse/core/configuration").Plot): import("@jbrowse/core/configuration").AnyConfigurationModel;
269
+ plotProblems(draft: import("@jbrowse/core/configuration").Plot): string[];
270
+ plotWrites(draft: import("@jbrowse/core/configuration").Plot): Record<string, unknown>;
271
+ } & {
272
+ applyPlot(draft: import("@jbrowse/core/configuration").Plot): void;
263
273
  } & {
264
274
  scrollTop: number;
265
275
  } & {
@@ -777,6 +787,16 @@ export declare function createPerRegionTestEnvironment({ measuresBytes, gate, es
777
787
  error: string;
778
788
  }[];
779
789
  };
790
+ } & {
791
+ readonly plotKeys: string[];
792
+ readonly plot: import("@jbrowse/core/configuration").Plot;
793
+ readonly plotExamples: readonly import("@jbrowse/core/configuration").PlotExample[];
794
+ readonly configDocsUrl: string;
795
+ liftPlot(draft: import("@jbrowse/core/configuration").Plot): import("@jbrowse/core/configuration").AnyConfigurationModel;
796
+ plotProblems(draft: import("@jbrowse/core/configuration").Plot): string[];
797
+ plotWrites(draft: import("@jbrowse/core/configuration").Plot): Record<string, unknown>;
798
+ } & {
799
+ applyPlot(draft: import("@jbrowse/core/configuration").Plot): void;
780
800
  } & {
781
801
  scrollTop: number;
782
802
  } & {
@@ -1183,6 +1203,16 @@ export declare function createPerRegionTestEnvironment({ measuresBytes, gate, es
1183
1203
  error: string;
1184
1204
  }[];
1185
1205
  };
1206
+ } & {
1207
+ readonly plotKeys: string[];
1208
+ readonly plot: import("@jbrowse/core/configuration").Plot;
1209
+ readonly plotExamples: readonly import("@jbrowse/core/configuration").PlotExample[];
1210
+ readonly configDocsUrl: string;
1211
+ liftPlot(draft: import("@jbrowse/core/configuration").Plot): import("@jbrowse/core/configuration").AnyConfigurationModel;
1212
+ plotProblems(draft: import("@jbrowse/core/configuration").Plot): string[];
1213
+ plotWrites(draft: import("@jbrowse/core/configuration").Plot): Record<string, unknown>;
1214
+ } & {
1215
+ applyPlot(draft: import("@jbrowse/core/configuration").Plot): void;
1186
1216
  } & {
1187
1217
  scrollTop: number;
1188
1218
  } & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-linear-genome-view",
3
- "version": "5.0.0-beta.10",
3
+ "version": "5.0.0-beta.13",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 linear genome view",
6
6
  "keywords": [
@@ -21,15 +21,15 @@
21
21
  "esm"
22
22
  ],
23
23
  "dependencies": {
24
- "@jbrowse/mobx-state-tree": "^6.6.1",
24
+ "@jbrowse/mobx-state-tree": "^6.7.0",
25
25
  "@mui/icons-material": "^9.4.0",
26
26
  "@mui/material": "^9.4.0",
27
- "mobx": "^7.0.5",
27
+ "mobx": "^7.0.6",
28
28
  "mobx-react": "^10.0.2",
29
- "@jbrowse/core": "^5.0.0-beta.10",
30
- "@jbrowse/display-kit": "^5.0.0-beta.10",
31
- "@jbrowse/render-core": "^5.0.0-beta.10",
32
- "@jbrowse/display-ui": "^5.0.0-beta.10"
29
+ "@jbrowse/core": "^5.0.0-beta.13",
30
+ "@jbrowse/render-core": "^5.0.0-beta.13",
31
+ "@jbrowse/display-kit": "^5.0.0-beta.13",
32
+ "@jbrowse/display-ui": "^5.0.0-beta.13"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": ">=19.0.0",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "sideEffects": false,
42
42
  "devDependencies": {
43
- "@jbrowse/display-test-utils": "^5.0.0-beta.10"
43
+ "@jbrowse/display-test-utils": "^5.0.0-beta.13"
44
44
  },
45
45
  "scripts": {
46
46
  "prebuild": "pnpm clean",