@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
package/esm/scale.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `getScale` hands back: callable, plus the domain and ticks its callers
|
|
3
|
+
* read. The scales underneath are pure functions over a {@link ScaleSpec}, so
|
|
4
|
+
* this has no setters — the domain and range come in through `getScale`.
|
|
5
|
+
*/
|
|
6
|
+
export interface Scale {
|
|
7
|
+
(x: number): number;
|
|
8
|
+
domain(): [number, number];
|
|
9
|
+
range(): [number, number];
|
|
10
|
+
ticks(count?: number): number[];
|
|
11
|
+
}
|
|
12
|
+
export interface ScaleOpts {
|
|
13
|
+
domain: number[];
|
|
14
|
+
range: number[];
|
|
15
|
+
scaleType: string;
|
|
16
|
+
/**
|
|
17
|
+
* symlog's linear-region width. `0` (the config default) resolves from the
|
|
18
|
+
* domain — see {@link resolveSymlogConstant}. Ignored by the other scales.
|
|
19
|
+
*/
|
|
20
|
+
symlogConstant?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Round the domain to nice endpoints before building the scale. Defaults to
|
|
23
|
+
* true, which is what a caller handing over raw data wants. Pass false when
|
|
24
|
+
* the domain is already the one something else is drawing with — nicing it
|
|
25
|
+
* again moves the axis off that drawing, and can invent an endpoint outside
|
|
26
|
+
* it (a raw `[1, 1000]` nices to `[1, 1024]`, so the axis labels a tick at a
|
|
27
|
+
* value the data never reaches).
|
|
28
|
+
*/
|
|
29
|
+
nice?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* #api
|
|
33
|
+
* Builds a d3 scale (linear/log/symlog) from a `ScaleOpts`, nicing the domain
|
|
34
|
+
* unless `nice: false` says it is already the one being drawn with.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getScale({ domain, range, scaleType, symlogConstant, nice, }: ScaleOpts): Scale;
|
|
37
|
+
/**
|
|
38
|
+
* #api
|
|
39
|
+
* Rounds a domain to "nice" endpoints, clamped to the origin. An end given an
|
|
40
|
+
* explicit `bounds` value keeps that value exactly — only an autoscaled end is
|
|
41
|
+
* rounded. A log scale's floor still outranks a bound it cannot hold.
|
|
42
|
+
*
|
|
43
|
+
* The result never descends: a bound that would put `min` above `max` widens
|
|
44
|
+
* the other end instead, so no consumer has to guess what a backwards domain
|
|
45
|
+
* means.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getNiceDomain({ scaleType, domain, bounds, }: {
|
|
48
|
+
scaleType: string;
|
|
49
|
+
domain: readonly [number, number];
|
|
50
|
+
bounds: readonly [number | undefined, number | undefined];
|
|
51
|
+
}): [number, number];
|
|
52
|
+
/**
|
|
53
|
+
* #api
|
|
54
|
+
* Returns a niced `{min, max}` domain for a maximum score value.
|
|
55
|
+
* Uses log base-2 when `useLogScale` is true (domain is clamped to [1, max]).
|
|
56
|
+
*/
|
|
57
|
+
export declare function getNiceScale(maxScore: number, useLogScale?: boolean): {
|
|
58
|
+
min: number;
|
|
59
|
+
max: number;
|
|
60
|
+
};
|
package/esm/scale.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { resolveSymlogConstant } from './normalize.js';
|
|
2
|
+
import { symlogTicks } from './symlogTicks.js';
|
|
3
|
+
import { niceDomain, scaleTicks, scaleValue, } from './vendor/d3-scale.js';
|
|
4
|
+
function buildScale(spec) {
|
|
5
|
+
const scale = ((x) => scaleValue(spec, x));
|
|
6
|
+
scale.domain = () => [...spec.domain];
|
|
7
|
+
scale.range = () => [...spec.range];
|
|
8
|
+
// symlog gets its ticks spaced down the axis rather than across the scores —
|
|
9
|
+
// see symlogTicks, which is this module's choice and not d3's. `scaleTicks`
|
|
10
|
+
// goes on answering exactly what d3 answers for all three kinds.
|
|
11
|
+
scale.ticks = (count) => spec.kind === 'symlog'
|
|
12
|
+
? symlogTicks(spec.domain, spec.constant ?? 1, count)
|
|
13
|
+
: scaleTicks(spec, count);
|
|
14
|
+
return scale;
|
|
15
|
+
}
|
|
16
|
+
function specForType(scaleType, domain, symlogConstant = 0) {
|
|
17
|
+
const range = [0, 1];
|
|
18
|
+
if (scaleType === 'linear') {
|
|
19
|
+
return { kind: 'linear', domain, range };
|
|
20
|
+
}
|
|
21
|
+
if (scaleType === 'log') {
|
|
22
|
+
return { kind: 'log', base: 2, domain, range };
|
|
23
|
+
}
|
|
24
|
+
if (scaleType === 'symlog') {
|
|
25
|
+
// The axis has to be built with the same constant the renderer normalizes
|
|
26
|
+
// with, or the ticks label positions the bars are not drawn at. Both
|
|
27
|
+
// resolve through resolveSymlogConstant from the same domain.
|
|
28
|
+
const [min, max] = domain;
|
|
29
|
+
return {
|
|
30
|
+
kind: 'symlog',
|
|
31
|
+
constant: resolveSymlogConstant(min, max, symlogConstant),
|
|
32
|
+
domain,
|
|
33
|
+
range,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`undefined scaleType: ${scaleType}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* #api
|
|
40
|
+
* Builds a d3 scale (linear/log/symlog) from a `ScaleOpts`, nicing the domain
|
|
41
|
+
* unless `nice: false` says it is already the one being drawn with.
|
|
42
|
+
*/
|
|
43
|
+
export function getScale({ domain, range, scaleType, symlogConstant, nice = true, }) {
|
|
44
|
+
const [min, max] = domain;
|
|
45
|
+
if (min === undefined || max === undefined) {
|
|
46
|
+
throw new Error('invalid domain');
|
|
47
|
+
}
|
|
48
|
+
const [rangeMin, rangeMax] = range;
|
|
49
|
+
if (rangeMin === undefined || rangeMax === undefined) {
|
|
50
|
+
throw new Error('invalid range');
|
|
51
|
+
}
|
|
52
|
+
// Nice first, then build the spec from the domain that came out. The renderer
|
|
53
|
+
// resolves symlog's constant from the domain it is handed, so a spec built
|
|
54
|
+
// before nicing would give the axis a different constant and label heights
|
|
55
|
+
// the bars are not drawn at. `niceDomain` ignores the constant, so it can
|
|
56
|
+
// take a throwaway spec.
|
|
57
|
+
const nicedDomain = nice
|
|
58
|
+
? niceDomain(specForType(scaleType, [min, max]))
|
|
59
|
+
: [min, max];
|
|
60
|
+
return buildScale({
|
|
61
|
+
...specForType(scaleType, nicedDomain, symlogConstant),
|
|
62
|
+
range: [rangeMin, rangeMax],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* #api
|
|
67
|
+
* Rounds a domain to "nice" endpoints, clamped to the origin. An end given an
|
|
68
|
+
* explicit `bounds` value keeps that value exactly — only an autoscaled end is
|
|
69
|
+
* rounded. A log scale's floor still outranks a bound it cannot hold.
|
|
70
|
+
*
|
|
71
|
+
* The result never descends: a bound that would put `min` above `max` widens
|
|
72
|
+
* the other end instead, so no consumer has to guess what a backwards domain
|
|
73
|
+
* means.
|
|
74
|
+
*/
|
|
75
|
+
// No `symlogConstant` parameter: `niceDomain` puts symlog through d3's linear
|
|
76
|
+
// path, which never reads the constant, so one passed here only looked like it
|
|
77
|
+
// did something.
|
|
78
|
+
export function getNiceDomain({ scaleType, domain, bounds, }) {
|
|
79
|
+
const [minScore, maxScore] = bounds;
|
|
80
|
+
let [min, max] = domain;
|
|
81
|
+
// symlog joins linear here rather than log: it is defined at 0, so the
|
|
82
|
+
// domain should reach the baseline its bars grow from instead of being
|
|
83
|
+
// floored off it the way a log domain has to be.
|
|
84
|
+
if (scaleType === 'linear' || scaleType === 'symlog') {
|
|
85
|
+
if (max < 0) {
|
|
86
|
+
max = 0;
|
|
87
|
+
}
|
|
88
|
+
if (min > 0) {
|
|
89
|
+
min = 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (scaleType === 'log') {
|
|
93
|
+
if (min >= 0 && max > 1) {
|
|
94
|
+
min = 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (minScore !== undefined) {
|
|
98
|
+
min = minScore;
|
|
99
|
+
}
|
|
100
|
+
if (maxScore !== undefined) {
|
|
101
|
+
max = maxScore;
|
|
102
|
+
}
|
|
103
|
+
if (scaleType === 'log') {
|
|
104
|
+
// d3 scaleLog is undefined at values <= 0, so autoscale (esp. localsd) or
|
|
105
|
+
// data that crosses zero (e.g. log-ratio bigwigs) would otherwise yield a
|
|
106
|
+
// domain that produces NaN ticks and a blank plot. Floor the domain to a
|
|
107
|
+
// positive, non-degenerate range so the axis renders its valid portion
|
|
108
|
+
// instead of silently disappearing.
|
|
109
|
+
if (min <= 0) {
|
|
110
|
+
min = max > 1 ? 1 : max > 0 ? max / 100 : 1;
|
|
111
|
+
}
|
|
112
|
+
if (max <= min) {
|
|
113
|
+
max = min * 2;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (max < min) {
|
|
117
|
+
// A bound can invert the domain — `minScore: 200` against an autoscaled max
|
|
118
|
+
// of 60, or `maxScore: -100` against an autoscaled min of -40. A descending
|
|
119
|
+
// domain is not a picture: the two score normalizers disagree about it, so
|
|
120
|
+
// the same view draws an empty band on the GPU and a solid full-height
|
|
121
|
+
// block in an SVG export. Widen whichever end the user did not pin, keeping
|
|
122
|
+
// the one they did; with both pinned there is no free end, so order them.
|
|
123
|
+
// (log has its own version above — its positivity floor has already moved
|
|
124
|
+
// `min`, so it can only ever widen upward.)
|
|
125
|
+
if (maxScore === undefined) {
|
|
126
|
+
max = min * 2;
|
|
127
|
+
}
|
|
128
|
+
else if (minScore === undefined) {
|
|
129
|
+
min = max * 2;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
;
|
|
133
|
+
[min, max] = [max, min];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Rounding is for an autoscaled end, whose only meaning is "roughly the
|
|
137
|
+
// data's extent". A bound the user typed into Set min/max score means itself,
|
|
138
|
+
// and the menu row goes on displaying it, so it is taken as given.
|
|
139
|
+
//
|
|
140
|
+
// `min`/`max` rather than `minScore`/`maxScore`: the log guards above may
|
|
141
|
+
// have moved a bound scaleLog cannot hold, and that correction has to
|
|
142
|
+
// survive.
|
|
143
|
+
const [nicedMin, nicedMax] = niceDomain(specForType(scaleType, [min, max]));
|
|
144
|
+
return [
|
|
145
|
+
minScore === undefined ? nicedMin : min,
|
|
146
|
+
maxScore === undefined ? nicedMax : max,
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* #api
|
|
151
|
+
* Returns a niced `{min, max}` domain for a maximum score value.
|
|
152
|
+
* Uses log base-2 when `useLogScale` is true (domain is clamped to [1, max]).
|
|
153
|
+
*/
|
|
154
|
+
export function getNiceScale(maxScore, useLogScale) {
|
|
155
|
+
// A log scale needs a domain strictly inside its base, so guard against
|
|
156
|
+
// degenerate KR-normalized data where the top count is < 2.
|
|
157
|
+
const [min, max] = useLogScale
|
|
158
|
+
? niceDomain(specForType('log', [1, Math.max(2, maxScore)]))
|
|
159
|
+
: niceDomain(specForType('linear', [0, maxScore]));
|
|
160
|
+
return { min, max };
|
|
161
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ConfigModelForFields } from '@jbrowse/core/configuration';
|
|
2
|
+
export declare const scoreAxisConfigSchemaFields: {
|
|
3
|
+
readonly minScore: {
|
|
4
|
+
readonly type: 'number';
|
|
5
|
+
readonly defaultValue: number;
|
|
6
|
+
readonly description: 'Fixed minimum score bound. The default (Number.MIN_VALUE) is a sentinel meaning "unset, use autoscale"';
|
|
7
|
+
readonly advanced: true;
|
|
8
|
+
};
|
|
9
|
+
readonly maxScore: {
|
|
10
|
+
readonly type: 'number';
|
|
11
|
+
readonly defaultValue: number;
|
|
12
|
+
readonly description: 'Fixed maximum score bound. The default (Number.MAX_VALUE) is a sentinel meaning "unset, use autoscale"';
|
|
13
|
+
readonly advanced: true;
|
|
14
|
+
};
|
|
15
|
+
readonly scaleType: {
|
|
16
|
+
readonly type: 'stringEnum';
|
|
17
|
+
readonly model: import("@jbrowse/mobx-state-tree").ISimpleType<"linear" | "log">;
|
|
18
|
+
readonly defaultValue: 'linear';
|
|
19
|
+
readonly description: 'Scale type (linear or log)';
|
|
20
|
+
};
|
|
21
|
+
readonly autoscale: {
|
|
22
|
+
readonly type: 'stringEnum';
|
|
23
|
+
readonly model: import("@jbrowse/mobx-state-tree").ISimpleType<"local" | "localpercentile" | "localsd">;
|
|
24
|
+
readonly defaultValue: 'localpercentile';
|
|
25
|
+
readonly description: 'Autoscale type: "local" uses the min/max in the visible region, "localsd" uses mean ± numStdDev standard deviations, "localpercentile" uses the numQuantile-th percentile score as the max (robust to skewed/peaky data)';
|
|
26
|
+
};
|
|
27
|
+
readonly numStdDev: {
|
|
28
|
+
readonly type: 'number';
|
|
29
|
+
readonly defaultValue: 3;
|
|
30
|
+
readonly description: 'Number of standard deviations to use for the localsd autoscale type';
|
|
31
|
+
readonly advanced: true;
|
|
32
|
+
};
|
|
33
|
+
readonly displayCrossHatches: {
|
|
34
|
+
readonly type: 'boolean';
|
|
35
|
+
readonly defaultValue: false;
|
|
36
|
+
readonly description: 'Rule the score axis with horizontal cross hatches at the tick positions — the config form of the score menu\'s "Show cross hatches". Ignored by the density rendering types, which spend color rather than height on the score and so have no axis to rule';
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What `ScoreScaleMixin` asks a composing display's `configuration` to be — the
|
|
41
|
+
* slots above and nothing else, which is all the mixin touches. Narrow so
|
|
42
|
+
* `getConf`/`setConf` still check the slot name; see `ConfigModelForFields`.
|
|
43
|
+
*/
|
|
44
|
+
export type ScoreAxisConfigModel = ConfigModelForFields<typeof scoreAxisConfigSchemaFields>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { types } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
// The score AXIS alone: exactly the slots `ScoreScaleMixin` reads, and nothing
|
|
3
|
+
// else. Split out of `wiggleConfigSchemaFields` below so a display with a score
|
|
4
|
+
// axis but none of wiggle's palette/rendering vocabulary can declare the axis
|
|
5
|
+
// without inheriting the rest — LinearManhattanDisplay used to extend the whole
|
|
6
|
+
// wiggle display schema and advertised twelve slots that did nothing on a GWAS
|
|
7
|
+
// track.
|
|
8
|
+
//
|
|
9
|
+
// `numQuantile` is deliberately NOT here even though `autoscale` names
|
|
10
|
+
// `localpercentile`: it is read by the autoscale *computation*
|
|
11
|
+
// (`WiggleCommonMixin`), which only the wiggle displays run.
|
|
12
|
+
export const scoreAxisConfigSchemaFields = {
|
|
13
|
+
minScore: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
defaultValue: Number.MIN_VALUE,
|
|
16
|
+
description: 'Fixed minimum score bound. The default (Number.MIN_VALUE) is a sentinel meaning "unset, use autoscale"',
|
|
17
|
+
advanced: true,
|
|
18
|
+
},
|
|
19
|
+
maxScore: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
defaultValue: Number.MAX_VALUE,
|
|
22
|
+
description: 'Fixed maximum score bound. The default (Number.MAX_VALUE) is a sentinel meaning "unset, use autoscale"',
|
|
23
|
+
advanced: true,
|
|
24
|
+
},
|
|
25
|
+
// #region stringEnumSlot
|
|
26
|
+
scaleType: {
|
|
27
|
+
type: 'stringEnum',
|
|
28
|
+
model: types.enumeration('Scale type', ['linear', 'log']),
|
|
29
|
+
defaultValue: 'linear',
|
|
30
|
+
description: 'Scale type (linear or log)',
|
|
31
|
+
},
|
|
32
|
+
// #endregion
|
|
33
|
+
autoscale: {
|
|
34
|
+
type: 'stringEnum',
|
|
35
|
+
model: types.enumeration('Autoscale type', [
|
|
36
|
+
'local',
|
|
37
|
+
'localsd',
|
|
38
|
+
'localpercentile',
|
|
39
|
+
]),
|
|
40
|
+
defaultValue: 'localpercentile',
|
|
41
|
+
description: 'Autoscale type: "local" uses the min/max in the visible region, "localsd" uses mean ± numStdDev standard deviations, "localpercentile" uses the numQuantile-th percentile score as the max (robust to skewed/peaky data)',
|
|
42
|
+
},
|
|
43
|
+
numStdDev: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
defaultValue: 3,
|
|
46
|
+
description: 'Number of standard deviations to use for the localsd autoscale type',
|
|
47
|
+
advanced: true,
|
|
48
|
+
},
|
|
49
|
+
displayCrossHatches: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
defaultValue: false,
|
|
52
|
+
description: 'Rule the score axis with horizontal cross hatches at the tick positions — the config form of the score menu\'s "Show cross hatches". Ignored by the density rendering types, which spend color rather than height on the score and so have no axis to rule',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MenuItem } from '@jbrowse/core/ui';
|
|
2
|
+
import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
export interface ScoreScaleModel extends IStateTreeNode {
|
|
4
|
+
scaleType: string;
|
|
5
|
+
autoscaleType: string;
|
|
6
|
+
minScore: number;
|
|
7
|
+
maxScore: number;
|
|
8
|
+
minScoreBound: number | undefined;
|
|
9
|
+
maxScoreBound: number | undefined;
|
|
10
|
+
setScaleType: (v: string) => void;
|
|
11
|
+
setAutoscale: (v?: string) => void;
|
|
12
|
+
setMinScore: (n?: number) => void;
|
|
13
|
+
setMaxScore: (n?: number) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function makeScaleTypeSubMenu(self: {
|
|
16
|
+
scaleType: string;
|
|
17
|
+
setScaleType: (v: string) => void;
|
|
18
|
+
}): MenuItem;
|
|
19
|
+
export declare function makeAutoscaleTypeSubMenu(self: {
|
|
20
|
+
autoscaleType: string;
|
|
21
|
+
setAutoscale: (v?: string) => void;
|
|
22
|
+
}, options?: [string, string][]): MenuItem;
|
|
23
|
+
export declare function makeSetMinMaxScoreItem(self: ScoreScaleModel): MenuItem;
|
|
24
|
+
export declare function makeCrossHatchItem(self: {
|
|
25
|
+
displayCrossHatches: boolean;
|
|
26
|
+
toggleCrossHatches: () => void;
|
|
27
|
+
}): MenuItem;
|
|
28
|
+
export declare function makeScoreSubMenu(self: ScoreScaleModel, opts?: {
|
|
29
|
+
label?: string;
|
|
30
|
+
scaleType?: boolean;
|
|
31
|
+
autoscale?: boolean;
|
|
32
|
+
autoscaleOptions?: [string, string][];
|
|
33
|
+
leadingItems?: MenuItem[];
|
|
34
|
+
trailingItems?: MenuItem[];
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
disabledHelpText?: string;
|
|
37
|
+
}): MenuItem;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { lazy } from 'react';
|
|
2
|
+
import { checkboxItem, radioItems } from '@jbrowse/core/ui/menuItems';
|
|
3
|
+
import { getDialogHost } from '@jbrowse/core/util';
|
|
4
|
+
import EqualizerIcon from '@mui/icons-material/Equalizer';
|
|
5
|
+
import { DEFAULT_AUTOSCALE_OPTIONS } from './autoscale.js';
|
|
6
|
+
const SetMinMaxDialog = lazy(() => import('./SetMinMaxDialog.js'));
|
|
7
|
+
// All three scales, so a display offering this menu at all must hold all three
|
|
8
|
+
// in its own `scaleType` enum. `scoreAxisConfigSchemaFields` deliberately does
|
|
9
|
+
// not — symlog is widened in by whoever implements it — so a display that spreads
|
|
10
|
+
// the shared axis fields unchanged has to opt out with `scaleType: false` the way
|
|
11
|
+
// manhattan does, or the radio writes a value its enumeration rejects.
|
|
12
|
+
export function makeScaleTypeSubMenu(self) {
|
|
13
|
+
return {
|
|
14
|
+
label: 'Scale type',
|
|
15
|
+
subMenu: radioItems([
|
|
16
|
+
{ value: 'linear', label: 'Linear scale' },
|
|
17
|
+
{ value: 'log', label: 'Log scale' },
|
|
18
|
+
{ value: 'symlog', label: 'Symlog scale (allows zero)' },
|
|
19
|
+
], self.scaleType, v => {
|
|
20
|
+
self.setScaleType(v);
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function makeAutoscaleTypeSubMenu(self, options = DEFAULT_AUTOSCALE_OPTIONS) {
|
|
25
|
+
return {
|
|
26
|
+
label: 'Autoscale type',
|
|
27
|
+
subMenu: radioItems(options.map(([value, label]) => ({ value, label })), self.autoscaleType, v => {
|
|
28
|
+
self.setAutoscale(v);
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// The model resolves the Number.MIN_VALUE / MAX_VALUE "unset, fall back to
|
|
33
|
+
// autoscale" sentinel into minScoreBound/maxScoreBound (undefined = autoscale).
|
|
34
|
+
// Surfacing them lets the menu show that a manual range is in force — otherwise
|
|
35
|
+
// an autoscale-type radio still reads as checked while a fixed bound silently
|
|
36
|
+
// overrides it.
|
|
37
|
+
function resolveScoreBounds(self) {
|
|
38
|
+
const min = self.minScoreBound;
|
|
39
|
+
const max = self.maxScoreBound;
|
|
40
|
+
return { min, max, hasManual: min !== undefined || max !== undefined };
|
|
41
|
+
}
|
|
42
|
+
export function makeSetMinMaxScoreItem(self) {
|
|
43
|
+
const { min, max, hasManual } = resolveScoreBounds(self);
|
|
44
|
+
return {
|
|
45
|
+
label: hasManual
|
|
46
|
+
? `Set min/max score (${min ?? 'auto'} – ${max ?? 'auto'})...`
|
|
47
|
+
: 'Set min/max score...',
|
|
48
|
+
onClick: () => {
|
|
49
|
+
getDialogHost(self).queueDialog(handleClose => [
|
|
50
|
+
SetMinMaxDialog,
|
|
51
|
+
{ model: self, handleClose },
|
|
52
|
+
]);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Only offered when a manual bound is set; resets both to the sentinel (same
|
|
57
|
+
// path the dialog takes when its fields are cleared) so autoscale resumes.
|
|
58
|
+
function makeClearMinMaxScoreItem(self) {
|
|
59
|
+
return {
|
|
60
|
+
label: 'Clear manual min/max',
|
|
61
|
+
onClick: () => {
|
|
62
|
+
self.setMinScore(undefined);
|
|
63
|
+
self.setMaxScore(undefined);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function makeCrossHatchItem(self) {
|
|
68
|
+
return checkboxItem('Show cross hatches', self.displayCrossHatches, () => {
|
|
69
|
+
self.toggleCrossHatches();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// The single Score submenu used by every wiggle-family display. Composition is
|
|
73
|
+
// capability-driven: `leadingItems` lets wiggle prepend its Resolution/Summary
|
|
74
|
+
// submenus, `trailingItems` appends what belongs after the range controls rather
|
|
75
|
+
// than before them (the alignments band's allele-fraction floor); `scaleType` is
|
|
76
|
+
// dropped by manhattan (linear-only); `autoscaleOptions` is overridden by
|
|
77
|
+
// coverage's reduced + dynamic-σ list.
|
|
78
|
+
//
|
|
79
|
+
// `autoscale` is the same kind of opt-out as `scaleType`, and exists for the
|
|
80
|
+
// same reason: a display whose domain doesn't consult `autoscaleType` must not
|
|
81
|
+
// offer radios for it. Manhattan takes plain min/max over the loaded regions and
|
|
82
|
+
// applies only the manual bounds, so its Autoscale-type radios wrote the config
|
|
83
|
+
// slot and changed nothing on screen — a control that lies is worse than a
|
|
84
|
+
// missing one. Opt-out rather than opt-in so a display that grows a domain
|
|
85
|
+
// without wiring autoscale keeps the menu it already had.
|
|
86
|
+
export function makeScoreSubMenu(self, opts = {}) {
|
|
87
|
+
const { label = 'Score', scaleType = true, autoscale = true, autoscaleOptions, leadingItems = [], trailingItems = [], disabled, disabledHelpText, } = opts;
|
|
88
|
+
return {
|
|
89
|
+
label,
|
|
90
|
+
icon: EqualizerIcon,
|
|
91
|
+
disabled,
|
|
92
|
+
disabledHelpText,
|
|
93
|
+
subMenu: [
|
|
94
|
+
...leadingItems,
|
|
95
|
+
...(scaleType ? [makeScaleTypeSubMenu(self)] : []),
|
|
96
|
+
...(autoscale ? [makeAutoscaleTypeSubMenu(self, autoscaleOptions)] : []),
|
|
97
|
+
makeSetMinMaxScoreItem(self),
|
|
98
|
+
...(resolveScoreBounds(self).hasManual
|
|
99
|
+
? [makeClearMinMaxScoreItem(self)]
|
|
100
|
+
: []),
|
|
101
|
+
...trailingItems,
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #api
|
|
3
|
+
* One horizontal rule across a score plot, at a score the user chose.
|
|
4
|
+
*
|
|
5
|
+
* `label` is free text and carries no meaning this package assigns. That is
|
|
6
|
+
* deliberate: the obvious use is reading a coverage or CNV track against copy
|
|
7
|
+
* number, and there is no ploidy JBrowse could assume on the user's behalf. A
|
|
8
|
+
* whole-genome triplication is not diploid, plenty of genomes are not diploid to
|
|
9
|
+
* begin with, and a cancer sample can be neither — so "2 copies" is a claim only
|
|
10
|
+
* the person looking at the track can make.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScoreRule {
|
|
13
|
+
value: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ScoreRuleMark extends ScoreRule {
|
|
18
|
+
y: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* #api
|
|
22
|
+
* Screen y for each rule that falls inside the plotted domain, dropping the
|
|
23
|
+
* rest.
|
|
24
|
+
*
|
|
25
|
+
* Out-of-domain is a real case rather than a guard: the domain is whatever
|
|
26
|
+
* autoscale resolved for the visible data, so panning to a quiet stretch can put
|
|
27
|
+
* a rule above everything on screen, and a rule pinned to the top edge there
|
|
28
|
+
* reads as "the whole view is over the line".
|
|
29
|
+
*
|
|
30
|
+
* `normalize` is the display's OWN score normalizer — the same one the renderer
|
|
31
|
+
* draws with. It is a parameter rather than a linear interpolation of the domain
|
|
32
|
+
* because the axis need not be linear: on a log or symlog track, placing a rule
|
|
33
|
+
* at `(value - min) / (max - min)` puts the line somewhere the data it is meant
|
|
34
|
+
* to be read against is not.
|
|
35
|
+
*
|
|
36
|
+
* `box` is likewise the caller's own — hand it the same `{yTop, yBottom}` the
|
|
37
|
+
* display's ticks were built with (a `YScaleTicks` satisfies it). Recomputing a
|
|
38
|
+
* box here would silently disagree with any band that lays its axis out
|
|
39
|
+
* differently, and the alignments coverage band does.
|
|
40
|
+
*/
|
|
41
|
+
export declare function scoreRuleMarks({ rules, domain, box, normalize, }: {
|
|
42
|
+
rules: readonly ScoreRule[];
|
|
43
|
+
domain: [number, number] | undefined;
|
|
44
|
+
box: {
|
|
45
|
+
yTop: number;
|
|
46
|
+
yBottom: number;
|
|
47
|
+
};
|
|
48
|
+
normalize: (score: number) => number;
|
|
49
|
+
}): ScoreRuleMark[];
|
|
50
|
+
/**
|
|
51
|
+
* #api
|
|
52
|
+
* Normalizes whatever a `scoreRules` config slot holds into `ScoreRule`s,
|
|
53
|
+
* dropping entries that are not usable. Config is user-authored JSON, so a bare
|
|
54
|
+
* number, a missing value or a non-numeric one all have to survive being read.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseScoreRules(value: unknown): ScoreRule[];
|
|
57
|
+
/**
|
|
58
|
+
* #api
|
|
59
|
+
* Widens an autoscaled range so every configured rule stays on the axis.
|
|
60
|
+
*
|
|
61
|
+
* Without this a rule silently disappears in exactly the window that makes it
|
|
62
|
+
* worth having. Autoscale follows the visible data, so over a homozygous
|
|
63
|
+
* deletion a coverage domain collapses to about `[0, 1]` and a rule at the
|
|
64
|
+
* diploid depth falls outside it — and "2 copies would be up there" is the most
|
|
65
|
+
* informative thing that view can say. The reader has no menu to check either:
|
|
66
|
+
* `scoreRules` is set by whoever wrote the config, so a rule that vanishes
|
|
67
|
+
* leaves nothing behind to notice.
|
|
68
|
+
*
|
|
69
|
+
* Applied to the raw range, before `getNiceDomain` takes the `minScore` /
|
|
70
|
+
* `maxScore` bounds. Those still win: a rule outside an explicitly bounded axis
|
|
71
|
+
* is one the config asked not to be shown, and it drops as before.
|
|
72
|
+
*/
|
|
73
|
+
export declare function widenRangeToRules(range: [number, number], ruleValues: readonly number[]): [number, number];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { clampStrokeInsideAxis, scoreToAxisY } from './yScaleTicks.js';
|
|
2
|
+
/**
|
|
3
|
+
* #api
|
|
4
|
+
* Screen y for each rule that falls inside the plotted domain, dropping the
|
|
5
|
+
* rest.
|
|
6
|
+
*
|
|
7
|
+
* Out-of-domain is a real case rather than a guard: the domain is whatever
|
|
8
|
+
* autoscale resolved for the visible data, so panning to a quiet stretch can put
|
|
9
|
+
* a rule above everything on screen, and a rule pinned to the top edge there
|
|
10
|
+
* reads as "the whole view is over the line".
|
|
11
|
+
*
|
|
12
|
+
* `normalize` is the display's OWN score normalizer — the same one the renderer
|
|
13
|
+
* draws with. It is a parameter rather than a linear interpolation of the domain
|
|
14
|
+
* because the axis need not be linear: on a log or symlog track, placing a rule
|
|
15
|
+
* at `(value - min) / (max - min)` puts the line somewhere the data it is meant
|
|
16
|
+
* to be read against is not.
|
|
17
|
+
*
|
|
18
|
+
* `box` is likewise the caller's own — hand it the same `{yTop, yBottom}` the
|
|
19
|
+
* display's ticks were built with (a `YScaleTicks` satisfies it). Recomputing a
|
|
20
|
+
* box here would silently disagree with any band that lays its axis out
|
|
21
|
+
* differently, and the alignments coverage band does.
|
|
22
|
+
*/
|
|
23
|
+
export function scoreRuleMarks({ rules, domain, box, normalize, }) {
|
|
24
|
+
if (!domain) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const [min, max] = domain;
|
|
28
|
+
if (max === min) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return rules
|
|
32
|
+
.filter(r => Number.isFinite(r.value) && r.value >= min && r.value <= max)
|
|
33
|
+
.map(r => ({
|
|
34
|
+
...r,
|
|
35
|
+
y: clampStrokeInsideAxis(scoreToAxisY(normalize(r.value), {
|
|
36
|
+
...box,
|
|
37
|
+
plotHeight: box.yBottom - box.yTop,
|
|
38
|
+
}), box.yBottom),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* #api
|
|
43
|
+
* Normalizes whatever a `scoreRules` config slot holds into `ScoreRule`s,
|
|
44
|
+
* dropping entries that are not usable. Config is user-authored JSON, so a bare
|
|
45
|
+
* number, a missing value or a non-numeric one all have to survive being read.
|
|
46
|
+
*/
|
|
47
|
+
export function parseScoreRules(value) {
|
|
48
|
+
if (!Array.isArray(value)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
if (typeof entry === 'number') {
|
|
54
|
+
if (Number.isFinite(entry)) {
|
|
55
|
+
out.push({ value: entry });
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (entry && typeof entry === 'object') {
|
|
60
|
+
const { value: v, label, color } = entry;
|
|
61
|
+
const n = typeof v === 'string' ? Number(v) : v;
|
|
62
|
+
if (typeof n === 'number' && Number.isFinite(n)) {
|
|
63
|
+
out.push({
|
|
64
|
+
value: n,
|
|
65
|
+
...(typeof label === 'string' && label ? { label } : {}),
|
|
66
|
+
...(typeof color === 'string' && color ? { color } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* #api
|
|
75
|
+
* Widens an autoscaled range so every configured rule stays on the axis.
|
|
76
|
+
*
|
|
77
|
+
* Without this a rule silently disappears in exactly the window that makes it
|
|
78
|
+
* worth having. Autoscale follows the visible data, so over a homozygous
|
|
79
|
+
* deletion a coverage domain collapses to about `[0, 1]` and a rule at the
|
|
80
|
+
* diploid depth falls outside it — and "2 copies would be up there" is the most
|
|
81
|
+
* informative thing that view can say. The reader has no menu to check either:
|
|
82
|
+
* `scoreRules` is set by whoever wrote the config, so a rule that vanishes
|
|
83
|
+
* leaves nothing behind to notice.
|
|
84
|
+
*
|
|
85
|
+
* Applied to the raw range, before `getNiceDomain` takes the `minScore` /
|
|
86
|
+
* `maxScore` bounds. Those still win: a rule outside an explicitly bounded axis
|
|
87
|
+
* is one the config asked not to be shown, and it drops as before.
|
|
88
|
+
*/
|
|
89
|
+
export function widenRangeToRules(range, ruleValues) {
|
|
90
|
+
let [min, max] = range;
|
|
91
|
+
for (const value of ruleValues) {
|
|
92
|
+
if (Number.isFinite(value)) {
|
|
93
|
+
min = Math.min(min, value);
|
|
94
|
+
max = Math.max(max, value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [min, max];
|
|
98
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tick values for a symlog domain, spaced evenly down the axis rather than
|
|
3
|
+
* evenly across the scores.
|
|
4
|
+
*
|
|
5
|
+
* The domain endpoints are always ticks: symlog's whole point is that the small
|
|
6
|
+
* end stays legible, and both ends of an autoscaled domain are what tell a
|
|
7
|
+
* reader what the plot's extent is. Zero joins them when the domain crosses it —
|
|
8
|
+
* on a log-ratio track that is the one score with an absolute meaning, and
|
|
9
|
+
* rounding never lands on it.
|
|
10
|
+
*
|
|
11
|
+
* Rounding can put an interior tick on top of a neighbour (a domain crossing
|
|
12
|
+
* zero rounds its smallest magnitudes to something a pixel from the zero line),
|
|
13
|
+
* so a candidate closer than half a slot to a tick already kept is dropped
|
|
14
|
+
* rather than drawn over it. Endpoints and zero are kept in preference to any
|
|
15
|
+
* rounded value, which is the same order they are stated in above.
|
|
16
|
+
*/
|
|
17
|
+
export declare function symlogTicks(domain: readonly [number, number], constant: number, count?: number): number[];
|