@jbrowse/display-ui 5.0.0-beta.4 → 5.0.0-beta.7
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 +30 -0
- package/esm/AxisGutter.d.ts +12 -0
- package/esm/AxisGutter.js +25 -0
- package/esm/CrossHatches.d.ts +12 -0
- package/esm/CrossHatches.js +31 -0
- package/esm/FloatingLegend.d.ts +3 -4
- package/esm/FloatingLegend.js +56 -7
- package/esm/ScoreDomainCaption.d.ts +14 -0
- package/esm/ScoreDomainCaption.js +22 -0
- package/esm/ScoreRules.d.ts +21 -0
- package/esm/ScoreRules.js +37 -0
- package/esm/YScaleBar.d.ts +6 -0
- package/esm/YScaleBar.js +46 -0
- package/esm/YScaleBarOverlay.d.ts +11 -0
- package/esm/YScaleBarOverlay.js +21 -0
- package/esm/axisPlacement.d.ts +25 -0
- package/esm/axisPlacement.js +35 -0
- package/esm/formatScore.d.ts +1 -0
- package/esm/formatScore.js +14 -0
- package/esm/index.d.ts +14 -1
- package/esm/index.js +11 -1
- package/esm/valueScale.d.ts +50 -0
- package/esm/valueScale.js +1 -0
- package/esm/yAxisConstants.d.ts +1 -0
- package/esm/yAxisConstants.js +5 -0
- package/esm/yScaleTicks.d.ts +48 -0
- package/esm/yScaleTicks.js +46 -0
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -430,4 +430,34 @@ carries position only.
|
|
|
430
430
|
|
|
431
431
|
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/trackControl/useTrackControlMenu.tsx)
|
|
432
432
|
|
|
433
|
+
### ValueScale
|
|
434
|
+
|
|
435
|
+
A value scale a display places its y through, declared so the chrome can derive
|
|
436
|
+
the axis from it — the score axis's counterpart to a colour scale.
|
|
437
|
+
|
|
438
|
+
`domain` is the resolved `[min, max]`; `height` the band it rules and `offset`
|
|
439
|
+
the inset of the plot box inside that band (`axisPlotBox(height, offset)`);
|
|
440
|
+
`minimalTicks` and `symlogConstant` how the ticks are chosen. `ticks` is the
|
|
441
|
+
display's own ladder where the band's arithmetic is its own (the coverage band's
|
|
442
|
+
octaves against the shader's box, the read cloud's decades against the arc
|
|
443
|
+
geometry); `ScoreScaleMixin` derives one through `computeYTicks` otherwise.
|
|
444
|
+
|
|
445
|
+
`bandTops` is the screen y of each band the scale rules — one at 0 unless the
|
|
446
|
+
display stacks the same plot: the multi-wiggle's rows, a grouped alignments
|
|
447
|
+
track's coverage band per group, each projected through the display's own
|
|
448
|
+
scroll; the chrome drops the ones off screen. `[]` is a scale the display maps
|
|
449
|
+
to colour rather than to y (density rows each in their own colour), which gets
|
|
450
|
+
the `[min, max]` caption and no axis, as a scale whose bands are too short for
|
|
451
|
+
one does. `side` and `left` are which of the band's edges the display's own
|
|
452
|
+
panels leave clear for a gutter: `right` where a group label chip takes the
|
|
453
|
+
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.
|
|
455
|
+
|
|
456
|
+
A member describes the scale or the band it rules, never the axis — not an
|
|
457
|
+
orientation, a form, a gutter width or a font. The chrome reads a member to keep
|
|
458
|
+
off what the display put in the band, never to choose between two drawings of it
|
|
459
|
+
(ADR-109).
|
|
460
|
+
|
|
461
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/display-ui/src/valueScale.ts)
|
|
462
|
+
|
|
433
463
|
<!-- API_DOCS_END -->
|
|
@@ -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, and
|
|
6
|
+
* the caption, where the scale has one, rotated along the outer edge. The
|
|
7
|
+
* on-screen overlay and the export both draw through this, so the two cannot
|
|
8
|
+
* place a spine differently.
|
|
9
|
+
*/
|
|
10
|
+
export default function AxisGutter({ axis }: {
|
|
11
|
+
axis: YAxis;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import 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;
|
|
8
|
+
/**
|
|
9
|
+
* One band's axis drawn inside its gutter, the origin at the gutter's left
|
|
10
|
+
* 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
|
|
13
|
+
* on-screen overlay and the export both draw through this, so the two cannot
|
|
14
|
+
* place a spine differently.
|
|
15
|
+
*/
|
|
16
|
+
export default function AxisGutter({ axis }) {
|
|
17
|
+
const palette = usePalette();
|
|
18
|
+
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] }));
|
|
25
|
+
}
|
|
@@ -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,11 +1,10 @@
|
|
|
1
1
|
import type { LegendItem, LegendSection } from '@jbrowse/core/ui/legendSpec';
|
|
2
|
-
|
|
3
|
-
export type {
|
|
4
|
-
declare const FloatingLegend: ({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems, maxWidth, top,
|
|
2
|
+
export declare const FLOATING_LEGEND_TOP_PX = 10;
|
|
3
|
+
export type { LegendGradient, LegendItem, LegendSection, LegendSpec, LegendSwatch, } from '@jbrowse/core/ui/legendSpec';
|
|
4
|
+
declare const FloatingLegend: ({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems, maxWidth, top, }: {
|
|
5
5
|
items?: LegendItem[];
|
|
6
6
|
sections?: LegendSection[];
|
|
7
7
|
title?: string;
|
|
8
|
-
children?: ReactNode;
|
|
9
8
|
onDismiss?: () => void;
|
|
10
9
|
onDismissSection?: (id: string) => void;
|
|
11
10
|
onItemClick?: (item: LegendItem, section: LegendSection) => void;
|
package/esm/FloatingLegend.js
CHANGED
|
@@ -10,6 +10,8 @@ import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
|
|
|
10
10
|
import { observer } from 'mobx-react';
|
|
11
11
|
import Tooltip from './tooltip/Tooltip.js';
|
|
12
12
|
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;
|
|
13
15
|
const useStyles = makeStyles()(theme => ({
|
|
14
16
|
legend: {
|
|
15
17
|
position: 'absolute',
|
|
@@ -134,6 +136,24 @@ const useStyles = makeStyles()(theme => ({
|
|
|
134
136
|
overflow: 'hidden',
|
|
135
137
|
textOverflow: 'ellipsis',
|
|
136
138
|
},
|
|
139
|
+
gradientRow: {
|
|
140
|
+
marginBottom: 1,
|
|
141
|
+
},
|
|
142
|
+
gradientBar: {
|
|
143
|
+
width: GRADIENT_BAR_WIDTH,
|
|
144
|
+
height: 10,
|
|
145
|
+
borderRadius: 2,
|
|
146
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
147
|
+
// clip the gradient to the padding box so it doesn't paint under the
|
|
148
|
+
// translucent divider border — otherwise the border composites over the
|
|
149
|
+
// gradient's end and the edge renders a dark sliver
|
|
150
|
+
backgroundClip: 'padding-box',
|
|
151
|
+
},
|
|
152
|
+
gradientLabels: {
|
|
153
|
+
display: 'flex',
|
|
154
|
+
justifyContent: 'space-between',
|
|
155
|
+
width: GRADIENT_BAR_WIDTH,
|
|
156
|
+
},
|
|
137
157
|
// was `<Link component="button" underline="hover">`, which is the same
|
|
138
158
|
// element with Material's typography on it — see `closeButton` above for why
|
|
139
159
|
// that mattered
|
|
@@ -155,9 +175,33 @@ const useStyles = makeStyles()(theme => ({
|
|
|
155
175
|
}));
|
|
156
176
|
const DEFAULT_MAX_ITEMS = 12;
|
|
157
177
|
const DEFAULT_MAX_WIDTH = 200;
|
|
158
|
-
|
|
159
|
-
//
|
|
178
|
+
const GRADIENT_BAR_WIDTH = 100;
|
|
179
|
+
// Side of one swatch box.
|
|
160
180
|
const SWATCH = 12;
|
|
181
|
+
// A ramp row: the caption, the bar and the domain ends. The bar is a CSS
|
|
182
|
+
// gradient over the same stops the export's <linearGradient> takes, so the
|
|
183
|
+
// two draw one ramp. Stops are flattened over the paper because a translucent
|
|
184
|
+
// first stop (Hi-C's juicebox fade) rasterizes to a dark sliver at the bar's
|
|
185
|
+
// left edge.
|
|
186
|
+
function gradientCss({ stops }, paper) {
|
|
187
|
+
const list = stops.map(({ color, opacity = 1, offset }) => {
|
|
188
|
+
const flat = opacity >= 1
|
|
189
|
+
? color
|
|
190
|
+
: `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, ${paper})`;
|
|
191
|
+
return `${flat} ${offset * 100}%`;
|
|
192
|
+
});
|
|
193
|
+
return `linear-gradient(to right, ${list.join(', ')})`;
|
|
194
|
+
}
|
|
195
|
+
function GradientRow({ label, gradient, }) {
|
|
196
|
+
const { classes, theme } = useStyles();
|
|
197
|
+
return (_jsxs("div", { className: classes.gradientRow, children: [label ? _jsx("div", { className: classes.label, children: label }) : null, _jsx("div", { className: classes.gradientBar,
|
|
198
|
+
// backgroundImage (longhand), not the `background` shorthand: the
|
|
199
|
+
// shorthand resets background-clip back to border-box, undoing the
|
|
200
|
+
// padding-box clip
|
|
201
|
+
style: {
|
|
202
|
+
backgroundImage: gradientCss(gradient, theme.palette.background.paper),
|
|
203
|
+
} }), _jsxs("div", { className: classes.gradientLabels, children: [_jsx("span", { children: gradient.minLabel }), _jsx("span", { children: gradient.maxLabel })] })] }));
|
|
204
|
+
}
|
|
161
205
|
// One list of swatches with its own independent collapse state, so each section
|
|
162
206
|
// in a multi-section legend expands/collapses on its own.
|
|
163
207
|
const LegendItemList = observer(function LegendItemList({ items, maxItems, onItemClick, }) {
|
|
@@ -167,12 +211,17 @@ const LegendItemList = observer(function LegendItemList({ items, maxItems, onIte
|
|
|
167
211
|
const shown = collapsible && !expanded ? items.slice(0, maxItems) : items;
|
|
168
212
|
const hiddenCount = items.length - maxItems;
|
|
169
213
|
// Every row reserves the widest row's swatch column, so labels stay in one
|
|
170
|
-
// vertical line whether a row draws one
|
|
214
|
+
// vertical line whether a row draws one box or two — and so a color-less
|
|
171
215
|
// heading row indents like the swatches it heads, as the fixed-size box it
|
|
172
216
|
// replaces used to.
|
|
173
217
|
const columns = Math.max(1, ...shown.map(i => legendSwatches(i).length));
|
|
174
218
|
return (_jsxs(_Fragment, { children: [shown.map((item, i) => {
|
|
175
|
-
|
|
219
|
+
if (item.gradient) {
|
|
220
|
+
return (_jsx(GradientRow
|
|
221
|
+
// eslint-disable-next-line @eslint-react/no-array-index-key
|
|
222
|
+
, { label: item.label, gradient: item.gradient }, `${item.label}-${i}`));
|
|
223
|
+
}
|
|
224
|
+
const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: classes.swatches, style: { minWidth: columns * SWATCH + (columns - 1) * 2 }, children: legendSwatches(item).map(swatch => (_jsx("svg", { width: SWATCH, height: SWATCH, children: _jsx(LegendSwatchGlyph, { swatch: swatch, size: SWATCH }) }, swatch.color))) }), _jsx("span", { className: classes.label, children: item.label })] }));
|
|
176
225
|
return onItemClick ? (_jsx("button", { type: "button", className: cx(classes.item, classes.itemButton), title: "Show only these rows", onClick: () => {
|
|
177
226
|
onItemClick(item);
|
|
178
227
|
}, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className: classes.item, children: row }, `${item.label}-${i}`));
|
|
@@ -191,10 +240,10 @@ const LegendItemList = observer(function LegendItemList({ items, maxItems, onIte
|
|
|
191
240
|
// rows, clicking one focuses those rows. It is handed the section too, so a
|
|
192
241
|
// display with several vocabularies can act on the one that names rows and
|
|
193
242
|
// leave the others inert.
|
|
194
|
-
const FloatingLegend = observer(function FloatingLegend({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top =
|
|
243
|
+
const FloatingLegend = observer(function FloatingLegend({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top = FLOATING_LEGEND_TOP_PX, }) {
|
|
195
244
|
const { classes } = useStyles();
|
|
196
245
|
const nonEmpty = nonEmptyLegendSections({ items, sections });
|
|
197
|
-
if (nonEmpty.length === 0
|
|
246
|
+
if (nonEmpty.length === 0) {
|
|
198
247
|
return null;
|
|
199
248
|
}
|
|
200
249
|
const multiSection = nonEmpty.length > 1;
|
|
@@ -202,7 +251,7 @@ const FloatingLegend = observer(function FloatingLegend({ items, sections, title
|
|
|
202
251
|
// whole-genome / multi-region scale (see TrackOverlayPortal)
|
|
203
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: () => {
|
|
204
253
|
onDismiss();
|
|
205
|
-
}, children: "\u00D7" }) })) : null, title ? _jsx("div", { className: classes.topTitle, children: title }) : null,
|
|
254
|
+
}, 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
255
|
onDismissSection(section.id);
|
|
207
256
|
}, children: "\u00D7" }) })) : null] })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick
|
|
208
257
|
? item => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const SCORE_CAPTION_HEIGHT = 16;
|
|
2
|
+
/**
|
|
3
|
+
* The one-line `[min, max]` caption that stands in for a y axis where none can
|
|
4
|
+
* be drawn — a scale whose bands are too short for tick labels, and a density
|
|
5
|
+
* track whose rows each carry their own colour, so no single ramp describes
|
|
6
|
+
* them. An axis stand-in rather than a key, which is why it is not a colour
|
|
7
|
+
* scale and draws whether or not the legend does. Right-aligned to
|
|
8
|
+
* `canvasWidth` at the top of the display.
|
|
9
|
+
*/
|
|
10
|
+
export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, }: {
|
|
11
|
+
domain: [number, number];
|
|
12
|
+
scaleType: string;
|
|
13
|
+
canvasWidth: number;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { measureLegendText } from '@jbrowse/core/ui/measureLegendText';
|
|
4
|
+
import { stripAlpha } from '@jbrowse/core/util/svgColorProps';
|
|
5
|
+
import { formatScore } from './formatScore.js';
|
|
6
|
+
export const SCORE_CAPTION_HEIGHT = 16;
|
|
7
|
+
/**
|
|
8
|
+
* The one-line `[min, max]` caption that stands in for a y axis where none can
|
|
9
|
+
* be drawn — a scale whose bands are too short for tick labels, and a density
|
|
10
|
+
* track whose rows each carry their own colour, so no single ramp describes
|
|
11
|
+
* them. An axis stand-in rather than a key, which is why it is not a colour
|
|
12
|
+
* scale and draws whether or not the legend does. Right-aligned to
|
|
13
|
+
* `canvasWidth` at the top of the display.
|
|
14
|
+
*/
|
|
15
|
+
export default function ScoreDomainCaption({ domain, scaleType, canvasWidth, }) {
|
|
16
|
+
const palette = usePalette();
|
|
17
|
+
const suffix = scaleType === 'log' ? ' (log)' : scaleType === 'symlog' ? ' (symlog)' : '';
|
|
18
|
+
const text = `[${formatScore(domain[0])}, ${formatScore(domain[1])}]${suffix}`;
|
|
19
|
+
const len = measureLegendText(text, 12);
|
|
20
|
+
const xpos = Math.max(0, canvasWidth - len - 10);
|
|
21
|
+
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 })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One horizontal rule across a score plot at a chosen value, placed in the
|
|
3
|
+
* plot's own pixel space; `label` is free text the reader chose.
|
|
4
|
+
*/
|
|
5
|
+
export interface ScoreRuleMark {
|
|
6
|
+
value: number;
|
|
7
|
+
y: number;
|
|
8
|
+
label?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function ScoreRuleLines({ marks, width, offsetY, }: {
|
|
12
|
+
marks: ScoreRuleMark[];
|
|
13
|
+
width: number;
|
|
14
|
+
offsetY?: number;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
16
|
+
export default function ScoreRules({ marks, width, height, bandTops, }: {
|
|
17
|
+
marks: ScoreRuleMark[];
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
bandTops: number[];
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
const DEFAULT_RULE_COLOR = 'rgb(120,120,120)';
|
|
4
|
+
// Bare marks, no wrapping <svg>, so the on-screen overlay and the SVG export
|
|
5
|
+
// draw the same elements from the same y. Same split as CrossHatchLines.
|
|
6
|
+
export function ScoreRuleLines({ marks, width, offsetY = 0, }) {
|
|
7
|
+
// A caption sits over the plot, not beside it, so it needs the halo the tick
|
|
8
|
+
// labels next to it already draw (YScaleBar) — without one a rule crossing
|
|
9
|
+
// filled bars captions itself in grey on the fill and cannot be read. From
|
|
10
|
+
// the palette rather than a hardcoded white, which inverts into a glare in
|
|
11
|
+
// dark mode.
|
|
12
|
+
const haloColor = usePalette().background.default;
|
|
13
|
+
return (_jsx(_Fragment, { children: marks.map(({ value, label, color, y }) => {
|
|
14
|
+
const lineY = offsetY + y;
|
|
15
|
+
const stroke = color ?? DEFAULT_RULE_COLOR;
|
|
16
|
+
return (_jsxs("g", { children: [_jsx("line", { x1: 0, x2: width, y1: lineY, y2: lineY, stroke: stroke,
|
|
17
|
+
// separate attribute rather than an rgba() string: the SVG
|
|
18
|
+
// export's renderToStaticMarkup strips rgba() alpha
|
|
19
|
+
strokeOpacity: 0.9, strokeWidth: 1, strokeDasharray: "4 3" }), label ? (_jsx("text", { x: width - 4, y: lineY - 2, textAnchor: "end", fontSize: 10,
|
|
20
|
+
// full alpha, unlike the line: 0.9 is there to let the rule
|
|
21
|
+
// recede into the plot, and a caption that has to be read wants
|
|
22
|
+
// the opposite
|
|
23
|
+
fill: stroke, stroke: haloColor, strokeWidth: 2.5, paintOrder: "stroke", children: label })) : null] }, `${value}-${y}`));
|
|
24
|
+
}) }));
|
|
25
|
+
}
|
|
26
|
+
// Once per band the scale rules, like CrossHatches; pointer-events disabled so
|
|
27
|
+
// the canvas underneath still gets mouse events.
|
|
28
|
+
export default function ScoreRules({ marks, width, height, bandTops, }) {
|
|
29
|
+
return (_jsx("svg", { style: {
|
|
30
|
+
position: 'absolute',
|
|
31
|
+
top: 0,
|
|
32
|
+
left: 0,
|
|
33
|
+
pointerEvents: 'none',
|
|
34
|
+
height,
|
|
35
|
+
width,
|
|
36
|
+
}, children: bandTops.map(top => (_jsx(ScoreRuleLines, { marks: marks, width: width, offsetY: top }, top))) }));
|
|
37
|
+
}
|
package/esm/YScaleBar.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePalette } from '@jbrowse/core/ui/PaletteContext';
|
|
3
|
+
import { clampStrokeInsideAxis } from './yScaleTicks.js';
|
|
4
|
+
// Not an `observer`, and deliberately: there is nothing here to observe. Every
|
|
5
|
+
// caller already reads `model.ticks` inside its own observer and hands the
|
|
6
|
+
// resolved plain object down, and `usePalette` is a React context, not MobX.
|
|
7
|
+
// Wrapping it anyway allocated a Reaction per instance — multi-wiggle renders
|
|
8
|
+
// one per sample row, on screen as well as in export — and, per CLAUDE.md, put
|
|
9
|
+
// the component out of `babel-plugin-react-compiler`'s reach, so it got neither
|
|
10
|
+
// the MobX tracking it doesn't need nor the memoization it does. Its sibling
|
|
11
|
+
// `CrossHatchLines`, consuming the same ticks, is already plain.
|
|
12
|
+
// Half the vertical space a 10px label needs, halo included: digits reach about
|
|
13
|
+
// 3.6px above the centered baseline and the 2.5px-wide background stroke another
|
|
14
|
+
// 1.25px past that.
|
|
15
|
+
const LABEL_HALF_HEIGHT_PX = 5;
|
|
16
|
+
export default function YScaleBar({ ticks, orientation, bandHeight, }) {
|
|
17
|
+
const palette = usePalette();
|
|
18
|
+
if (!ticks) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const { items, yTop, yBottom } = ticks;
|
|
22
|
+
const bg = palette.background.default;
|
|
23
|
+
const fg = palette.text.primary;
|
|
24
|
+
const isLeft = orientation !== 'right';
|
|
25
|
+
const k = isLeft ? -1 : 1;
|
|
26
|
+
const tickLength = 6;
|
|
27
|
+
// Crispen to `y + 0.5` so each 1px stroke fills one pixel instead of
|
|
28
|
+
// straddling two, then clamp, which only bites at the bottom edge: there the
|
|
29
|
+
// stroke goes on the last pixel inside the box rather than the first one
|
|
30
|
+
// below it.
|
|
31
|
+
const strokeY = (y) => clampStrokeInsideAxis(y + 0.5, yBottom);
|
|
32
|
+
// The tick keeps the group transform; only the text moves off it, and only
|
|
33
|
+
// under `bandHeight` within half a label of an end. A tick is where the data
|
|
34
|
+
// is, so it never moves — the label is then drawn against the tick rather than
|
|
35
|
+
// across it, which is what any axis does at the edge of its own frame.
|
|
36
|
+
// `undefined` rather than 0 for the labels that don't move, so the markup this
|
|
37
|
+
// emits is unchanged wherever the inset doesn't apply.
|
|
38
|
+
const labelDy = (sy) => {
|
|
39
|
+
if (bandHeight === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const inset = Math.min(Math.max(sy, LABEL_HALF_HEIGHT_PX), bandHeight - LABEL_HALF_HEIGHT_PX);
|
|
43
|
+
return inset === sy ? undefined : inset - sy;
|
|
44
|
+
};
|
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { YAxis } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The on-screen axis of one band: an `<svg>` the width of the gutter,
|
|
4
|
+
* positioned absolutely over the band at `top`, with `AxisGutter` inside. A
|
|
5
|
+
* right-side gutter clears the vertical scrollbar a display may mount.
|
|
6
|
+
*/
|
|
7
|
+
export default function YScaleBarOverlay({ axis, top, width, }: {
|
|
8
|
+
axis: YAxis;
|
|
9
|
+
top: number;
|
|
10
|
+
width: number;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { VERTICAL_SCROLLBAR_CLEARANCE } from '@jbrowse/core/ui/VerticalScrollbar';
|
|
3
|
+
import AxisGutter from './AxisGutter.js';
|
|
4
|
+
import { axisGutterLeft } from './axisPlacement.js';
|
|
5
|
+
import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
|
|
6
|
+
/**
|
|
7
|
+
* The on-screen axis of one band: an `<svg>` the width of the gutter,
|
|
8
|
+
* positioned absolutely over the band at `top`, with `AxisGutter` inside. A
|
|
9
|
+
* right-side gutter clears the vertical scrollbar a display may mount.
|
|
10
|
+
*/
|
|
11
|
+
export default function YScaleBarOverlay({ axis, top, width, }) {
|
|
12
|
+
return (_jsx("svg", { style: {
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
top,
|
|
15
|
+
left: axisGutterLeft(axis, width, VERTICAL_SCROLLBAR_CLEARANCE),
|
|
16
|
+
pointerEvents: 'none',
|
|
17
|
+
height: axis.height,
|
|
18
|
+
width: AXIS_GUTTER_WIDTH_PX,
|
|
19
|
+
overflow: 'visible',
|
|
20
|
+
}, children: _jsx(AxisGutter, { axis: axis }) }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ValueScale } from './valueScale.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Below this band height a full `YScaleBar`'s tick labels overlap each other,
|
|
4
|
+
* so the chrome draws one `ScoreDomainCaption` for the scale instead of an
|
|
5
|
+
* axis per band. Its own module because `YScaleBar`'s label geometry is what
|
|
6
|
+
* makes 30 the number.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMPACT_AXIS_HEIGHT = 30;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a scale gets an axis at all: it rules at least one band, and the
|
|
11
|
+
* band has room for tick labels. A scale that fails either is captioned.
|
|
12
|
+
*/
|
|
13
|
+
export declare function axisDrawn(scale: Pick<ValueScale, 'height' | 'bandTops'>): boolean;
|
|
14
|
+
/** How far in from the plot's right edge a right-side gutter ends in an export. */
|
|
15
|
+
export declare const AXIS_RIGHT_INSET_PX = 4;
|
|
16
|
+
/**
|
|
17
|
+
* Left edge of the gutter an axis is drawn in. A right-side axis takes the
|
|
18
|
+
* gutter inside the plot's right edge, less `rightInset` (the vertical
|
|
19
|
+
* scrollbar a display may mount on screen; a margin in an export). A left-side
|
|
20
|
+
* axis takes the gutter at the display's left edge, past whatever panel the
|
|
21
|
+
* 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.
|
|
24
|
+
*/
|
|
25
|
+
export declare function axisGutterLeft(scale: Pick<ValueScale, 'side' | 'left'>, width: number, rightInset: number, exportContentLeft?: number): number;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AXIS_GUTTER_WIDTH_PX } from './yScaleTicks.js';
|
|
2
|
+
/**
|
|
3
|
+
* Below this band height a full `YScaleBar`'s tick labels overlap each other,
|
|
4
|
+
* so the chrome draws one `ScoreDomainCaption` for the scale instead of an
|
|
5
|
+
* axis per band. Its own module because `YScaleBar`'s label geometry is what
|
|
6
|
+
* makes 30 the number.
|
|
7
|
+
*/
|
|
8
|
+
export const COMPACT_AXIS_HEIGHT = 30;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a scale gets an axis at all: it rules at least one band, and the
|
|
11
|
+
* band has room for tick labels. A scale that fails either is captioned.
|
|
12
|
+
*/
|
|
13
|
+
export function axisDrawn(scale) {
|
|
14
|
+
return ((scale.bandTops?.length ?? 1) > 0 && scale.height >= COMPACT_AXIS_HEIGHT);
|
|
15
|
+
}
|
|
16
|
+
/** How far in from the plot's right edge a right-side gutter ends in an export. */
|
|
17
|
+
export const AXIS_RIGHT_INSET_PX = 4;
|
|
18
|
+
/**
|
|
19
|
+
* Left edge of the gutter an axis is drawn in. A right-side axis takes the
|
|
20
|
+
* gutter inside the plot's right edge, less `rightInset` (the vertical
|
|
21
|
+
* scrollbar a display may mount on screen; a margin in an export). A left-side
|
|
22
|
+
* axis takes the gutter at the display's left edge, past whatever panel the
|
|
23
|
+
* 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.
|
|
26
|
+
*/
|
|
27
|
+
export function axisGutterLeft(scale, width, rightInset, exportContentLeft) {
|
|
28
|
+
if (scale.side === 'right') {
|
|
29
|
+
return width - rightInset - AXIS_GUTTER_WIDTH_PX;
|
|
30
|
+
}
|
|
31
|
+
const left = scale.left ?? 0;
|
|
32
|
+
return exportContentLeft !== undefined && left === 0
|
|
33
|
+
? exportContentLeft - AXIS_GUTTER_WIDTH_PX
|
|
34
|
+
: left;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatScore(n: number): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Only the fractional padding toPrecision adds is noise; guard on the decimal
|
|
2
|
+
// point because toPrecision can round UP out of the fractional form entirely —
|
|
3
|
+
// 99.95 comes back as "100", where an unguarded trailing-zero strip eats two
|
|
4
|
+
// significant digits and reports the score as 1.
|
|
5
|
+
export function formatScore(n) {
|
|
6
|
+
if (n === 0) {
|
|
7
|
+
return '0';
|
|
8
|
+
}
|
|
9
|
+
if (Math.abs(n) >= 100) {
|
|
10
|
+
return n.toFixed(0);
|
|
11
|
+
}
|
|
12
|
+
const s = n.toPrecision(3);
|
|
13
|
+
return s.includes('.') ? s.replace(/\.?0+$/, '') : s;
|
|
14
|
+
}
|
package/esm/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { default as plainTrackControl } from './trackControl/plainTrackControl.t
|
|
|
7
7
|
export { useTrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
|
|
8
8
|
export type { TrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
|
|
9
9
|
export type { TrackControlComponent, TrackControlIcon, TrackControlOption, TrackControlProps, } from './trackControl/types.ts';
|
|
10
|
-
export { default as FloatingLegend } from './FloatingLegend.tsx';
|
|
10
|
+
export { default as FloatingLegend, FLOATING_LEGEND_TOP_PX, } from './FloatingLegend.tsx';
|
|
11
11
|
export type { LegendItem, LegendSection } from './FloatingLegend.tsx';
|
|
12
12
|
export { FloatingSvgOverlay } from './FloatingSvgOverlay.tsx';
|
|
13
13
|
export { default as Tooltip } from './tooltip/Tooltip.tsx';
|
|
@@ -21,3 +21,16 @@ export { TrackOverlaySlot } from './trackOverlay/TrackOverlaySlot.tsx';
|
|
|
21
21
|
export { tooLargeBannerText } from './tooLargeBannerText.ts';
|
|
22
22
|
export { isLiveModel } from './isLiveModel.ts';
|
|
23
23
|
export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.ts';
|
|
24
|
+
export type { AxisSide, ValueScale, YAxis } from './valueScale.ts';
|
|
25
|
+
export { AXIS_GUTTER_WIDTH_PX, axisPlotBox, clampStrokeInsideAxis, scoreToAxisY, } from './yScaleTicks.ts';
|
|
26
|
+
export type { YScaleTicks } from './yScaleTicks.ts';
|
|
27
|
+
export { YSCALEBAR_LABEL_OFFSET } from './yAxisConstants.ts';
|
|
28
|
+
export { AXIS_RIGHT_INSET_PX, COMPACT_AXIS_HEIGHT, axisDrawn, axisGutterLeft, } from './axisPlacement.ts';
|
|
29
|
+
export { default as AxisGutter } from './AxisGutter.tsx';
|
|
30
|
+
export { default as YScaleBar } from './YScaleBar.tsx';
|
|
31
|
+
export { default as YScaleBarOverlay } from './YScaleBarOverlay.tsx';
|
|
32
|
+
export { default as ScoreDomainCaption, SCORE_CAPTION_HEIGHT, } from './ScoreDomainCaption.tsx';
|
|
33
|
+
export { formatScore } from './formatScore.ts';
|
|
34
|
+
export { CrossHatchLines, default as CrossHatches } from './CrossHatches.tsx';
|
|
35
|
+
export { ScoreRuleLines, default as ScoreRules } from './ScoreRules.tsx';
|
|
36
|
+
export type { ScoreRuleMark } from './ScoreRules.tsx';
|
package/esm/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export { useTrackControlMenu } from './trackControl/useTrackControlMenu.js';
|
|
|
22
22
|
// no UI toolkit, and it is chrome by the definition this package uses — a thing
|
|
23
23
|
// a display draws that is not its data. `@jbrowse/plugin-linear-genome-view`
|
|
24
24
|
// re-exports it, so its published name is unchanged.
|
|
25
|
-
export { default as FloatingLegend } from './FloatingLegend.js';
|
|
25
|
+
export { default as FloatingLegend, FLOATING_LEGEND_TOP_PX, } from './FloatingLegend.js';
|
|
26
26
|
// The <svg> counterpart of that legend, for a display whose key is drawn by an
|
|
27
27
|
// SVG-export path as well as on screen. Beside it for the same reasons — it
|
|
28
28
|
// reaches no UI toolkit, and portaling chrome above the region masks is what
|
|
@@ -55,3 +55,13 @@ export { isLiveModel } from './isLiveModel.js';
|
|
|
55
55
|
// contract the prop types cannot carry, so it belongs with them rather than
|
|
56
56
|
// with the chrome that happens to mount it.
|
|
57
57
|
export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.js';
|
|
58
|
+
export { AXIS_GUTTER_WIDTH_PX, axisPlotBox, clampStrokeInsideAxis, scoreToAxisY, } from './yScaleTicks.js';
|
|
59
|
+
export { YSCALEBAR_LABEL_OFFSET } from './yAxisConstants.js';
|
|
60
|
+
export { AXIS_RIGHT_INSET_PX, COMPACT_AXIS_HEIGHT, axisDrawn, axisGutterLeft, } from './axisPlacement.js';
|
|
61
|
+
export { default as AxisGutter } from './AxisGutter.js';
|
|
62
|
+
export { default as YScaleBar } from './YScaleBar.js';
|
|
63
|
+
export { default as YScaleBarOverlay } from './YScaleBarOverlay.js';
|
|
64
|
+
export { default as ScoreDomainCaption, SCORE_CAPTION_HEIGHT, } from './ScoreDomainCaption.js';
|
|
65
|
+
export { formatScore } from './formatScore.js';
|
|
66
|
+
export { CrossHatchLines, default as CrossHatches } from './CrossHatches.js';
|
|
67
|
+
export { ScoreRuleLines, default as ScoreRules } from './ScoreRules.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { YScaleTicks } from './yScaleTicks.ts';
|
|
2
|
+
export type AxisSide = 'left' | 'right';
|
|
3
|
+
/**
|
|
4
|
+
* #api
|
|
5
|
+
* A value scale a display places its y through, declared so the chrome can
|
|
6
|
+
* derive the axis from it — the score axis's counterpart to a colour scale.
|
|
7
|
+
*
|
|
8
|
+
* `domain` is the resolved `[min, max]`; `height` the band it rules and
|
|
9
|
+
* `offset` the inset of the plot box inside that band (`axisPlotBox(height,
|
|
10
|
+
* offset)`); `minimalTicks` and `symlogConstant` how the ticks are chosen.
|
|
11
|
+
* `ticks` is the display's own ladder where the band's arithmetic is its own
|
|
12
|
+
* (the coverage band's octaves against the shader's box, the read cloud's
|
|
13
|
+
* decades against the arc geometry); `ScoreScaleMixin` derives one through
|
|
14
|
+
* `computeYTicks` otherwise.
|
|
15
|
+
*
|
|
16
|
+
* `bandTops` is the screen y of each band the scale rules — one at 0 unless
|
|
17
|
+
* the display stacks the same plot: the multi-wiggle's rows, a grouped
|
|
18
|
+
* alignments track's coverage band per group, each projected through the
|
|
19
|
+
* display's own scroll; the chrome drops the ones off screen. `[]` is a scale
|
|
20
|
+
* the display maps to colour rather than to y (density rows each in their own
|
|
21
|
+
* colour), which gets the `[min, max]` caption and no axis, as a scale whose
|
|
22
|
+
* bands are too short for one does. `side` and `left` are which of the band's
|
|
23
|
+
* edges the display's own panels leave clear for a gutter: `right` where a
|
|
24
|
+
* group label chip takes the left, `left: n` where a dendrogram takes the
|
|
25
|
+
* first `n` px. `caption` is what the scale measures (`TLEN`), as a colour
|
|
26
|
+
* scale's `field` is.
|
|
27
|
+
*
|
|
28
|
+
* A member describes the scale or the band it rules, never the axis — not an
|
|
29
|
+
* orientation, a form, a gutter width or a font. The chrome reads a member to
|
|
30
|
+
* keep off what the display put in the band, never to choose between two
|
|
31
|
+
* drawings of it (ADR-109).
|
|
32
|
+
*/
|
|
33
|
+
export interface ValueScale {
|
|
34
|
+
domain: [number, number] | undefined;
|
|
35
|
+
scaleType: string;
|
|
36
|
+
height: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
minimalTicks?: boolean;
|
|
39
|
+
symlogConstant?: number;
|
|
40
|
+
ticks?: YScaleTicks;
|
|
41
|
+
bandTops?: number[];
|
|
42
|
+
side?: AxisSide;
|
|
43
|
+
left?: number;
|
|
44
|
+
caption?: string;
|
|
45
|
+
}
|
|
46
|
+
/** A value scale whose ticks resolved: what the chrome draws. */
|
|
47
|
+
export interface YAxis extends ValueScale {
|
|
48
|
+
domain: [number, number];
|
|
49
|
+
ticks: YScaleTicks;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const YSCALEBAR_LABEL_OFFSET = 5;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Vertical inset a plot box reserves above and below itself so the top and
|
|
2
|
+
// bottom y-axis labels aren't clipped. Its own module because both index.ts
|
|
3
|
+
// (which re-exports computeYTicks) and computeYTicks.ts need it — importing it
|
|
4
|
+
// from index.ts would close a cycle.
|
|
5
|
+
export const YSCALEBAR_LABEL_OFFSET = 5;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface YScaleTicks {
|
|
2
|
+
items: {
|
|
3
|
+
value: number;
|
|
4
|
+
y: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}[];
|
|
7
|
+
yTop: number;
|
|
8
|
+
yBottom: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The plot box a wiggle-family display insets inside its own height, for both
|
|
12
|
+
* sides of the contract above: the renderer positions its canvas at `yTop` and
|
|
13
|
+
* draws `plotHeight` tall, and the axis puts domain-max at `yTop` and
|
|
14
|
+
* domain-min at `yBottom`.
|
|
15
|
+
*
|
|
16
|
+
* One function because the two have to agree exactly, and `height - 2 * offset`
|
|
17
|
+
* spelled out at each end is how they stopped: `computeYTicks` derived its own
|
|
18
|
+
* bottom as `height - offset - 1`, and every tick above it inherited a fraction
|
|
19
|
+
* of that pixel.
|
|
20
|
+
*/
|
|
21
|
+
export declare function axisPlotBox(height: number, offset?: number): {
|
|
22
|
+
yTop: number;
|
|
23
|
+
yBottom: number;
|
|
24
|
+
plotHeight: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Screen y for an already-normalized ([0,1], 0 at domain-min) score. The
|
|
28
|
+
* composition of {@link axisPlotBox} with a `makeScoreNormalizer` result, which
|
|
29
|
+
* is what a tick at that score must land on.
|
|
30
|
+
*/
|
|
31
|
+
export declare function scoreToAxisY(normalized: number, box: ReturnType<typeof axisPlotBox>): number;
|
|
32
|
+
/**
|
|
33
|
+
* Width of the gutter an axis is drawn in, on screen and in the export
|
|
34
|
+
* margin alike. `YScaleBar` grows its ticks and numbers away from the spine,
|
|
35
|
+
* and `x = ±9`, `fontSize 10` put a label of ordinary width inside this many
|
|
36
|
+
* px; the alignments coverage axis and the MAF band axes had each arrived at
|
|
37
|
+
* the same number independently, so a change to the labels moved neither.
|
|
38
|
+
*/
|
|
39
|
+
export declare const AXIS_GUTTER_WIDTH_PX = 50;
|
|
40
|
+
/**
|
|
41
|
+
* Keep a 1px stroke's center inside the axis box. At `yBottom` — the box's own
|
|
42
|
+
* bottom edge — the stroke has to cover the pixel *above* the boundary, since
|
|
43
|
+
* the one below belongs to whatever is drawn next; in multi-wiggle, where rows
|
|
44
|
+
* stack edge to edge, that is literally the next sample's first row of pixels.
|
|
45
|
+
* Callers pass whatever stroke center their own convention gives (the axis
|
|
46
|
+
* crispens to `y + 0.5`, the guide lines don't); only the bottom edge moves.
|
|
47
|
+
*/
|
|
48
|
+
export declare function clampStrokeInsideAxis(strokeY: number, yBottom: number): number;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { YSCALEBAR_LABEL_OFFSET } from './yAxisConstants.js';
|
|
2
|
+
/**
|
|
3
|
+
* The plot box a wiggle-family display insets inside its own height, for both
|
|
4
|
+
* sides of the contract above: the renderer positions its canvas at `yTop` and
|
|
5
|
+
* draws `plotHeight` tall, and the axis puts domain-max at `yTop` and
|
|
6
|
+
* domain-min at `yBottom`.
|
|
7
|
+
*
|
|
8
|
+
* One function because the two have to agree exactly, and `height - 2 * offset`
|
|
9
|
+
* spelled out at each end is how they stopped: `computeYTicks` derived its own
|
|
10
|
+
* bottom as `height - offset - 1`, and every tick above it inherited a fraction
|
|
11
|
+
* of that pixel.
|
|
12
|
+
*/
|
|
13
|
+
export function axisPlotBox(height, offset = YSCALEBAR_LABEL_OFFSET) {
|
|
14
|
+
return {
|
|
15
|
+
yTop: offset,
|
|
16
|
+
yBottom: height - offset,
|
|
17
|
+
plotHeight: height - 2 * offset,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Screen y for an already-normalized ([0,1], 0 at domain-min) score. The
|
|
22
|
+
* composition of {@link axisPlotBox} with a `makeScoreNormalizer` result, which
|
|
23
|
+
* is what a tick at that score must land on.
|
|
24
|
+
*/
|
|
25
|
+
export function scoreToAxisY(normalized, box) {
|
|
26
|
+
return box.yTop + (1 - normalized) * box.plotHeight;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Width of the gutter an axis is drawn in, on screen and in the export
|
|
30
|
+
* margin alike. `YScaleBar` grows its ticks and numbers away from the spine,
|
|
31
|
+
* and `x = ±9`, `fontSize 10` put a label of ordinary width inside this many
|
|
32
|
+
* px; the alignments coverage axis and the MAF band axes had each arrived at
|
|
33
|
+
* the same number independently, so a change to the labels moved neither.
|
|
34
|
+
*/
|
|
35
|
+
export const AXIS_GUTTER_WIDTH_PX = 50;
|
|
36
|
+
/**
|
|
37
|
+
* Keep a 1px stroke's center inside the axis box. At `yBottom` — the box's own
|
|
38
|
+
* bottom edge — the stroke has to cover the pixel *above* the boundary, since
|
|
39
|
+
* the one below belongs to whatever is drawn next; in multi-wiggle, where rows
|
|
40
|
+
* stack edge to edge, that is literally the next sample's first row of pixels.
|
|
41
|
+
* Callers pass whatever stroke center their own convention gives (the axis
|
|
42
|
+
* crispens to `y + 0.5`, the guide lines don't); only the bottom edge moves.
|
|
43
|
+
*/
|
|
44
|
+
export function clampStrokeInsideAxis(strokeY, yBottom) {
|
|
45
|
+
return Math.min(strokeY, yBottom - 0.5);
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/display-ui",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The UI a display draws that is not data: the swappable chrome contract, its toolkit-free implementations, and the track overlay layer",
|
|
6
6
|
"keywords": [
|
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
},
|
|
19
19
|
"author": "JBrowse Team",
|
|
20
20
|
"main": "esm/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./esm/index.js",
|
|
23
|
+
"./yAxisConstants": "./esm/yAxisConstants.js"
|
|
24
|
+
},
|
|
21
25
|
"files": [
|
|
22
26
|
"esm"
|
|
23
27
|
],
|
|
24
28
|
"dependencies": {
|
|
25
29
|
"@jbrowse/mobx-state-tree": "^6.5.1",
|
|
26
30
|
"mobx-react": "^10.0.2",
|
|
27
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
31
|
+
"@jbrowse/core": "^5.0.0-beta.7"
|
|
28
32
|
},
|
|
29
33
|
"peerDependencies": {
|
|
30
34
|
"react": ">=19.0.0",
|
|
@@ -41,8 +45,5 @@
|
|
|
41
45
|
"clean": "rimraf esm *.tsbuildinfo",
|
|
42
46
|
"prebuild": "pnpm clean"
|
|
43
47
|
},
|
|
44
|
-
"types": "esm/index.d.ts"
|
|
45
|
-
"exports": {
|
|
46
|
-
".": "./esm/index.js"
|
|
47
|
-
}
|
|
48
|
+
"types": "esm/index.d.ts"
|
|
48
49
|
}
|