@jbrowse/core 5.0.0-beta.10 → 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 (145) hide show
  1. package/README.md +210 -80
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/ArrayValue.js +0 -2
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.d.ts +3 -0
  4. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.js +14 -1
  5. package/esm/BaseFeatureWidget/BaseFeatureDetail/BasicValue.js +22 -6
  6. package/esm/BaseFeatureWidget/BaseFeatureDetail/jexlFilterActions.js +3 -4
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.d.ts +2 -0
  8. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.js +5 -5
  9. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.js +1 -1
  10. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.d.ts +3 -1
  11. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.js +2 -2
  12. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.d.ts +2 -1
  13. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.js +7 -14
  14. package/esm/BaseFeatureWidget/SequenceFeatureDetails/seqtypes/CDNASequence.js +3 -3
  15. package/esm/BaseFeatureWidget/SequenceFeatureDetails/useSequenceData.js +8 -5
  16. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +7 -0
  17. package/esm/BaseFeatureWidget/stateModelFactory.js +11 -0
  18. package/esm/BaseFeatureWidget/types.d.ts +2 -0
  19. package/esm/CorePlugin.js +32 -1
  20. package/esm/PluginManager.d.ts +6 -3
  21. package/esm/PluginManager.js +6 -3
  22. package/esm/ReExports/coreModules.generated.js +291 -283
  23. package/esm/ReExports/coreWorkerModules.generated.js +288 -281
  24. package/esm/ReExports/list.js +10 -2
  25. package/esm/TextSearch/TextSearchManager.d.ts +3 -3
  26. package/esm/TextSearch/TextSearchManager.js +6 -13
  27. package/esm/configuration/configurationSchema.d.ts +33 -95
  28. package/esm/configuration/configurationSchema.js +50 -218
  29. package/esm/configuration/configurationSlot.d.ts +18 -64
  30. package/esm/configuration/configurationSlot.js +31 -97
  31. package/esm/configuration/formatDetailsConfigSchema.js +2 -0
  32. package/esm/configuration/getConf.d.ts +7 -36
  33. package/esm/configuration/getConf.js +7 -37
  34. package/esm/configuration/index.d.ts +5 -2
  35. package/esm/configuration/index.js +3 -1
  36. package/esm/configuration/partitionAdvanced.d.ts +12 -0
  37. package/esm/configuration/partitionAdvanced.js +42 -0
  38. package/esm/configuration/plot.d.ts +67 -0
  39. package/esm/configuration/plot.js +175 -0
  40. package/esm/configuration/readConfObject.d.ts +15 -17
  41. package/esm/configuration/readConfObject.js +19 -50
  42. package/esm/configuration/schemaTypes.js +5 -1
  43. package/esm/configuration/snapshotPreprocess.js +1 -3
  44. package/esm/configuration/types.d.ts +60 -159
  45. package/esm/pluggableElementTypes/DisplayType.d.ts +14 -0
  46. package/esm/pluggableElementTypes/DisplayType.js +13 -0
  47. package/esm/pluggableElementTypes/RpcMethodType.d.ts +7 -0
  48. package/esm/pluggableElementTypes/RpcMethodType.js +9 -0
  49. package/esm/pluggableElementTypes/WidgetType.d.ts +6 -0
  50. package/esm/pluggableElementTypes/WidgetType.js +6 -0
  51. package/esm/pluggableElementTypes/addMenuItems.d.ts +2 -1
  52. package/esm/pluggableElementTypes/addMenuItems.js +2 -1
  53. package/esm/pluggableElementTypes/index.js +1 -8
  54. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +58 -1
  55. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +84 -2
  56. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +2 -4
  57. package/esm/pluggableElementTypes/models/BaseTrackModel.js +2 -4
  58. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +2 -2
  59. package/esm/pluggableElementTypes/models/BaseViewModel.js +1 -1
  60. package/esm/pluggableElementTypes/models/baseTrackConfig.js +3 -0
  61. package/esm/rpc/RpcRegistry.d.ts +2 -1
  62. package/esm/rpc/byteBudget.d.ts +7 -0
  63. package/esm/rpc/byteBudget.js +10 -0
  64. package/esm/rpc/coreRpcMethods.d.ts +1 -1
  65. package/esm/rpc/coreRpcMethods.js +1 -1
  66. package/esm/rpc/explainTransferError.js +3 -3
  67. package/esm/rpc/methods/CoreLoadTrackCode.d.ts +17 -0
  68. package/esm/rpc/methods/CoreLoadTrackCode.js +24 -0
  69. package/esm/ui/AppLogo.js +43 -3
  70. package/esm/ui/JexlFilterDialog.d.ts +1 -1
  71. package/esm/ui/JexlFilterDialog.js +2 -3
  72. package/esm/ui/buildExtraTrackMenuItems.d.ts +2 -2
  73. package/esm/ui/colors.d.ts +41 -30
  74. package/esm/ui/colors.js +81 -44
  75. package/esm/ui/multiTrackMenuItems.d.ts +2 -2
  76. package/esm/util/TimeTraveller.d.ts +3 -0
  77. package/esm/util/TimeTraveller.js +34 -2
  78. package/esm/util/alignedBytes.d.ts +19 -0
  79. package/esm/util/alignedBytes.js +37 -0
  80. package/esm/util/analytics.d.ts +5 -6
  81. package/esm/util/analytics.js +5 -3
  82. package/esm/util/baseDisplayConfig.d.ts +8 -0
  83. package/esm/util/baseDisplayConfig.js +27 -0
  84. package/esm/util/bezierConnector.js +5 -1
  85. package/esm/util/cachedSetup.d.ts +16 -6
  86. package/esm/util/cachedSetup.js +108 -38
  87. package/esm/util/categoricalField.d.ts +23 -3
  88. package/esm/util/categoricalField.js +36 -3
  89. package/esm/util/cellMatches.d.ts +23 -0
  90. package/esm/util/cellMatches.js +264 -0
  91. package/esm/util/cellsStep.d.ts +27 -0
  92. package/esm/util/cellsStep.js +439 -0
  93. package/esm/util/color-bits/parse.js +22 -11
  94. package/esm/util/colorRamp.js +6 -0
  95. package/esm/util/colorScale.d.ts +9 -2
  96. package/esm/util/colorScale.js +33 -2
  97. package/esm/util/colorSchemes.d.ts +4 -2
  98. package/esm/util/colorSchemes.js +4 -1
  99. package/esm/util/copyToClipboard.js +8 -1
  100. package/esm/util/createAbortRotation.js +3 -3
  101. package/esm/util/featureTransforms.d.ts +7 -6
  102. package/esm/util/featureTransforms.js +131 -479
  103. package/esm/util/fetchPhases.d.ts +1 -1
  104. package/esm/util/formatBytes.js +1 -1
  105. package/esm/util/formatGuessers.d.ts +25 -14
  106. package/esm/util/formatGuessers.js +42 -27
  107. package/esm/util/formatRelativeTime.js +1 -1
  108. package/esm/util/getLocationUri.d.ts +2 -1
  109. package/esm/util/getLocationUri.js +8 -2
  110. package/esm/util/groupKeys.d.ts +4 -5
  111. package/esm/util/groupKeys.js +7 -6
  112. package/esm/util/intervals.js +9 -4
  113. package/esm/util/io/index.js +3 -1
  114. package/esm/util/isoformRank.d.ts +39 -0
  115. package/esm/util/isoformRank.js +118 -0
  116. package/esm/util/jexlFilters.d.ts +31 -49
  117. package/esm/util/jexlFilters.js +29 -39
  118. package/esm/util/locString.js +5 -2
  119. package/esm/util/markEncoding.js +48 -8
  120. package/esm/util/markEncodingTypes.d.ts +11 -2
  121. package/esm/util/quantileExtent.d.ts +22 -0
  122. package/esm/util/quantileExtent.js +24 -1
  123. package/esm/util/springAnimate.js +1 -1
  124. package/esm/util/stepTables.d.ts +14 -0
  125. package/esm/util/stepTables.js +38 -4
  126. package/esm/util/systemColorScheme.d.ts +4 -0
  127. package/esm/util/systemColorScheme.js +21 -0
  128. package/esm/util/tabix.d.ts +7 -3
  129. package/esm/util/tabix.js +3 -3
  130. package/esm/util/temporaryAssembly.d.ts +2 -2
  131. package/esm/util/temporaryAssembly.js +2 -3
  132. package/esm/util/trackConfigDelta.d.ts +1 -1
  133. package/esm/util/tracks.d.ts +28 -45
  134. package/esm/util/tracks.js +71 -82
  135. package/esm/util/types/index.d.ts +15 -7
  136. package/esm/util/types/index.js +21 -2
  137. package/esm/util/unknownSnapshotKeys.d.ts +3 -1
  138. package/esm/util/unknownSnapshotKeys.js +5 -11
  139. package/esm/util/usePanZoom.js +10 -3
  140. package/esm/util/useRafCommit.js +1 -0
  141. package/esm/util/useRowVirtualScroll.d.ts +1 -0
  142. package/esm/util/withFeatureDetails.js +4 -0
  143. package/package.json +13 -9
  144. package/esm/rpc/methods/CoreLoadAdapterCode.d.ts +0 -13
  145. package/esm/rpc/methods/CoreLoadAdapterCode.js +0 -16
