@jbrowse/plugin-linear-genome-view 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/esm/LaunchLinearGenomeView/index.js +2 -13
- package/esm/LinearGenomeView/afterAttach.js +16 -18
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +1 -1
- package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +1 -1
- package/esm/LinearGenomeView/components/PaddingBlocks.js +11 -1
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +37 -3
- package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +1 -1
- 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/components/ZoomButton.js +6 -1
- package/esm/LinearGenomeView/components/ZoomTransform.js +4 -0
- package/esm/LinearGenomeView/components/searchModes.d.ts +1 -1
- package/esm/LinearGenomeView/components/searchModes.js +1 -1
- 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/index.d.ts +1 -0
- package/esm/LinearGenomeView/index.js +8 -1
- package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
- package/esm/LinearGenomeView/launchKeys.js +23 -0
- package/esm/LinearGenomeView/lazyDialogs.d.ts +1 -1
- package/esm/LinearGenomeView/linkedViewSync.d.ts +1 -1
- package/esm/LinearGenomeView/linkedViewSync.js +6 -0
- package/esm/LinearGenomeView/model.d.ts +1779 -41
- package/esm/LinearGenomeView/model.js +192 -96
- package/esm/LinearGenomeView/svgcomponents/figureContract.js +14 -14
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
- package/esm/LinearGenomeView/types.d.ts +1 -1
- package/esm/LinearGenomeView/util.d.ts +31 -0
- package/esm/LinearGenomeView/util.js +48 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +145 -55
- package/esm/displayKitTests/perRegionTestEnv.js +26 -25
- package/esm/index.d.ts +2 -9
- package/esm/index.js +2 -3
- package/esm/searchUtils.d.ts +6 -3
- package/esm/searchUtils.js +37 -11
- package/package.json +8 -8
- package/esm/LinearGenomeView/FloatingSvgOverlay.d.ts +0 -5
- package/esm/LinearGenomeView/FloatingSvgOverlay.js +0 -25
- package/esm/LinearGenomeView/initKeys.d.ts +0 -13
- package/esm/LinearGenomeView/initKeys.js +0 -104
|
@@ -8,22 +8,29 @@ import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
|
|
|
8
8
|
import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
|
|
9
9
|
import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
|
|
10
10
|
import { tickLabelsWorthDrawing } from '@jbrowse/core/util/tickLabels';
|
|
11
|
-
import { hideTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
|
|
11
|
+
import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
|
|
12
12
|
import { ElementId } from '@jbrowse/core/util/types/mst';
|
|
13
|
-
import { computeViewStatus } from '@jbrowse/core/util/viewStatus';
|
|
13
|
+
import { assemblyErrorMessage, computeViewStatus, } from '@jbrowse/core/util/viewStatus';
|
|
14
|
+
import { pendingLaunch, withLaunchInput, } from '@jbrowse/core/util/withLaunchInput';
|
|
15
|
+
import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
|
|
14
16
|
import { cast, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
15
|
-
import { when } from 'mobx';
|
|
17
|
+
import { observable, when } from 'mobx';
|
|
16
18
|
import { handleSelectedRegion } from '../searchUtils.js';
|
|
17
19
|
import { doAfterAttach } from './afterAttach.js';
|
|
18
20
|
import { shouldSwapTracks } from './components/util.js';
|
|
19
21
|
import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, MIN_BP_PER_PX, MINIMIZED_TRACK_HEIGHT, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, SHOW_ALL_REGIONS_FILL, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from './consts.js';
|
|
20
22
|
import { planFlight } from './flyTo.js';
|
|
21
23
|
import { setupKeyboardHandler } from './keyboardHandler.js';
|
|
24
|
+
import { lgvLaunchKeys } from './launchKeys.js';
|
|
22
25
|
import { buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
|
|
23
26
|
import { sharedScaleContainerOf } from './sharedScaleContainer.js';
|
|
24
27
|
import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getScalebarRefNameLabels, regionsOrientation, groupContiguousBlocks, labelFitsInBlock, makeBlockTicks, runRefNameLabelPx, tickLabelWidth, } from './util.js';
|
|
25
28
|
// lazies
|
|
26
29
|
const SearchResultsDialog = lazy(() => import('./components/SearchResultsDialog.js'));
|
|
30
|
+
// The one array `paddingSpans` returns when a view has nothing to mask, so that
|
|
31
|
+
// computed's value repeats by identity. Frozen because every consumer maps over
|
|
32
|
+
// it and a caller that sorted in place would do so for the whole session.
|
|
33
|
+
const NO_PADDING_SPANS = Object.freeze([]);
|
|
27
34
|
/**
|
|
28
35
|
* Calculate the offsetPx needed to center content within a viewport.
|
|
29
36
|
* Returns a negative offset when content is smaller than viewport (padding on left).
|
|
@@ -141,30 +148,28 @@ function isTrackSelectorVisible(self) {
|
|
|
141
148
|
* #category view
|
|
142
149
|
*
|
|
143
150
|
* #example
|
|
144
|
-
* A `LinearGenomeView` is what you hand-author under `defaultSession.views
|
|
145
|
-
*
|
|
151
|
+
* A `LinearGenomeView` is what you hand-author under `defaultSession.views`, and
|
|
152
|
+
* every setting goes directly on the view object. `assembly`/`loc` fill in
|
|
153
|
+
* `displayedRegions`/`bpPerPx`/`offsetPx` for you:
|
|
146
154
|
* ```js
|
|
147
155
|
* defaultSession: {
|
|
148
156
|
* name: 'My session',
|
|
149
157
|
* views: [
|
|
150
158
|
* {
|
|
151
159
|
* type: 'LinearGenomeView',
|
|
152
|
-
* // plain persisted props sit alongside init, not inside it
|
|
153
160
|
* colorByCDS: true,
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* tracks: ['genes', 'alignments'],
|
|
158
|
-
* },
|
|
161
|
+
* assembly: 'hg38',
|
|
162
|
+
* loc: 'chr1:1,000,000-1,100,000',
|
|
163
|
+
* tracks: ['genes', 'alignments'],
|
|
159
164
|
* },
|
|
160
165
|
* ],
|
|
161
166
|
* }
|
|
162
167
|
* ```
|
|
163
|
-
* `
|
|
164
|
-
*
|
|
165
|
-
* `colorByCDS`, `showAminoAcids`, `showCenterLine`, `trackLabels
|
|
166
|
-
* `showHighlightChips` are
|
|
167
|
-
*
|
|
168
|
+
* `assembly`, `loc`, `tracks`, `tracklist`, `nav` and `highlight` need
|
|
169
|
+
* on-attach resolution and are captured into the `launch` property below;
|
|
170
|
+
* `colorByCDS`, `showAminoAcids`, `showCenterLine`, `trackLabels` and
|
|
171
|
+
* `showHighlightChips` are plain view props MST restores natively. Both are
|
|
172
|
+
* written the same way.
|
|
168
173
|
* At runtime the same model is driven imperatively — every property and action
|
|
169
174
|
* below is reachable on `viewState.session.views[0]`:
|
|
170
175
|
* ```js
|
|
@@ -175,7 +180,7 @@ function isTrackSelectorVisible(self) {
|
|
|
175
180
|
* ```
|
|
176
181
|
*/
|
|
177
182
|
export function stateModelFactory(pluginManager) {
|
|
178
|
-
|
|
183
|
+
const model = types
|
|
179
184
|
.compose('LinearGenomeView', BaseViewModel, HighlightsMixin(), types.model({
|
|
180
185
|
/**
|
|
181
186
|
* #property
|
|
@@ -321,27 +326,29 @@ export function stateModelFactory(pluginManager) {
|
|
|
321
326
|
scalebarOnly: types.stripDefault(types.boolean, false),
|
|
322
327
|
/**
|
|
323
328
|
* #property
|
|
324
|
-
* transient
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
* "loc": "chr1:1,000,000-2,000,000",
|
|
333
|
-
* "tracks": ["genes", "variants"]
|
|
334
|
-
* }
|
|
335
|
-
* ```
|
|
336
|
-
*/
|
|
337
|
-
init: types.frozen(),
|
|
329
|
+
* transient launch state: the settings written on the view object that
|
|
330
|
+
* need resolving before they can be view state — an assembly, a
|
|
331
|
+
* location, track recipes, highlights. `preProcessSnapshot` moves them
|
|
332
|
+
* here off the snapshot, the afterAttach autorun applies them and
|
|
333
|
+
* clears this, so a saved session never retains it. Not written by
|
|
334
|
+
* hand: author every setting directly on the view.
|
|
335
|
+
*/
|
|
336
|
+
launch: types.frozen(),
|
|
338
337
|
}))
|
|
339
|
-
.volatile(
|
|
338
|
+
.volatile(() => {
|
|
340
339
|
// typed locals so `unknown`/`Record` aren't narrowed to `undefined`/`{}`; inline
|
|
341
340
|
// type assertions here get stripped by no-unnecessary-type-assertion
|
|
342
341
|
const volatileError = undefined;
|
|
343
|
-
const trackRefs = {};
|
|
344
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(),
|
|
345
352
|
/**
|
|
346
353
|
* #volatile
|
|
347
354
|
*/
|
|
@@ -362,10 +369,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
362
369
|
* #volatile
|
|
363
370
|
*/
|
|
364
371
|
volatileError,
|
|
365
|
-
/**
|
|
366
|
-
* #volatile
|
|
367
|
-
*/
|
|
368
|
-
trackRefs,
|
|
369
372
|
/**
|
|
370
373
|
* #volatile
|
|
371
374
|
*/
|
|
@@ -377,14 +380,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
377
380
|
/**
|
|
378
381
|
* #volatile
|
|
379
382
|
*/
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
// `coarseBpPerPx` rather than the live one, and a zero there is a
|
|
385
|
-
// degenerate layout for the frame or two before the debounced autorun
|
|
386
|
-
// first writes it.
|
|
387
|
-
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,
|
|
388
387
|
/**
|
|
389
388
|
* #volatile
|
|
390
389
|
*/
|
|
@@ -413,6 +412,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
413
412
|
// ahead of every other getter so that the ~300 reads of `self.bpPerPx` /
|
|
414
413
|
// `self.offsetPx` across the codebase go on resolving by name. Only the
|
|
415
414
|
// writes had to move, and they are six actions below.
|
|
415
|
+
/**
|
|
416
|
+
* #getter
|
|
417
|
+
* the launch state that still has something to apply — the gate every
|
|
418
|
+
* loading and error path below reads.
|
|
419
|
+
*/
|
|
420
|
+
get pendingLaunch() {
|
|
421
|
+
return pendingLaunch(self.launch);
|
|
422
|
+
},
|
|
416
423
|
/**
|
|
417
424
|
* #getter
|
|
418
425
|
* corresponds roughly to the zoom level, base-pairs per pixel.
|
|
@@ -522,6 +529,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
522
529
|
},
|
|
523
530
|
}))
|
|
524
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
|
+
},
|
|
525
540
|
/**
|
|
526
541
|
* #getter
|
|
527
542
|
* Assembly-name prefix for the scalebar refName labels, or undefined for
|
|
@@ -555,10 +570,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
555
570
|
*/
|
|
556
571
|
get assemblyErrors() {
|
|
557
572
|
const { assemblyManager } = getSession(self);
|
|
558
|
-
return self.assemblyNames
|
|
559
|
-
.map(name => assemblyManager.get(name)?.error)
|
|
560
|
-
.filter(e => !!e)
|
|
561
|
-
.join(', ');
|
|
573
|
+
return assemblyErrorMessage(assemblyManager, self.assemblyNames);
|
|
562
574
|
},
|
|
563
575
|
/**
|
|
564
576
|
* #getter
|
|
@@ -575,8 +587,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
575
587
|
* and error checks resolve it directly through here.
|
|
576
588
|
*/
|
|
577
589
|
get initAssembly() {
|
|
578
|
-
return self.
|
|
579
|
-
? getSession(self).assemblyManager.get(self.
|
|
590
|
+
return self.pendingLaunch
|
|
591
|
+
? getSession(self).assemblyManager.get(self.pendingLaunch.assembly)
|
|
580
592
|
: undefined;
|
|
581
593
|
},
|
|
582
594
|
/**
|
|
@@ -587,7 +599,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
587
599
|
return false;
|
|
588
600
|
}
|
|
589
601
|
// if init is set, wait for that assembly to have regions loaded
|
|
590
|
-
if (self.
|
|
602
|
+
if (self.pendingLaunch) {
|
|
591
603
|
const asm = this.initAssembly;
|
|
592
604
|
return !!(asm?.initialized && asm.regions);
|
|
593
605
|
}
|
|
@@ -606,7 +618,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
606
618
|
* displayed assemblies that hasn't finished loading.
|
|
607
619
|
*/
|
|
608
620
|
get loadingAssembly() {
|
|
609
|
-
return self.
|
|
621
|
+
return self.pendingLaunch
|
|
610
622
|
? this.initAssembly
|
|
611
623
|
: getSession(self).assemblyManager.loadingAssembly(self.assemblyNames);
|
|
612
624
|
},
|
|
@@ -646,7 +658,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
646
658
|
* #getter
|
|
647
659
|
*/
|
|
648
660
|
get hasSomethingToShow() {
|
|
649
|
-
return this.hasDisplayedRegions || !!self.
|
|
661
|
+
return this.hasDisplayedRegions || !!self.pendingLaunch;
|
|
650
662
|
},
|
|
651
663
|
/**
|
|
652
664
|
* #getter
|
|
@@ -656,7 +668,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
656
668
|
* would mount over empty regions and pxToBp/hover would throw.
|
|
657
669
|
*
|
|
658
670
|
* **Not dotplot's or synteny's `initPending`**, which is the bare
|
|
659
|
-
* `!!self.
|
|
671
|
+
* `!!self.pendingLaunch` — "an init blob has not been applied" — and which those
|
|
660
672
|
* views read from their `settled` gate rather than from `showLoading`.
|
|
661
673
|
* This one is narrower on purpose: it closes only the window where there
|
|
662
674
|
* is nothing on screen. Once navigation has produced regions the view is
|
|
@@ -666,7 +678,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
666
678
|
* theirs.
|
|
667
679
|
*/
|
|
668
680
|
get awaitingInitNavigation() {
|
|
669
|
-
return !!self.
|
|
681
|
+
return !!self.pendingLaunch && !this.hasDisplayedRegions;
|
|
670
682
|
},
|
|
671
683
|
/**
|
|
672
684
|
* #getter
|
|
@@ -754,14 +766,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
754
766
|
* #getter
|
|
755
767
|
* A track's full cost beyond its display height.
|
|
756
768
|
*
|
|
757
|
-
* The track *label* is not counted
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
* overlapping, and short by one label box per labelled track otherwise.
|
|
762
|
-
* Anything needing the offset to the pixel with labels showing measures
|
|
763
|
-
* the DOM; `BreakpointSplitViewOverlay` does, and falls back to this
|
|
764
|
-
* 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.
|
|
765
773
|
*/
|
|
766
774
|
get trackChromeHeight() {
|
|
767
775
|
return this.trackLeadingChrome + this.trackTrailingChrome;
|
|
@@ -812,6 +820,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
812
820
|
? MINIMIZED_TRACK_HEIGHT
|
|
813
821
|
: track.activeDisplay.height;
|
|
814
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
|
+
},
|
|
815
831
|
/**
|
|
816
832
|
* #getter
|
|
817
833
|
*/
|
|
@@ -822,7 +838,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
822
838
|
* #getter
|
|
823
839
|
*/
|
|
824
840
|
get trackHeightsWithChrome() {
|
|
825
|
-
return this.trackHeights +
|
|
841
|
+
return (this.trackHeights +
|
|
842
|
+
self.tracks.length * this.trackChromeHeight +
|
|
843
|
+
sum(self.tracks.map(t => this.trackLabelBand(t))));
|
|
826
844
|
},
|
|
827
845
|
/**
|
|
828
846
|
* #getter
|
|
@@ -840,14 +858,15 @@ export function stateModelFactory(pluginManager) {
|
|
|
840
858
|
* first, then unpinned), from the same constants TrackContainer lays its
|
|
841
859
|
* Paper out with. Returns `undefined` if the track is not present.
|
|
842
860
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
*
|
|
846
|
-
*
|
|
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.
|
|
847
865
|
*/
|
|
848
866
|
getTrackYOffset(trackId) {
|
|
849
867
|
let y = this.headerHeight + this.scalebarHeight + this.trackLeadingChrome;
|
|
850
868
|
for (const t of [...self.pinnedTracks, ...self.unpinnedTracks]) {
|
|
869
|
+
y += this.trackLabelBand(t);
|
|
851
870
|
if (t.configuration.trackId === trackId) {
|
|
852
871
|
return y;
|
|
853
872
|
}
|
|
@@ -935,17 +954,17 @@ export function stateModelFactory(pluginManager) {
|
|
|
935
954
|
return this.assembliesNotFound;
|
|
936
955
|
}
|
|
937
956
|
// Check init assembly for errors (displayedRegions may be empty during init)
|
|
938
|
-
if (self.
|
|
957
|
+
if (self.pendingLaunch) {
|
|
939
958
|
// `assembly` is required by InitState, but init is a frozen blob and
|
|
940
959
|
// hand-authored JSON is what fills it, so the type is not a guarantee.
|
|
941
960
|
// Naming the authoring mistake beats the downstream symptom, which is
|
|
942
961
|
// the literal string "Assembly undefined not found".
|
|
943
|
-
if (!self.
|
|
962
|
+
if (!self.pendingLaunch.assembly) {
|
|
944
963
|
return 'LinearGenomeView init needs an "assembly"';
|
|
945
964
|
}
|
|
946
965
|
const asm = this.initAssembly;
|
|
947
966
|
if (!asm) {
|
|
948
|
-
return `Assembly ${self.
|
|
967
|
+
return `Assembly ${self.pendingLaunch.assembly} not found`;
|
|
949
968
|
}
|
|
950
969
|
if (asm.error) {
|
|
951
970
|
return asm.error;
|
|
@@ -1058,7 +1077,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
1058
1077
|
// the gesture into one refetch, its in-flight guard caps concurrent
|
|
1059
1078
|
// batches at one, and rpcDataMap is overwritten in place rather than
|
|
1060
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
|
+
},
|
|
1061
1088
|
setWidth(newWidth) {
|
|
1089
|
+
const unmeasured = self.bpPerPx <= 0;
|
|
1062
1090
|
self.volatileWidth = newWidth;
|
|
1063
1091
|
if (newWidth > 0 && self.windowWidthBp <= 0) {
|
|
1064
1092
|
// First measure, and nothing has established a window yet: a migrated
|
|
@@ -1069,6 +1097,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
1069
1097
|
self.windowWidthBp = (self.legacyBpPerPx || 1) * newWidth;
|
|
1070
1098
|
self.legacyBpPerPx = 0;
|
|
1071
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
|
+
}
|
|
1072
1105
|
},
|
|
1073
1106
|
/**
|
|
1074
1107
|
* #action
|
|
@@ -1221,6 +1254,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
1221
1254
|
hideTrack(trackId) {
|
|
1222
1255
|
return hideTrackGeneric(self, trackId);
|
|
1223
1256
|
},
|
|
1257
|
+
}))
|
|
1258
|
+
.actions(self => ({
|
|
1259
|
+
/**
|
|
1260
|
+
* #action
|
|
1261
|
+
* showTrack for a track whose display state model may be lazily
|
|
1262
|
+
* loaded: loads it, then shows
|
|
1263
|
+
*/
|
|
1264
|
+
async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
|
|
1265
|
+
return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
1266
|
+
},
|
|
1224
1267
|
}))
|
|
1225
1268
|
.actions(self => ({
|
|
1226
1269
|
/**
|
|
@@ -1289,6 +1332,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
1289
1332
|
toggleTrack(trackId) {
|
|
1290
1333
|
return toggleTrackGeneric(self, trackId);
|
|
1291
1334
|
},
|
|
1335
|
+
/**
|
|
1336
|
+
* #action
|
|
1337
|
+
* toggleTrack with launchTrack's loading behavior
|
|
1338
|
+
*/
|
|
1339
|
+
async launchToggleTrack(trackId) {
|
|
1340
|
+
return launchToggleTrackGeneric(self, trackId);
|
|
1341
|
+
},
|
|
1292
1342
|
/**
|
|
1293
1343
|
* #action
|
|
1294
1344
|
*/
|
|
@@ -1402,18 +1452,25 @@ export function stateModelFactory(pluginManager) {
|
|
|
1402
1452
|
/**
|
|
1403
1453
|
* #action
|
|
1404
1454
|
*/
|
|
1405
|
-
|
|
1406
|
-
self.
|
|
1455
|
+
setLaunch(launch) {
|
|
1456
|
+
self.launch = launch;
|
|
1407
1457
|
},
|
|
1408
1458
|
/**
|
|
1409
1459
|
* #method
|
|
1410
|
-
*
|
|
1460
|
+
* renders the view to SVG markup, which it returns; saves it through
|
|
1461
|
+
* FileSaver unless `save: false`
|
|
1411
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).
|
|
1412
1466
|
async exportSvg(opts = {}) {
|
|
1413
1467
|
const { renderToSvg } = await import('./svgcomponents/SVGLinearGenomeView.js');
|
|
1414
1468
|
const html = await renderToSvg(self, opts);
|
|
1415
|
-
|
|
1416
|
-
|
|
1469
|
+
if (opts.save !== false) {
|
|
1470
|
+
const { saveSvgAsImage } = await import('@jbrowse/core/svg/saveSvgAsImage');
|
|
1471
|
+
await saveSvgAsImage(html, opts);
|
|
1472
|
+
}
|
|
1473
|
+
return html;
|
|
1417
1474
|
},
|
|
1418
1475
|
}))
|
|
1419
1476
|
.actions(self => {
|
|
@@ -1866,6 +1923,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
1866
1923
|
* The seam is the one that must survive: regions lay out contiguously,
|
|
1867
1924
|
* so it is all that separates two of them.
|
|
1868
1925
|
*/
|
|
1926
|
+
// Annotated with the shape spelled out rather than as `PaddingSpan`,
|
|
1927
|
+
// because the API doc's type column is where a host drawing its own
|
|
1928
|
+
// chrome reads the field names, and a named type prints as its name.
|
|
1929
|
+
// `spans` and NO_PADDING_SPANS are both `PaddingSpan[]`, so the two
|
|
1930
|
+
// cannot drift without failing here.
|
|
1869
1931
|
get paddingSpans() {
|
|
1870
1932
|
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1871
1933
|
const spans = [];
|
|
@@ -1892,7 +1954,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
1892
1954
|
});
|
|
1893
1955
|
}
|
|
1894
1956
|
}
|
|
1895
|
-
|
|
1957
|
+
// One shared array when there is nothing to draw, so the computed's
|
|
1958
|
+
// value repeats and MobX's `===` stops the chain here. This is the
|
|
1959
|
+
// COMMON case, not an edge one: a view sitting inside one contig has
|
|
1960
|
+
// no region seam, no elision and no boundary, and that is where a
|
|
1961
|
+
// reader spends nearly all of a session. A fresh `[]` each frame
|
|
1962
|
+
// instead re-rendered a PaddingBlocks per track plus one for the
|
|
1963
|
+
// container, every frame of every gesture, to draw nothing.
|
|
1964
|
+
return spans.length > 0 ? spans : NO_PADDING_SPANS;
|
|
1896
1965
|
},
|
|
1897
1966
|
/**
|
|
1898
1967
|
* #getter
|
|
@@ -1962,6 +2031,32 @@ export function stateModelFactory(pluginManager) {
|
|
|
1962
2031
|
};
|
|
1963
2032
|
});
|
|
1964
2033
|
},
|
|
2034
|
+
/**
|
|
2035
|
+
* #getter
|
|
2036
|
+
* Right edge (px, viewport-relative) of the on-screen content, clamped
|
|
2037
|
+
* to the track — where a right-pinned overlay such as a wiggle's colour
|
|
2038
|
+
* or score legend belongs, rather than out in the empty gutter the
|
|
2039
|
+
* regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
|
|
2040
|
+
* `contentRightEdgePx` states the rule; an SVG export applies the same
|
|
2041
|
+
* one against the export's canvas width instead of this track's.
|
|
2042
|
+
*
|
|
2043
|
+
* Published as a SCALAR on purpose. `visibleRegions` rebuilds a fresh
|
|
2044
|
+
* array of fresh objects on every pan and zoom frame, so a component
|
|
2045
|
+
* that derived this number itself re-rendered on every frame of every
|
|
2046
|
+
* gesture — to produce, whenever content fills the track, the unchanged
|
|
2047
|
+
* `trackWidthPx`. Reading the scalar lets MobX's `===` stop the chain at
|
|
2048
|
+
* this computed instead. Measured over 20 zoom frames: the wiggle
|
|
2049
|
+
* family's three body instances fell from 66 renders to 7.
|
|
2050
|
+
*/
|
|
2051
|
+
get contentRightEdgePx() {
|
|
2052
|
+
// The `trackWidthPx` rule sends a DISPLAY to `model.canvasWidthPx`
|
|
2053
|
+
// so it cannot pick the wrong one of four view getters. This is the
|
|
2054
|
+
// view, publishing its own geometry, and `canvasWidthPx` is defined
|
|
2055
|
+
// as exactly this host's `trackWidthPx` (MultiRegionDisplayMixin) —
|
|
2056
|
+
// the same source of truth, read from the side that owns it.
|
|
2057
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
2058
|
+
return contentRightEdgePx(this.visibleRegions, self.trackWidthPx);
|
|
2059
|
+
},
|
|
1965
2060
|
/**
|
|
1966
2061
|
* #getter
|
|
1967
2062
|
* visibleRegions expanded by a half-screen buffer on each side,
|
|
@@ -2386,13 +2481,15 @@ export function stateModelFactory(pluginManager) {
|
|
|
2386
2481
|
* navigating to the locstring
|
|
2387
2482
|
* @param grow - optional multiplier to expand the region by (e.g., 0.2
|
|
2388
2483
|
* adds 20% padding on each side)
|
|
2484
|
+
* @param opts - `showHitTrack: false` keeps a text-search hit from also
|
|
2485
|
+
* showing the track its index was built from
|
|
2389
2486
|
* @returns whether the view moved. A multi-hit search raises the picker
|
|
2390
2487
|
* instead of navigating, and both resolve — see `handleSelectedRegion`.
|
|
2391
2488
|
*/
|
|
2392
2489
|
// annotated, not inferred: the same type cycle `showTrack` names. Left to
|
|
2393
2490
|
// infer, the `boolean` reaches this model's type through `self` and 180
|
|
2394
2491
|
// unrelated files lose their parameter types to it.
|
|
2395
|
-
async navToLocString(input, optAssemblyName, grow) {
|
|
2492
|
+
async navToLocString(input, optAssemblyName, grow, opts) {
|
|
2396
2493
|
const { assemblyNames } = self;
|
|
2397
2494
|
const session = getSession(self);
|
|
2398
2495
|
const { assemblyManager } = session;
|
|
@@ -2407,6 +2504,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2407
2504
|
input,
|
|
2408
2505
|
assemblyName,
|
|
2409
2506
|
grow,
|
|
2507
|
+
showHitTrack: opts?.showHitTrack,
|
|
2410
2508
|
model: self,
|
|
2411
2509
|
});
|
|
2412
2510
|
},
|
|
@@ -2675,7 +2773,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2675
2773
|
* `centerAt`'s animated twin: the same destination, reached along the
|
|
2676
2774
|
* arc instead of jumped to, at the zoom the view is already on.
|
|
2677
2775
|
*/
|
|
2678
|
-
flyToCenter(coord, refName) {
|
|
2776
|
+
flyToCenter(coord, refName, displayedRegionIndex) {
|
|
2679
2777
|
// `bpToLinearBp` rather than `bpToPx`, which rounds to a whole pixel
|
|
2680
2778
|
// at the current zoom: mid-flight that zoom is a point on somebody
|
|
2681
2779
|
// else's arc, and a destination quantized against it moves with it.
|
|
@@ -2683,6 +2781,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2683
2781
|
refName,
|
|
2684
2782
|
coord,
|
|
2685
2783
|
displayedRegions: self.displayedRegions,
|
|
2784
|
+
displayedRegionIndex,
|
|
2686
2785
|
});
|
|
2687
2786
|
if (centerBp !== undefined) {
|
|
2688
2787
|
flyTo(centerBp, heading ? heading.windowWidthBp : self.windowWidthBp);
|
|
@@ -2697,7 +2796,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
2697
2796
|
afterAttach() {
|
|
2698
2797
|
doAfterAttach(self);
|
|
2699
2798
|
},
|
|
2700
|
-
}))
|
|
2799
|
+
}));
|
|
2800
|
+
return withLaunchInput(model, lgvLaunchKeys, {
|
|
2801
|
+
registry: pluginManager,
|
|
2802
|
+
materialized: snap => !!snap.displayedRegions?.length,
|
|
2803
|
+
})
|
|
2701
2804
|
.preProcessSnapshot((snap) => {
|
|
2702
2805
|
if (!snap) {
|
|
2703
2806
|
return snap;
|
|
@@ -2739,23 +2842,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
2739
2842
|
};
|
|
2740
2843
|
})
|
|
2741
2844
|
.postProcessSnapshot(snap => {
|
|
2742
|
-
//
|
|
2743
|
-
//
|
|
2744
|
-
//
|
|
2745
|
-
//
|
|
2746
|
-
//
|
|
2747
|
-
//
|
|
2748
|
-
//
|
|
2749
|
-
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;
|
|
2750
2853
|
return {
|
|
2751
2854
|
...rest,
|
|
2752
|
-
// keep init until displayedRegions exist, so a snapshot taken before the
|
|
2753
|
-
// launch autorun navigates (e.g. autosave firing mid-load) can still
|
|
2754
|
-
// rebuild the view instead of dropping to the import form.
|
|
2755
|
-
// displayedRegions is stripDefault, so it's absent (not []) when empty —
|
|
2756
|
-
// the optional chain is runtime-necessary despite the non-nullish type.
|
|
2757
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
2758
|
-
...(init && !snap.displayedRegions?.length ? { init } : {}),
|
|
2759
2855
|
...(!showCytobands ? { showCytobands } : {}),
|
|
2760
2856
|
...(trackLabels ? { trackLabels } : {}),
|
|
2761
2857
|
...(colorByCDS ? { colorByCDS } : {}),
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { svgNodeId } from '@jbrowse/core/svg/svgId';
|
|
3
|
+
import { contractReportsOn, reportContractViolation, } from '@jbrowse/render-core/contractReports';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* No-ops in production.
|
|
5
|
+
* Checks that a live SVG figure is being used the way it can work.
|
|
6
6
|
*
|
|
7
7
|
* `useViewSvgFigure` publishes a figure whose two halves are drawn at different
|
|
8
8
|
* moments and only agree by construction, and both ways of breaking that are
|
|
9
9
|
* invisible in the output: the drawing is still a plausible picture of
|
|
10
10
|
* somewhere, just not of one moment or not with its own clips. So the run
|
|
11
11
|
* reports instead, through the `[jbrowse <family> contract]` channel every
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* check in the tree uses — `config/jest/contractGate.js` fails the test that
|
|
13
|
+
* collects one, and ARCHITECTURAL_LIMITS.md §"Ordering is the contract" is the
|
|
14
|
+
* register. `figure` rather than `svg-figure` because the gate's family pattern
|
|
15
|
+
* is `\w+`, which a hyphen is not.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Reported and never thrown, as the rest of that family: a figure is chrome a
|
|
18
|
+
* host renders beside its own, and taking the host's tree down over a drawing
|
|
19
|
+
* is worse than the drawing being wrong.
|
|
20
20
|
*/
|
|
21
21
|
function report(message) {
|
|
22
|
-
|
|
22
|
+
reportContractViolation('figure', message);
|
|
23
23
|
}
|
|
24
24
|
// Live figures per view, keyed the way their SVG ids are. A count rather than a
|
|
25
25
|
// flag: the report belongs on the second mount, and the first has to survive the
|
|
@@ -43,9 +43,6 @@ const figuresPerView = new Map();
|
|
|
43
43
|
*/
|
|
44
44
|
export function useOneFigurePerView(view) {
|
|
45
45
|
useEffect(() => {
|
|
46
|
-
if (process.env.NODE_ENV === 'production') {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
46
|
// captured, not re-derived on cleanup: by then the view may have left the
|
|
50
47
|
// tree, and `svgNodeId` walks it
|
|
51
48
|
const key = svgNodeId(view);
|
|
@@ -99,7 +96,10 @@ export function useFrozenFigureContract(ref, snapshot) {
|
|
|
99
96
|
rendered.current = snapshot;
|
|
100
97
|
useEffect(() => {
|
|
101
98
|
const el = ref.current;
|
|
102
|
-
|
|
99
|
+
// the one check here with a cost of its own — a `MutationObserver` over the
|
|
100
|
+
// whole figure subtree — so unlike its neighbour it asks whether the
|
|
101
|
+
// channel is armed rather than running and reporting into it
|
|
102
|
+
if (!contractReportsOn() || !el) {
|
|
103
103
|
return undefined;
|
|
104
104
|
}
|
|
105
105
|
const observer = new MutationObserver(() => {
|
|
@@ -101,8 +101,8 @@ async function renderFigure(view, opts) {
|
|
|
101
101
|
*/
|
|
102
102
|
const FrozenSvgFigure = memo(function FrozenSvgFigure({ view, snapshot, }) {
|
|
103
103
|
const { width, height, margin, theme } = snapshot;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// it watches for the one thing this component cannot enforce: an observer
|
|
105
|
+
// INSIDE the tree, which the memo above is not between
|
|
106
106
|
const ref = useRef(null);
|
|
107
107
|
useFrozenFigureContract(ref, snapshot);
|
|
108
108
|
return (_jsx(ThemeProvider, { theme: createJBrowseTheme(theme), children: _jsx(StyleThemeProvider, { theme: resolveStyleTheme({ configTheme: theme }), children: _jsx("svg", { ref: ref, width: width, height: height, viewBox: `0 0 ${width} ${height}`,
|
|
@@ -41,4 +41,4 @@ export interface InitState {
|
|
|
41
41
|
nav?: boolean;
|
|
42
42
|
highlight?: (string | HighlightType)[];
|
|
43
43
|
}
|
|
44
|
-
export type LinearGenomeViewLaunchProps = Partial<Omit<SnapshotIn<LinearGenomeViewStateModel>, keyof InitState | 'id' | 'type' | '
|
|
44
|
+
export type LinearGenomeViewLaunchProps = Partial<Omit<SnapshotIn<LinearGenomeViewStateModel>, keyof InitState | 'id' | 'type' | 'launch'>>;
|