@jbrowse/wiggle-core 4.3.0
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/LICENSE +201 -0
- package/README.md +292 -0
- package/esm/CrossHatches.d.ts +11 -0
- package/esm/CrossHatches.js +32 -0
- package/esm/ResolutionStepper.d.ts +17 -0
- package/esm/ResolutionStepper.js +56 -0
- package/esm/ScoreRules.d.ts +11 -0
- package/esm/ScoreRules.js +37 -0
- package/esm/ScoreScaleMixin.d.ts +105 -0
- package/esm/ScoreScaleMixin.js +136 -0
- package/esm/SetMinMaxDialog.d.ts +11 -0
- package/esm/SetMinMaxDialog.js +18 -0
- package/esm/YScaleBar.d.ts +6 -0
- package/esm/YScaleBar.js +46 -0
- package/esm/YScaleBarOverlay.d.ts +6 -0
- package/esm/YScaleBarOverlay.js +24 -0
- package/esm/autoscale.d.ts +55 -0
- package/esm/autoscale.js +252 -0
- package/esm/computeYTicks.d.ts +15 -0
- package/esm/computeYTicks.js +44 -0
- package/esm/constants.d.ts +2 -0
- package/esm/constants.js +10 -0
- package/esm/dataTypes.d.ts +26 -0
- package/esm/dataTypes.js +1 -0
- package/esm/displayModel.d.ts +24 -0
- package/esm/displayModel.js +1 -0
- package/esm/formatNumber.d.ts +1 -0
- package/esm/formatNumber.js +6 -0
- package/esm/gapBreak.d.ts +59 -0
- package/esm/gapBreak.js +58 -0
- package/esm/index.d.ts +36 -0
- package/esm/index.js +26 -0
- package/esm/normalize.d.ts +40 -0
- package/esm/normalize.js +106 -0
- package/esm/pointMarker.d.ts +3 -0
- package/esm/pointMarker.js +27 -0
- package/esm/pointSizeMenu.d.ts +13 -0
- package/esm/pointSizeMenu.js +21 -0
- package/esm/renderingBackendTypes.d.ts +39 -0
- package/esm/renderingBackendTypes.js +29 -0
- package/esm/resolveRenderState.d.ts +1 -0
- package/esm/resolveRenderState.js +20 -0
- package/esm/scale.d.ts +60 -0
- package/esm/scale.js +161 -0
- package/esm/scoreAxisConfigSchemaFields.d.ts +44 -0
- package/esm/scoreAxisConfigSchemaFields.js +54 -0
- package/esm/scoreMenuItems.d.ts +37 -0
- package/esm/scoreMenuItems.js +104 -0
- package/esm/scoreRuleMarks.d.ts +73 -0
- package/esm/scoreRuleMarks.js +98 -0
- package/esm/symlogTicks.d.ts +17 -0
- package/esm/symlogTicks.js +92 -0
- package/esm/transferables.d.ts +2 -0
- package/esm/transferables.js +32 -0
- package/esm/vendor/d3-scale.d.ts +33 -0
- package/esm/vendor/d3-scale.js +249 -0
- package/esm/visibleStatsDomain.d.ts +59 -0
- package/esm/visibleStatsDomain.js +33 -0
- package/esm/wiggleRenderModes.generated.d.ts +9 -0
- package/esm/wiggleRenderModes.generated.js +11 -0
- package/esm/yScaleTicks.d.ts +59 -0
- package/esm/yScaleTicks.js +59 -0
- package/package.json +54 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ScoreAxisConfigModel } from './scoreAxisConfigSchemaFields.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The whole of what `ScoreScaleMixin` needs a composing display to be. Exported
|
|
4
|
+
* because it is the mixin's contract and `ScoreScaleMixin.test.ts` pins it:
|
|
5
|
+
* widen it and the `@ts-expect-error`s there go unused.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScoreScaleHost {
|
|
8
|
+
configuration: ScoreAxisConfigModel;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* #stateModel ScoreScaleMixin
|
|
12
|
+
* #category display
|
|
13
|
+
* #crossCuttingMixin Score axis. Nothing — the config slots. Brings `scaleType` / `autoscaleType` / `minScore` / `maxScore` / `*Bound` / `numStdDev` and their setters, i.e. the whole `ScoreScaleModel` interface the shared score menu and `SetMinMaxDialog` consume
|
|
14
|
+
*
|
|
15
|
+
* The score axis every quantitative display shares: which scale, how to
|
|
16
|
+
* autoscale it, and the manual min/max bounds. This is the runtime half of
|
|
17
|
+
* {@link ScoreScaleModel} in `scoreMenuItems.ts` — that interface is what the
|
|
18
|
+
* shared Score menu, the autoscale/scale submenus and `SetMinMaxDialog` consume,
|
|
19
|
+
* and it was already the canonical contract while two displays hand-wrote
|
|
20
|
+
* identical implementations of it (`WiggleScoreConfigMixin`, and the alignments
|
|
21
|
+
* coverage band). Composing this is now how a display satisfies it, so a new
|
|
22
|
+
* score display cannot satisfy it *partially*.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately just the axis. Colors, `resolution`, cross-hatches and the
|
|
25
|
+
* autoscale *computation* stay in `WiggleScoreConfigMixin` / `WiggleCommonMixin`
|
|
26
|
+
* — the alignments coverage band shares this axis but none of the rest.
|
|
27
|
+
*
|
|
28
|
+
* `minScore`/`maxScore` are the **raw** slot values with their
|
|
29
|
+
* `Number.MIN_VALUE`/`Number.MAX_VALUE` "unset" sentinels intact, because that
|
|
30
|
+
* is what the dialog round-trips; `minScoreBound`/`maxScoreBound` are the
|
|
31
|
+
* resolved bounds, where `undefined` means "autoscale this end". Every consumer
|
|
32
|
+
* that computes a domain reads the `*Bound` pair.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ScoreScaleMixin(): import("@jbrowse/mobx-state-tree").IModelType<{}, {
|
|
35
|
+
/**
|
|
36
|
+
* #getter
|
|
37
|
+
*/
|
|
38
|
+
readonly scaleType: string;
|
|
39
|
+
/**
|
|
40
|
+
* #getter
|
|
41
|
+
*/
|
|
42
|
+
readonly autoscaleType: string;
|
|
43
|
+
/**
|
|
44
|
+
* #getter
|
|
45
|
+
*/
|
|
46
|
+
readonly numStdDev: number;
|
|
47
|
+
/**
|
|
48
|
+
* #getter
|
|
49
|
+
* Raw slot value, sentinel intact — see the class comment.
|
|
50
|
+
*/
|
|
51
|
+
readonly minScore: number;
|
|
52
|
+
/**
|
|
53
|
+
* #getter
|
|
54
|
+
* Raw slot value, sentinel intact — see the class comment.
|
|
55
|
+
*/
|
|
56
|
+
readonly maxScore: number;
|
|
57
|
+
/**
|
|
58
|
+
* #getter
|
|
59
|
+
* Overridable hook: what each end of the domain falls back to where the
|
|
60
|
+
* config leaves its bound unset. `[undefined, undefined]` — the default —
|
|
61
|
+
* means autoscale both ends, which is right for a track whose scores have
|
|
62
|
+
* no absolute meaning (a bigwig's units are its own).
|
|
63
|
+
*
|
|
64
|
+
* A display whose scores are bounded *by construction* overrides it, so
|
|
65
|
+
* the axis stops being a function of what happens to be on screen: GC
|
|
66
|
+
* content is a fraction, so 0 and 1 are its real limits and mean the same
|
|
67
|
+
* thing at every locus. Autoscaled, the same GC value drew at different
|
|
68
|
+
* heights depending on where the user had panned, and the track could not
|
|
69
|
+
* be read across loci.
|
|
70
|
+
*
|
|
71
|
+
* A hook rather than a config default because the answer can depend on
|
|
72
|
+
* display state — GC's does, on `gcMode` — and rather than each display
|
|
73
|
+
* re-resolving the sentinels below, which is the one thing that must not
|
|
74
|
+
* be duplicated: config bounds still win, precisely because they are
|
|
75
|
+
* checked before this is consulted.
|
|
76
|
+
*/
|
|
77
|
+
readonly defaultScoreDomain: [number | undefined, number | undefined];
|
|
78
|
+
/**
|
|
79
|
+
* #getter
|
|
80
|
+
* Resolved lower bound; `undefined` means autoscale this end.
|
|
81
|
+
*/
|
|
82
|
+
readonly minScoreBound: number | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* #getter
|
|
85
|
+
* Resolved upper bound; `undefined` means autoscale this end.
|
|
86
|
+
*/
|
|
87
|
+
readonly maxScoreBound: number | undefined;
|
|
88
|
+
} & {
|
|
89
|
+
/**
|
|
90
|
+
* #action
|
|
91
|
+
*/
|
|
92
|
+
setScaleType(scaleType: string): void;
|
|
93
|
+
/**
|
|
94
|
+
* #action
|
|
95
|
+
*/
|
|
96
|
+
setAutoscale(val?: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* #action
|
|
99
|
+
*/
|
|
100
|
+
setMinScore(val?: number): void;
|
|
101
|
+
/**
|
|
102
|
+
* #action
|
|
103
|
+
*/
|
|
104
|
+
setMaxScore(val?: number): void;
|
|
105
|
+
}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getConf, setConf } from '@jbrowse/core/configuration';
|
|
2
|
+
import { types } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
// The mixin composes onto a display that declares this, not the other way
|
|
4
|
+
// round, so its own `self` isn't typed with it. Cast once, narrowed to the
|
|
5
|
+
// field table beside it rather than `AnyConfigurationModel`, which is what keeps
|
|
6
|
+
// the slot names below checked.
|
|
7
|
+
const confNode = (self) => self;
|
|
8
|
+
/**
|
|
9
|
+
* #stateModel ScoreScaleMixin
|
|
10
|
+
* #category display
|
|
11
|
+
* #crossCuttingMixin Score axis. Nothing — the config slots. Brings `scaleType` / `autoscaleType` / `minScore` / `maxScore` / `*Bound` / `numStdDev` and their setters, i.e. the whole `ScoreScaleModel` interface the shared score menu and `SetMinMaxDialog` consume
|
|
12
|
+
*
|
|
13
|
+
* The score axis every quantitative display shares: which scale, how to
|
|
14
|
+
* autoscale it, and the manual min/max bounds. This is the runtime half of
|
|
15
|
+
* {@link ScoreScaleModel} in `scoreMenuItems.ts` — that interface is what the
|
|
16
|
+
* shared Score menu, the autoscale/scale submenus and `SetMinMaxDialog` consume,
|
|
17
|
+
* and it was already the canonical contract while two displays hand-wrote
|
|
18
|
+
* identical implementations of it (`WiggleScoreConfigMixin`, and the alignments
|
|
19
|
+
* coverage band). Composing this is now how a display satisfies it, so a new
|
|
20
|
+
* score display cannot satisfy it *partially*.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately just the axis. Colors, `resolution`, cross-hatches and the
|
|
23
|
+
* autoscale *computation* stay in `WiggleScoreConfigMixin` / `WiggleCommonMixin`
|
|
24
|
+
* — the alignments coverage band shares this axis but none of the rest.
|
|
25
|
+
*
|
|
26
|
+
* `minScore`/`maxScore` are the **raw** slot values with their
|
|
27
|
+
* `Number.MIN_VALUE`/`Number.MAX_VALUE` "unset" sentinels intact, because that
|
|
28
|
+
* is what the dialog round-trips; `minScoreBound`/`maxScoreBound` are the
|
|
29
|
+
* resolved bounds, where `undefined` means "autoscale this end". Every consumer
|
|
30
|
+
* that computes a domain reads the `*Bound` pair.
|
|
31
|
+
*/
|
|
32
|
+
export function ScoreScaleMixin() {
|
|
33
|
+
return types
|
|
34
|
+
.model('ScoreScaleMixin', {})
|
|
35
|
+
.views(self => ({
|
|
36
|
+
/**
|
|
37
|
+
* #getter
|
|
38
|
+
*/
|
|
39
|
+
get scaleType() {
|
|
40
|
+
return getConf(confNode(self), 'scaleType');
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* #getter
|
|
44
|
+
*/
|
|
45
|
+
get autoscaleType() {
|
|
46
|
+
return getConf(confNode(self), 'autoscale');
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* #getter
|
|
50
|
+
*/
|
|
51
|
+
get numStdDev() {
|
|
52
|
+
return getConf(confNode(self), 'numStdDev');
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* #getter
|
|
56
|
+
* Raw slot value, sentinel intact — see the class comment.
|
|
57
|
+
*/
|
|
58
|
+
get minScore() {
|
|
59
|
+
return getConf(confNode(self), 'minScore');
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* #getter
|
|
63
|
+
* Raw slot value, sentinel intact — see the class comment.
|
|
64
|
+
*/
|
|
65
|
+
get maxScore() {
|
|
66
|
+
return getConf(confNode(self), 'maxScore');
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* #getter
|
|
70
|
+
* Overridable hook: what each end of the domain falls back to where the
|
|
71
|
+
* config leaves its bound unset. `[undefined, undefined]` — the default —
|
|
72
|
+
* means autoscale both ends, which is right for a track whose scores have
|
|
73
|
+
* no absolute meaning (a bigwig's units are its own).
|
|
74
|
+
*
|
|
75
|
+
* A display whose scores are bounded *by construction* overrides it, so
|
|
76
|
+
* the axis stops being a function of what happens to be on screen: GC
|
|
77
|
+
* content is a fraction, so 0 and 1 are its real limits and mean the same
|
|
78
|
+
* thing at every locus. Autoscaled, the same GC value drew at different
|
|
79
|
+
* heights depending on where the user had panned, and the track could not
|
|
80
|
+
* be read across loci.
|
|
81
|
+
*
|
|
82
|
+
* A hook rather than a config default because the answer can depend on
|
|
83
|
+
* display state — GC's does, on `gcMode` — and rather than each display
|
|
84
|
+
* re-resolving the sentinels below, which is the one thing that must not
|
|
85
|
+
* be duplicated: config bounds still win, precisely because they are
|
|
86
|
+
* checked before this is consulted.
|
|
87
|
+
*/
|
|
88
|
+
get defaultScoreDomain() {
|
|
89
|
+
return [undefined, undefined];
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* #getter
|
|
93
|
+
* Resolved lower bound; `undefined` means autoscale this end.
|
|
94
|
+
*/
|
|
95
|
+
get minScoreBound() {
|
|
96
|
+
return this.minScore === Number.MIN_VALUE
|
|
97
|
+
? this.defaultScoreDomain[0]
|
|
98
|
+
: this.minScore;
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* #getter
|
|
102
|
+
* Resolved upper bound; `undefined` means autoscale this end.
|
|
103
|
+
*/
|
|
104
|
+
get maxScoreBound() {
|
|
105
|
+
return this.maxScore === Number.MAX_VALUE
|
|
106
|
+
? this.defaultScoreDomain[1]
|
|
107
|
+
: this.maxScore;
|
|
108
|
+
},
|
|
109
|
+
}))
|
|
110
|
+
.actions(self => ({
|
|
111
|
+
/**
|
|
112
|
+
* #action
|
|
113
|
+
*/
|
|
114
|
+
setScaleType(scaleType) {
|
|
115
|
+
setConf(confNode(self), 'scaleType', scaleType);
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* #action
|
|
119
|
+
*/
|
|
120
|
+
setAutoscale(val) {
|
|
121
|
+
setConf(confNode(self), 'autoscale', val);
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* #action
|
|
125
|
+
*/
|
|
126
|
+
setMinScore(val) {
|
|
127
|
+
setConf(confNode(self), 'minScore', val);
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* #action
|
|
131
|
+
*/
|
|
132
|
+
setMaxScore(val) {
|
|
133
|
+
setConf(confNode(self), 'maxScore', val);
|
|
134
|
+
},
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const _default: (props: {
|
|
2
|
+
model: {
|
|
3
|
+
minScore: number;
|
|
4
|
+
maxScore: number;
|
|
5
|
+
scaleType: string;
|
|
6
|
+
setMinScore: (arg?: number) => void;
|
|
7
|
+
setMaxScore: (arg?: number) => void;
|
|
8
|
+
};
|
|
9
|
+
handleClose: () => void;
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { NumberTextField, SubmitDialog } from '@jbrowse/core/ui';
|
|
4
|
+
import { Typography } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
6
|
+
export default observer(function SetMinMaxDialog(props) {
|
|
7
|
+
const { model, handleClose } = props;
|
|
8
|
+
const { minScore, maxScore, scaleType } = model;
|
|
9
|
+
const [min, setMin] = useState(minScore === Number.MIN_VALUE ? undefined : minScore);
|
|
10
|
+
const [max, setMax] = useState(maxScore === Number.MAX_VALUE ? undefined : maxScore);
|
|
11
|
+
const rangeOk = min === undefined || max === undefined || max > min;
|
|
12
|
+
const logOk = !(scaleType === 'log' && min !== undefined && min <= 0);
|
|
13
|
+
return (_jsxs(SubmitDialog, { open: true, title: "Set min/max score for track", submitDisabled: !rangeOk || !logOk, onCancel: handleClose, onSubmit: () => {
|
|
14
|
+
model.setMinScore(min);
|
|
15
|
+
model.setMaxScore(max);
|
|
16
|
+
handleClose();
|
|
17
|
+
}, children: [_jsx(Typography, { children: "Enter min/max score: " }), !rangeOk ? (_jsx(Typography, { color: "error", children: "Max must be greater than min" })) : null, !logOk ? (_jsx(Typography, { color: "error", children: "Min score should be greater than 0 for log scale" })) : null, _jsx(NumberTextField, { defaultValue: min, onValueChange: setMin, placeholder: "Enter min score" }), _jsx(NumberTextField, { defaultValue: max, onValueChange: setMax, placeholder: "Enter max score" })] }));
|
|
18
|
+
});
|
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, insetLabels, }) {
|
|
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 `insetLabels` 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 (!insetLabels) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const inset = Math.min(Math.max(sy, yTop + LABEL_HALF_HEIGHT_PX), yBottom - 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,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import YScaleBar from './YScaleBar.js';
|
|
3
|
+
import { ONSCREEN_AXIS_LEFT_PX } from './constants.js';
|
|
4
|
+
// Y-axis tick labels positioned absolutely, indented from the track's left edge
|
|
5
|
+
// so a right-oriented axis reads inside the plot. The SVG export instead anchors
|
|
6
|
+
// a left-oriented axis at the content edge, putting the labels in the export
|
|
7
|
+
// margin — see WiggleFamilySvgFrame.
|
|
8
|
+
//
|
|
9
|
+
// `width` is the track's, not a fixed strip: an <svg> clips to its own box, and
|
|
10
|
+
// the hardcoded 70px this used to carry silently truncated any label past ~11
|
|
11
|
+
// characters. Not hypothetical on a log axis — a domain under 1 nices to
|
|
12
|
+
// negative powers of two, so `0.0009765625` is an ordinary tick on a
|
|
13
|
+
// mappability track and the labels grow exactly where the numbers get small.
|
|
14
|
+
// The track's own right edge is the real boundary.
|
|
15
|
+
export default function YScaleBarOverlay({ ticks, height, width, }) {
|
|
16
|
+
return (_jsx("svg", { style: {
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
top: 0,
|
|
19
|
+
left: ONSCREEN_AXIS_LEFT_PX,
|
|
20
|
+
pointerEvents: 'none',
|
|
21
|
+
height,
|
|
22
|
+
width: Math.max(0, width - ONSCREEN_AXIS_LEFT_PX),
|
|
23
|
+
}, children: _jsx(YScaleBar, { ticks: ticks, orientation: "right" }) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const DEFAULT_AUTOSCALE_OPTIONS: [string, string][];
|
|
2
|
+
export interface FeatureArrays {
|
|
3
|
+
featurePositions: Uint32Array;
|
|
4
|
+
featureScores: Float32Array;
|
|
5
|
+
featureMinScores: Float32Array;
|
|
6
|
+
featureMaxScores: Float32Array;
|
|
7
|
+
numFeatures: number;
|
|
8
|
+
hasSummaryScores: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface Dataset {
|
|
11
|
+
data: FeatureArrays;
|
|
12
|
+
visStart?: number;
|
|
13
|
+
visEnd?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ScoreStats {
|
|
16
|
+
scoreMin: number;
|
|
17
|
+
scoreMax: number;
|
|
18
|
+
scoreMean: number;
|
|
19
|
+
scoreStdDev: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* #api
|
|
23
|
+
* Per-feature scalar score array for a summary mode: the min/max summary array
|
|
24
|
+
* for `'min'`/`'max'`, otherwise the average score.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getEffectiveScores(data: {
|
|
27
|
+
featureScores: Float32Array;
|
|
28
|
+
featureMinScores: Float32Array;
|
|
29
|
+
featureMaxScores: Float32Array;
|
|
30
|
+
}, summaryScoreMode: string): Float32Array<ArrayBufferLike>;
|
|
31
|
+
export declare function computeScoreStats(summaryScoreMode: string, datasets: Dataset[]): ScoreStats | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* #api
|
|
34
|
+
* Converts score stats into a `[min, max]` domain, applying std-dev
|
|
35
|
+
* expansion for the `localsd` autoscale type.
|
|
36
|
+
*/
|
|
37
|
+
export declare function domainFromStats(stats: ScoreStats, autoscaleType: string, numStdDev: number): [number, number];
|
|
38
|
+
export declare function autoscaleDomainFromStats({ stats, autoscaleType, summaryScoreMode, numStdDev, numQuantile, visibleEntries, }: {
|
|
39
|
+
stats: ScoreStats;
|
|
40
|
+
autoscaleType: string;
|
|
41
|
+
summaryScoreMode: string;
|
|
42
|
+
numStdDev: number;
|
|
43
|
+
numQuantile?: number;
|
|
44
|
+
visibleEntries: Dataset[];
|
|
45
|
+
}): [number, number];
|
|
46
|
+
/**
|
|
47
|
+
* #api
|
|
48
|
+
* Computes a score domain from the visible feature arrays for the `local` /
|
|
49
|
+
* `localsd` / `localpercentile` autoscale types.
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeAutoscaleDomain(autoscaleType: string, summaryScoreMode: string, numStdDev: number, visibleEntries: {
|
|
52
|
+
data: FeatureArrays;
|
|
53
|
+
visStart: number;
|
|
54
|
+
visEnd: number;
|
|
55
|
+
}[], numQuantile?: number): [number, number] | undefined;
|
package/esm/autoscale.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Default autoscale modes shared by wiggle / multi-wiggle. The alignments
|
|
2
|
+
// coverage band exposes only a subset and a dynamic σ value, so it passes its
|
|
3
|
+
// own option list.
|
|
4
|
+
//
|
|
5
|
+
// Here rather than beside the menu that renders it because a `[value, label]`
|
|
6
|
+
// table is the one thing a menu holds that something outside the app needs to
|
|
7
|
+
// read: the website's figure recipes name these labels in a click path, and a
|
|
8
|
+
// module importing React or a lazy .tsx cannot be loaded by the node script
|
|
9
|
+
// that builds them. A leaf module makes the recipe import the label instead of
|
|
10
|
+
// retyping it, which is the difference between a copy that drifts and one that
|
|
11
|
+
// cannot.
|
|
12
|
+
export const DEFAULT_AUTOSCALE_OPTIONS = [
|
|
13
|
+
['local', 'Local'],
|
|
14
|
+
['localpercentile', 'Local (99th percentile)'],
|
|
15
|
+
['localsd', 'Local ± 3σ'],
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* #api
|
|
19
|
+
* Per-feature scalar score array for a summary mode: the min/max summary array
|
|
20
|
+
* for `'min'`/`'max'`, otherwise the average score.
|
|
21
|
+
*/
|
|
22
|
+
export function getEffectiveScores(data, summaryScoreMode) {
|
|
23
|
+
return summaryScoreMode === 'min'
|
|
24
|
+
? data.featureMinScores
|
|
25
|
+
: summaryScoreMode === 'max'
|
|
26
|
+
? data.featureMaxScores
|
|
27
|
+
: data.featureScores;
|
|
28
|
+
}
|
|
29
|
+
// Half-open overlap test between a feature span and the visible window.
|
|
30
|
+
function overlaps(fStart, fEnd, visStart, visEnd) {
|
|
31
|
+
return fEnd > visStart && fStart < visEnd;
|
|
32
|
+
}
|
|
33
|
+
// Which per-feature array each end of the domain comes from. Whiskers spreads
|
|
34
|
+
// the two ends across the min/max summary arrays; every other mode draws both
|
|
35
|
+
// from a single scalar. One table because `computeScoreStats` and the
|
|
36
|
+
// percentile histogram have to agree — a domain whose extent and whose clipped
|
|
37
|
+
// bound were measured off different arrays is a domain that clips its own data.
|
|
38
|
+
function boundArrays(summaryScoreMode) {
|
|
39
|
+
const useWhiskers = summaryScoreMode === 'whiskers';
|
|
40
|
+
return {
|
|
41
|
+
low: (data) => useWhiskers
|
|
42
|
+
? data.featureMinScores
|
|
43
|
+
: getEffectiveScores(data, summaryScoreMode),
|
|
44
|
+
high: (data) => useWhiskers
|
|
45
|
+
? data.featureMaxScores
|
|
46
|
+
: getEffectiveScores(data, summaryScoreMode),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Bin count for the approximate percentile histogram. 1024 bins gives ~0.1%
|
|
50
|
+
// resolution on the domain, which is finer than any autoscale needs and keeps
|
|
51
|
+
// the second pass O(n) with a fixed, trivial allocation.
|
|
52
|
+
const NUM_HISTOGRAM_BINS = 1024;
|
|
53
|
+
// First index whose feature STARTS at or after `bp`. `featurePositions` is
|
|
54
|
+
// sorted by start — the same property `findFeatureAtBp` binary-searches on.
|
|
55
|
+
function lowerBoundByStart(featurePositions, numFeatures, bp) {
|
|
56
|
+
let lo = 0;
|
|
57
|
+
let hi = numFeatures;
|
|
58
|
+
while (lo < hi) {
|
|
59
|
+
const mid = (lo + hi) >> 1;
|
|
60
|
+
if (featurePositions[mid * 2] >= bp) {
|
|
61
|
+
hi = mid;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lo = mid + 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return lo;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The half-open index range that can overlap `[visStart, visEnd)`.
|
|
71
|
+
*
|
|
72
|
+
* A fetch covers `bufferedVisibleRegions` — the viewport plus half a screen on
|
|
73
|
+
* each side — so roughly half of what these passes walk is off-screen, and
|
|
74
|
+
* `localpercentile` (the default) walks it two or three times over. Both bounds
|
|
75
|
+
* come from a binary search on the sorted starts instead.
|
|
76
|
+
*
|
|
77
|
+
* The upper bound needs nothing but that sortedness: a feature starting at or
|
|
78
|
+
* after `visEnd` cannot reach back into the window. The lower bound also leans
|
|
79
|
+
* on wiggle features being non-overlapping bins — bigWig summary levels and
|
|
80
|
+
* bedGraph both are — walking back over any run that does reach in. The callers
|
|
81
|
+
* still test `overlaps` per feature inside the range, so a dataset that broke
|
|
82
|
+
* that assumption could only lose a long early feature, never gain one.
|
|
83
|
+
*/
|
|
84
|
+
function visibleIndexRange(featurePositions, numFeatures, visStart, visEnd) {
|
|
85
|
+
if (visStart === undefined || visEnd === undefined) {
|
|
86
|
+
return { from: 0, to: numFeatures };
|
|
87
|
+
}
|
|
88
|
+
const to = lowerBoundByStart(featurePositions, numFeatures, visEnd);
|
|
89
|
+
let from = lowerBoundByStart(featurePositions, numFeatures, visStart);
|
|
90
|
+
while (from > 0 && featurePositions[(from - 1) * 2 + 1] > visStart) {
|
|
91
|
+
from--;
|
|
92
|
+
}
|
|
93
|
+
return { from, to };
|
|
94
|
+
}
|
|
95
|
+
// Min/max/mean/stddev of the visible features for a summary mode, in one pass.
|
|
96
|
+
// Exported (not #api — internal plumbing shared with the wiggle displays) so a
|
|
97
|
+
// caller needing both a domain and the raw extent computes the stats once and
|
|
98
|
+
// feeds them to autoscaleDomainFromStats instead of walking the arrays twice.
|
|
99
|
+
export function computeScoreStats(summaryScoreMode, datasets) {
|
|
100
|
+
const { low, high } = boundArrays(summaryScoreMode);
|
|
101
|
+
let min = Infinity;
|
|
102
|
+
let max = -Infinity;
|
|
103
|
+
let sum = 0;
|
|
104
|
+
let sumSq = 0;
|
|
105
|
+
let count = 0;
|
|
106
|
+
for (const { data, visStart, visEnd } of datasets) {
|
|
107
|
+
const { featureScores, featurePositions, numFeatures } = data;
|
|
108
|
+
// Selecting the arrays once per dataset keeps the mode check out of the
|
|
109
|
+
// per-feature loop.
|
|
110
|
+
const minScores = low(data);
|
|
111
|
+
const maxScores = high(data);
|
|
112
|
+
const { from, to } = visibleIndexRange(featurePositions, numFeatures, visStart, visEnd);
|
|
113
|
+
for (let i = from; i < to; i++) {
|
|
114
|
+
if (visStart !== undefined &&
|
|
115
|
+
visEnd !== undefined &&
|
|
116
|
+
!overlaps(featurePositions[i * 2], featurePositions[i * 2 + 1], visStart, visEnd)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
min = Math.min(min, minScores[i]);
|
|
120
|
+
max = Math.max(max, maxScores[i]);
|
|
121
|
+
// Mean/stddev always use featureScores (the average) regardless of
|
|
122
|
+
// summaryScoreMode; min/max for the domain bounds come from the mode-
|
|
123
|
+
// selected arrays above. Intentional: sd-based autoscale centers on the
|
|
124
|
+
// average-value distribution even in whiskers/min/max summary modes.
|
|
125
|
+
const avg = featureScores[i];
|
|
126
|
+
sum += avg;
|
|
127
|
+
sumSq += avg * avg;
|
|
128
|
+
count++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (count === 0 || !Number.isFinite(min) || !Number.isFinite(max)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const mean = sum / count;
|
|
135
|
+
const stdDev = Math.sqrt(Math.max(0, sumSq / count - mean * mean));
|
|
136
|
+
return { scoreMin: min, scoreMax: max, scoreMean: mean, scoreStdDev: stdDev };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* #api
|
|
140
|
+
* Converts score stats into a `[min, max]` domain, applying std-dev
|
|
141
|
+
* expansion for the `localsd` autoscale type.
|
|
142
|
+
*/
|
|
143
|
+
export function domainFromStats(stats, autoscaleType, numStdDev) {
|
|
144
|
+
if (autoscaleType === 'localsd') {
|
|
145
|
+
const { scoreMean, scoreStdDev, scoreMin } = stats;
|
|
146
|
+
return [
|
|
147
|
+
scoreMin >= 0 ? 0 : scoreMean - numStdDev * scoreStdDev,
|
|
148
|
+
scoreMean + numStdDev * scoreStdDev,
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
return [stats.scoreMin, stats.scoreMax];
|
|
152
|
+
}
|
|
153
|
+
// The `quantile`-th percentile magnitude of one signed side of the score
|
|
154
|
+
// distribution: features are filtered to a single sign (`positiveSide`), their
|
|
155
|
+
// magnitudes binned over `[0, maxMag]`, and the magnitude below which `quantile`
|
|
156
|
+
// of that side's mass falls is returned — clipping the outermost `1 - quantile`
|
|
157
|
+
// as outliers. Returns 0 when the side is empty. A fixed histogram keeps this an
|
|
158
|
+
// O(n) pass with no sort, approximate to ~1/NUM_HISTOGRAM_BINS of maxMag, far
|
|
159
|
+
// finer than the display needs.
|
|
160
|
+
function sideMagnitudePercentile(datasets, scoresFor, positiveSide, maxMag, quantile) {
|
|
161
|
+
if (maxMag <= 0) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
const bins = new Int32Array(NUM_HISTOGRAM_BINS);
|
|
165
|
+
const scale = NUM_HISTOGRAM_BINS / maxMag;
|
|
166
|
+
let count = 0;
|
|
167
|
+
for (const { data, visStart, visEnd } of datasets) {
|
|
168
|
+
const { featurePositions, numFeatures } = data;
|
|
169
|
+
const scores = scoresFor(data);
|
|
170
|
+
const { from, to } = visibleIndexRange(featurePositions, numFeatures, visStart, visEnd);
|
|
171
|
+
for (let i = from; i < to; i++) {
|
|
172
|
+
if (visStart !== undefined &&
|
|
173
|
+
visEnd !== undefined &&
|
|
174
|
+
!overlaps(featurePositions[i * 2], featurePositions[i * 2 + 1], visStart, visEnd)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const mag = positiveSide ? scores[i] : -scores[i];
|
|
178
|
+
if (mag > 0) {
|
|
179
|
+
const bin = Math.min(NUM_HISTOGRAM_BINS - 1, Math.floor(mag * scale));
|
|
180
|
+
bins[bin]++;
|
|
181
|
+
count++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (count === 0) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const target = quantile * count;
|
|
189
|
+
let cumulative = 0;
|
|
190
|
+
for (let bin = 0; bin < NUM_HISTOGRAM_BINS; bin++) {
|
|
191
|
+
cumulative += bins[bin];
|
|
192
|
+
// +1 so the target bin is included rather than clipped.
|
|
193
|
+
if (cumulative >= target) {
|
|
194
|
+
return ((bin + 1) / NUM_HISTOGRAM_BINS) * maxMag;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return maxMag;
|
|
198
|
+
}
|
|
199
|
+
// Builds a `[low, high]` domain by clipping each side of the score distribution
|
|
200
|
+
// to its central `quantile` fraction (e.g. 0.99 → clip the outermost 1% of each
|
|
201
|
+
// sign). Unlike localsd it makes no normality assumption, so it stays robust on
|
|
202
|
+
// the heavily skewed score distributions typical of coverage/wiggle data.
|
|
203
|
+
//
|
|
204
|
+
// The two signs are clipped INDEPENDENTLY, anchored at 0. A single combined
|
|
205
|
+
// percentile spends its whole budget on the dominant side, so on strongly
|
|
206
|
+
// one-sided signed data (e.g. phyloP: mostly-positive conservation with a
|
|
207
|
+
// sparse, small negative tail) the minority tail's 1st percentile lands at or
|
|
208
|
+
// above 0 and the negative extent collapses to a flat band. Measuring each
|
|
209
|
+
// side's percentile from 0 outward keeps a small-but-real opposite tail visible.
|
|
210
|
+
function percentileDomainFromHistogram(stats, summaryScoreMode, quantile, datasets) {
|
|
211
|
+
const { scoreMin, scoreMax } = stats;
|
|
212
|
+
if (scoreMax - scoreMin <= 0) {
|
|
213
|
+
return [scoreMin, scoreMax];
|
|
214
|
+
}
|
|
215
|
+
const arrays = boundArrays(summaryScoreMode);
|
|
216
|
+
const high = scoreMax > 0
|
|
217
|
+
? sideMagnitudePercentile(datasets, arrays.high, true, scoreMax, quantile)
|
|
218
|
+
: 0;
|
|
219
|
+
const negExtent = scoreMin < 0
|
|
220
|
+
? sideMagnitudePercentile(datasets, arrays.low, false, -scoreMin, quantile)
|
|
221
|
+
: 0;
|
|
222
|
+
// Anchor the low bound at 0 for all-positive data (matching localsd's origin
|
|
223
|
+
// convention); otherwise extend it to the negative side's clipped extent.
|
|
224
|
+
return [scoreMin < 0 ? -negExtent : 0, high];
|
|
225
|
+
}
|
|
226
|
+
// Turns already-computed stats into the displayed domain for the `local` /
|
|
227
|
+
// `localsd` / `localpercentile` autoscale types. `localpercentile` re-walks the
|
|
228
|
+
// entries to build its histogram; the other types read the stats alone.
|
|
229
|
+
// computeAutoscaleDomain (#api) is the one-shot form.
|
|
230
|
+
export function autoscaleDomainFromStats({ stats, autoscaleType, summaryScoreMode, numStdDev, numQuantile = 0.99, visibleEntries, }) {
|
|
231
|
+
return autoscaleType === 'localpercentile'
|
|
232
|
+
? percentileDomainFromHistogram(stats, summaryScoreMode, numQuantile, visibleEntries)
|
|
233
|
+
: domainFromStats(stats, autoscaleType, numStdDev);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* #api
|
|
237
|
+
* Computes a score domain from the visible feature arrays for the `local` /
|
|
238
|
+
* `localsd` / `localpercentile` autoscale types.
|
|
239
|
+
*/
|
|
240
|
+
export function computeAutoscaleDomain(autoscaleType, summaryScoreMode, numStdDev, visibleEntries, numQuantile = 0.99) {
|
|
241
|
+
const stats = computeScoreStats(summaryScoreMode, visibleEntries);
|
|
242
|
+
return stats
|
|
243
|
+
? autoscaleDomainFromStats({
|
|
244
|
+
stats,
|
|
245
|
+
autoscaleType,
|
|
246
|
+
summaryScoreMode,
|
|
247
|
+
numStdDev,
|
|
248
|
+
numQuantile,
|
|
249
|
+
visibleEntries,
|
|
250
|
+
})
|
|
251
|
+
: undefined;
|
|
252
|
+
}
|