@@ -1,4 +1,11 @@
1
+ import { checkAbortSignal, makeAbortError } from './aborting.js';
1
2
  import { downloadStatus } from './progress.js';
3
+ /**
4
+ * How long a setup with no live waiter keeps running before its signal aborts:
5
+ * `createAbortRotation.begin()` aborts the superseded call before it posts the
6
+ * replacement, which joins within this window instead of restarting from byte 0.
7
+ */
8
+ export const ABANDONED_SETUP_GRACE_MS = 100;
2
9
  /**
3
10
  * Memoize an adapter's one-time setup (open the file, read the header, build a
4
11
  * parser, download and parse a whole file) into a loader every method can
@@ -15,11 +22,13 @@ import { downloadStatus } from './progress.js';
15
22
  * moment that fetch was superseded — the display's latest-wins guard gates the
16
23
  * old callback off — so the fetch replacing it awaited a multi-GB parse behind
17
24
  * a blank loading overlay.
18
- * - **`signal` is withheld from the shared work.** Honoring one caller's
19
- * cancel would abort a parse the caller replacing it is already waiting on,
20
- * and reject them both. A superseded fetch just stops listening. Cancellation
21
- * stays with the per-call work (indexed range queries), which no one else is
22
- * waiting on.
25
+ * - **Cancellation is shared, not per caller.** The setup gets one signal that
26
+ * aborts once its last live waiter has aborted and nobody rejoins within
27
+ * {@link ABANDONED_SETUP_GRACE_MS}, so a superseded fetch cannot cancel a
28
+ * parse its replacement is waiting on, yet a download nobody awaits stops.
29
+ * Each caller's own await rejects the moment its own signal aborts. An
30
+ * abandoned setup leaves the memo first, so the next call starts afresh and
31
+ * an abandoned run's late statuses reach no one.
23
32
  * - **`label` is shown only while the *first* attempt is in flight.** Re-entry
24
33
  * on pan/zoom (every getFeatures and byte estimate awaits the loader) would
25
34
  * otherwise re-flash "Downloading index" over an index that is already
@@ -30,48 +39,109 @@ import { downloadStatus } from './progress.js';
30
39
  * a whole-file fetch driving its own download bar — since two labels for one
31
40
  * download is worse than one, and phases nest (see `openPhase` in progress.ts).
32
41
  * `onProgress` is for handing to an index reader that can upgrade the label to a
33
- * determinate bar.
42
+ * determinate bar; like the status, it fans out to every live waiter. It is
43
+ * undefined without a `label`, so a setup that narrates itself never opts its
44
+ * reads into streamed progress.
34
45
  */
