@jbrowse/core 5.0.0-beta.11 → 5.0.0-beta.13

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 (99) hide show
  1. package/README.md +97 -75
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.d.ts +3 -0
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.js +14 -1
  4. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.d.ts +2 -0
  5. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.js +5 -5
  6. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.js +1 -1
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.d.ts +3 -1
  8. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.js +2 -2
  9. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.d.ts +2 -1
  10. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.js +7 -14
  11. package/esm/BaseFeatureWidget/SequenceFeatureDetails/seqtypes/CDNASequence.js +3 -3
  12. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +7 -0
  13. package/esm/BaseFeatureWidget/stateModelFactory.js +11 -0
  14. package/esm/BaseFeatureWidget/types.d.ts +2 -0
  15. package/esm/PluginManager.d.ts +6 -3
  16. package/esm/PluginManager.js +6 -3
  17. package/esm/ReExports/coreModules.generated.js +291 -285
  18. package/esm/ReExports/coreWorkerModules.generated.js +288 -283
  19. package/esm/ReExports/list.js +7 -0
  20. package/esm/TextSearch/TextSearchManager.d.ts +3 -3
  21. package/esm/TextSearch/TextSearchManager.js +6 -13
  22. package/esm/configuration/configurationSchema.d.ts +33 -95
  23. package/esm/configuration/configurationSchema.js +50 -218
  24. package/esm/configuration/configurationSlot.d.ts +18 -64
  25. package/esm/configuration/configurationSlot.js +31 -97
  26. package/esm/configuration/formatDetailsConfigSchema.js +2 -0
  27. package/esm/configuration/getConf.d.ts +7 -36
  28. package/esm/configuration/getConf.js +7 -37
  29. package/esm/configuration/index.d.ts +3 -2
  30. package/esm/configuration/index.js +2 -1
  31. package/esm/configuration/partitionAdvanced.d.ts +12 -0
  32. package/esm/configuration/partitionAdvanced.js +42 -0
  33. package/esm/configuration/plot.d.ts +6 -0
  34. package/esm/configuration/plot.js +34 -1
  35. package/esm/configuration/readConfObject.d.ts +15 -17
  36. package/esm/configuration/readConfObject.js +19 -50
  37. package/esm/configuration/schemaTypes.js +5 -1
  38. package/esm/configuration/snapshotPreprocess.js +1 -3
  39. package/esm/configuration/types.d.ts +60 -159
  40. package/esm/pluggableElementTypes/DisplayType.d.ts +7 -0
  41. package/esm/pluggableElementTypes/DisplayType.js +7 -0
  42. package/esm/pluggableElementTypes/RpcMethodType.d.ts +7 -0
  43. package/esm/pluggableElementTypes/RpcMethodType.js +9 -0
  44. package/esm/pluggableElementTypes/addMenuItems.d.ts +2 -1
  45. package/esm/pluggableElementTypes/addMenuItems.js +2 -1
  46. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +3 -2
  47. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +5 -5
  48. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +2 -4
  49. package/esm/pluggableElementTypes/models/BaseTrackModel.js +2 -4
  50. package/esm/pluggableElementTypes/models/baseTrackConfig.js +3 -0
  51. package/esm/rpc/RpcRegistry.d.ts +2 -1
  52. package/esm/rpc/byteBudget.d.ts +7 -0
  53. package/esm/rpc/byteBudget.js +10 -0
  54. package/esm/rpc/coreRpcMethods.d.ts +1 -1
  55. package/esm/rpc/coreRpcMethods.js +1 -1
  56. package/esm/rpc/explainTransferError.js +3 -3
  57. package/esm/rpc/methods/CoreLoadTrackCode.d.ts +17 -0
  58. package/esm/rpc/methods/CoreLoadTrackCode.js +24 -0
  59. package/esm/ui/AppLogo.js +43 -3
  60. package/esm/ui/buildExtraTrackMenuItems.d.ts +2 -2
  61. package/esm/ui/colors.d.ts +41 -30
  62. package/esm/ui/colors.js +81 -44
  63. package/esm/ui/multiTrackMenuItems.d.ts +2 -2
  64. package/esm/util/alignedBytes.d.ts +19 -0
  65. package/esm/util/alignedBytes.js +37 -0
  66. package/esm/util/analytics.d.ts +5 -6
  67. package/esm/util/analytics.js +5 -3
  68. package/esm/util/bezierConnector.js +5 -1
  69. package/esm/util/categoricalField.d.ts +23 -3
  70. package/esm/util/categoricalField.js +36 -3
  71. package/esm/util/cellMatches.js +14 -14
  72. package/esm/util/cellsStep.d.ts +3 -5
  73. package/esm/util/cellsStep.js +31 -29
  74. package/esm/util/color-bits/parse.js +22 -11
  75. package/esm/util/colorRamp.js +6 -0
  76. package/esm/util/colorScale.d.ts +9 -2
  77. package/esm/util/colorScale.js +33 -2
  78. package/esm/util/colorSchemes.d.ts +4 -2
  79. package/esm/util/colorSchemes.js +4 -1
  80. package/esm/util/isoformRank.d.ts +39 -0
  81. package/esm/util/isoformRank.js +118 -0
  82. package/esm/util/markEncodingTypes.d.ts +3 -1
  83. package/esm/util/quantileExtent.d.ts +22 -0
  84. package/esm/util/quantileExtent.js +24 -1
  85. package/esm/util/systemColorScheme.d.ts +4 -0
  86. package/esm/util/systemColorScheme.js +21 -0
  87. package/esm/util/tabix.d.ts +1 -1
  88. package/esm/util/tabix.js +1 -1
  89. package/esm/util/temporaryAssembly.d.ts +2 -2
  90. package/esm/util/temporaryAssembly.js +2 -3
  91. package/esm/util/trackConfigDelta.d.ts +1 -1
  92. package/esm/util/tracks.d.ts +25 -15
  93. package/esm/util/tracks.js +67 -43
  94. package/esm/util/types/index.d.ts +15 -7
  95. package/esm/util/types/index.js +21 -2
  96. package/esm/util/useRowVirtualScroll.d.ts +1 -0
  97. package/package.json +7 -4
  98. package/esm/rpc/methods/CoreLoadAdapterCode.d.ts +0 -13
  99. package/esm/rpc/methods/CoreLoadAdapterCode.js +0 -16
