@jbrowse/wiggle-core 5.0.0-beta.1 → 5.0.0-beta.10

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 (73) hide show
  1. package/README.md +76 -107
  2. package/esm/AutoscaleGroupDialog.d.ts +7 -0
  3. package/esm/AutoscaleGroupDialog.js +26 -0
  4. package/esm/ScoreAxisMixin.d.ts +134 -0
  5. package/esm/ScoreAxisMixin.js +213 -0
  6. package/esm/ScoreFieldConfigMixin.d.ts +72 -0
  7. package/esm/ScoreFieldConfigMixin.js +25 -0
  8. package/esm/ScorePlotChrome.d.ts +45 -0
  9. package/esm/ScorePlotChrome.js +47 -0
  10. package/esm/ScorePlotSvgFrame.d.ts +27 -0
  11. package/esm/ScorePlotSvgFrame.js +23 -0
  12. package/esm/ScoreScaleMixin.d.ts +113 -50
  13. package/esm/ScoreScaleMixin.js +148 -70
  14. package/esm/SetMinMaxDialog.d.ts +3 -2
  15. package/esm/SetMinMaxDialog.js +20 -6
  16. package/esm/SetScoreRulesDialog.d.ts +14 -0
  17. package/esm/SetScoreRulesDialog.js +45 -0
  18. package/esm/WiggleScoreConfigMixin.d.ts +89 -0
  19. package/esm/WiggleScoreConfigMixin.js +54 -0
  20. package/esm/autoscale.d.ts +54 -14
  21. package/esm/autoscale.js +105 -180
  22. package/esm/autoscaleGroup.d.ts +30 -0
  23. package/esm/autoscaleGroup.js +97 -0
  24. package/esm/chrome.d.ts +5 -0
  25. package/esm/chrome.js +7 -0
  26. package/esm/computeYTicks.d.ts +1 -1
  27. package/esm/computeYTicks.js +4 -3
  28. package/esm/constants.d.ts +1 -2
  29. package/esm/constants.js +1 -10
  30. package/esm/dataTypes.d.ts +4 -6
  31. package/esm/displayModel.d.ts +1 -2
  32. package/esm/gapBreak.d.ts +2 -3
  33. package/esm/gapBreak.js +2 -3
  34. package/esm/index.d.ts +24 -25
  35. package/esm/index.js +20 -18
  36. package/esm/normalize.d.ts +1 -26
  37. package/esm/normalize.js +1 -86
  38. package/esm/pointMarker.d.ts +7 -2
  39. package/esm/pointMarker.js +14 -18
  40. package/esm/pointSizeMenu.d.ts +30 -11
  41. package/esm/pointSizeMenu.js +42 -19
  42. package/esm/renderingBackendTypes.d.ts +13 -4
  43. package/esm/renderingBackendTypes.js +4 -1
  44. package/esm/rowLabelOffset.d.ts +8 -0
  45. package/esm/rowLabelOffset.js +16 -0
  46. package/esm/scale.d.ts +12 -8
  47. package/esm/scale.js +27 -14
  48. package/esm/scoreFieldConfigSchemaFields.d.ts +10 -0
  49. package/esm/scoreFieldConfigSchemaFields.js +8 -0
  50. package/esm/scoreMenuItems.d.ts +35 -16
  51. package/esm/scoreMenuItems.js +95 -62
  52. package/esm/scoreRuleMarks.d.ts +14 -39
  53. package/esm/scoreRuleMarks.js +12 -45
  54. package/esm/transferables.js +4 -9
  55. package/esm/valueScaleConfigSchema.d.ts +253 -0
  56. package/esm/valueScaleConfigSchema.js +270 -0
  57. package/esm/visibleStatsDomain.d.ts +20 -8
  58. package/esm/visibleStatsDomain.js +32 -27
  59. package/esm/wiggleRenderModes.generated.d.ts +1 -2
  60. package/esm/wiggleRenderModes.generated.js +1 -2
  61. package/package.json +14 -9
  62. package/esm/CrossHatches.d.ts +0 -11
  63. package/esm/CrossHatches.js +0 -32
  64. package/esm/ScoreRules.d.ts +0 -11
  65. package/esm/ScoreRules.js +0 -37
  66. package/esm/YScaleBar.d.ts +0 -6
  67. package/esm/YScaleBar.js +0 -46
  68. package/esm/YScaleBarOverlay.d.ts +0 -6
  69. package/esm/YScaleBarOverlay.js +0 -24
  70. package/esm/scoreAxisConfigSchemaFields.d.ts +0 -44
  71. package/esm/scoreAxisConfigSchemaFields.js +0 -54
  72. package/esm/yScaleTicks.d.ts +0 -59
  73. package/esm/yScaleTicks.js +0 -59