35
46
  export function cachedSetup({ setup, label, }) {
36
- const waiting = new Set();
37
- let cached;
38
- let ready = false;
39
- const run = (opts, onProgress) => {
40
- cached ??= setup({
41
- ...opts,
42
- signal: undefined,
43
- statusCallback: status => {
44
- for (const cb of waiting) {
45
- cb(status);
47
+ let flight;
48
+ let resident;
49
+ const launch = (started, opts) => setup({
50
+ ...opts,
51
+ signal: started.controller.signal,
52
+ statusCallback: status => {
53
+ for (const cb of started.waiting.keys()) {
54
+ cb(status);
55
+ }
56
+ },
57
+ }, label === undefined
58
+ ? undefined
59
+ : (current, total) => {
60
+ for (const report of started.reporters.keys()) {
61
+ report(current, total);
62
+ }
63
+ }).then(value => {
64
+ clearTimeout(started.grace);
65
+ if (flight === started) {
66
+ resident = { value };
67
+ }
68
+ return value;
69
+ }, (e) => {
70
+ clearTimeout(started.grace);
71
+ if (flight === started) {
72
+ flight = undefined;
73
+ }
74
+ throw e;
75
+ });
76
+ const abandon = (started) => {
77
+ if (flight === started) {
78
+ flight = undefined;
79
+ started.controller.abort();
80
+ }
81
+ };
82
+ const join = (opts, onProgress) => {
83
+ const { signal, statusCallback } = opts;
84
+ const started = (flight ??= {
85
+ controller: new AbortController(),
86
+ waiting: new Map(),
87
+ reporters: new Map(),
88
+ live: 0,
89
+ });
90
+ clearTimeout(started.grace);
91
+ started.live++;
92
+ count(started.waiting, statusCallback);
93
+ count(started.reporters, onProgress);
94
+ const promise = (started.promise ??= launch(started, opts));
95
+ return new Promise((resolve, reject) => {
96
+ let joined = true;
97
+ const leave = () => {
98
+ if (joined) {
99
+ joined = false;
100
+ signal?.removeEventListener('abort', onAbort);
101
+ started.live--;
102
+ uncount(started.waiting, statusCallback);
103
+ uncount(started.reporters, onProgress);
104
+ }
105
+ };
106
+ const onAbort = () => {
107
+ leave();
108
+ reject(makeAbortError());
109
+ if (started.live === 0) {
110
+ started.grace = setTimeout(() => {
111
+ abandon(started);
112
+ }, ABANDONED_SETUP_GRACE_MS);
46
113
  }
47
- },
48
- }, onProgress)
49
- .then(result => {
50
- ready = true;
51
- return result;
52
- })
53
- .catch((e) => {
54
- cached = undefined;
55
- throw e;
114
+ };
115
+ signal?.addEventListener('abort', onAbort, { once: true });
116
+ promise.finally(leave).then(resolve, reject);
56
117
  });
57
- return cached;
58
118
  };
59
119
  return async (opts = {}) => {
60
- const { statusCallback } = opts;
61
- if (statusCallback) {
62
- waiting.add(statusCallback);
120
+ if (resident) {
121
+ return resident.value;
63
122
  }
64
- try {
65
- return await (label === undefined || ready
66
- ? run(opts)
67
- : downloadStatus(label, statusCallback, onProgress => run(opts, onProgress)));
123
+ checkAbortSignal(opts.signal);
124
+ const wait = (onProgress) => join(opts, onProgress);
125
+ return label === undefined
126
+ ? wait()
127
+ : downloadStatus(label, opts.statusCallback, wait);
128
+ };
129
+ }
130
+ function count(counts, key) {
131
+ if (key) {
132
+ counts.set(key, (counts.get(key) ?? 0) + 1);
133
+ }
134
+ }
135
+ function uncount(counts, key) {
136
+ if (key) {
137
+ const left = counts.get(key) - 1;
138
+ if (left > 0) {
139
+ counts.set(key, left);
68
140
  }
69
- finally {
70
- if (statusCallback) {
71
- waiting.delete(statusCallback);
72
- }
141
+ else {
142
+ counts.delete(key);
73
143
  }
74
- };
144
+ }
75
145
  }
76
146
  /**
77
147
  * {@link cachedSetup} taking a bare function instead of an options object.
@@ -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
+ }
@@ -0,0 +1,23 @@
1
+ import type { AggregateStep, BinStep, CellsStep, TransformStep } from './markEncodingTypes.ts';
2
+ import type { Staged } from './stepTables.ts';
3
+ /**
4
+ * Whether a `cells` step, an interval `bin` and an `aggregate` run as one
5
+ * walk: the bin cuts the runs' own extents at whole-base edges it writes as
6
+ * `start` and `end`, the aggregate groups by those, and every op is the sum or
7
+ * mean of `match` weighted by `overlap`, so a bin's answer is its bases
8
+ * matched and compared.
9
+ */
10
+ export declare function fusesCellMatches(_cells: CellsStep, bin: BinStep, agg: AggregateStep): boolean;
11
+ /** Whether a step list starts with the bin and aggregate a `cells` fuses with. */
12
+ export declare function fusesAfterCells(cells: CellsStep, steps: readonly TransformStep[] | undefined): boolean;
13
+ /**
14
+ * `cells`, then a `bin` over the runs' extents and the `aggregate` of their
15
+ * `match` weighted by `overlap`, in one walk with no run table: each bin's
16
+ * bases matched and compared, counted straight off the texts. Every run
17
+ * meets its bins in the order its lanes would hold it, so the groups come out
18
+ * where the three steps put them, and a weighted sum of 0s and 1s is an
19
+ * integer, so each sum is theirs to the bit. Undefined where a row's start is
20
+ * not a whole base inside a Uint32 or the bins spread too thinly for a dense
21
+ * index, and the steps run as written.
22
+ */
23
+ export declare function binnedCellMatches({ table, bounds }: Staged, cells: CellsStep, bin: BinStep, agg: AggregateStep): Staged | undefined;
@@ -0,0 +1,264 @@
1
+ // A `cells` step, the interval `bin` behind it and the `aggregate` of its
2
+ // matches as one walk over the rows' bytes, which `runSteps` in
3
+ // featureTransforms.ts reaches by relative path only.
4
+ import { DASH, SPACE, firstDrawn, isGapByte, lastDrawn, } from './alignedBytes.js';
5
+ import { fusesBinAggregate } from './binnedAggregate.js';
6
+ import { DEFAULT_CELLS_FIELD, MATCH, NO_STATE, cellState, comparesBase, opensRun, runKey, walkedTexts, } from './cellsStep.js';
7
+ import { numberReaderOf, readerOf } from './featureTable.js';
8
+ import { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, OpSums, groupKey, madeGroups, } from './stepTables.js';
9
+ const isEdges = (pair) => pair?.length === 2 && pair[0] === 'start' && pair[1] === 'end';
10
+ /**
11
+ * Whether a `cells` step, an interval `bin` and an `aggregate` run as one
12
+ * walk: the bin cuts the runs' own extents at whole-base edges it writes as
13
+ * `start` and `end`, the aggregate groups by those, and every op is the sum or
14
+ * mean of `match` weighted by `overlap`, so a bin's answer is its bases
15
+ * matched and compared.
16
+ */
17
+ export function fusesCellMatches(_cells, bin, agg) {
18
+ return (fusesBinAggregate(bin, agg) &&
19
+ isEdges(bin.fields) &&
20
+ isEdges(bin.as ?? DEFAULT_BIN_AS) &&
21
+ Number.isInteger(bin.step) &&
22
+ bin.step > 0 &&
23
+ agg.ops.length > 0 &&
24
+ agg.ops.every(op => (op.op === 'mean' || op.op === 'sum') &&
25
+ op.field === 'match' &&
26
+ op.weight === BIN_OVERLAP_FIELD));
27
+ }
28
+ /** Whether a step list starts with the bin and aggregate a `cells` fuses with. */
29
+ export function fusesAfterCells(cells, steps) {
30
+ const [bin, agg] = steps ?? [];
31
+ return (bin?.type === 'bin' &&
32
+ agg?.type === 'aggregate' &&
33
+ fusesCellMatches(cells, bin, agg));
34
+ }
35
+ // Where the widest section spans more bins than this over the bins all the
36
+ // rows could reach, its dense index costs more than the runs would, and the
37
+ // steps run as written.
38
+ const SPARSE_BINS_SLACK = 65536;
39
+ // A run's start is a Uint32 lane in the unfused `cells`.
40
+ const MAX_POSITION = 2 ** 32;
41
+ /**
42
+ * The groups the pieces of a section's runs meet, numbered in the order they
43
+ * meet them, over one dense index of the section's bins that each section
44
+ * reuses; with each bin's bases matched and compared.
45
+ */
46
+ class SectionBins {
47
+ groupOfBin;
48
+ matched;
49
+ compared;
50
+ groupRow;
51
+ groupBin;
52
+ groupSection;
53
+ groupMatched;
54
+ groupCompared;
55
+ groups = 0;
56
+ low = 0;
57
+ section = 0;
58
+ sectionStart = 0;
59
+ constructor(span, capacity) {
60
+ this.groupOfBin = new Int32Array(span).fill(-1);
61
+ this.matched = new Float64Array(span);
62
+ this.compared = new Float64Array(span);
63
+ this.groupRow = new Uint32Array(capacity);
64
+ this.groupBin = new Float64Array(capacity);
65
+ this.groupSection = new Uint32Array(capacity);
66
+ this.groupMatched = new Float64Array(capacity);
67
+ this.groupCompared = new Float64Array(capacity);
68
+ }
69
+ open(section, low) {
70
+ this.section = section;
71
+ this.low = low;
72
+ this.sectionStart = this.groups;
73
+ }
74
+ // A piece's bin from `from` to `to`, a new group where no piece met it.
75
+ touch(from, to, r) {
76
+ const { groupOfBin, low } = this;
77
+ for (let b = from; b <= to; b++) {
78
+ if (groupOfBin[b - low] < 0) {
79
+ const g = this.groups++;
80
+ groupOfBin[b - low] = g;
81
+ this.groupRow[g] = r;
82
+ this.groupBin[g] = b;
83
+ this.groupSection[g] = this.section;
84
+ }
85
+ }
86
+ }
87
+ // The section's counts onto its groups, and its bins cleared for the next.
88
+ close() {
89
+ const { groupOfBin, matched, compared, low } = this;
90
+ for (let g = this.sectionStart; g < this.groups; g++) {
91
+ const k = this.groupBin[g] - low;
92
+ this.groupMatched[g] = matched[k];
93
+ this.groupCompared[g] = compared[k];
94
+ matched[k] = 0;
95
+ compared[k] = 0;
96
+ groupOfBin[k] = -1;
97
+ }
98
+ }
99
+ /**
100
+ * One row's runs as `cells` writes them, each met by its bins as the bin
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.
105
+ */
106
+ walk(r, startPos, size, rowBytes, rowAt, rowLen, refBytes, refAt, refLen) {
107
+ const { matched, compared, low } = this;
108
+ const first = firstDrawn(rowBytes, rowAt, rowLen);
109
+ const last = lastDrawn(rowBytes, rowAt, rowLen, first);
110
+ let pos = startPos;
111
+ let bin = Math.floor(pos / size);
112
+ let edge = bin * size + size;
113
+ let m = 0;
114
+ let c = 0;
115
+ let runOpen = false;
116
+ let runBin = 0;
117
+ let runState = NO_STATE;
118
+ let openKey = -1;
119
+ let inserting = false;
120
+ for (let col = 0; col < refLen; col++) {
121
+ const refByte = refBytes[refAt + col];
122
+ const rowByte = col < rowLen ? rowBytes[rowAt + col] : SPACE;
123
+ if (refByte === DASH) {
124
+ if (!inserting && col < rowLen && !isGapByte(rowByte)) {
125
+ inserting = true;
126
+ }
127
+ continue;
128
+ }
129
+ if (inserting) {
130
+ this.touch(bin, bin, r);
131
+ inserting = false;
132
+ }
133
+ const st = cellState(refByte, rowByte, col >= first && col <= last && col < rowLen);
134
+ const key = runKey(st, refByte);
135
+ if (opensRun(st, key, runState, openKey)) {
136
+ if (runOpen) {
137
+ this.touch(runBin, pos > edge - size ? bin : bin - 1, r);
138
+ }
139
+ runOpen = st !== NO_STATE;
140
+ runBin = bin;
141
+ runState = st;
142
+ openKey = key;
143
+ }
144
+ if (comparesBase(st, refByte)) {
145
+ c++;
146
+ if (st === MATCH) {
147
+ m++;
148
+ }
149
+ }
150
+ pos++;
151
+ if (pos === edge) {
152
+ if (c > 0) {
153
+ matched[bin - low] += m;
154
+ compared[bin - low] += c;
155
+ m = 0;
156
+ c = 0;
157
+ }
158
+ bin++;
159
+ edge += size;
160
+ }
161
+ }
162
+ if (runOpen) {
163
+ this.touch(runBin, pos > edge - size ? bin : bin - 1, r);
164
+ }
165
+ if (c > 0) {
166
+ matched[bin - low] += m;
167
+ compared[bin - low] += c;
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * `cells`, then a `bin` over the runs' extents and the `aggregate` of their
173
+ * `match` weighted by `overlap`, in one walk with no run table: each bin's
174
+ * bases matched and compared, counted straight off the texts. Every run
175
+ * meets its bins in the order its lanes would hold it, so the groups come out
176
+ * where the three steps put them, and a weighted sum of 0s and 1s is an
177
+ * integer, so each sum is theirs to the bit. Undefined where a row's start is
178
+ * not a whole base inside a Uint32 or the bins spread too thinly for a dense
179
+ * index, and the steps run as written.
180
+ */
181
+ export function binnedCellMatches({ table, bounds }, cells, bin, agg) {
182
+ const { field = DEFAULT_CELLS_FIELD } = cells;
183
+ const texts = walkedTexts(table, field);
184
+ const readStart = numberReaderOf(table.column('start'));
185
+ const size = bin.step;
186
+ const sections = bounds.length - 1;
187
+ const lowBin = new Float64Array(sections);
188
+ let widest = 0;
189
+ let reach = 0;
190
+ let spans = 0;
191
+ for (let s = 0; s < sections; s++) {
192
+ let low = Infinity;
193
+ let high = -Infinity;
194
+ for (let r = bounds[s]; r < bounds[s + 1]; r++) {
195
+ const refLen = texts.reference.length[texts.refOf(r)];
196
+ const pos = readStart(r);
197
+ if (refLen > 0 && pos >= 0) {
198
+ if (!Number.isInteger(pos) || pos + refLen >= MAX_POSITION) {
199
+ return undefined;
200
+ }
201
+ const lo = Math.floor(pos / size);
202
+ const hi = Math.floor((pos + refLen) / size);
203
+ low = Math.min(low, lo);
204
+ high = Math.max(high, hi);
205
+ reach += hi - lo + 1;
206
+ }
207
+ }
208
+ lowBin[s] = low;
209
+ const span = high >= low ? high - low + 1 : 0;
210
+ widest = Math.max(widest, span);
211
+ spans += span;
212
+ }
213
+ if (widest > 2 * reach + SPARSE_BINS_SLACK) {
214
+ return undefined;
215
+ }
216
+ const { bytes: rowBytes, offset: rowOffset, length: rowLength } = texts.row;
217
+ const { bytes: refBytes, offset: refOffset, length: refLength, } = texts.reference;
218
+ const walk = new SectionBins(widest, Math.min(spans, reach));
219
+ for (let s = 0; s < sections; s++) {
220
+ walk.open(s, lowBin[s]);
221
+ for (let r = bounds[s]; r < bounds[s + 1]; r++) {
222
+ const f = texts.refOf(r);
223
+ const refLen = refLength[f];
224
+ const pos = readStart(r);
225
+ if (refLen > 0 && pos >= 0) {
226
+ const k = texts.rowOf(r);
227
+ walk.walk(r, pos, size, rowBytes, rowOffset[k], rowLength[k], refBytes, refOffset[f], refLen);
228
+ }
229
+ }
230
+ walk.close();
231
+ }
232
+ const { groups, groupRow, groupBin, groupSection } = walk;
233
+ const ops = agg.ops.map(op => {
234
+ const sums = new OpSums(op, groups);
235
+ sums.sum.set(walk.groupMatched.subarray(0, groups));
236
+ sums.weight.set(walk.groupCompared.subarray(0, groups));
237
+ return sums.column(groups);
238
+ });
239
+ const start = new Float64Array(groups);
240
+ const end = new Float64Array(groups);
241
+ for (let g = 0; g < groups; g++) {
242
+ start[g] = groupBin[g] * size;
243
+ end[g] = start[g] + size;
244
+ }
245
+ const readRef = readerOf(table.column('refName'));
246
+ const refNames = new Array(groups);
247
+ for (let g = 0; g < groups; g++) {
248
+ refNames[g] = readRef(groupRow[g]);
249
+ }
250
+ return madeGroups({
251
+ groups,
252
+ keys: (agg.groupby ?? []).map((field) => [
253
+ field,
254
+ Array.from(field === 'start' ? start : end, groupKey),
255
+ ]),
256
+ ops,
257
+ step: agg,
258
+ refNames,
259
+ start,
260
+ end,
261
+ sectionOf: groupSection,
262
+ sections: bounds.length,
263
+ });
264
+ }
@@ -0,0 +1,27 @@
1
+ import type { FeatureTable, TextColumn } from './featureTable.ts';
2
+ import type { CellsStep } from './markEncodingTypes.ts';
3
+ import type { Staged } from './stepTables.ts';
4
+ export declare const DEFAULT_CELLS_FIELD = "seq";
5
+ export declare const MATCH = 0;
6
+ export declare const MISMATCH = 1;
7
+ export declare const NO_STATE = 255;
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;
12
+ /**
13
+ * The texts a `cells` walk reads, by the row of the table it walked: each
14
+ * row's own, and the reference it is read against, which a row fanned out of
15
+ * a container reads off the container.
16
+ */
17
+ declare class WalkedTexts {
18
+ readonly row: TextColumn;
19
+ readonly reference: TextColumn;
20
+ private readonly refRow;
21
+ constructor(row: TextColumn, ref: TextColumn, refRow?: Uint32Array);
22
+ rowOf(r: number): number;
23
+ refOf(r: number): number;
24
+ }
25
+ export declare function walkedTexts(table: FeatureTable, field: string): WalkedTexts;
26
+ export declare function cells({ table, bounds }: Staged, step: CellsStep): Staged;
27
+ export {};