@jbrowse/core 5.0.0-beta.1 → 5.0.0-beta.3
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 +112 -43
- package/esm/BaseFeatureWidget/BaseFeatureDetail/FeatureDetails.d.ts +2 -3
- package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.d.ts +4 -0
- package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.js +17 -0
- package/esm/BaseFeatureWidget/BaseFeatureDetail/index.js +4 -3
- package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.d.ts +2 -2
- package/esm/BaseFeatureWidget/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
- package/esm/BaseFeatureWidget/index.d.ts +1 -1
- package/esm/BaseFeatureWidget/stateModelFactory.d.ts +8 -1
- package/esm/BaseFeatureWidget/stateModelFactory.js +6 -0
- package/esm/BaseFeatureWidget/types.d.ts +10 -2
- package/esm/PluginLoader.d.ts +1 -0
- package/esm/PluginLoader.js +52 -8
- package/esm/PluginManager.d.ts +99 -17
- package/esm/PluginManager.js +194 -26
- package/esm/ReExports/BaseFeatureDetails.js +1 -1
- package/esm/ReExports/MuiDataGridReExports.d.ts +1 -0
- package/esm/ReExports/MuiDataGridReExports.js +2 -2
- package/esm/ReExports/barrelOnlyNames.d.ts +5 -0
- package/esm/ReExports/barrelOnlyNames.js +63 -0
- package/esm/ReExports/modules.js +31 -109
- package/esm/ReExports/publicTracks.d.ts +1 -0
- package/esm/ReExports/publicTracks.js +14 -0
- package/esm/ReExports/publicUi.d.ts +4 -0
- package/esm/ReExports/publicUi.js +33 -0
- package/esm/ReExports/publicUtil.d.ts +1 -1
- package/esm/ReExports/publicUtil.js +8 -4
- package/esm/ReExports/sharedModules.d.ts +96 -0
- package/esm/ReExports/sharedModules.js +66 -0
- package/esm/ReExports/uiStub.d.ts +2 -0
- package/esm/ReExports/uiStub.js +11 -0
- package/esm/ReExports/workerModules.d.ts +2 -0
- package/esm/ReExports/workerModules.js +15 -0
- package/esm/ReExports/workerNamespaceNames.d.ts +11 -0
- package/esm/ReExports/workerNamespaceNames.js +281 -0
- package/esm/configuration/configurationSchema.d.ts +5 -38
- package/esm/configuration/configurationSchema.js +64 -40
- package/esm/configuration/index.d.ts +2 -2
- package/esm/configuration/index.js +1 -1
- package/esm/configuration/promotableDefaults.d.ts +54 -19
- package/esm/configuration/promotableDefaults.js +117 -48
- package/esm/configuration/promotablePin.d.ts +50 -40
- package/esm/configuration/promotableResolve.d.ts +1 -1
- package/esm/configuration/promotableResolve.js +1 -1
- package/esm/configuration/tabixIndexFields.d.ts +2 -2
- package/esm/configuration/tabixIndexFields.js +2 -2
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +35 -4
- package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +41 -0
- package/esm/data_adapters/BaseAdapter/featureDensity.js +32 -0
- package/esm/data_adapters/BaseAdapter/index.d.ts +2 -1
- package/esm/data_adapters/BaseAdapter/index.js +1 -0
- package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
- package/esm/data_adapters/BaseAdapter/stats.js +52 -13
- package/esm/data_adapters/BaseAdapter/types.d.ts +3 -8
- package/esm/pluggableElementTypes/AdapterType.d.ts +2 -1
- package/esm/pluggableElementTypes/AdapterType.js +1 -1
- package/esm/pluggableElementTypes/DisplayType.d.ts +4 -12
- package/esm/pluggableElementTypes/DisplayType.js +3 -10
- package/esm/pluggableElementTypes/LazyStateModelElement.d.ts +27 -0
- package/esm/pluggableElementTypes/LazyStateModelElement.js +71 -0
- package/esm/pluggableElementTypes/ViewType.d.ts +20 -4
- package/esm/pluggableElementTypes/ViewType.js +32 -4
- package/esm/pluggableElementTypes/extendElementType.d.ts +1 -2
- package/esm/pluggableElementTypes/extendElementType.js +9 -5
- package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +44 -3
- package/esm/pluggableElementTypes/models/BaseDisplayModel.js +75 -2
- package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
- package/esm/pluggableElementTypes/models/BaseTrackModel.js +34 -2
- package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
- package/esm/pluggableElementTypes/models/BaseViewModel.js +28 -0
- package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +14 -10
- package/esm/pluggableElementTypes/models/assertDisplayContract.js +25 -32
- package/esm/pluggableElementTypes/models/baseTrackConfig.js +7 -1
- package/esm/rpc/RpcRegistry.d.ts +10 -0
- package/esm/rpc/byteBudget.d.ts +9 -0
- package/esm/rpc/coreRpcMethods.d.ts +1 -0
- package/esm/rpc/coreRpcMethods.js +1 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
- package/esm/svg/svgReady.d.ts +1 -0
- package/esm/ui/BaseExportSvgDialog.d.ts +1 -1
- package/esm/ui/CascadingMenu.js +30 -8
- package/esm/ui/ErrorMessageStackTraceDialog.js +5 -6
- package/esm/ui/ExportSvgDialog.d.ts +1 -1
- package/esm/ui/LoadingOverlay.d.ts +14 -0
- package/esm/ui/LoadingOverlay.js +1 -1
- package/esm/ui/MenuTypes.d.ts +14 -5
- package/esm/ui/MenuTypes.js +3 -0
- package/esm/ui/PaletteContext.d.ts +11 -4
- package/esm/ui/PaletteContext.js +34 -5
- package/esm/ui/PinAdornment.js +31 -43
- package/esm/ui/ScrollChrome.d.ts +16 -0
- package/esm/ui/ScrollChrome.js +12 -0
- package/esm/ui/ScrollZoomToggle.d.ts +4 -3
- package/esm/ui/ScrollZoomToggle.js +4 -3
- package/esm/ui/SizeSliderRow.js +1 -1
- package/esm/ui/index.d.ts +3 -3
- package/esm/ui/index.js +4 -4
- package/esm/ui/launchViewMenu.js +7 -1
- package/esm/ui/legendMenuItem.d.ts +10 -16
- package/esm/ui/legendMenuItem.js +9 -20
- package/esm/ui/makeSizeMenu.js +1 -1
- package/esm/ui/menuItemAdornment.d.ts +2 -3
- package/esm/ui/menuItemAdornment.js +2 -3
- package/esm/ui/menuItems.d.ts +2 -3
- package/esm/ui/menuItems.js +1 -2
- package/esm/ui/palette.d.ts +11 -11
- package/esm/ui/palette.js +11 -11
- package/esm/ui/promotablePinCoverage.d.ts +16 -2
- package/esm/ui/promotablePinCoverage.js +40 -3
- package/esm/ui/theme.js +5 -0
- package/esm/ui/toggleMenuItems.d.ts +14 -9
- package/esm/ui/toggleMenuItems.js +32 -15
- package/esm/util/Base1DUtils.d.ts +4 -2
- package/esm/util/Base1DUtils.js +12 -4
- package/esm/util/SvgCanvas.js +4 -3
- package/esm/util/adapterConfigKey.d.ts +12 -0
- package/esm/util/adapterConfigKey.js +14 -0
- package/esm/util/adapterMetadata.js +2 -1
- package/esm/util/addAndShowTrack.d.ts +1 -1
- package/esm/util/addAndShowTrack.js +5 -1
- package/esm/util/addTrackFromWidget.js +2 -2
- package/esm/util/analytics.js +4 -5
- package/esm/util/cacheBudgets.d.ts +22 -19
- package/esm/util/cacheBudgets.js +22 -19
- package/esm/util/clampToContig.d.ts +17 -0
- package/esm/util/clampToContig.js +19 -0
- package/esm/util/colorBits.d.ts +2 -1
- package/esm/util/colorBits.js +2 -5
- package/esm/util/createStopTokenRotation.d.ts +9 -7
- package/esm/util/createStopTokenRotation.js +7 -27
- package/esm/util/featureTypes.d.ts +24 -0
- package/esm/util/featureTypes.js +35 -0
- package/esm/util/fetchContext.d.ts +2 -2
- package/esm/util/fetchContext.js +2 -2
- package/esm/util/fetchPhases.d.ts +10 -10
- package/esm/util/hooks.d.ts +8 -0
- package/esm/util/hooks.js +27 -0
- package/esm/util/index.d.ts +6 -5
- package/esm/util/index.js +5 -4
- package/esm/util/installFetch.d.ts +71 -34
- package/esm/util/installFetch.js +57 -29
- package/esm/util/installInitAutorun.d.ts +14 -13
- package/esm/util/installInitAutorun.js +17 -17
- package/esm/util/installPrerequisiteFetch.d.ts +86 -0
- package/esm/util/installPrerequisiteFetch.js +73 -0
- package/esm/util/lazyAfterAttach.d.ts +13 -0
- package/esm/util/lazyAfterAttach.js +31 -0
- package/esm/util/legendCandidates.d.ts +50 -0
- package/esm/util/legendCandidates.js +87 -0
- package/esm/util/mstUtils.d.ts +12 -1
- package/esm/util/mstUtils.js +20 -2
- package/esm/util/openFeatureWidget.d.ts +5 -0
- package/esm/util/openFeatureWidget.js +1 -0
- package/esm/util/openViews.d.ts +16 -0
- package/esm/util/openViews.js +22 -0
- package/esm/util/parentWalk.d.ts +1 -1
- package/esm/util/parentWalk.js +1 -1
- package/esm/util/pluginStore.d.ts +1 -0
- package/esm/util/pluginStore.js +8 -1
- package/esm/util/range.d.ts +13 -9
- package/esm/util/range.js +17 -8
- package/esm/util/renderToStaticMarkup.js +10 -0
- package/esm/util/rxjs.d.ts +16 -0
- package/esm/util/rxjs.js +49 -0
- package/esm/util/scheduleDetachedDestroy.d.ts +18 -0
- package/esm/util/scheduleDetachedDestroy.js +24 -0
- package/esm/util/scrollLatch.d.ts +1 -0
- package/esm/util/scrollLatch.js +8 -0
- package/esm/util/stopToken.d.ts +6 -5
- package/esm/util/tabix.d.ts +10 -4
- package/esm/util/tabix.js +11 -5
- package/esm/util/trackConfigDelta.d.ts +6 -6
- package/esm/util/trackConfigDelta.js +3 -3
- package/esm/util/tracks.d.ts +48 -0
- package/esm/util/tracks.js +192 -66
- package/esm/util/types/index.d.ts +58 -10
- package/esm/util/types/index.js +3 -0
- package/esm/util/unknownSnapshotKeys.d.ts +36 -0
- package/esm/util/unknownSnapshotKeys.js +69 -0
- package/esm/util/usePanelVirtualScroll.d.ts +6 -2
- package/esm/util/usePanelVirtualScroll.js +6 -2
- package/esm/util/useRowVirtualScroll.d.ts +1 -1
- package/esm/util/useRowVirtualScroll.js +1 -1
- package/esm/util/useVirtualScrollWheel.js +11 -1
- package/esm/util/viewStatus.d.ts +23 -0
- package/esm/util/viewStatus.js +17 -0
- package/esm/util/wheelZoom.d.ts +3 -1
- package/esm/util/wheelZoom.js +39 -14
- package/esm/util/whenViewSettled.d.ts +17 -4
- package/esm/util/whenViewSettled.js +19 -6
- package/esm/util/withLaunchInput.d.ts +168 -0
- package/esm/util/withLaunchInput.js +273 -0
- package/package.json +19 -11
- package/esm/ui/promotableMenuItems.d.ts +0 -19
- package/esm/ui/promotableMenuItems.js +0 -89
- package/esm/util/applyInitSettings.d.ts +0 -79
- package/esm/util/applyInitSettings.js +0 -91
package/esm/util/installFetch.js
CHANGED
|
@@ -33,7 +33,7 @@ export async function runFetchOnce(self, active, args, { run, commit, setError,
|
|
|
33
33
|
const { stopToken, isCurrent, statusCallback, end } = active;
|
|
34
34
|
// synchronous, so it lands while this run is by construction the current one
|
|
35
35
|
setError(undefined);
|
|
36
|
-
onBegin?.(
|
|
36
|
+
onBegin?.(stopToken);
|
|
37
37
|
try {
|
|
38
38
|
const result = await run(args, makeFetchContext(self, {
|
|
39
39
|
stopToken,
|
|
@@ -65,7 +65,7 @@ export async function runFetchOnce(self, active, args, { run, commit, setError,
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
* The two
|
|
68
|
+
* The two contract checks every fetch installer owes, together because they are
|
|
69
69
|
* the same doctrine: one reports a foundation attached twice, the other reports
|
|
70
70
|
* a `reload()` the gate still declines — the dead Retry button.
|
|
71
71
|
*/
|
|
@@ -73,38 +73,62 @@ function installContractChecks(self, contract) {
|
|
|
73
73
|
assertDisplayContract(self, contract);
|
|
74
74
|
return makeRetryContractCheck(self);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The rotation for an installation that was lent none: created here, so
|
|
78
|
+
* disposed here — a lent one is the host's, and resetting its status window
|
|
79
|
+
* would drop a write the host's other operations are still owed.
|
|
80
|
+
*/
|
|
81
|
+
function ownRotation(self, report) {
|
|
82
|
+
const rotation = createStopTokenRotation(self, report);
|
|
83
|
+
addDisposer(self, () => {
|
|
84
|
+
rotation.dispose();
|
|
85
|
+
});
|
|
86
|
+
return rotation;
|
|
87
|
+
}
|
|
76
88
|
/**
|
|
77
89
|
* A whole fetch, installed: the rotation, the leading-edge autorun, the
|
|
78
90
|
* unconditional trigger reads, the gates, and {@link runFetchOnce} under them.
|
|
79
91
|
*
|
|
80
|
-
* Every fetch in the tree is this shape except one — the prerequisite
|
|
81
|
-
* (HiC's header, the multi-sample sample list),
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* the
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
92
|
+
* Every fetch in the tree is this shape except one family — the prerequisite
|
|
93
|
+
* reads (HiC's header, the multi-sample sample list), the two keyed families
|
|
94
|
+
* (`installGlobalFetchAutorun` and `installComparativeFetchAutorun`, which lend
|
|
95
|
+
* `FetchMixin`'s rotation through the `rotation` option so `cancelFetch` and
|
|
96
|
+
* the Cancel button reach the fetch they install), the circular view's chord
|
|
97
|
+
* fetch, the breakpoint overlay fetch and the multi-way synteny display's two
|
|
98
|
+
* DEPENDENT fetches (lane genes, lane links, each gated on the key its own
|
|
99
|
+
* commit stamps) all take it. The
|
|
100
|
+
* exception is the per-region family's `FetchMixin.runFetch`: its trigger is
|
|
101
|
+
* `planRegionFetch`'s autorun and its commits stream per region through
|
|
102
|
+
* `ctx.commitRegion`, so it holds `runFetchOnce` directly and the autorun above
|
|
103
|
+
* it is that foundation's.
|
|
89
104
|
*
|
|
90
105
|
* What a caller supplies is exactly what differs between them: which reads wake
|
|
91
106
|
* it (`prepare`, plus `gate`), what a round trip commits, where the loading
|
|
92
107
|
* flag lives (`onBegin` / `onEnd`) and where the status goes (`report`).
|
|
93
|
-
*
|
|
94
|
-
* Returns the rotation's `cancel`, for a family whose model owns the user-facing
|
|
95
|
-
* Cancel button: the flag alone is not a cancel — nothing rotates the token, the
|
|
96
|
-
* stopped RPC stays current, and it commits over the load the user just stopped.
|
|
97
108
|
*/
|
|
98
|
-
export function installFetch(self,
|
|
109
|
+
export function installFetch(self, opts) {
|
|
110
|
+
const { name, delay, gate, contract, fetchKey, loadedKey, heldAnswers, prepare, run, commit, setError, onBegin, onEnd, } = opts;
|
|
99
111
|
const noteFetchAutorunRun = contract === undefined ? undefined : installContractChecks(self, contract);
|
|
100
|
-
const
|
|
112
|
+
const lent = opts.rotation;
|
|
113
|
+
const rotation = lent === undefined ? ownRotation(self, opts.report) : lent;
|
|
101
114
|
// The `reloadCounter` this installation last issued a fetch at. Per
|
|
102
115
|
// installation and per instance, so a host with several fetches gives each its
|
|
103
116
|
// own — and a closure rather than model state, because nothing outside reads
|
|
104
117
|
// it and a volatile would be one more thing a host has to declare.
|
|
105
118
|
let issuedEpoch;
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
// The skeleton's own stamp exists only where there is a key to stamp — a
|
|
120
|
+
// fetch with no `fetchKey` never reads it, so allocating one would only feed
|
|
121
|
+
// it `undefined` on every commit.
|
|
122
|
+
const stamp = loadedKey === undefined && fetchKey !== undefined
|
|
123
|
+
? observable.box()
|
|
124
|
+
: undefined;
|
|
125
|
+
const heldKey = loadedKey ?? (() => stamp?.get());
|
|
126
|
+
// the freshness gate in whichever form the caller declared it
|
|
127
|
+
const held = heldAnswers !== undefined
|
|
128
|
+
? heldAnswers
|
|
129
|
+
: fetchKey !== undefined
|
|
130
|
+
? (args) => isDataCurrent(heldKey(), fetchKey(args))
|
|
131
|
+
: undefined;
|
|
108
132
|
const commitAndStamp = stamp
|
|
109
133
|
? (result, args) => {
|
|
110
134
|
runInAction(() => {
|
|
@@ -113,9 +137,6 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
|
|
|
113
137
|
});
|
|
114
138
|
}
|
|
115
139
|
: commit;
|
|
116
|
-
addDisposer(self, () => {
|
|
117
|
-
rotation.dispose();
|
|
118
|
-
});
|
|
119
140
|
leadingEdgeAutorun(self,
|
|
120
141
|
// #region voidTracking
|
|
121
142
|
() => {
|
|
@@ -128,13 +149,23 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
|
|
|
128
149
|
// Cancel would be a one-way door with a Retry button on it. Order, not
|
|
129
150
|
// just position: counter first, then this, then the gates.
|
|
130
151
|
const canceled = self.fetchCanceled === true;
|
|
152
|
+
// Above the gate, not only above `prepare`: teardown mutates the
|
|
153
|
+
// observables this body reads before the disposers run, and every `gate`
|
|
154
|
+
// but the breakpoint view's reaches the containing view or track through a
|
|
155
|
+
// parent walk (`host.initialized`, `isMinimized`, `getContainingView`),
|
|
156
|
+
// which throws once the node has left the tree. This catches the destroyed
|
|
157
|
+
// node; the detached-and-still-alive window (ADR-069) is covered only by
|
|
158
|
+
// the walk's own cache. Nothing is reported for a dead node — there is no
|
|
159
|
+
// Retry button left to be dead, and the check would read three more
|
|
160
|
+
// members of a corpse to decide it.
|
|
161
|
+
if (!isAlive(self)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
131
164
|
if (canceled || gate?.() === false) {
|
|
132
165
|
noteFetchAutorunRun?.('gated');
|
|
133
166
|
return false;
|
|
134
167
|
}
|
|
135
|
-
|
|
136
|
-
// and getContainingView on a detached node warns then throws.
|
|
137
|
-
const args = isAlive(self) ? prepare() : undefined;
|
|
168
|
+
const args = prepare();
|
|
138
169
|
if (args === undefined) {
|
|
139
170
|
noteFetchAutorunRun?.('declined');
|
|
140
171
|
return false;
|
|
@@ -145,9 +176,7 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
|
|
|
145
176
|
// consuming the retry here costs nothing — while a reload landing
|
|
146
177
|
// mid-flight is answered by the re-run the counter read above already
|
|
147
178
|
// guarantees.
|
|
148
|
-
if (
|
|
149
|
-
isDataCurrent(heldKey(), fetchKey(args)) &&
|
|
150
|
-
reloadEpoch === issuedEpoch) {
|
|
179
|
+
if (held !== undefined && held(args) && reloadEpoch === issuedEpoch) {
|
|
151
180
|
noteFetchAutorunRun?.('declined');
|
|
152
181
|
return false;
|
|
153
182
|
}
|
|
@@ -174,5 +203,4 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
|
|
|
174
203
|
},
|
|
175
204
|
// #endregion
|
|
176
205
|
{ name, delay });
|
|
177
|
-
return rotation.cancel;
|
|
178
206
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
2
2
|
interface InitAutorunHost<T> extends IStateTreeNode {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
pendingLaunch?: T;
|
|
4
|
+
setLaunch: (launch?: T) => void;
|
|
5
5
|
setError: (error: unknown) => void;
|
|
6
6
|
}
|
|
7
7
|
export interface InitApplyContext {
|
|
8
8
|
/**
|
|
9
9
|
* True once this init can no longer be the one being applied: the node is
|
|
10
|
-
* gone, or a newer `
|
|
10
|
+
* gone, or a newer `setLaunch` has replaced it. Any mid-apply wait that can
|
|
11
11
|
* park indefinitely must fold this into its condition — an unbounded wait on
|
|
12
12
|
* state that never arrives holds the drain open, and the newer init that
|
|
13
13
|
* would have superseded it is exactly what gets stranded. This is the causal
|
|
@@ -26,35 +26,36 @@ interface InitAutorunOptions<T> {
|
|
|
26
26
|
*/
|
|
27
27
|
ready: () => boolean;
|
|
28
28
|
/**
|
|
29
|
-
* Whether the view has materialized — the
|
|
30
|
-
*
|
|
31
|
-
* synteny: rows built). Decides the failure policy, see
|
|
29
|
+
* Whether the view has materialized — the line `withLaunchInput`'s
|
|
30
|
+
* `materialized` draws for persistence, read from the live node (dotplot:
|
|
31
|
+
* assemblyNames set; synteny: rows built). Decides the failure policy, see
|
|
32
|
+
* below.
|
|
32
33
|
*/
|
|
33
34
|
materialized: () => boolean;
|
|
34
35
|
/** Apply one init blob. Owns the ordered steps; never clears `init`. */
|
|
35
36
|
apply: (init: T, ctx: InitApplyContext) => Promise<void>;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
|
-
* The
|
|
39
|
+
* The launch state machine shared by the views that take a declarative launch
|
|
39
40
|
* blob (LinearGenomeView, DotplotView, LinearSyntenyView): the re-entry guard,
|
|
40
41
|
* the readiness gate, the serialized drain, the identity-checked clear, the
|
|
41
42
|
* isAlive guards, and one failure policy.
|
|
42
43
|
*
|
|
43
|
-
* The failure policy keys off `materialized`, the same line
|
|
44
|
-
*
|
|
44
|
+
* The failure policy keys off `materialized`, the same line `withLaunchInput`
|
|
45
|
+
* draws for persistence:
|
|
45
46
|
*
|
|
46
|
-
* - Not materialized — nothing is on screen and `
|
|
47
|
+
* - Not materialized — nothing is on screen and `launch` is still persisted, so
|
|
47
48
|
* keep it for a reload retry and `setError`. That flips the view onto its
|
|
48
49
|
* import form, which renders the error in its own banner; a snackbar would
|
|
49
50
|
* state the same failure twice and the banner is the one that persists.
|
|
50
51
|
* - Materialized — the view works, so a later failure is one sub-step's
|
|
51
52
|
* problem and must not take the view down with it. `setError` would, since
|
|
52
53
|
* showImportForm keys off `error` alone and would discard rows that loaded
|
|
53
|
-
* fine, so report it as a snackbar instead. Clearing `
|
|
54
|
+
* fine, so report it as a snackbar instead. Clearing `launch` is also what
|
|
54
55
|
* disarms the re-fire: `ready` usually folds in the measured width, so
|
|
55
|
-
* leaving it set lets any resize re-run `apply` over a half-applied
|
|
56
|
+
* leaving it set lets any resize re-run `apply` over a half-applied blob.
|
|
56
57
|
*
|
|
57
|
-
* `apply` therefore never clears `
|
|
58
|
+
* `apply` therefore never clears `launch` and never catches for reporting — it
|
|
58
59
|
* catches only the failures it wants to keep going through (a bad locstring in
|
|
59
60
|
* one row), and everything it lets escape is fatal-as-of-that-point.
|
|
60
61
|
*/
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { addDisposer, isAlive } from '@jbrowse/mobx-state-tree';
|
|
2
2
|
import { autorun } from 'mobx';
|
|
3
3
|
import { getNotificationSink } from './sessionServices.js';
|
|
4
|
-
// Clear only when
|
|
5
|
-
// lands while `apply` is awaiting (React StrictMode remounts, a
|
|
6
|
-
// re-launch) swaps in a new object; clearing unconditionally would
|
|
7
|
-
// drop
|
|
4
|
+
// Clear only when the pending blob is still the exact object we applied. A
|
|
5
|
+
// setLaunch that lands while `apply` is awaiting (React StrictMode remounts, a
|
|
6
|
+
// programmatic re-launch) swaps in a new object; clearing unconditionally would
|
|
7
|
+
// silently drop it. Leaving it lets the drain loop apply it next.
|
|
8
8
|
function clearIfUnchanged(self, init) {
|
|
9
|
-
if (isAlive(self) && self.
|
|
10
|
-
self.
|
|
9
|
+
if (isAlive(self) && self.pendingLaunch === init) {
|
|
10
|
+
self.setLaunch(undefined);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
async function applyInitOnce(self, init, { apply, materialized }) {
|
|
14
14
|
try {
|
|
15
15
|
await apply(init, {
|
|
16
|
-
superseded: () => !isAlive(self) || self.
|
|
16
|
+
superseded: () => !isAlive(self) || self.pendingLaunch !== init,
|
|
17
17
|
});
|
|
18
18
|
clearIfUnchanged(self, init);
|
|
19
19
|
}
|
|
@@ -34,7 +34,7 @@ async function applyInitOnce(self, init, { apply, materialized }) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
function currentInit(self, { ready }) {
|
|
37
|
-
return isAlive(self) && ready() ? self.
|
|
37
|
+
return isAlive(self) && ready() ? self.pendingLaunch : undefined;
|
|
38
38
|
}
|
|
39
39
|
// Apply pending inits one at a time until none remain. Serializing (rather
|
|
40
40
|
// than letting the autorun run `apply` concurrently) is what keeps re-entrant
|
|
@@ -47,32 +47,32 @@ async function drainInit(self, opts) {
|
|
|
47
47
|
while (pending !== undefined) {
|
|
48
48
|
await applyInitOnce(self, pending, opts);
|
|
49
49
|
const next = currentInit(self, opts);
|
|
50
|
-
// a fatal failure keeps `
|
|
50
|
+
// a fatal failure keeps `launch` for a reload retry; the same object still
|
|
51
51
|
// sitting there means it was not consumed, so stop rather than spin on it
|
|
52
52
|
pending = next === pending ? undefined : next;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* The
|
|
56
|
+
* The launch state machine shared by the views that take a declarative launch
|
|
57
57
|
* blob (LinearGenomeView, DotplotView, LinearSyntenyView): the re-entry guard,
|
|
58
58
|
* the readiness gate, the serialized drain, the identity-checked clear, the
|
|
59
59
|
* isAlive guards, and one failure policy.
|
|
60
60
|
*
|
|
61
|
-
* The failure policy keys off `materialized`, the same line
|
|
62
|
-
*
|
|
61
|
+
* The failure policy keys off `materialized`, the same line `withLaunchInput`
|
|
62
|
+
* draws for persistence:
|
|
63
63
|
*
|
|
64
|
-
* - Not materialized — nothing is on screen and `
|
|
64
|
+
* - Not materialized — nothing is on screen and `launch` is still persisted, so
|
|
65
65
|
* keep it for a reload retry and `setError`. That flips the view onto its
|
|
66
66
|
* import form, which renders the error in its own banner; a snackbar would
|
|
67
67
|
* state the same failure twice and the banner is the one that persists.
|
|
68
68
|
* - Materialized — the view works, so a later failure is one sub-step's
|
|
69
69
|
* problem and must not take the view down with it. `setError` would, since
|
|
70
70
|
* showImportForm keys off `error` alone and would discard rows that loaded
|
|
71
|
-
* fine, so report it as a snackbar instead. Clearing `
|
|
71
|
+
* fine, so report it as a snackbar instead. Clearing `launch` is also what
|
|
72
72
|
* disarms the re-fire: `ready` usually folds in the measured width, so
|
|
73
|
-
* leaving it set lets any resize re-run `apply` over a half-applied
|
|
73
|
+
* leaving it set lets any resize re-run `apply` over a half-applied blob.
|
|
74
74
|
*
|
|
75
|
-
* `apply` therefore never clears `
|
|
75
|
+
* `apply` therefore never clears `launch` and never catches for reporting — it
|
|
76
76
|
* catches only the failures it wants to keep going through (a bad locstring in
|
|
77
77
|
* one row), and everything it lets escape is fatal-as-of-that-point.
|
|
78
78
|
*/
|
|
@@ -93,7 +93,7 @@ export function installInitAutorun(self, opts) {
|
|
|
93
93
|
// reading a detached node throws — here into an unawaited promise.
|
|
94
94
|
const alive = isAlive(self);
|
|
95
95
|
const isReady = alive && opts.ready();
|
|
96
|
-
const init = alive ? self.
|
|
96
|
+
const init = alive ? self.pendingLaunch : undefined;
|
|
97
97
|
if (isReady && init !== undefined && !draining) {
|
|
98
98
|
draining = true;
|
|
99
99
|
try {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { StatusReporter } from './createStopTokenRotation.ts';
|
|
2
|
+
import type { FetchContext } from './fetchContext.ts';
|
|
3
|
+
import type { FetchSkeletonHost } from './installFetch.ts';
|
|
4
|
+
/**
|
|
5
|
+
* What a prerequisite read needs of the display it runs on, beside the
|
|
6
|
+
* skeleton's own three. Duck-typed rather than a foundation's `Instance` type,
|
|
7
|
+
* because the hosts compose different foundations — one per fetch family, and
|
|
8
|
+
* the comparative displays compose none of them — and this is the only thing
|
|
9
|
+
* they have in common.
|
|
10
|
+
*/
|
|
11
|
+
export interface PrerequisiteFetchHost extends FetchSkeletonHost {
|
|
12
|
+
/**
|
|
13
|
+
* Read in `prepare`, so it is **tracked**: an adapter edited in the config
|
|
14
|
+
* editor has to re-read, and `run`'s own reads are untracked by contract.
|
|
15
|
+
*/
|
|
16
|
+
adapterConfig: Record<string, unknown>;
|
|
17
|
+
isMinimized: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A **prerequisite read**: one RPC about the adapter itself, beside the
|
|
21
|
+
* display's primary fetch. Three in the tree — HiC's `CoreGetInfo` header read,
|
|
22
|
+
* whose `resolutions` every contacts fetch waits on; the multi-sample sample
|
|
23
|
+
* list, which `fetchNeeded` declines until; and the tiered alignment file's
|
|
24
|
+
* `CoreGetInfo` header, which decides the LOD tier a synteny fetch asks for —
|
|
25
|
+
* and they sit on three different fetch foundations, which is why this sits in
|
|
26
|
+
* core beside the skeleton and not beside any of them.
|
|
27
|
+
*
|
|
28
|
+
* They had the shape below hand-copied, comment for comment, and each term is a
|
|
29
|
+
* rule rather than a preference:
|
|
30
|
+
*
|
|
31
|
+
* - **the adapter config is the whole trigger and the whole key.** Tracked in
|
|
32
|
+
* `prepare` so a config-editor edit re-reads; keyed on the same value so an
|
|
33
|
+
* un-minimize over the same file re-reads nothing. Getting one without the
|
|
34
|
+
* other gives either a stale header or a walk of a `.hic` norm-vector index on
|
|
35
|
+
* every expand.
|
|
36
|
+
* - **minimized is the gate**, and a display with a second condition of its own
|
|
37
|
+
* passes `gate` — the sample-list read waits for the LGV to be measured as
|
|
38
|
+
* well, so a full-file scan does not start ahead of the display's own first
|
|
39
|
+
* fetch, and the tier read waits on the track declaring a tier threshold at
|
|
40
|
+
* all.
|
|
41
|
+
* - **no `contract`.** This is the second fetch on a display whose foundation
|
|
42
|
+
* installed both checks already: `assertDisplayContract` would report the
|
|
43
|
+
* double-attach it exists to catch, and a second retry check would consume the
|
|
44
|
+
* same `reloadCounter` bumps as the first.
|
|
45
|
+
* - **where the status goes is the caller's** (`report`). The two LGV reads
|
|
46
|
+
* lend the display's own status window, so the read takes a slot beside the
|
|
47
|
+
* primary fetch rather than opening a second writer over the one status field
|
|
48
|
+
* (ADR-081) — and it matters more here than for most fetches: a prerequisite
|
|
49
|
+
* runs inside the pre-first-paint window, where the scrim is up because
|
|
50
|
+
* nothing is drawn rather than because `isLoading` is, so `statusMessage` is
|
|
51
|
+
* the only thing that can say what is happening. The tier read narrates
|
|
52
|
+
* nothing; a header is not a load.
|
|
53
|
+
*
|
|
54
|
+
* `createAdapterMetadataFetch` keys on the same adapter config and is
|
|
55
|
+
* deliberately NOT one of these: no trigger, no stop token and no status,
|
|
56
|
+
* because it is fetched lazily when a details widget opens and the widget opens
|
|
57
|
+
* on the result. A prerequisite read is the one a display's own fetch waits on.
|
|
58
|
+
*
|
|
59
|
+
* **A declaration, not the skeleton this name used to be.** There was an
|
|
60
|
+
* `installPrerequisiteFetch` until 2026-08-23, and `3c5aa7fce2` deleted it for
|
|
61
|
+
* a good reason: it carried a `runOne` of its own — a fifth copy of begin →
|
|
62
|
+
* clear → run → commit-if-current → `handleFetchError` → end — and once
|
|
63
|
+
* `installFetch` owned that sequence, a second one was pure drift surface. This
|
|
64
|
+
* one adds no sequence, no rotation and no error rule; it is terms handed to
|
|
65
|
+
* `installFetch`, the same shape `installGlobalFetchAutorun` and
|
|
66
|
+
* `installComparativeFetchAutorun` are. Add nothing here that a family could
|
|
67
|
+
* not state as an option to the skeleton.
|
|
68
|
+
*
|
|
69
|
+
* Everything the skeleton gives it is the point of going through the skeleton:
|
|
70
|
+
* the latest-wins rotation (so a superseding read aborts the one it replaced,
|
|
71
|
+
* and only a superseding read does — a user cancel of the primary fetch cannot
|
|
72
|
+
* strand the display the way sharing its token would), the clear at the start,
|
|
73
|
+
* the currency-guarded error rule, the unconditional `reloadCounter` read that
|
|
74
|
+
* makes the chrome's Retry re-run this, and the retired status slot that keeps a
|
|
75
|
+
* failed read from leaving a progress chip up for good.
|
|
76
|
+
*/
|
|
77
|
+
export declare function installPrerequisiteFetch<TResult>(self: PrerequisiteFetchHost, { name, delay, report, gate, run, commit, setError, }: {
|
|
78
|
+
name: string;
|
|
79
|
+
delay: number;
|
|
80
|
+
report: StatusReporter;
|
|
81
|
+
/** an extra term of the display's own, ANDed under `!isMinimized` */
|
|
82
|
+
gate?: () => boolean;
|
|
83
|
+
run: (adapterConfig: Record<string, unknown>, ctx: FetchContext) => Promise<TResult | undefined>;
|
|
84
|
+
commit: (result: TResult) => void;
|
|
85
|
+
setError: (error?: unknown) => void;
|
|
86
|
+
}): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { adapterConfigKey } from './adapterConfigKey.js';
|
|
2
|
+
import { installFetch } from './installFetch.js';
|
|
3
|
+
/**
|
|
4
|
+
* A **prerequisite read**: one RPC about the adapter itself, beside the
|
|
5
|
+
* display's primary fetch. Three in the tree — HiC's `CoreGetInfo` header read,
|
|
6
|
+
* whose `resolutions` every contacts fetch waits on; the multi-sample sample
|
|
7
|
+
* list, which `fetchNeeded` declines until; and the tiered alignment file's
|
|
8
|
+
* `CoreGetInfo` header, which decides the LOD tier a synteny fetch asks for —
|
|
9
|
+
* and they sit on three different fetch foundations, which is why this sits in
|
|
10
|
+
* core beside the skeleton and not beside any of them.
|
|
11
|
+
*
|
|
12
|
+
* They had the shape below hand-copied, comment for comment, and each term is a
|
|
13
|
+
* rule rather than a preference:
|
|
14
|
+
*
|
|
15
|
+
* - **the adapter config is the whole trigger and the whole key.** Tracked in
|
|
16
|
+
* `prepare` so a config-editor edit re-reads; keyed on the same value so an
|
|
17
|
+
* un-minimize over the same file re-reads nothing. Getting one without the
|
|
18
|
+
* other gives either a stale header or a walk of a `.hic` norm-vector index on
|
|
19
|
+
* every expand.
|
|
20
|
+
* - **minimized is the gate**, and a display with a second condition of its own
|
|
21
|
+
* passes `gate` — the sample-list read waits for the LGV to be measured as
|
|
22
|
+
* well, so a full-file scan does not start ahead of the display's own first
|
|
23
|
+
* fetch, and the tier read waits on the track declaring a tier threshold at
|
|
24
|
+
* all.
|
|
25
|
+
* - **no `contract`.** This is the second fetch on a display whose foundation
|
|
26
|
+
* installed both checks already: `assertDisplayContract` would report the
|
|
27
|
+
* double-attach it exists to catch, and a second retry check would consume the
|
|
28
|
+
* same `reloadCounter` bumps as the first.
|
|
29
|
+
* - **where the status goes is the caller's** (`report`). The two LGV reads
|
|
30
|
+
* lend the display's own status window, so the read takes a slot beside the
|
|
31
|
+
* primary fetch rather than opening a second writer over the one status field
|
|
32
|
+
* (ADR-081) — and it matters more here than for most fetches: a prerequisite
|
|
33
|
+
* runs inside the pre-first-paint window, where the scrim is up because
|
|
34
|
+
* nothing is drawn rather than because `isLoading` is, so `statusMessage` is
|
|
35
|
+
* the only thing that can say what is happening. The tier read narrates
|
|
36
|
+
* nothing; a header is not a load.
|
|
37
|
+
*
|
|
38
|
+
* `createAdapterMetadataFetch` keys on the same adapter config and is
|
|
39
|
+
* deliberately NOT one of these: no trigger, no stop token and no status,
|
|
40
|
+
* because it is fetched lazily when a details widget opens and the widget opens
|
|
41
|
+
* on the result. A prerequisite read is the one a display's own fetch waits on.
|
|
42
|
+
*
|
|
43
|
+
* **A declaration, not the skeleton this name used to be.** There was an
|
|
44
|
+
* `installPrerequisiteFetch` until 2026-08-23, and `3c5aa7fce2` deleted it for
|
|
45
|
+
* a good reason: it carried a `runOne` of its own — a fifth copy of begin →
|
|
46
|
+
* clear → run → commit-if-current → `handleFetchError` → end — and once
|
|
47
|
+
* `installFetch` owned that sequence, a second one was pure drift surface. This
|
|
48
|
+
* one adds no sequence, no rotation and no error rule; it is terms handed to
|
|
49
|
+
* `installFetch`, the same shape `installGlobalFetchAutorun` and
|
|
50
|
+
* `installComparativeFetchAutorun` are. Add nothing here that a family could
|
|
51
|
+
* not state as an option to the skeleton.
|
|
52
|
+
*
|
|
53
|
+
* Everything the skeleton gives it is the point of going through the skeleton:
|
|
54
|
+
* the latest-wins rotation (so a superseding read aborts the one it replaced,
|
|
55
|
+
* and only a superseding read does — a user cancel of the primary fetch cannot
|
|
56
|
+
* strand the display the way sharing its token would), the clear at the start,
|
|
57
|
+
* the currency-guarded error rule, the unconditional `reloadCounter` read that
|
|
58
|
+
* makes the chrome's Retry re-run this, and the retired status slot that keeps a
|
|
59
|
+
* failed read from leaving a progress chip up for good.
|
|
60
|
+
*/
|
|
61
|
+
export function installPrerequisiteFetch(self, { name, delay, report, gate = () => true, run, commit, setError, }) {
|
|
62
|
+
installFetch(self, {
|
|
63
|
+
name,
|
|
64
|
+
delay,
|
|
65
|
+
report,
|
|
66
|
+
gate: () => !self.isMinimized && gate(),
|
|
67
|
+
prepare: () => ({ adapterConfig: self.adapterConfig }),
|
|
68
|
+
fetchKey: ({ adapterConfig }) => adapterConfigKey(adapterConfig),
|
|
69
|
+
run: ({ adapterConfig }, ctx) => run(adapterConfig, ctx),
|
|
70
|
+
commit,
|
|
71
|
+
setError,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
/**
|
|
3
|
+
* #api core/util
|
|
4
|
+
* Run a display's `afterAttach` body from a module loaded on demand, so the
|
|
5
|
+
* autorun installers stay out of the display's eager bundle.
|
|
6
|
+
*
|
|
7
|
+
* One policy for the gap the `await` opens: a node torn down before the module
|
|
8
|
+
* lands installs nothing, and a module that fails to load is reported where the
|
|
9
|
+
* user can see it rather than onto the display's own error slot — that slot is
|
|
10
|
+
* what `reload()` clears, and nothing would re-run the install behind it.
|
|
11
|
+
* Three displays hand-rolled this IIFE and each drew the lines differently.
|
|
12
|
+
*/
|
|
13
|
+
export declare function runLazyAfterAttach<Self extends IStateTreeNode>(self: Self, load: () => Promise<(self: Self) => void>): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isAlive } from '@jbrowse/mobx-state-tree';
|
|
2
|
+
import { getNotificationSink } from './sessionServices.js';
|
|
3
|
+
/**
|
|
4
|
+
* #api core/util
|
|
5
|
+
* Run a display's `afterAttach` body from a module loaded on demand, so the
|
|
6
|
+
* autorun installers stay out of the display's eager bundle.
|
|
7
|
+
*
|
|
8
|
+
* One policy for the gap the `await` opens: a node torn down before the module
|
|
9
|
+
* lands installs nothing, and a module that fails to load is reported where the
|
|
10
|
+
* user can see it rather than onto the display's own error slot — that slot is
|
|
11
|
+
* what `reload()` clears, and nothing would re-run the install behind it.
|
|
12
|
+
* Three displays hand-rolled this IIFE and each drew the lines differently.
|
|
13
|
+
*/
|
|
14
|
+
export function runLazyAfterAttach(self, load) {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
16
|
+
;
|
|
17
|
+
(async () => {
|
|
18
|
+
try {
|
|
19
|
+
const install = await load();
|
|
20
|
+
if (isAlive(self)) {
|
|
21
|
+
install(self);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
if (isAlive(self)) {
|
|
26
|
+
console.error(e);
|
|
27
|
+
getNotificationSink(self).notifyError(`${e}`, e);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface LegendCandidate {
|
|
2
|
+
rowIndex: number;
|
|
3
|
+
label: string;
|
|
4
|
+
color: number;
|
|
5
|
+
}
|
|
6
|
+
export interface LegendEntry {
|
|
7
|
+
label: string;
|
|
8
|
+
color: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const MAX_LEGEND_ENTRIES = 30;
|
|
11
|
+
export declare const MAX_LEGEND_CANDIDATES = 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Accumulator for the distinct (row, name, color) combinations a packer walks
|
|
14
|
+
* past, in first-seen order and bounded — the small list `unionLegendCandidates`
|
|
15
|
+
* derives a key from, so the main thread never re-walks the features. Fed scalar
|
|
16
|
+
* by scalar, from the loop that already holds the three values.
|
|
17
|
+
*
|
|
18
|
+
* Deduped on the whole triple rather than on (row, color): the union takes the
|
|
19
|
+
* first NAME it sees for a color, so where that name is already spoken for, the
|
|
20
|
+
* second name on that color has to still be in the list to carry it.
|
|
21
|
+
*
|
|
22
|
+
* A nameless feature contributes nothing, which is the union's rule too — so a
|
|
23
|
+
* track whose features have no names ships an empty list rather than one entry
|
|
24
|
+
* per feature.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createLegendCandidateCollector(maxCandidates?: number): {
|
|
27
|
+
candidates: LegendCandidate[];
|
|
28
|
+
add(rowIndex: number, label: string, color: number): void;
|
|
29
|
+
};
|
|
30
|
+
export interface LegendCandidateSource {
|
|
31
|
+
candidates: readonly LegendCandidate[];
|
|
32
|
+
rowPaintsCandidateColor: (rowIndex: number) => boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The categorical color key over any number of packed regions: one entry per
|
|
36
|
+
* distinct color, labeled by the first name seen in that color, in first-seen
|
|
37
|
+
* order.
|
|
38
|
+
*
|
|
39
|
+
* Keyed by color rather than by name, so each key row is 1:1 with a color — a
|
|
40
|
+
* row is normally a toggle and hiding a category hides features BY color, so two
|
|
41
|
+
* names sharing one color must collapse to a single row. A name reused across
|
|
42
|
+
* two colors keeps its first-seen color.
|
|
43
|
+
*
|
|
44
|
+
* `[]` when the data carries no categorical signal: nothing named, or more than
|
|
45
|
+
* `maxEntries` distinct colors.
|
|
46
|
+
*
|
|
47
|
+
* `regions` is consumed lazily, so a caller able to answer without reading any
|
|
48
|
+
* of them can pass a generator and have none of them touched.
|
|
49
|
+
*/
|
|
50
|
+
export declare function unionLegendCandidates<T>(regions: Iterable<T>, resolve: (region: T) => LegendCandidateSource, maxEntries?: number): LegendEntry[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// How many distinct colors a derived key may name and still BE a key. Past this
|
|
2
|
+
// the data is not a categorical vocabulary (a track keyed by unique per-feature
|
|
3
|
+
// names), and a list of every feature is the thing a key exists instead of.
|
|
4
|
+
//
|
|
5
|
+
// A different question from `legendIsReadable`'s lower bar, which asks whether a
|
|
6
|
+
// key a display already has is worth the rows it costs.
|
|
7
|
+
export const MAX_LEGEND_ENTRIES = 30;
|
|
8
|
+
// Bound on the candidate list one region ships. Far above MAX_LEGEND_ENTRIES, so
|
|
9
|
+
// a vocabulary that is categorical at all crosses whole, and a track with a name
|
|
10
|
+
// per feature ships a prefix that reads the same way it would have.
|
|
11
|
+
export const MAX_LEGEND_CANDIDATES = 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Accumulator for the distinct (row, name, color) combinations a packer walks
|
|
14
|
+
* past, in first-seen order and bounded — the small list `unionLegendCandidates`
|
|
15
|
+
* derives a key from, so the main thread never re-walks the features. Fed scalar
|
|
16
|
+
* by scalar, from the loop that already holds the three values.
|
|
17
|
+
*
|
|
18
|
+
* Deduped on the whole triple rather than on (row, color): the union takes the
|
|
19
|
+
* first NAME it sees for a color, so where that name is already spoken for, the
|
|
20
|
+
* second name on that color has to still be in the list to carry it.
|
|
21
|
+
*
|
|
22
|
+
* A nameless feature contributes nothing, which is the union's rule too — so a
|
|
23
|
+
* track whose features have no names ships an empty list rather than one entry
|
|
24
|
+
* per feature.
|
|
25
|
+
*/
|
|
26
|
+
export function createLegendCandidateCollector(maxCandidates = MAX_LEGEND_CANDIDATES) {
|
|
27
|
+
const candidates = [];
|
|
28
|
+
const colorsSeen = new Map();
|
|
29
|
+
return {
|
|
30
|
+
candidates,
|
|
31
|
+
add(rowIndex, label, color) {
|
|
32
|
+
if (label !== '' && candidates.length < maxCandidates) {
|
|
33
|
+
let byLabel = colorsSeen.get(rowIndex);
|
|
34
|
+
if (byLabel === undefined) {
|
|
35
|
+
byLabel = new Map();
|
|
36
|
+
colorsSeen.set(rowIndex, byLabel);
|
|
37
|
+
}
|
|
38
|
+
let colors = byLabel.get(label);
|
|
39
|
+
if (colors === undefined) {
|
|
40
|
+
colors = new Set();
|
|
41
|
+
byLabel.set(label, colors);
|
|
42
|
+
}
|
|
43
|
+
if (!colors.has(color)) {
|
|
44
|
+
colors.add(color);
|
|
45
|
+
candidates.push({ rowIndex, label, color });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The categorical color key over any number of packed regions: one entry per
|
|
53
|
+
* distinct color, labeled by the first name seen in that color, in first-seen
|
|
54
|
+
* order.
|
|
55
|
+
*
|
|
56
|
+
* Keyed by color rather than by name, so each key row is 1:1 with a color — a
|
|
57
|
+
* row is normally a toggle and hiding a category hides features BY color, so two
|
|
58
|
+
* names sharing one color must collapse to a single row. A name reused across
|
|
59
|
+
* two colors keeps its first-seen color.
|
|
60
|
+
*
|
|
61
|
+
* `[]` when the data carries no categorical signal: nothing named, or more than
|
|
62
|
+
* `maxEntries` distinct colors.
|
|
63
|
+
*
|
|
64
|
+
* `regions` is consumed lazily, so a caller able to answer without reading any
|
|
65
|
+
* of them can pass a generator and have none of them touched.
|
|
66
|
+
*/
|
|
67
|
+
export function unionLegendCandidates(regions, resolve, maxEntries = MAX_LEGEND_ENTRIES) {
|
|
68
|
+
const entries = [];
|
|
69
|
+
const seenColors = new Set();
|
|
70
|
+
const seenLabels = new Set();
|
|
71
|
+
for (const region of regions) {
|
|
72
|
+
const { candidates, rowPaintsCandidateColor } = resolve(region);
|
|
73
|
+
for (const { rowIndex, label, color } of candidates) {
|
|
74
|
+
if (rowPaintsCandidateColor(rowIndex) &&
|
|
75
|
+
!seenLabels.has(label) &&
|
|
76
|
+
!seenColors.has(color)) {
|
|
77
|
+
seenLabels.add(label);
|
|
78
|
+
seenColors.add(color);
|
|
79
|
+
entries.push({ label, color });
|
|
80
|
+
if (entries.length > maxEntries) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|