@@ -1,37 +1,56 @@
1
- import type { MenuItem } from '@jbrowse/core/ui';
1
+ import type { AutoscalePeer } from './autoscaleGroup.ts';
2
+ import type { CheckboxMenuItem, MenuItem } from '@jbrowse/core/ui';
3
+ import type { ValueScaleRule } from '@jbrowse/display-ui';
2
4
  import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
3
5
  export interface ScoreScaleModel extends IStateTreeNode {
4
6
  scaleType: string;
5
- autoscaleType: string;
6
- minScore: number;
7
- maxScore: number;
7
+ scaleZero: boolean;
8
+ domainQuantile: number;
9
+ clipQuantile: number;
10
+ manualMinScore: number | undefined;
11
+ manualMaxScore: number | undefined;
8
12
  minScoreBound: number | undefined;
9
13
  maxScoreBound: number | undefined;
14
+ hasManualScoreBounds: boolean;
15
+ autoscaledDomain: [number, number] | undefined;
10
16
  setScaleType: (v: string) => void;
11
- setAutoscale: (v?: string) => void;
17
+ setScaleZero: (zero: boolean) => void;
12
18
  setMinScore: (n?: number) => void;
13
19
  setMaxScore: (n?: number) => void;
20
+ setDomainQuantile: (quantile: number) => void;
21
+ }
22
+ export interface ScoreRulesModel {
23
+ scoreRulesDrawn: boolean;
24
+ scoreRules: ValueScaleRule[];
25
+ setScoreRules: (rules: ValueScaleRule[]) => void;
14
26
  }
15
27
  export declare function makeScaleTypeSubMenu(self: {
16
28
  scaleType: string;
17
29
  setScaleType: (v: string) => void;
18
30
  }): MenuItem;
19
- export declare function makeAutoscaleTypeSubMenu(self: {
20
- autoscaleType: string;
21
- setAutoscale: (v?: string) => void;
22
- }, options?: [string, string][]): MenuItem;
31
+ export declare const CLIP_OUTLIERS_LABEL = "Clip outliers";
32
+ export declare function makeClipOutliersItem(self: {
33
+ domainQuantile: number;
34
+ clipQuantile: number;
35
+ setDomainQuantile: (quantile: number) => void;
36
+ }): CheckboxMenuItem;
37
+ export declare const AXIS_ZERO_LABEL = "Start axis at 0";
38
+ export declare function makeAxisZeroItem(self: {
39
+ scaleZero: boolean;
40
+ setScaleZero: (zero: boolean) => void;
41
+ }): CheckboxMenuItem;
23
42
  export declare function makeSetMinMaxScoreItem(self: ScoreScaleModel): MenuItem;
24
43
  export declare function makeCrossHatchItem(self: {
25
- displayCrossHatches: boolean;
26
- toggleCrossHatches: () => void;
44
+ grid: boolean;
45
+ setGrid: (grid: boolean) => void;
27
46
  }): MenuItem;
