@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.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +292 -0
  3. package/esm/CrossHatches.d.ts +11 -0
  4. package/esm/CrossHatches.js +32 -0
  5. package/esm/ResolutionStepper.d.ts +17 -0
  6. package/esm/ResolutionStepper.js +56 -0
  7. package/esm/ScoreRules.d.ts +11 -0
  8. package/esm/ScoreRules.js +37 -0
  9. package/esm/ScoreScaleMixin.d.ts +105 -0
  10. package/esm/ScoreScaleMixin.js +136 -0
  11. package/esm/SetMinMaxDialog.d.ts +11 -0
  12. package/esm/SetMinMaxDialog.js +18 -0
  13. package/esm/YScaleBar.d.ts +6 -0
  14. package/esm/YScaleBar.js +46 -0
  15. package/esm/YScaleBarOverlay.d.ts +6 -0
  16. package/esm/YScaleBarOverlay.js +24 -0
  17. package/esm/autoscale.d.ts +55 -0
  18. package/esm/autoscale.js +252 -0
  19. package/esm/computeYTicks.d.ts +15 -0
  20. package/esm/computeYTicks.js +44 -0
  21. package/esm/constants.d.ts +2 -0
  22. package/esm/constants.js +10 -0
  23. package/esm/dataTypes.d.ts +26 -0
  24. package/esm/dataTypes.js +1 -0
  25. package/esm/displayModel.d.ts +24 -0
  26. package/esm/displayModel.js +1 -0
  27. package/esm/formatNumber.d.ts +1 -0
  28. package/esm/formatNumber.js +6 -0
  29. package/esm/gapBreak.d.ts +59 -0
  30. package/esm/gapBreak.js +58 -0
  31. package/esm/index.d.ts +36 -0
  32. package/esm/index.js +26 -0
  33. package/esm/normalize.d.ts +40 -0
  34. package/esm/normalize.js +106 -0
  35. package/esm/pointMarker.d.ts +3 -0
  36. package/esm/pointMarker.js +27 -0
  37. package/esm/pointSizeMenu.d.ts +13 -0
  38. package/esm/pointSizeMenu.js +21 -0
  39. package/esm/renderingBackendTypes.d.ts +39 -0
  40. package/esm/renderingBackendTypes.js +29 -0
  41. package/esm/resolveRenderState.d.ts +1 -0
  42. package/esm/resolveRenderState.js +20 -0
  43. package/esm/scale.d.ts +60 -0
  44. package/esm/scale.js +161 -0
  45. package/esm/scoreAxisConfigSchemaFields.d.ts +44 -0
  46. package/esm/scoreAxisConfigSchemaFields.js +54 -0
  47. package/esm/scoreMenuItems.d.ts +37 -0
  48. package/esm/scoreMenuItems.js +104 -0
  49. package/esm/scoreRuleMarks.d.ts +73 -0
  50. package/esm/scoreRuleMarks.js +98 -0
  51. package/esm/symlogTicks.d.ts +17 -0
  52. package/esm/symlogTicks.js +92 -0
  53. package/esm/transferables.d.ts +2 -0
  54. package/esm/transferables.js +32 -0
  55. package/esm/vendor/d3-scale.d.ts +33 -0
  56. package/esm/vendor/d3-scale.js +249 -0
  57. package/esm/visibleStatsDomain.d.ts +59 -0
  58. package/esm/visibleStatsDomain.js +33 -0
  59. package/esm/wiggleRenderModes.generated.d.ts +9 -0
  60. package/esm/wiggleRenderModes.generated.js +11 -0
  61. package/esm/yScaleTicks.d.ts +59 -0
  62. package/esm/yScaleTicks.js +59 -0
  63. 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[];