@jbrowse/core 5.0.0-beta.10 → 5.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -5
- package/esm/BaseFeatureWidget/BaseFeatureDetail/ArrayValue.js +0 -2
- package/esm/BaseFeatureWidget/BaseFeatureDetail/BasicValue.js +22 -6
- package/esm/BaseFeatureWidget/BaseFeatureDetail/jexlFilterActions.js +3 -4
- package/esm/BaseFeatureWidget/SequenceFeatureDetails/useSequenceData.js +8 -5
- package/esm/CorePlugin.js +32 -1
- package/esm/ReExports/coreModules.generated.js +248 -246
- package/esm/ReExports/coreWorkerModules.generated.js +246 -244
- package/esm/ReExports/list.js +3 -2
- package/esm/configuration/index.d.ts +2 -0
- package/esm/configuration/index.js +1 -0
- package/esm/configuration/plot.d.ts +61 -0
- package/esm/configuration/plot.js +142 -0
- package/esm/pluggableElementTypes/DisplayType.d.ts +7 -0
- package/esm/pluggableElementTypes/DisplayType.js +6 -0
- package/esm/pluggableElementTypes/WidgetType.d.ts +6 -0
- package/esm/pluggableElementTypes/WidgetType.js +6 -0
- package/esm/pluggableElementTypes/index.js +1 -8
- package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +57 -1
- package/esm/pluggableElementTypes/models/BaseDisplayModel.js +84 -2
- package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +2 -2
- package/esm/pluggableElementTypes/models/BaseViewModel.js +1 -1
- package/esm/ui/JexlFilterDialog.d.ts +1 -1
- package/esm/ui/JexlFilterDialog.js +2 -3
- package/esm/util/TimeTraveller.d.ts +3 -0
- package/esm/util/TimeTraveller.js +34 -2
- package/esm/util/baseDisplayConfig.d.ts +8 -0
- package/esm/util/baseDisplayConfig.js +27 -0
- package/esm/util/cachedSetup.d.ts +16 -6
- package/esm/util/cachedSetup.js +108 -38
- package/esm/util/cellMatches.d.ts +23 -0
- package/esm/util/cellMatches.js +264 -0
- package/esm/util/cellsStep.d.ts +29 -0
- package/esm/util/cellsStep.js +437 -0
- package/esm/util/copyToClipboard.js +8 -1
- package/esm/util/createAbortRotation.js +3 -3
- package/esm/util/featureTransforms.d.ts +7 -6
- package/esm/util/featureTransforms.js +131 -479
- package/esm/util/fetchPhases.d.ts +1 -1
- package/esm/util/formatBytes.js +1 -1
- package/esm/util/formatGuessers.d.ts +25 -14
- package/esm/util/formatGuessers.js +42 -27
- package/esm/util/formatRelativeTime.js +1 -1
- package/esm/util/getLocationUri.d.ts +2 -1
- package/esm/util/getLocationUri.js +8 -2
- package/esm/util/groupKeys.d.ts +4 -5
- package/esm/util/groupKeys.js +7 -6
- package/esm/util/intervals.js +9 -4
- package/esm/util/io/index.js +3 -1
- package/esm/util/jexlFilters.d.ts +31 -49
- package/esm/util/jexlFilters.js +29 -39
- package/esm/util/locString.js +5 -2
- package/esm/util/markEncoding.js +48 -8
- package/esm/util/markEncodingTypes.d.ts +8 -1
- package/esm/util/springAnimate.js +1 -1
- package/esm/util/stepTables.d.ts +14 -0
- package/esm/util/stepTables.js +38 -4
- package/esm/util/tabix.d.ts +6 -2
- package/esm/util/tabix.js +2 -2
- package/esm/util/tracks.d.ts +3 -30
- package/esm/util/tracks.js +4 -39
- package/esm/util/unknownSnapshotKeys.d.ts +3 -1
- package/esm/util/unknownSnapshotKeys.js +5 -11
- package/esm/util/usePanZoom.js +10 -3
- package/esm/util/useRafCommit.js +1 -0
- package/esm/util/withFeatureDetails.js +4 -0
- package/package.json +9 -8
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { BaseOptions } from '../data_adapters/BaseAdapter/types.ts';
|
|
2
2
|
/** byte-granularity progress reporter to hand an index reader's `onProgress` */
|
|
3
3
|
type OnProgress = (current: number, total?: number) => void;
|
|
4
|
+
/**
|
|
5
|
+
* How long a setup with no live waiter keeps running before its signal aborts:
|
|
6
|
+
* `createAbortRotation.begin()` aborts the superseded call before it posts the
|
|
7
|
+
* replacement, which joins within this window instead of restarting from byte 0.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ABANDONED_SETUP_GRACE_MS = 100;
|
|
4
10
|
/**
|
|
5
11
|
* Memoize an adapter's one-time setup (open the file, read the header, build a
|
|
6
12
|
* parser, download and parse a whole file) into a loader every method can
|
|
@@ -17,11 +23,13 @@ type OnProgress = (current: number, total?: number) => void;
|
|
|
17
23
|
* moment that fetch was superseded — the display's latest-wins guard gates the
|
|
18
24
|
* old callback off — so the fetch replacing it awaited a multi-GB parse behind
|
|
19
25
|
* a blank loading overlay.
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* - **Cancellation is shared, not per caller.** The setup gets one signal that
|
|
27
|
+
* aborts once its last live waiter has aborted and nobody rejoins within
|
|
28
|
+
* {@link ABANDONED_SETUP_GRACE_MS}, so a superseded fetch cannot cancel a
|
|
29
|
+
* parse its replacement is waiting on, yet a download nobody awaits stops.
|
|
30
|
+
* Each caller's own await rejects the moment its own signal aborts. An
|
|
31
|
+
* abandoned setup leaves the memo first, so the next call starts afresh and
|
|
32
|
+
* an abandoned run's late statuses reach no one.
|
|
25
33
|
* - **`label` is shown only while the *first* attempt is in flight.** Re-entry
|
|
26
34
|
* on pan/zoom (every getFeatures and byte estimate awaits the loader) would
|
|
27
35
|
* otherwise re-flash "Downloading index" over an index that is already
|
|
@@ -32,7 +40,9 @@ type OnProgress = (current: number, total?: number) => void;
|
|
|
32
40
|
* a whole-file fetch driving its own download bar — since two labels for one
|
|
33
41
|
* download is worse than one, and phases nest (see `openPhase` in progress.ts).
|
|
34
42
|
* `onProgress` is for handing to an index reader that can upgrade the label to a
|
|
35
|
-
* determinate bar.
|
|
43
|
+
* determinate bar; like the status, it fans out to every live waiter. It is
|
|
44
|
+
* undefined without a `label`, so a setup that narrates itself never opts its
|
|
45
|
+
* reads into streamed progress.
|
|
36
46
|
*/
|
|
37
47
|
export declare function cachedSetup<T>({ setup, label, }: {
|
|
38
48
|
setup: (opts: BaseOptions, onProgress?: OnProgress) => Promise<T>;
|
package/esm/util/cachedSetup.js
CHANGED
|
@@ -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
|
-
* -
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
.then(
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
waiting.add(statusCallback);
|
|
120
|
+
if (resident) {
|
|
121
|
+
return resident.value;
|
|
63
122
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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.
|
|
@@ -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 { 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 { numberReaderOf, readerOf } from './featureTable.js';
|
|
7
|
+
import { BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, OpSums, groupKey, madeGroups, } from './stepTables.js';
|
|
8
|
+
const isEdges = (pair) => pair?.length === 2 && pair[0] === 'start' && pair[1] === 'end';
|
|
9
|
+
/**
|
|
10
|
+
* Whether a `cells` step, an interval `bin` and an `aggregate` run as one
|
|
11
|
+
* walk: the bin cuts the runs' own extents at whole-base edges it writes as
|
|
12
|
+
* `start` and `end`, the aggregate groups by those, and every op is the sum or
|
|
13
|
+
* mean of `match` weighted by `overlap`, so a bin's answer is its bases
|
|
14
|
+
* matched and compared.
|
|
15
|
+
*/
|
|
16
|
+
export function fusesCellMatches(_cells, bin, agg) {
|
|
17
|
+
return (fusesBinAggregate(bin, agg) &&
|
|
18
|
+
isEdges(bin.fields) &&
|
|
19
|
+
isEdges(bin.as ?? DEFAULT_BIN_AS) &&
|
|
20
|
+
Number.isInteger(bin.step) &&
|
|
21
|
+
bin.step > 0 &&
|
|
22
|
+
agg.ops.length > 0 &&
|
|
23
|
+
agg.ops.every(op => (op.op === 'mean' || op.op === 'sum') &&
|
|
24
|
+
op.field === 'match' &&
|
|
25
|
+
op.weight === BIN_OVERLAP_FIELD));
|
|
26
|
+
}
|
|
27
|
+
/** Whether a step list starts with the bin and aggregate a `cells` fuses with. */
|
|
28
|
+
export function fusesAfterCells(cells, steps) {
|
|
29
|
+
const [bin, agg] = steps ?? [];
|
|
30
|
+
return (bin?.type === 'bin' &&
|
|
31
|
+
agg?.type === 'aggregate' &&
|
|
32
|
+
fusesCellMatches(cells, bin, agg));
|
|
33
|
+
}
|
|
34
|
+
// Where the widest section spans more bins than this over the bins all the
|
|
35
|
+
// rows could reach, its dense index costs more than the runs would, and the
|
|
36
|
+
// steps run as written.
|
|
37
|
+
const SPARSE_BINS_SLACK = 65536;
|
|
38
|
+
// A run's start is a Uint32 lane in the unfused `cells`.
|
|
39
|
+
const MAX_POSITION = 2 ** 32;
|
|
40
|
+
/**
|
|
41
|
+
* The groups the pieces of a section's runs meet, numbered in the order they
|
|
42
|
+
* meet them, over one dense index of the section's bins that each section
|
|
43
|
+
* reuses; with each bin's bases matched and compared.
|
|
44
|
+
*/
|
|
45
|
+
class SectionBins {
|
|
46
|
+
groupOfBin;
|
|
47
|
+
matched;
|
|
48
|
+
compared;
|
|
49
|
+
groupRow;
|
|
50
|
+
groupBin;
|
|
51
|
+
groupSection;
|
|
52
|
+
groupMatched;
|
|
53
|
+
groupCompared;
|
|
54
|
+
groups = 0;
|
|
55
|
+
low = 0;
|
|
56
|
+
section = 0;
|
|
57
|
+
sectionStart = 0;
|
|
58
|
+
constructor(span, capacity) {
|
|
59
|
+
this.groupOfBin = new Int32Array(span).fill(-1);
|
|
60
|
+
this.matched = new Float64Array(span);
|
|
61
|
+
this.compared = new Float64Array(span);
|
|
62
|
+
this.groupRow = new Uint32Array(capacity);
|
|
63
|
+
this.groupBin = new Float64Array(capacity);
|
|
64
|
+
this.groupSection = new Uint32Array(capacity);
|
|
65
|
+
this.groupMatched = new Float64Array(capacity);
|
|
66
|
+
this.groupCompared = new Float64Array(capacity);
|
|
67
|
+
}
|
|
68
|
+
open(section, low) {
|
|
69
|
+
this.section = section;
|
|
70
|
+
this.low = low;
|
|
71
|
+
this.sectionStart = this.groups;
|
|
72
|
+
}
|
|
73
|
+
// A piece's bin from `from` to `to`, a new group where no piece met it.
|
|
74
|
+
touch(from, to, r) {
|
|
75
|
+
const { groupOfBin, low } = this;
|
|
76
|
+
for (let b = from; b <= to; b++) {
|
|
77
|
+
if (groupOfBin[b - low] < 0) {
|
|
78
|
+
const g = this.groups++;
|
|
79
|
+
groupOfBin[b - low] = g;
|
|
80
|
+
this.groupRow[g] = r;
|
|
81
|
+
this.groupBin[g] = b;
|
|
82
|
+
this.groupSection[g] = this.section;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// The section's counts onto its groups, and its bins cleared for the next.
|
|
87
|
+
close() {
|
|
88
|
+
const { groupOfBin, matched, compared, low } = this;
|
|
89
|
+
for (let g = this.sectionStart; g < this.groups; g++) {
|
|
90
|
+
const k = this.groupBin[g] - low;
|
|
91
|
+
this.groupMatched[g] = matched[k];
|
|
92
|
+
this.groupCompared[g] = compared[k];
|
|
93
|
+
matched[k] = 0;
|
|
94
|
+
compared[k] = 0;
|
|
95
|
+
groupOfBin[k] = -1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 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.
|
|
104
|
+
*/
|
|
105
|
+
walk(r, startPos, size, rowBytes, rowAt, rowLen, refBytes, refAt, refLen) {
|
|
106
|
+
const { matched, compared, low } = this;
|
|
107
|
+
const first = firstDrawn(rowBytes, rowAt, rowLen);
|
|
108
|
+
const last = lastDrawn(rowBytes, rowAt, rowLen, first);
|
|
109
|
+
let pos = startPos;
|
|
110
|
+
let bin = Math.floor(pos / size);
|
|
111
|
+
let edge = bin * size + size;
|
|
112
|
+
let m = 0;
|
|
113
|
+
let c = 0;
|
|
114
|
+
let runOpen = false;
|
|
115
|
+
let runBin = 0;
|
|
116
|
+
let runState = NO_STATE;
|
|
117
|
+
let runBase = -1;
|
|
118
|
+
let inserting = false;
|
|
119
|
+
for (let col = 0; col < refLen; col++) {
|
|
120
|
+
const refByte = refBytes[refAt + col];
|
|
121
|
+
const rowByte = col < rowLen ? rowBytes[rowAt + col] : SPACE;
|
|
122
|
+
if (refByte === DASH) {
|
|
123
|
+
if (!inserting && col < rowLen && !isGapByte(rowByte)) {
|
|
124
|
+
inserting = true;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (inserting) {
|
|
129
|
+
this.touch(bin, bin, r);
|
|
130
|
+
inserting = false;
|
|
131
|
+
}
|
|
132
|
+
const st = cellState(refByte, rowByte, col >= first && col <= last && col < rowLen);
|
|
133
|
+
const base = st === MISMATCH ? rowByte : -1;
|
|
134
|
+
if (st !== runState || base !== runBase) {
|
|
135
|
+
if (runOpen) {
|
|
136
|
+
this.touch(runBin, pos > edge - size ? bin : bin - 1, r);
|
|
137
|
+
}
|
|
138
|
+
runOpen = st !== NO_STATE;
|
|
139
|
+
runBin = bin;
|
|
140
|
+
runState = st;
|
|
141
|
+
runBase = base;
|
|
142
|
+
}
|
|
143
|
+
if (st === MATCH) {
|
|
144
|
+
m++;
|
|
145
|
+
c++;
|
|
146
|
+
}
|
|
147
|
+
else if (st === MISMATCH) {
|
|
148
|
+
c++;
|
|
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,29 @@
|
|
|
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 DASH = 45;
|
|
6
|
+
export declare const SPACE = 32;
|
|
7
|
+
export declare const MATCH = 0;
|
|
8
|
+
export declare const MISMATCH = 1;
|
|
9
|
+
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
|
+
export declare function cellState(refByte: number, rowByte: number, drawn: boolean): 0 | 1 | 2 | 255;
|
|
14
|
+
/**
|
|
15
|
+
* The texts a `cells` walk reads, by the row of the table it walked: each
|
|
16
|
+
* row's own, and the reference it is read against, which a row fanned out of
|
|
17
|
+
* a container reads off the container.
|
|
18
|
+
*/
|
|
19
|
+
declare class WalkedTexts {
|
|
20
|
+
readonly row: TextColumn;
|
|
21
|
+
readonly reference: TextColumn;
|
|
22
|
+
private readonly refRow;
|
|
23
|
+
constructor(row: TextColumn, ref: TextColumn, refRow?: Uint32Array);
|
|
24
|
+
rowOf(r: number): number;
|
|
25
|
+
refOf(r: number): number;
|
|
26
|
+
}
|
|
27
|
+
export declare function walkedTexts(table: FeatureTable, field: string): WalkedTexts;
|
|
28
|
+
export declare function cells({ table, bounds }: Staged, step: CellsStep): Staged;
|
|
29
|
+
export {};
|