28
- export declare function makeScoreSubMenu(self: ScoreScaleModel, opts?: {
47
+ export interface ScoreSubMenuOptions {
29
48
  label?: string;
30
- scaleType?: boolean;
31
- autoscale?: boolean;
32
- autoscaleOptions?: [string, string][];
33
49
  leadingItems?: MenuItem[];
34
50
  trailingItems?: MenuItem[];
35
51
  disabled?: boolean;
36
52
  disabledHelpText?: string;
37
- }): MenuItem;
53
+ }
54
+ export declare function makeAutoscaleGroupItem(self: IStateTreeNode & AutoscalePeer): MenuItem;
55
+ export declare function makeSetScoreRulesItem(self: IStateTreeNode & ScoreRulesModel): MenuItem;
56
+ export declare function makeScoreSubMenu(self: ScoreScaleModel & Partial<ScoreRulesModel> & Partial<AutoscalePeer>, opts?: ScoreSubMenuOptions): MenuItem;
@@ -1,90 +1,123 @@
1
1
  import { lazy } from 'react';
2
- import { checkboxItem, radioItems } from '@jbrowse/core/ui/menuItems';
2
+ import { checkboxItem, radioItems, toggleItem, } from '@jbrowse/core/ui/menuItems';
3
3
  import { getDialogHost } from '@jbrowse/core/util';
4
4
  import EqualizerIcon from '@mui/icons-material/Equalizer';
5
- import { DEFAULT_AUTOSCALE_OPTIONS } from './autoscale.js';
5
+ import { autoscaleGroupMembers, autoscalePeers } from './autoscaleGroup.js';
6
+ import { VALUE_SCALE_TYPES } from './valueScaleConfigSchema.js';
7
+ const AutoscaleGroupDialog = lazy(() => import('./AutoscaleGroupDialog.js'));
6
8
  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.
9
+ const SetScoreRulesDialog = lazy(() => import('./SetScoreRulesDialog.js'));
10
+ const SCALE_TYPE_LABELS = {
11
+ linear: 'Linear scale',
12
+ log: 'Log scale',
13
+ symlog: 'Symlog scale (allows zero)',
14
+ };
12
15
  export function makeScaleTypeSubMenu(self) {
13
16
  return {
14
17
  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 => {
18
+ subMenu: radioItems(VALUE_SCALE_TYPES.map(value => ({
19
+ value,
20
+ label: SCALE_TYPE_LABELS[value],
21
+ })), self.scaleType, v => {
20
22
  self.setScaleType(v);
21
23
  }),
22
24
  };
23
25
  }
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
- };
26
+ // Quoted by the docs' click paths, so one literal string.
27
+ export const CLIP_OUTLIERS_LABEL = 'Clip outliers';
28
+ // One checkbox rather than a radio over modes: what a reader decides is whether
29
+ // a spike may take the axis, and the quantile it clips at is the config's.
30
+ export function makeClipOutliersItem(self) {
31
+ const clipsAt = self.domainQuantile < 1 ? self.domainQuantile : self.clipQuantile;
32
+ const percent = Math.round(clipsAt * 100);
33
+ return toggleItem(CLIP_OUTLIERS_LABEL, self.domainQuantile < 1, on => {
34
+ self.setDomainQuantile(on ? self.clipQuantile : 1);
35
+ }, {
36
+ helpText: `An unpinned end follows the ${percent}th percentile of each sign rather than the extremes, so one spike no longer flattens the rest.`,
37
+ });
31
38
  }
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 };
39
+ // Quoted by the docs' click paths, so one literal string.
40
+ export const AXIS_ZERO_LABEL = 'Start axis at 0';
41
+ export function makeAxisZeroItem(self) {
42
+ return toggleItem(AXIS_ZERO_LABEL, self.scaleZero, zero => {
43
+ self.setScaleZero(zero);
44
+ }, {
45
+ helpText: 'A linear or symlog axis reaches 0 whatever the values in view span. Off, it spans those values alone.',
46
+ });
41
47
  }