@@ -1,6 +1,7 @@
1
1
  import { effectiveRenderer, getGraphicsCapabilities, } from '@jbrowse/render-core/graphicsCapabilities';
2
2
  import { readConfObject } from '../configuration/index.js';
3
3
  import { isElectron, rIC } from '../util/index.js';
4
+ import { getConfAssemblyNamesOrNone } from './tracks.js';
4
5
  // Both writers are fire-and-forget from several call sites, so they swallow
5
6
  // their own failures rather than rejecting: a floating rejected promise trips
6
7
  // the webpack-dev-server overlay (it listens for unhandledrejection), and an
@@ -8,7 +9,7 @@ import { isElectron, rIC } from '../util/index.js';
8
9
  export async function writeAWSAnalytics(rootModel, initialTimestamp, sessionQuery) {
9
10
  try {
10
11
  const url = 'https://analytics.jbrowse.org/api/v1';
11
- const multiAssemblyTracks = rootModel.jbrowse.tracks.filter(track => readConfObject(track, 'assemblyNames').length > 1).length;
12
+ const multiAssemblyTracks = rootModel.jbrowse.tracks.filter(track => getConfAssemblyNamesOrNone(track).length > 1).length;
12
13
  // The rung createGpuHal built where a display has built one by idle time,
13
14
  // else the ladder's prediction with any page-wide pin applied — so a
14
15
  // session that fell back to Canvas2D is counted as Canvas2D rather than as
@@ -138,7 +139,8 @@ export function doAnalytics(rootModel, initialTimestamp, initialSessionQuery) {
138
139
  // `[data-app-phase="ready"]`, or a timeout for a page that never shows one
139
140
  // (Desktop's start screen)
140
141
  function afterAppReady(callback, maxWaitMs = 10_000) {
141
- const isReady = () => document.querySelector('[data-app-phase="ready"]') !== null;
142
+ const doc = document;
143
+ const isReady = () => doc.querySelector('[data-app-phase="ready"]') !== null;
142
144
  if (isReady()) {
143
145
  callback();
144
146
  return;
@@ -158,7 +160,7 @@ function afterAppReady(callback, maxWaitMs = 10_000) {
158
160
  }
159
161
  });
160
162
  const timer = setTimeout(finish, maxWaitMs);
161
- observer.observe(document.body, {
163
+ observer.observe(doc.body, {
162
164
  subtree: true,
163
165
  childList: true,
164
166
  attributeFilter: ['data-app-phase'],
@@ -19,6 +19,9 @@ const SPAN_FACTOR = 0.3;
19
19
  // its leading end: unbounded, that hook grows with the arc, and a full-width
20
20
  // inversion flings a ~500px curl back across unrelated reads.
21
21
  const MAX_HANDLE_PX = 200;
22
+ // The handles are horizontal, so a tall vertical gap (stacked split-view
23
+ // panels) only widens the sideways bulge; past this height it earns no more.
24
+ const MAX_VERTICAL_SPAN_PX = 150;
22
25
  // A symmetric cubic's extreme sits 3/4 of the way from its baseline to its
23
26
  // control points, so an arc asked to reach a given apex puts its controls 4/3
24
27
  // past it. Exported because the sashimi overlay solves the same equation in the
@@ -62,7 +65,8 @@ function tangentSign(strand, leading, reversed) {
62
65
  // How much shaping (handle length, bow height) this curve may spend: a fraction
63
66
  // of the distance it has to cover.
64
67
  function shapingBudget(p1, p2) {
65
- return Math.hypot(p2.x - p1.x, p2.y - p1.y) * SPAN_FACTOR;
68
+ const dy = Math.min(Math.abs(p2.y - p1.y), MAX_VERTICAL_SPAN_PX);
69
+ return Math.hypot(p2.x - p1.x, dy) * SPAN_FACTOR;
66
70
  }
67
71
  // Fades to zero as the endpoints' rows separate, since a cross-row curve
68
72
  // already has vertical extent — and via the budget as they close in
@@ -1,3 +1,4 @@
1
+ import type { HeldSlots } from '../ui/colors.ts';
1
2
  /** #api */
2
3
  export declare const STRAND_FIELD = "strand";
3
4
  /**
@@ -19,7 +20,9 @@ export declare const NO_VALUE_LABEL = "(no value)";
19
20
  * orders keys (the `domain` first, the rest by `compareGroupKeys`, `''`
20
21
  * after them), `label` names a key in a legend, `sectionLabel` on a chip, and
21
22
  * `color` paints it. A key's color depends only on the key and the
22
- * declaration, so every region agrees on it.
23
+ * declaration, so every region agrees on it, except that a field dealing its
24
+ * colors (`categoricalField`'s `held`) also steps a key off the colors of the
25
+ * keys met before it.
23
26
  */
24
27
  export interface CategoricalField {
25
28
  field: string;
@@ -47,10 +50,27 @@ export declare function keyNames(keys: readonly unknown[], labels?: readonly str
47
50
  /**
48
51
  * #api
49
52
  * `labels` names the `domain`'s values in a key, one each in order, where a
50
- * config spells them for a reader rather than as the data does.
53
+ * config spells them for a reader rather than as the data does. `held` deals
54
+ * each key the domain does not list a color of its own on first sight
55
+ * (`categoricalScale`); the caller keeps it for as long as the colors should
56
+ * hold.
51
57
  */
52
- export declare function categoricalField(field: string, { domain, range, labels, }?: {
58
+ export declare function categoricalField(field: string, { domain, range, labels, held, }?: {
53
59
  domain?: readonly string[];
54
60
  range?: readonly string[];
55
61
  labels?: readonly string[];
62
+ held?: HeldSlots;
56
63
  }): CategoricalField;
64
+ /**
65
+ * #api
66
+ * Deals `keys` their colours in `field.compare` order, so the colours a set
67
+ * of keys first seen together takes do not depend on the order they arrive
68
+ * in. A no-op for a field that deals nothing.
69
+ */
70
+ export declare function dealKeyColors(field: CategoricalField, keys: Iterable<string>): void;
71
+ /**
72
+ * #api
73
+ * The `HeldSlots` `owner` keeps under `key`, made empty on first ask. Lives as
74
+ * long as `owner` and is never persisted, so a reload deals afresh.
75
+ */
76
+ export declare function heldSlotsOf(owner: object, key: string): HeldSlots;
@@ -37,9 +37,12 @@ export function keyNames(keys, labels = []) {
37
37
  /**
38
38
  * #api
39
39
  * `labels` names the `domain`'s values in a key, one each in order, where a
40
- * config spells them for a reader rather than as the data does.
40
+ * config spells them for a reader rather than as the data does. `held` deals
41
+ * each key the domain does not list a color of its own on first sight
42
+ * (`categoricalScale`); the caller keeps it for as long as the colors should
43
+ * hold.
41
44
  */
42
- export function categoricalField(field, { domain = [], range = [], labels = [], } = {}) {
45
+ export function categoricalField(field, { domain = [], range = [], labels = [], held, } = {}) {
43
46
  // a field with a vocabulary of its own reads it on every channel, a facet's
44
47
  // sections as well as a colour's key
45
48
  const vocabulary = universalPresetOf(field);
@@ -66,8 +69,38 @@ export function categoricalField(field, { domain = [], range = [], labels = [],
66
69
  if (key === '') {
67
70
  return NO_CATEGORY_COLOR;
68
71
  }
69
- colorOf ??= categoricalColorScale(paired, colors);
72
+ colorOf ??= categoricalColorScale(paired, colors, held);
70
73
  return colorOf(key);
71
74
  },
72
75
  };
73
76
  }
77
+ /**
78
+ * #api
79
+ * Deals `keys` their colours in `field.compare` order, so the colours a set
80
+ * of keys first seen together takes do not depend on the order they arrive
81
+ * in. A no-op for a field that deals nothing.
82
+ */
83
+ export function dealKeyColors(field, keys) {
84
+ for (const key of [...new Set(keys)].sort(field.compare)) {
85
+ field.color(key);
86
+ }
87
+ }
88
+ const heldByOwner = new WeakMap();
89
+ /**
90
+ * #api
91
+ * The `HeldSlots` `owner` keeps under `key`, made empty on first ask. Lives as
92
+ * long as `owner` and is never persisted, so a reload deals afresh.
93
+ */
94
+ export function heldSlotsOf(owner, key) {
95
+ let byKey = heldByOwner.get(owner);
96
+ if (byKey === undefined) {
97
+ byKey = new Map();
98
+ heldByOwner.set(owner, byKey);
99
+ }
100
+ let held = byKey.get(key);
101
+ if (held === undefined) {
102
+ held = new Map();
103
+ byKey.set(key, held);
104
+ }
105
+ return held;
106
+ }
@@ -1,8 +1,9 @@
1
1
  // A `cells` step, the interval `bin` behind it and the `aggregate` of its
2
2
  // matches as one walk over the rows' bytes, which `runSteps` in
3
3
  // featureTransforms.ts reaches by relative path only.
4
+ import { DASH, SPACE, firstDrawn, isGapByte, lastDrawn, } from './alignedBytes.js';
4
5
  import { fusesBinAggregate } from './binnedAggregate.js';
5
- import { DASH, DEFAULT_CELLS_FIELD, MATCH, MISMATCH, NO_STATE, SPACE, cellState, firstDrawn, isGapByte, lastDrawn, walkedTexts, } from './cellsStep.js';
6
+ import { DEFAULT_CELLS_FIELD, MATCH, NO_STATE, cellState, comparesBase, opensRun, runKey, walkedTexts, } from './cellsStep.js';
6
7
  import { numberReaderOf, readerOf } from './featureTable.js';
7
8
  import { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, OpSums, groupKey, madeGroups, } from './stepTables.js';
8
9
  const isEdges = (pair) => pair?.length === 2 && pair[0] === 'start' && pair[1] === 'end';
@@ -97,10 +98,10 @@ class SectionBins {
97
98
  }
98
99
  /**
99
100
  * One row's runs as `cells` writes them, each met by its bins as the bin
100
- * step cuts it and in the order the lanes hold them: a run when the next
101
- * column's state differs, an insertion at the reference base after it,
102
- * ahead of the run still open. The bases a run of matches or mismatches
103
- * holds are counted in their bins as the walk passes them.
101
+ * step cuts it and in the order the lanes hold them: a run where the next
102
+ * column `opensRun`, an insertion at the reference base after it,
103
+ * ahead of the run still open. The compared bases a run holds are counted
104
+ * in their bins as the walk passes them.
104
105
  */
105
106
  walk(r, startPos, size, rowBytes, rowAt, rowLen, refBytes, refAt, refLen) {
106
107
  const { matched, compared, low } = this;
@@ -114,7 +115,7 @@ class SectionBins {
114
115
  let runOpen = false;
115
116
  let runBin = 0;
116
117
  let runState = NO_STATE;
117
- let runBase = -1;
118
+ let openKey = -1;
118
119
  let inserting = false;
119
120
  for (let col = 0; col < refLen; col++) {
120
121
  const refByte = refBytes[refAt + col];
@@ -130,22 +131,21 @@ class SectionBins {
130
131
  inserting = false;
131
132
  }
132
133
  const st = cellState(refByte, rowByte, col >= first && col <= last && col < rowLen);
133
- const base = st === MISMATCH ? rowByte : -1;
134
- if (st !== runState || base !== runBase) {
134
+ const key = runKey(st, refByte);
135
+ if (opensRun(st, key, runState, openKey)) {
135
136
  if (runOpen) {
136
137
  this.touch(runBin, pos > edge - size ? bin : bin - 1, r);
137
138
  }
138
139
  runOpen = st !== NO_STATE;
139
140
  runBin = bin;
140
141
  runState = st;
141
- runBase = base;
142
+ openKey = key;
142
143
  }
143
- if (st === MATCH) {
144
- m++;
145
- c++;
146
- }
147
- else if (st === MISMATCH) {
144
+ if (comparesBase(st, refByte)) {
148
145
  c++;
146
+ if (st === MATCH) {
147
+ m++;
148
+ }
149
149
  }
150
150
  pos++;
151
151
  if (pos === edge) {
@@ -2,15 +2,13 @@ import type { FeatureTable, TextColumn } from './featureTable.ts';
2
2
  import type { CellsStep } from './markEncodingTypes.ts';
3
3
  import type { Staged } from './stepTables.ts';
4
4
  export declare const DEFAULT_CELLS_FIELD = "seq";
5
- export declare const DASH = 45;
6
- export declare const SPACE = 32;
7
5
  export declare const MATCH = 0;
8
6
  export declare const MISMATCH = 1;
9
7
  export declare const NO_STATE = 255;
10
- export declare function isGapByte(b: number): b is 32 | 45;
11
- export declare function firstDrawn(bytes: Uint8Array, at: number, len: number): number;
12
- export declare function lastDrawn(bytes: Uint8Array, at: number, len: number, first: number): number;
13
8
  export declare function cellState(refByte: number, rowByte: number, drawn: boolean): 0 | 1 | 2 | 255;
9
+ export declare function comparesBase(state: number, refByte: number): boolean;
10
+ export declare function runKey(state: number, refByte: number): 0 | 1;
11
+ export declare function opensRun(state: number, key: number, runState: number, openKey: number): boolean;
14
12
  /**
15
13
  * The texts a `cells` walk reads, by the row of the table it walked: each
16
14
  * row's own, and the reference it is read against, which a row fanned out of
@@ -1,38 +1,16 @@
1
1
  // The `cells` step: each row's runs against the reference it was fanned out
2
2
  // of, and the texts its walk reads, which the fused identity kernel in
3
3
  // cellMatches.ts walks too. Imported by relative path only.
4
+ import { DASH, SPACE, firstDrawn, isGapByte, isUnknownBase, lastDrawn, sameBase, } from './alignedBytes.js';
4
5
  import { DerivedTable, numberReaderOf, readerOf, valueAt, } from './featureTable.js';
5
6
  import { FannedTable, boundsThrough } from './stepTables.js';
6
7
  export const DEFAULT_CELLS_FIELD = 'seq';
7
- export const DASH = 45;
8
- export const SPACE = 32;
9
- const LOWER_BIT = 0x20;
10
8
  const CELL_STATES = ['match', 'mismatch', 'gap', 'insertion'];
11
9
  export const MATCH = 0;
12
10
  export const MISMATCH = 1;
13
11
  const GAP = 2;
14
12
  const INSERTION = 3;
15
13
  export const NO_STATE = 255;
16
- export function isGapByte(b) {
17
- return b === DASH || b === SPACE;
18
- }
19
- // The columns carrying the row's own sequence: a gap run reaching either end
20
- // of the row measures where the block was cut, not the alignment, so it is no
21
- // cell either.
22
- export function firstDrawn(bytes, at, len) {
23
- let first = 0;
24
- while (first < len && isGapByte(bytes[at + first])) {
25
- first++;
26
- }
27
- return first;
28
- }
29
- export function lastDrawn(bytes, at, len, first) {
30
- let last = len - 1;
31
- while (last > first && isGapByte(bytes[at + last])) {
32
- last--;
33
- }
34
- return last;
35
- }
36
14
  // A reference base's cell: no state outside the drawn columns, else the row's
37
15
  // gap, or its base matched or not regardless of case.
38
16
  export function cellState(refByte, rowByte, drawn) {
@@ -40,10 +18,25 @@ export function cellState(refByte, rowByte, drawn) {
40
18
  ? NO_STATE
41
19
  : isGapByte(rowByte)
42
20
  ? GAP
43
- : (refByte | LOWER_BIT) === (rowByte | LOWER_BIT)
21
+ : sameBase(refByte, rowByte)
44
22
  ? MATCH
45
23
  : MISMATCH;
46
24
  }
25
+ // Whether a cell counts toward identity: matched or not against a known
26
+ // reference base.
27
+ export function comparesBase(state, refByte) {
28
+ return state <= MISMATCH && !isUnknownBase(refByte);
29
+ }
30
+ // What the cells of one run share besides their state: whether they are
31
+ // compared, so a run's `match` is one value.
32
+ export function runKey(state, refByte) {
33
+ return state <= MISMATCH && isUnknownBase(refByte) ? 1 : 0;
34
+ }
35
+ // A mismatch always starts a run of its own, so each differing base is one
36
+ // instance and a text over `base` letters every one.
37
+ export function opensRun(state, key, runState, openKey) {
38
+ return state !== runState || key !== openKey || state === MISMATCH;
39
+ }
47
40
  // The row a cell step reads against: past any table that only reordered or
48
41
  // annotated the rows, the one they were fanned out of.
49
42
  function containerOf(table) {
@@ -218,7 +211,11 @@ class CellTable extends DerivedTable {
218
211
  const values = new Float32Array(this.length);
219
212
  for (let i = 0; i < this.length; i++) {
220
213
  const s = this.state[i];
221
- values[i] = s === MATCH ? 1 : s === MISMATCH ? 0 : Number.NaN;
214
+ values[i] = comparesBase(s, this.refByte(i))
215
+ ? s === MATCH
216
+ ? 1
217
+ : 0
218
+ : Number.NaN;
222
219
  }
223
220
  return { kind: 'number', values, at: undefined, nanIsAbsent: true };
224
221
  });
@@ -254,6 +251,11 @@ class CellTable extends DerivedTable {
254
251
  }
255
252
  }
256
253
  }
254
+ refByte(i) {
255
+ const { reference: ref } = this.texts;
256
+ const f = this.texts.refOf(this.parentOf(i));
257
+ return ref.bytes[ref.offset[f] + this.textAt[i]];
258
+ }
257
259
  mismatched(i) {
258
260
  const { bytes, offset } = this.texts.row;
259
261
  const k = this.texts.rowOf(this.parentOf(i));
@@ -342,7 +344,7 @@ class RunLanes {
342
344
  const last = lastDrawn(rowBytes, rowAt, rowLen, first);
343
345
  let runStart = -1;
344
346
  let runState = NO_STATE;
345
- let runBase = -1;
347
+ let openKey = -1;
346
348
  let runCol = 0;
347
349
  let insertAt = -1;
348
350
  for (let col = 0; col < refLen; col++) {
@@ -364,8 +366,8 @@ class RunLanes {
364
366
  insertAt = -1;
365
367
  }
366
368
  const st = cellState(refByte, rowByte, col >= first && col <= last && col < rowLen);
367
- const base = st === MISMATCH ? rowByte : -1;
368
- if (st !== runState || base !== runBase) {
369
+ const key = runKey(st, refByte);
370
+ if (opensRun(st, key, runState, openKey)) {
369
371
  if (runStart >= 0) {
370
372
  start[n] = runStart;
371
373
  end[n] = pos;
@@ -376,7 +378,7 @@ class RunLanes {
376
378
  }
377
379
  runStart = st === NO_STATE ? -1 : pos;
378
380
  runState = st;
379
- runBase = base;
381
+ openKey = key;
380
382
  runCol = col;
381
383
  }
382
384
  pos++;
@@ -239,16 +239,27 @@ export function parseColor(color) {
239
239
  }
240
240
  throw new Error(`Color.parse(): invalid CSS color: "${color}"`);
241
241
  }
242
+ /** A channel's leading number, throwing where it has none, as `rgb(a,b,1)`. */
243
+ function parseNumber(value) {
244
+ const n = Number.parseFloat(value);
245
+ if (Number.isNaN(n)) {
246
+ throw new TypeError(`Color.parse(): invalid CSS color channel: "${value}"`);
247
+ }
248
+ return n;
249
+ }
242
250
  /**
243
- * Accepts: "50%", "128"
251
+ * Accepts: "50%", "128", "none"
244
252
  * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#values
245
253
  * @returns a value in the 0 to 255 range
246
254
  */
247
255
  function parseColorChannel(channel) {
256
+ if (channel === 'none') {
257
+ return 0;
258
+ }
248
259
  if (channel.charCodeAt(channel.length - 1) === PERCENT) {
249
- return Math.round((Number.parseFloat(channel) / 100) * 255);
260
+ return Math.round((parseNumber(channel) / 100) * 255);
250
261
  }
251
- return Math.round(Number.parseFloat(channel));
262
+ return Math.round(parseNumber(channel));
252
263
  }
253
264
  /**
254
265
  * Accepts: "50%", ".5", "0.5"
@@ -268,9 +279,9 @@ function parseAlphaValue(channel) {
268
279
  return 0;
269
280
  }
270
281
  if (channel.charCodeAt(channel.length - 1) === PERCENT) {
271
- return Number.parseFloat(channel) / 100;
282
+ return parseNumber(channel) / 100;
272
283
  }
273
- return Number.parseFloat(channel);
284
+ return parseNumber(channel);
274
285
  }
275
286
  /**
276
287
  * Accepts: "360", "360deg", "400grad", "6.28rad", "1turn", "none"
@@ -302,7 +313,7 @@ function parseAngle(angle) {
302
313
  factor = 360;
303
314
  }
304
315
  }
305
- const turns = Number.parseFloat(angle) / factor;
316
+ const turns = parseNumber(angle) / factor;
306
317
  // Hue is periodic, and every caller of this is a hue. `hueToRGB` corrects its
307
318
  // argument by at most one turn, so it reads `hsl(-720deg)` as a hue two turns
308
319
  // below the wheel rather than as red — folding here is what makes the three
@@ -317,7 +328,7 @@ function parsePercentage(value) {
317
328
  if (value.charCodeAt(0) === N) {
318
329
  return 0;
319
330
  }
320
- return Number.parseFloat(value) / 100;
331
+ return parseNumber(value) / 100;
321
332
  }
322
333
  /**
323
334
  * Accepts: "1.0", "100%", "none"
@@ -328,9 +339,9 @@ function parsePercentageOrValue(value) {
328
339
  return 0;
329
340
  }
330
341
  if (value.charCodeAt(value.length - 1) === PERCENT) {
331
- return Number.parseFloat(value) / 100;
342
+ return parseNumber(value) / 100;
332
343
  }
333
- return Number.parseFloat(value);
344
+ return parseNumber(value);
334
345
  }
335
346
  /**
336
347
  * Accepts: "100", "100%", "none"
@@ -341,9 +352,9 @@ function parsePercentageFor(value, range) {
341
352
  return 0;
342
353
  }
343
354
  if (value.charCodeAt(value.length - 1) === PERCENT) {
344
- return (Number.parseFloat(value) / 100) * range;
355
+ return (parseNumber(value) / 100) * range;
345
356
  }
346
- return Number.parseFloat(value);
357
+ return parseNumber(value);
347
358
  }
348
359
  // HSL functions
349
360
  function hueToRGB(p, q, t) {
@@ -90,6 +90,11 @@ const PURPLEORANGE_STOPS = [
90
90
  [179, 88, 6, 255],
91
91
  [127, 59, 8, 255],
92
92
  ];
93
+ const REDGREYBLUE_STOPS = [
94
+ [199, 67, 56, 255],
95
+ [140, 140, 140, 255],
96
+ [47, 102, 176, 255],
97
+ ];
93
98
  const SCHEME_STOPS = {
94
99
  viridis: VIRIDIS_STOPS,
95
100
  juicebox: JUICEBOX_STOPS,
@@ -101,6 +106,7 @@ const SCHEME_STOPS = {
101
106
  cividis: stopsFromHex(CIVIDIS_HEX_SPEC),
102
107
  redblue: REDBLUE_STOPS,
103
108
  purpleorange: PURPLEORANGE_STOPS,
109
+ redgreyblue: REDGREYBLUE_STOPS,
104
110
  };
105
111
  function luminanceOverWhite([r, g, b, a]) {
106
112
  const over = (c) => 255 + (c - 255) * (a / 255);
@@ -36,8 +36,8 @@ export interface FieldPreset<S extends string = ColorScaleName> {
36
36
  export type FieldPresets<S extends string = ColorScaleName> = Readonly<Record<string, FieldPreset<S>>>;
37
37
  /**
38
38
  * The fields whose values have names, an order and colours of their own on
39
- * every display, beneath each display's own presets. Red forward and blue
40
- * reverse is what the synteny ribbons paint too. The SV classes are
39
+ * every display, beneath each display's own presets. The synteny ribbons
40
+ * paint strand from this range too. The SV classes are
41
41
  * `svClassOf`'s, deletion red and duplication blue as dbVar and gnomAD-SV
42
42
  * paint them, insertion the pileup's purple, and the rest kept apart under
43
43
  * deuteranopia and protanopia. A record with no class files under `''`, so
@@ -143,6 +143,13 @@ export interface ColorSlots {
143
143
  domainQuantile?: number;
144
144
  labels?: readonly unknown[];
145
145
  }
146
+ /** A quantitative scale's ends among the members `member` reads, each a number or unset. */
147
+ export declare function scaleEndsOf(member: (name: string) => unknown): ScaleEnds;
148
+ /**
149
+ * The members `colorProblems` reads among the ones `member` reads, each of the
150
+ * type it reads or unset, so a config snapshot and a config node answer alike.
151
+ */
152
+ export declare function colorSlotsOf(member: (name: string) => unknown): ColorSlots;
146
153
  /**
147
154
  * What a colour object's slots say together that no single slot can refuse,
148
155
  * since the config editor writes one slot at a time (ADR-133), read with its
@@ -20,8 +20,8 @@ export const IDENTITY_SCALE = 'identity';
20
20
  const CATEGORICAL_PRESET = { scale: 'categorical' };
21
21
  /**
22
22
  * The fields whose values have names, an order and colours of their own on
23
- * every display, beneath each display's own presets. Red forward and blue
24
- * reverse is what the synteny ribbons paint too. The SV classes are
23
+ * every display, beneath each display's own presets. The synteny ribbons
24
+ * paint strand from this range too. The SV classes are
25
25
  * `svClassOf`'s, deletion red and duplication blue as dbVar and gnomAD-SV
26
26
  * paint them, insertion the pileup's purple, and the rest kept apart under
27
27
  * deuteranopia and protanopia. A record with no class files under `''`, so
@@ -165,6 +165,37 @@ export function scaleEndProblems({ domainMin, domainMax, domainQuantile }, { rev
165
165
  export function noticeLines(setting, problems) {
166
166
  return problems.map(({ slot, message }) => `${setting}.${slot}: ${message}`);
167
167
  }
168
+ function numberOf(value) {
169
+ return typeof value === 'number' ? value : undefined;
170
+ }
171
+ function stringOf(value) {
172
+ return typeof value === 'string' ? value : undefined;
173
+ }
174
+ function listOf(value) {
175
+ return Array.isArray(value) ? value : undefined;
176
+ }
177
+ /** A quantitative scale's ends among the members `member` reads, each a number or unset. */
178
+ export function scaleEndsOf(member) {
179
+ return {
180
+ domainMin: numberOf(member('domainMin')),
181
+ domainMax: numberOf(member('domainMax')),
182
+ domainQuantile: numberOf(member('domainQuantile')),
183
+ };
184
+ }
185
+ /**
186
+ * The members `colorProblems` reads among the ones `member` reads, each of the
187
+ * type it reads or unset, so a config snapshot and a config node answer alike.
188
+ */
189
+ export function colorSlotsOf(member) {
190
+ return {
191
+ ...scaleEndsOf(member),
192
+ field: stringOf(member('field')) ?? '',
193
+ scale: stringOf(member('scale')),
194
+ domain: listOf(member('domain')),
195
+ range: listOf(member('range')),
196
+ labels: listOf(member('labels')),
197
+ };
198
+ }
168
199
  function pinned(entry) {
169
200
  return entry !== '' && Number.isFinite(Number(entry));
170
201
  }
@@ -7,9 +7,11 @@
7
7
  * transparent to red, as Juicebox paints contacts; `fall` runs white through
8
8
  * yellow and red to black, as HiGlass does; `reds` and `blues` are
9
9
  * ColorBrewer's, from white; `redblue` and `purpleorange` diverge through
10
- * white, ColorBrewer's RdBu and PuOr.
10
+ * white, ColorBrewer's RdBu and PuOr; `redgreyblue` diverges through a grey
11
+ * middle, which stays visible on a white page where a white middle would not,
12
+ * as the MAF display's identity ramp paints it.
11
13
  */
12
- export declare const COLOR_SCHEMES: readonly ['viridis', 'magma', 'inferno', 'cividis', 'juicebox', 'fall', 'reds', 'blues', 'redblue', 'purpleorange'];
14
+ export declare const COLOR_SCHEMES: readonly ['viridis', 'magma', 'inferno', 'cividis', 'juicebox', 'fall', 'reds', 'blues', 'redblue', 'purpleorange', 'redgreyblue'];
13
15
  /** #api */
14
16
  export type ColorSchemeName = (typeof COLOR_SCHEMES)[number];
15
17
  /**
@@ -7,7 +7,9 @@
7
7
  * transparent to red, as Juicebox paints contacts; `fall` runs white through
8
8
  * yellow and red to black, as HiGlass does; `reds` and `blues` are
9
9
  * ColorBrewer's, from white; `redblue` and `purpleorange` diverge through
10
- * white, ColorBrewer's RdBu and PuOr.
10
+ * white, ColorBrewer's RdBu and PuOr; `redgreyblue` diverges through a grey
11
+ * middle, which stays visible on a white page where a white middle would not,
12
+ * as the MAF display's identity ramp paints it.
11
13
  */
12
14
  export const COLOR_SCHEMES = [
13
15
  'viridis',
@@ -20,6 +22,7 @@ export const COLOR_SCHEMES = [
20
22
  'blues',
21
23
  'redblue',
22
24
  'purpleorange',
25
+ 'redgreyblue',
23
26
  ];
24
27
  /**
25
28
  * #api
@@ -0,0 +1,39 @@
1
+ import type { Feature, SimpleFeatureSerializedNoId } from './simpleFeature.ts';
2
+ /**
3
+ * Which isoform of a gene a display shows first: the canvas gene glyph's
4
+ * `longestCoding` collapse and the sequence panel's default transcript both
5
+ * rank with this, so the panel opens on the isoform the track drew. Generic
6
+ * over the feature shape because the worker ranks live `Feature`s and the
7
+ * panel ranks their serialized form.
8
+ */
9
+ export interface IsoformReader<T> {
10
+ type: (feature: T) => string | undefined;
11
+ start: (feature: T) => number;
12
+ end: (feature: T) => number;
13
+ subfeatures: (feature: T) => readonly T[];
14
+ attribute: (feature: T, field: string) => unknown;
15
+ }
16
+ export declare const featureIsoformReader: IsoformReader<Feature>;
17
+ export declare const serializedIsoformReader: IsoformReader<SimpleFeatureSerializedNoId>;
18
+ /**
19
+ * The attribute naming a gene's representative isoform, and the values of it
20
+ * that do, best first. The canvas display's `canonicalTranscriptField` and
21
+ * `canonicalTranscriptTags` slots default to these.
22
+ */
23
+ export interface CanonicalTranscripts {
24
+ field: string;
25
+ tags: readonly string[];
26
+ }
27
+ export declare const DEFAULT_CANONICAL_TRANSCRIPTS: CanonicalTranscripts;
28
+ export interface IsoformScore {
29
+ canonical: number;
30
+ coding: boolean;
31
+ size: number;
32
+ }
33
+ export declare function isCodingIsoform<T>(read: IsoformReader<T>, feature: T): boolean;
34
+ export declare function isoformScorer<T>(read: IsoformReader<T>, { field, tags }: CanonicalTranscripts): (feature: T) => IsoformScore;
35
+ export declare function rankIsoforms<T>(isoforms: readonly T[], scoreOf: (feature: T) => IsoformScore): T[];
36
+ export interface CanonicalTranscriptsHost {
37
+ canonicalTranscripts: CanonicalTranscripts;
38
+ }
39
+ export declare function isCanonicalTranscriptsHost(thing: unknown): thing is CanonicalTranscriptsHost;