@jbrowse/plugin-linear-genome-view 5.0.0-beta.2 → 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/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +1 -1
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +37 -3
- package/esm/LinearGenomeView/components/TrackContainer.js +46 -4
- package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +3 -2
- package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +2 -1
- package/esm/LinearGenomeView/components/TrackRenderingContainer.js +5 -15
- package/esm/LinearGenomeView/consts.js +1 -1
- package/esm/LinearGenomeView/containingLgv.d.ts +15 -0
- package/esm/LinearGenomeView/containingLgv.js +16 -0
- package/esm/LinearGenomeView/model.d.ts +71 -45
- package/esm/LinearGenomeView/model.js +81 -49
- package/esm/LinearGenomeView/util.d.ts +31 -0
- package/esm/LinearGenomeView/util.js +48 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +94 -27
- package/esm/displayKitTests/perRegionTestEnv.js +26 -22
- package/esm/index.d.ts +1 -20
- package/esm/index.js +1 -1
- package/package.json +6 -6
- package/esm/LinearGenomeView/initKeys.d.ts +0 -12
- package/esm/LinearGenomeView/initKeys.js +0 -68
|
@@ -14,7 +14,7 @@ import { assemblyErrorMessage, computeViewStatus, } from '@jbrowse/core/util/vie
|
|
|
14
14
|
import { pendingLaunch, withLaunchInput, } from '@jbrowse/core/util/withLaunchInput';
|
|
15
15
|
import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
|
|
16
16
|
import { cast, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
17
|
-
import { when } from 'mobx';
|
|
17
|
+
import { observable, when } from 'mobx';
|
|
18
18
|
import { handleSelectedRegion } from '../searchUtils.js';
|
|
19
19
|
import { doAfterAttach } from './afterAttach.js';
|
|
20
20
|
import { shouldSwapTracks } from './components/util.js';
|
|
@@ -335,12 +335,20 @@ export function stateModelFactory(pluginManager) {
|
|
|
335
335
|
*/
|
|
336
336
|
launch: types.frozen(),
|
|
337
337
|
}))
|
|
338
|
-
.volatile(
|
|
338
|
+
.volatile(() => {
|
|
339
339
|
// typed locals so `unknown`/`Record` aren't narrowed to `undefined`/`{}`; inline
|
|
340
340
|
// type assertions here get stripped by no-unnecessary-type-assertion
|
|
341
341
|
const volatileError = undefined;
|
|
342
|
-
const trackRefs = {};
|
|
343
342
|
return {
|
|
343
|
+
/**
|
|
344
|
+
* #volatile
|
|
345
|
+
* Height of each track's in-flow label band, measured by
|
|
346
|
+
* TrackContainer as the rendering container's offset inside its Paper.
|
|
347
|
+
* Absent (0) while the label is hidden, overlapping, or not yet
|
|
348
|
+
* measured. Observable, so the offset and height getters re-derive
|
|
349
|
+
* when a band changes.
|
|
350
|
+
*/
|
|
351
|
+
trackLabelBands: observable.map(),
|
|
344
352
|
/**
|
|
345
353
|
* #volatile
|
|
346
354
|
*/
|
|
@@ -361,10 +369,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
361
369
|
* #volatile
|
|
362
370
|
*/
|
|
363
371
|
volatileError,
|
|
364
|
-
/**
|
|
365
|
-
* #volatile
|
|
366
|
-
*/
|
|
367
|
-
trackRefs,
|
|
368
372
|
/**
|
|
369
373
|
* #volatile
|
|
370
374
|
*/
|
|
@@ -376,14 +380,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
376
380
|
/**
|
|
377
381
|
* #volatile
|
|
378
382
|
*/
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
// `coarseBpPerPx` rather than the live one, and a zero there is a
|
|
384
|
-
// degenerate layout for the frame or two before the debounced autorun
|
|
385
|
-
// first writes it.
|
|
386
|
-
coarseBpPerPx: self.legacyBpPerPx || 1,
|
|
383
|
+
// A real scale rather than the not-yet-measured 0, for the canvas
|
|
384
|
+
// displays that lay out against it; `setWidth` seeds the true one at
|
|
385
|
+
// the first measure.
|
|
386
|
+
coarseBpPerPx: 1,
|
|
387
387
|
/**
|
|
388
388
|
* #volatile
|
|
389
389
|
*/
|
|
@@ -529,6 +529,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
529
529
|
},
|
|
530
530
|
}))
|
|
531
531
|
.views(self => ({
|
|
532
|
+
/**
|
|
533
|
+
* #getter
|
|
534
|
+
* The census entry for this view: the tracks it holds itself. Declared
|
|
535
|
+
* rather than derived — see `BaseViewModel.ownTracks`.
|
|
536
|
+
*/
|
|
537
|
+
get ownTracks() {
|
|
538
|
+
return [...self.tracks];
|
|
539
|
+
},
|
|
532
540
|
/**
|
|
533
541
|
* #getter
|
|
534
542
|
* Assembly-name prefix for the scalebar refName labels, or undefined for
|
|
@@ -758,14 +766,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
758
766
|
* #getter
|
|
759
767
|
* A track's full cost beyond its display height.
|
|
760
768
|
*
|
|
761
|
-
* The track *label* is not counted
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
* overlapping, and short by one label box per labelled track otherwise.
|
|
766
|
-
* Anything needing the offset to the pixel with labels showing measures
|
|
767
|
-
* the DOM; `BreakpointSplitViewOverlay` does, and falls back to this
|
|
768
|
-
* arithmetic only for a track with no mounted div.
|
|
769
|
+
* The track *label* is not counted here: an offset label is an in-flow
|
|
770
|
+
* box whose height is whatever the theme renders a Paper of icon buttons
|
|
771
|
+
* at, so it is measured into `trackLabelBands` per track and added by
|
|
772
|
+
* `trackLabelBand` rather than derived.
|
|
769
773
|
*/
|
|
770
774
|
get trackChromeHeight() {
|
|
771
775
|
return this.trackLeadingChrome + this.trackTrailingChrome;
|
|
@@ -816,6 +820,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
816
820
|
? MINIMIZED_TRACK_HEIGHT
|
|
817
821
|
: track.activeDisplay.height;
|
|
818
822
|
},
|
|
823
|
+
/**
|
|
824
|
+
* #method
|
|
825
|
+
* the measured in-flow label band above a track's rendering container,
|
|
826
|
+
* 0 until TrackContainer has measured one or when no label is in flow
|
|
827
|
+
*/
|
|
828
|
+
trackLabelBand(track) {
|
|
829
|
+
return self.trackLabelBands.get(track.configuration.trackId) ?? 0;
|
|
830
|
+
},
|
|
819
831
|
/**
|
|
820
832
|
* #getter
|
|
821
833
|
*/
|
|
@@ -826,7 +838,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
826
838
|
* #getter
|
|
827
839
|
*/
|
|
828
840
|
get trackHeightsWithChrome() {
|
|
829
|
-
return this.trackHeights +
|
|
841
|
+
return (this.trackHeights +
|
|
842
|
+
self.tracks.length * this.trackChromeHeight +
|
|
843
|
+
sum(self.tracks.map(t => this.trackLabelBand(t))));
|
|
830
844
|
},
|
|
831
845
|
/**
|
|
832
846
|
* #getter
|
|
@@ -844,14 +858,15 @@ export function stateModelFactory(pluginManager) {
|
|
|
844
858
|
* first, then unpinned), from the same constants TrackContainer lays its
|
|
845
859
|
* Paper out with. Returns `undefined` if the track is not present.
|
|
846
860
|
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
861
|
+
* Includes each track's measured label band, this track's own included,
|
|
862
|
+
* since an in-flow label sits above the rendering container inside the
|
|
863
|
+
* same Paper. Exact once TrackContainer has measured; before that it is
|
|
864
|
+
* short by the unmeasured bands.
|
|
851
865
|
*/
|
|
852
866
|
getTrackYOffset(trackId) {
|
|
853
867
|
let y = this.headerHeight + this.scalebarHeight + this.trackLeadingChrome;
|
|
854
868
|
for (const t of [...self.pinnedTracks, ...self.unpinnedTracks]) {
|
|
869
|
+
y += this.trackLabelBand(t);
|
|
855
870
|
if (t.configuration.trackId === trackId) {
|
|
856
871
|
return y;
|
|
857
872
|
}
|
|
@@ -1062,7 +1077,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
1062
1077
|
// the gesture into one refetch, its in-flight guard caps concurrent
|
|
1063
1078
|
// batches at one, and rpcDataMap is overwritten in place rather than
|
|
1064
1079
|
// cleared, so nothing blanks (ADR-008, ADR-006).
|
|
1080
|
+
setTrackLabelBand(trackId, band) {
|
|
1081
|
+
if (band === 0) {
|
|
1082
|
+
self.trackLabelBands.delete(trackId);
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
self.trackLabelBands.set(trackId, band);
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1065
1088
|
setWidth(newWidth) {
|
|
1089
|
+
const unmeasured = self.bpPerPx <= 0;
|
|
1066
1090
|
self.volatileWidth = newWidth;
|
|
1067
1091
|
if (newWidth > 0 && self.windowWidthBp <= 0) {
|
|
1068
1092
|
// First measure, and nothing has established a window yet: a migrated
|
|
@@ -1073,6 +1097,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
1073
1097
|
self.windowWidthBp = (self.legacyBpPerPx || 1) * newWidth;
|
|
1074
1098
|
self.legacyBpPerPx = 0;
|
|
1075
1099
|
}
|
|
1100
|
+
if (unmeasured && self.bpPerPx > 0) {
|
|
1101
|
+
// The debounced coarse autorun is 500ms out; a restored snapshot's
|
|
1102
|
+
// consumers would lay out and fetch at 1 bp/px until then.
|
|
1103
|
+
self.coarseBpPerPx = self.bpPerPx;
|
|
1104
|
+
}
|
|
1076
1105
|
},
|
|
1077
1106
|
/**
|
|
1078
1107
|
* #action
|
|
@@ -1428,13 +1457,20 @@ export function stateModelFactory(pluginManager) {
|
|
|
1428
1457
|
},
|
|
1429
1458
|
/**
|
|
1430
1459
|
* #method
|
|
1431
|
-
*
|
|
1460
|
+
* renders the view to SVG markup, which it returns; saves it through
|
|
1461
|
+
* FileSaver unless `save: false`
|
|
1432
1462
|
*/
|
|
1463
|
+
// Promise<string> is stated, against the house preference for inferred
|
|
1464
|
+
// return types: renderToSvg is typed against this very model, so an
|
|
1465
|
+
// inferred return makes the model type reference itself (TS2456).
|
|
1433
1466
|
async exportSvg(opts = {}) {
|
|
1434
1467
|
const { renderToSvg } = await import('./svgcomponents/SVGLinearGenomeView.js');
|
|
1435
1468
|
const html = await renderToSvg(self, opts);
|
|
1436
|
-
|
|
1437
|
-
|
|
1469
|
+
if (opts.save !== false) {
|
|
1470
|
+
const { saveSvgAsImage } = await import('@jbrowse/core/svg/saveSvgAsImage');
|
|
1471
|
+
await saveSvgAsImage(html, opts);
|
|
1472
|
+
}
|
|
1473
|
+
return html;
|
|
1438
1474
|
},
|
|
1439
1475
|
}))
|
|
1440
1476
|
.actions(self => {
|
|
@@ -2737,7 +2773,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2737
2773
|
* `centerAt`'s animated twin: the same destination, reached along the
|
|
2738
2774
|
* arc instead of jumped to, at the zoom the view is already on.
|
|
2739
2775
|
*/
|
|
2740
|
-
flyToCenter(coord, refName) {
|
|
2776
|
+
flyToCenter(coord, refName, displayedRegionIndex) {
|
|
2741
2777
|
// `bpToLinearBp` rather than `bpToPx`, which rounds to a whole pixel
|
|
2742
2778
|
// at the current zoom: mid-flight that zoom is a point on somebody
|
|
2743
2779
|
// else's arc, and a destination quantized against it moves with it.
|
|
@@ -2745,6 +2781,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2745
2781
|
refName,
|
|
2746
2782
|
coord,
|
|
2747
2783
|
displayedRegions: self.displayedRegions,
|
|
2784
|
+
displayedRegionIndex,
|
|
2748
2785
|
});
|
|
2749
2786
|
if (centerBp !== undefined) {
|
|
2750
2787
|
flyTo(centerBp, heading ? heading.windowWidthBp : self.windowWidthBp);
|
|
@@ -2760,7 +2797,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
2760
2797
|
doAfterAttach(self);
|
|
2761
2798
|
},
|
|
2762
2799
|
}));
|
|
2763
|
-
return withLaunchInput(model, lgvLaunchKeys
|
|
2800
|
+
return withLaunchInput(model, lgvLaunchKeys, {
|
|
2801
|
+
registry: pluginManager,
|
|
2802
|
+
materialized: snap => !!snap.displayedRegions?.length,
|
|
2803
|
+
})
|
|
2764
2804
|
.preProcessSnapshot((snap) => {
|
|
2765
2805
|
if (!snap) {
|
|
2766
2806
|
return snap;
|
|
@@ -2802,24 +2842,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
2802
2842
|
};
|
|
2803
2843
|
})
|
|
2804
2844
|
.postProcessSnapshot(snap => {
|
|
2805
|
-
//
|
|
2806
|
-
//
|
|
2807
|
-
//
|
|
2808
|
-
//
|
|
2809
|
-
//
|
|
2810
|
-
//
|
|
2811
|
-
//
|
|
2812
|
-
const {
|
|
2845
|
+
// showCenterLine is purely a localStorage-backed preference (see
|
|
2846
|
+
// setupLocalStorageAutorun in afterAttach.ts) and is never persisted
|
|
2847
|
+
// into session snapshots. The remaining fields are also
|
|
2848
|
+
// localStorage-backed, but still persist when non-default: their strip
|
|
2849
|
+
// baseline is the universal default (hardcoded here), not the
|
|
2850
|
+
// localStorage-derived creation default, so a localStorage-set value
|
|
2851
|
+
// stays portable across browsers.
|
|
2852
|
+
const { showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
|
|
2813
2853
|
return {
|
|
2814
2854
|
...rest,
|
|
2815
|
-
// keep the launch state until displayedRegions exist, so a snapshot
|
|
2816
|
-
// taken before the launch autorun navigates (e.g. autosave firing
|
|
2817
|
-
// mid-load) can still rebuild the view instead of dropping to the
|
|
2818
|
-
// import form. displayedRegions is stripDefault, so it's absent (not
|
|
2819
|
-
// []) when empty — the optional chain is runtime-necessary despite the
|
|
2820
|
-
// non-nullish type.
|
|
2821
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
2822
|
-
...(launch && !snap.displayedRegions?.length ? { launch } : {}),
|
|
2823
2855
|
...(!showCytobands ? { showCytobands } : {}),
|
|
2824
2856
|
...(trackLabels ? { trackLabels } : {}),
|
|
2825
2857
|
...(colorByCDS ? { colorByCDS } : {}),
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { AssemblyManager, ParsedLocString } from '@jbrowse/core/util';
|
|
2
|
+
import type { PxToBpResult } from '@jbrowse/core/util/Base1DUtils';
|
|
2
3
|
import type { BaseBlock, ContentBlock } from '@jbrowse/core/util/blockTypes';
|
|
3
4
|
import type { Region } from '@jbrowse/core/util/types';
|
|
5
|
+
interface CenterKeepingView {
|
|
6
|
+
width: number;
|
|
7
|
+
displayedRegions: Region[];
|
|
8
|
+
pxToBp: (px: number) => PxToBpResult;
|
|
9
|
+
setDisplayedRegions: (regions: Region[]) => void;
|
|
10
|
+
centerAt: (coord: number, refName: string, index?: number) => void;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* The stored viewport of a view showing `totalBp` of displayed regions fitted to
|
|
6
14
|
* `width` — the regions filling it edge to edge, and centered in it only where
|
|
@@ -197,7 +205,29 @@ export declare function regionMoveActions(idx: number, numRegions: number): {
|
|
|
197
205
|
* the new region set).
|
|
198
206
|
*/
|
|
199
207
|
export declare function withRegionMoved(regions: Region[], from: number, to: number): Region[];
|
|
208
|
+
/**
|
|
209
|
+
* Replace the region list and keep looking at whatever was under the middle of
|
|
210
|
+
* the viewport, when that region is in the new list.
|
|
211
|
+
*
|
|
212
|
+
* `setDisplayedRegions` carries `offsetPx` across and clamps it, which is a
|
|
213
|
+
* linear position in a coordinate space the write just redefined: dropping the
|
|
214
|
+
* regions to the left of the one on screen strands the view at the end of what
|
|
215
|
+
* remains, on blank space. Where the centred region is gone the clamp is still
|
|
216
|
+
* the only answer, so this falls back to it.
|
|
217
|
+
*/
|
|
218
|
+
export declare function setDisplayedRegionsKeepingCenter(model: CenterKeepingView, regions: Region[]): void;
|
|
200
219
|
export declare function withRegionRemoved(regions: Region[], index: number): Region[];
|
|
220
|
+
/**
|
|
221
|
+
* The full run of same-orientation regions on one refName that a scalebar
|
|
222
|
+
* label's `firstRegionIndex`..`lastRegionIndex` sits inside. Those come off the
|
|
223
|
+
* static blocks, which are clipped to the viewport and broken by an elided
|
|
224
|
+
* block, so a zoomed-in collapsed-introns view brackets only the exons on
|
|
225
|
+
* screen — which is the wrong answer for anything acting on the whole run.
|
|
226
|
+
*/
|
|
227
|
+
export declare function regionRunBounds(regions: Region[], firstIndex: number, lastIndex: number): {
|
|
228
|
+
first: number;
|
|
229
|
+
last: number;
|
|
230
|
+
};
|
|
201
231
|
export declare function withRegionReversed(regions: Region[], index: number): Region[];
|
|
202
232
|
/**
|
|
203
233
|
* Whether a label occupying [leftPx, leftPx + labelWidth] fits within a block
|
|
@@ -348,3 +378,4 @@ export declare function generateLocations({ regions, assemblyManager, assemblyNa
|
|
|
348
378
|
*/
|
|
349
379
|
export declare function parseLocStrings(input: string, assemblyName: string, isValidRefName: (str: string, assemblyName: string) => boolean): ParsedLocString[];
|
|
350
380
|
export declare function calculateVisibleLocStrings(contentBlocks: ContentBlock[]): string;
|
|
381
|
+
export {};
|
|
@@ -415,9 +415,57 @@ export function withRegionMoved(regions, from, to) {
|
|
|
415
415
|
out.splice(to, 0, moved);
|
|
416
416
|
return out;
|
|
417
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Replace the region list and keep looking at whatever was under the middle of
|
|
420
|
+
* the viewport, when that region is in the new list.
|
|
421
|
+
*
|
|
422
|
+
* `setDisplayedRegions` carries `offsetPx` across and clamps it, which is a
|
|
423
|
+
* linear position in a coordinate space the write just redefined: dropping the
|
|
424
|
+
* regions to the left of the one on screen strands the view at the end of what
|
|
425
|
+
* remains, on blank space. Where the centred region is gone the clamp is still
|
|
426
|
+
* the only answer, so this falls back to it.
|
|
427
|
+
*/
|
|
428
|
+
export function setDisplayedRegionsKeepingCenter(model, regions) {
|
|
429
|
+
const center = model.displayedRegions.length
|
|
430
|
+
? model.pxToBp(model.width / 2)
|
|
431
|
+
: undefined;
|
|
432
|
+
model.setDisplayedRegions(regions);
|
|
433
|
+
const index = center
|
|
434
|
+
? regions.findIndex(r => r.refName === center.refName &&
|
|
435
|
+
r.start === center.start &&
|
|
436
|
+
r.end === center.end)
|
|
437
|
+
: -1;
|
|
438
|
+
if (center && index !== -1) {
|
|
439
|
+
model.centerAt(center.coord0, center.refName, index);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
418
442
|
export function withRegionRemoved(regions, index) {
|
|
419
443
|
return regions.filter((_, i) => i !== index);
|
|
420
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* The full run of same-orientation regions on one refName that a scalebar
|
|
447
|
+
* label's `firstRegionIndex`..`lastRegionIndex` sits inside. Those come off the
|
|
448
|
+
* static blocks, which are clipped to the viewport and broken by an elided
|
|
449
|
+
* block, so a zoomed-in collapsed-introns view brackets only the exons on
|
|
450
|
+
* screen — which is the wrong answer for anything acting on the whole run.
|
|
451
|
+
*/
|
|
452
|
+
export function regionRunBounds(regions, firstIndex, lastIndex) {
|
|
453
|
+
const anchor = regions[firstIndex];
|
|
454
|
+
const inRun = (r) => r !== undefined &&
|
|
455
|
+
anchor !== undefined &&
|
|
456
|
+
r.refName === anchor.refName &&
|
|
457
|
+
r.assemblyName === anchor.assemblyName &&
|
|
458
|
+
!!r.reversed === !!anchor.reversed;
|
|
459
|
+
let first = firstIndex;
|
|
460
|
+
let last = lastIndex;
|
|
461
|
+
while (inRun(regions[first - 1])) {
|
|
462
|
+
first--;
|
|
463
|
+
}
|
|
464
|
+
while (inRun(regions[last + 1])) {
|
|
465
|
+
last++;
|
|
466
|
+
}
|
|
467
|
+
return { first, last };
|
|
468
|
+
}
|
|
421
469
|
export function withRegionReversed(regions, index) {
|
|
422
470
|
return regions.map((region, i) => i === index ? { ...region, reversed: !region.reversed } : region);
|
|
423
471
|
}
|