48
+ // The label shows the PINNED pair, `auto` for an end nobody pinned: the
49
+ // resolved pair captioned every GC content track "(0 – 1)" off its default
50
+ // domain. "Use current range" copies `autoscaledDomain`, undefined while the
51
+ // alignments density tier's features per bin stand in for depth.
42
52
  export function makeSetMinMaxScoreItem(self) {
43
- const { min, max, hasManual } = resolveScoreBounds(self);
53
+ const { manualMinScore, manualMaxScore, autoscaledDomain: domain } = self;
44
54
  return {
45
- label: hasManual
46
- ? `Set min/max score (${min ?? 'auto'} – ${max ?? 'auto'})...`
55
+ label: self.hasManualScoreBounds
56
+ ? `Set min/max score (${manualMinScore ?? 'auto'} – ${manualMaxScore ?? 'auto'})...`
47
57
  : 'Set min/max score...',
48
58
  onClick: () => {
49
59
  getDialogHost(self).queueDialog(handleClose => [
50
60
  SetMinMaxDialog,
51
- { model: self, handleClose },
61
+ { model: self, domain, handleClose },
52
62
  ]);
53
63
  },
54
64
  };
55
65
  }
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) {
66
+ export function makeCrossHatchItem(self) {
67
+ return checkboxItem('Show cross hatches', self.grid, () => {
68
+ self.setGrid(!self.grid);
69
+ });
70
+ }
71
+ // The count of the other tracks in the group is in the label, as the min/max
72
+ // row carries its bounds: an axis that moves when another track pans is
73
+ // something the reader has to be able to find the cause of.
74
+ export function makeAutoscaleGroupItem(self) {
75
+ const { autoscaleGroup } = self;
76
+ const others = autoscaleGroup === undefined
77
+ ? 0
78
+ : autoscaleGroupMembers(self, autoscaleGroup).length - 1;
59
79
  return {
60
- label: 'Clear manual min/max',
80
+ label: others > 0
81
+ ? `Autoscale with other tracks (${others})...`
82
+ : 'Autoscale with other tracks...',
61
83
  onClick: () => {
62
- self.setMinScore(undefined);
63
- self.setMaxScore(undefined);
84
+ getDialogHost(self).queueDialog(handleClose => [
85
+ AutoscaleGroupDialog,
86
+ { model: self, handleClose },
87
+ ]);
64
88
  },
65
89
  };
66
90
  }
67
- export function makeCrossHatchItem(self) {
68
- return checkboxItem('Show cross hatches', self.displayCrossHatches, () => {
69
- self.toggleCrossHatches();
70
- });
91
+ // Offered once the view holds another track with a value axis to share.
92
+ function autoscalesInGroups(self) {
93
+ return self.setAutoscaleGroup !== undefined && autoscalePeers(self).length > 0;
94
+ }
95
+ function drawsScoreRules(self) {
96
+ return (self.scoreRulesDrawn === true &&
97
+ self.scoreRules !== undefined &&
98
+ self.setScoreRules !== undefined);
99
+ }
100
+ // A density plot maps its score to colour and rules no band, so its domain
101
+ // spans the values whatever `zero` says; the row is offered with the axis.
102
+ function rulesABand(self) {
103
+ return self.scoreRulesDrawn === true;
104
+ }
105
+ // The count is in the label, as the min/max row carries its bounds: a dashed
106
+ // line across a plot means nothing until the reader knows it was put there.
107
+ export function makeSetScoreRulesItem(self) {
108
+ const count = self.scoreRules.length;
109
+ return {
110
+ label: count > 0 ? `Reference lines (${count})...` : 'Reference lines...',
111
+ onClick: () => {
112
+ getDialogHost(self).queueDialog(handleClose => [
113
+ SetScoreRulesDialog,
114
+ { model: self, handleClose },
115
+ ]);
116
+ },
117
+ };
71
118
  }
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
119
  export function makeScoreSubMenu(self, opts = {}) {
87
- const { label = 'Score', scaleType = true, autoscale = true, autoscaleOptions, leadingItems = [], trailingItems = [], disabled, disabledHelpText, } = opts;
120
+ const { label = 'Score', leadingItems = [], trailingItems = [], disabled, disabledHelpText, } = opts;
88
121
  return {
89
122
  label,
90
123
  icon: EqualizerIcon,
@@ -92,12 +125,12 @@ export function makeScoreSubMenu(self, opts = {}) {
92
125
  disabledHelpText,
93
126
  subMenu: [
94
127
  ...leadingItems,
95
- ...(scaleType ? [makeScaleTypeSubMenu(self)] : []),
96
- ...(autoscale ? [makeAutoscaleTypeSubMenu(self, autoscaleOptions)] : []),
128
+ makeScaleTypeSubMenu(self),
129
+ makeClipOutliersItem(self),
130
+ ...(rulesABand(self) ? [makeAxisZeroItem(self)] : []),
97
131
  makeSetMinMaxScoreItem(self),
98
- ...(resolveScoreBounds(self).hasManual
99
- ? [makeClearMinMaxScoreItem(self)]
100
- : []),
132
+ ...(autoscalesInGroups(self) ? [makeAutoscaleGroupItem(self)] : []),
133
+ ...(drawsScoreRules(self) ? [makeSetScoreRulesItem(self)] : []),
101
134
  ...trailingItems,
102
135
  ],
103
136
  };
@@ -1,22 +1,5 @@
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
- }
1
+ import type { ScoreRuleMark, ValueScaleRule } from '@jbrowse/display-ui';
2
+ export type { ScoreRuleMark } from '@jbrowse/display-ui';
20
3
  /**
21
4
  * #api
22
5
  * Screen y for each rule that falls inside the plotted domain, dropping the
@@ -34,12 +17,12 @@ export interface ScoreRuleMark extends ScoreRule {
34
17
  * to be read against is not.
35
18
  *
36
19
  * `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.
20
+ * display's ticks were built with (a `YScaleTicks` satisfies it). A box
21
+ * recomputed here would place rules off the ticks of any band that lays its
22
+ * axis out differently, and the alignments coverage band does.
40
23
  */
41
24
  export declare function scoreRuleMarks({ rules, domain, box, normalize, }: {
42
- rules: readonly ScoreRule[];
25
+ rules: readonly ValueScaleRule[];
43
26
  domain: [number, number] | undefined;
44
27
  box: {
45
28
  yTop: number;
@@ -47,27 +30,19 @@ export declare function scoreRuleMarks({ rules, domain, box, normalize, }: {
47
30
  };
48
31
  normalize: (score: number) => number;
49
32
  }): 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
33
  /**
58
34
  * #api
59
35
  * Widens an autoscaled range so every configured rule stays on the axis.
60
36
  *
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.
37
+ * Without this a rule is dropped from the plot in the views where it is most
38
+ * useful. Autoscale follows the visible data, so over a homozygous deletion a
39
+ * coverage domain collapses to about `[0, 1]` and a rule at the diploid depth
40
+ * falls outside it, though the rule's position above the data is the most
41
+ * informative mark in that view, and a rule that vanishes leaves nothing
42
+ * behind to notice.
68
43
  *
69
- * Applied to the raw range, before `getNiceDomain` takes the `minScore` /
70
- * `maxScore` bounds. Those still win: a rule outside an explicitly bounded axis
44
+ * Applied to the raw range, before `getNiceDomain` takes the `scales.y`
45
+ * domain bounds. Those still win: a rule outside an explicitly bounded axis
71
46
  * is one the config asked not to be shown, and it drops as before.
72
47
  */
73
48
  export declare function widenRangeToRules(range: [number, number], ruleValues: readonly number[]): [number, number];
@@ -1,4 +1,4 @@
1
- import { clampStrokeInsideAxis, scoreToAxisY } from './yScaleTicks.js';
1
+ import { clampStrokeInsideAxis, scoreToAxisY, } from '@jbrowse/display-ui/yScaleTicks';
2
2
  /**
3
3
  * #api
4
4
  * Screen y for each rule that falls inside the plotted domain, dropping the
@@ -16,9 +16,9 @@ import { clampStrokeInsideAxis, scoreToAxisY } from './yScaleTicks.js';
16
16
  * to be read against is not.
17
17
  *
18
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.
19
+ * display's ticks were built with (a `YScaleTicks` satisfies it). A box
20
+ * recomputed here would place rules off the ticks of any band that lays its
21
+ * axis out differently, and the alignments coverage band does.
22
22
  */
23
23
  export function scoreRuleMarks({ rules, domain, box, normalize, }) {
24
24
  if (!domain) {
@@ -38,52 +38,19 @@ export function scoreRuleMarks({ rules, domain, box, normalize, }) {
38
38
  }), box.yBottom),
39
39
  }));
40
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
41
  /**
74
42
  * #api
75
43
  * Widens an autoscaled range so every configured rule stays on the axis.
76
44
  *
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.
45
+ * Without this a rule is dropped from the plot in the views where it is most
46
+ * useful. Autoscale follows the visible data, so over a homozygous deletion a
47
+ * coverage domain collapses to about `[0, 1]` and a rule at the diploid depth
48
+ * falls outside it, though the rule's position above the data is the most
49
+ * informative mark in that view, and a rule that vanishes leaves nothing
50
+ * behind to notice.
84
51
  *
85
- * Applied to the raw range, before `getNiceDomain` takes the `minScore` /
86
- * `maxScore` bounds. Those still win: a rule outside an explicitly bounded axis
52
+ * Applied to the raw range, before `getNiceDomain` takes the `scales.y`
53
+ * domain bounds. Those still win: a rule outside an explicitly bounded axis
87
54
  * is one the config asked not to be shown, and it drops as before.
88
55
  */
89
56
  export function widenRangeToRules(range, ruleValues) {
@@ -1,18 +1,17 @@
1
- // Walks every result's sources once and returns each source's eight per-feature
1
+ // Walks every result's sources once and returns each source's four per-feature
2
2
  // TypedArray buffers as one flat list for postMessage transfer. Field set
3
3
  // matches WiggleFeatureArrays — when fields change there, update here so the
4
4
  // buffers actually transfer instead of being structured-cloned.
5
5
  //
6
6
  // The Set dedupe is load-bearing, not insurance: processFeaturesFromArrays
7
- // aliases fields onto one buffer whenever a copy would be identical (no summary
8
- // data → min/max are the scores; an all-one-sided window → the pos or neg
9
- // arrays are the full arrays). postMessage throws on a repeated transferable.
7
+ // aliases min/max onto the scores where no feature is a summary, and
8
+ // postMessage throws on a repeated transferable.
10
9
  //
11
10
  // Takes ALL the results rather than one, so the dedupe spans regions too — a
12
11
  // per-result Set would list a shared buffer twice.
13
12
  //
14
13
  // Aliasing the adapter's own arrays here as well, rather than copying them, is
15
- // costed and declined in agent-docs/reference/REJECTED_IDEAS.md: it retains 20
14
+ // costed and declined in agent-docs/architecture-decision-records/: it retains 20
16
15
  // bytes a feature on the main thread where copying retains 12.
17
16
  export function collectWiggleTransferables(results) {
18
17
  const buffers = new Set();
@@ -22,10 +21,6 @@ export function collectWiggleTransferables(results) {
22
21
  buffers.add(s.featureScores.buffer);
23
22
  buffers.add(s.featureMinScores.buffer);
24
23
  buffers.add(s.featureMaxScores.buffer);
25
- buffers.add(s.posFeaturePositions.buffer);
26
- buffers.add(s.posFeatureScores.buffer);
27
- buffers.add(s.negFeaturePositions.buffer);
28
- buffers.add(s.negFeatureScores.buffer);
29
24
  }
30
25
  }
31
26
  return [...buffers];