@jbrowse/plugin-linear-genome-view 5.0.0-beta.2 → 5.0.0-beta.20
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/afterAttach.d.ts +22 -5
- package/esm/LinearGenomeView/afterAttach.js +129 -106
- package/esm/LinearGenomeView/axisConfigSchema.d.ts +18 -0
- package/esm/LinearGenomeView/axisConfigSchema.js +22 -0
- package/esm/LinearGenomeView/components/CenterLine.js +10 -5
- package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
- package/esm/LinearGenomeView/components/CrisprGuidePanel.js +3 -5
- package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +9 -4
- package/esm/LinearGenomeView/components/ExportSvgDialog.js +45 -8
- package/esm/LinearGenomeView/components/GetSequenceDialog.js +2 -2
- package/esm/LinearGenomeView/components/Header.js +11 -1
- package/esm/LinearGenomeView/components/HeaderClearHighlightButton.js +2 -2
- package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
- package/esm/LinearGenomeView/components/HeaderPanControls.js +6 -3
- package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +4 -1
- package/esm/LinearGenomeView/components/HeaderZoomControls.js +6 -35
- package/esm/LinearGenomeView/components/Highlight.js +27 -11
- package/esm/LinearGenomeView/components/HighlightBand.d.ts +11 -2
- package/esm/LinearGenomeView/components/HighlightBand.js +32 -13
- package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -3
- package/esm/LinearGenomeView/components/HighlightChip.js +13 -8
- package/esm/LinearGenomeView/components/HighlightLabelDialog.d.ts +6 -0
- package/esm/LinearGenomeView/components/HighlightLabelDialog.js +18 -0
- package/esm/LinearGenomeView/components/HoverPositionHighlight.js +3 -3
- package/esm/LinearGenomeView/components/ImportForm.js +7 -9
- package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
- package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +13 -14
- package/esm/LinearGenomeView/components/MiniControls.js +11 -5
- package/esm/LinearGenomeView/components/OverviewHighlight.js +3 -3
- package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
- package/esm/LinearGenomeView/components/OverviewHighlightBand.js +6 -5
- package/esm/LinearGenomeView/components/OverviewRubberband.js +25 -48
- package/esm/LinearGenomeView/components/OverviewScalebar.js +4 -5
- package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +5 -3
- package/esm/LinearGenomeView/components/PaddingBlocks.js +3 -8
- package/esm/LinearGenomeView/components/RangeSelectOverlay.js +5 -6
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
- package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.d.ts +5 -0
- package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +74 -0
- package/esm/LinearGenomeView/components/Rubberband.d.ts +1 -2
- package/esm/LinearGenomeView/components/Rubberband.js +3 -2
- package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
- package/esm/LinearGenomeView/components/SVGHighlightBand.js +16 -5
- package/esm/LinearGenomeView/components/Scalebar.d.ts +2 -6
- package/esm/LinearGenomeView/components/Scalebar.js +8 -5
- package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +51 -36
- package/esm/LinearGenomeView/components/SearchBox.js +10 -16
- package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
- package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
- package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -8
- package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
- package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +4 -7
- package/esm/LinearGenomeView/components/SearchResultsTable.js +25 -53
- package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +2 -3
- package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +2 -4
- package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
- package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
- package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -3
- package/esm/LinearGenomeView/components/TrackContainer.js +63 -10
- package/esm/LinearGenomeView/components/TrackLabel.js +14 -4
- package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
- package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +4 -5
- package/esm/LinearGenomeView/components/TrackLabelMenu.js +11 -14
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +3 -4
- package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +23 -10
- package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +6 -4
- package/esm/LinearGenomeView/components/TrackRenderingContainer.js +12 -24
- package/esm/LinearGenomeView/components/TracksContainer.js +11 -23
- package/esm/LinearGenomeView/components/VerticalGuide.d.ts +1 -2
- package/esm/LinearGenomeView/components/VerticalGuide.js +3 -16
- package/esm/LinearGenomeView/components/fetchSequence.d.ts +1 -2
- package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
- package/esm/LinearGenomeView/components/recentLocationsMenu.js +10 -0
- package/esm/LinearGenomeView/components/useRangeSelect.d.ts +0 -3
- package/esm/LinearGenomeView/components/useRangeSelect.js +26 -61
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
- package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
- package/esm/LinearGenomeView/components/useSideScroll.d.ts +17 -2
- package/esm/LinearGenomeView/components/useSideScroll.js +156 -102
- package/esm/LinearGenomeView/components/util.d.ts +4 -0
- package/esm/LinearGenomeView/components/util.js +4 -2
- package/esm/LinearGenomeView/consts.d.ts +2 -0
- package/esm/LinearGenomeView/consts.js +3 -1
- package/esm/LinearGenomeView/containingLgv.d.ts +21 -0
- package/esm/LinearGenomeView/containingLgv.js +23 -0
- package/esm/LinearGenomeView/headerFit.d.ts +4 -2
- package/esm/LinearGenomeView/headerFit.js +20 -7
- package/esm/LinearGenomeView/index.d.ts +2 -7
- package/esm/LinearGenomeView/index.js +5 -7
- package/esm/LinearGenomeView/keyboardHandler.d.ts +8 -7
- package/esm/LinearGenomeView/keyboardHandler.js +61 -23
- package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
- package/esm/LinearGenomeView/launchKeys.js +11 -6
- package/esm/LinearGenomeView/lazyDialogs.d.ts +16 -3
- package/esm/LinearGenomeView/lazyDialogs.js +1 -0
- package/esm/LinearGenomeView/linkedViewSync.d.ts +3 -11
- package/esm/LinearGenomeView/linkedViewSync.js +27 -49
- package/esm/LinearGenomeView/menuItems.d.ts +16 -18
- package/esm/LinearGenomeView/menuItems.js +264 -164
- package/esm/LinearGenomeView/model.d.ts +1192 -560
- package/esm/LinearGenomeView/model.js +805 -590
- package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +17 -0
- package/esm/LinearGenomeView/rExport/exportLinearView.js +117 -0
- package/esm/LinearGenomeView/rExport/rScript.d.ts +110 -0
- package/esm/LinearGenomeView/rExport/rScript.js +524 -0
- package/esm/LinearGenomeView/sharedScaleContainer.d.ts +1 -1
- package/esm/LinearGenomeView/sharedScaleContainer.js +2 -12
- package/esm/LinearGenomeView/showFiles.d.ts +2 -0
- package/esm/LinearGenomeView/showFiles.js +19 -0
- package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +1 -1
- package/esm/LinearGenomeView/showRegionsWithUndo.js +13 -5
- package/esm/LinearGenomeView/stickyChrome.d.ts +5 -9
- package/esm/LinearGenomeView/stickyChrome.js +5 -9
- package/esm/LinearGenomeView/svgcomponents/SVGGridlines.js +4 -4
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.d.ts +4 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHeader.js +8 -21
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +9 -9
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
- package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +5 -5
- package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +13 -18
- package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.js +2 -5
- package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.js +5 -6
- package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +9 -10
- package/esm/LinearGenomeView/svgcomponents/SVGScalebar.js +3 -3
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +26 -0
- package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +12 -0
- package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +21 -13
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +4 -5
- package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +7 -6
- package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +2 -3
- package/esm/LinearGenomeView/svgcomponents/SVGView.js +17 -3
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -20
- package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
- package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +74 -77
- package/esm/LinearGenomeView/svgcomponents/util.d.ts +28 -12
- package/esm/LinearGenomeView/svgcomponents/util.js +41 -29
- package/esm/LinearGenomeView/types.d.ts +13 -10
- package/esm/LinearGenomeView/types.js +0 -1
- package/esm/LinearGenomeView/util.d.ts +103 -46
- package/esm/LinearGenomeView/util.js +222 -170
- package/esm/MultiLevelRubberband/MultiLevelRubberband.js +9 -6
- package/esm/MultiLevelRubberband/index.d.ts +2 -0
- package/esm/MultiLevelRubberband/index.js +2 -0
- package/esm/MultiLevelRubberband/menuItems.d.ts +34 -0
- package/esm/MultiLevelRubberband/menuItems.js +52 -0
- package/esm/MultiLevelRubberband/rowLabel.d.ts +22 -0
- package/esm/MultiLevelRubberband/rowLabel.js +27 -0
- package/esm/MultiLevelRubberband/types.d.ts +1 -0
- package/esm/MultiLevelRubberband/useRangeSelect.d.ts +2 -1
- package/esm/MultiLevelRubberband/useRangeSelect.js +45 -59
- package/esm/TrackModifiers/index.d.ts +83 -0
- package/esm/TrackModifiers/index.js +231 -0
- package/esm/displayKitTests/perRegionTestEnv.d.ts +244 -68
- package/esm/displayKitTests/perRegionTestEnv.js +30 -26
- package/esm/index.d.ts +21 -99
- package/esm/index.js +17 -38
- package/esm/searchUtils.d.ts +26 -10
- package/esm/searchUtils.js +137 -137
- package/esm/shared/RubberbandSpan.d.ts +3 -2
- package/esm/shared/RubberbandSpan.js +6 -3
- package/esm/shared/coordLabels.d.ts +11 -4
- package/esm/shared/coordLabels.js +20 -5
- package/esm/shared/useWindowDrag.d.ts +18 -0
- package/esm/shared/useWindowDrag.js +47 -0
- package/package.json +12 -11
- package/esm/FeatureTrack/configSchema.d.ts +0 -124
- package/esm/FeatureTrack/configSchema.js +0 -54
- package/esm/FeatureTrack/index.d.ts +0 -2
- package/esm/FeatureTrack/index.js +0 -13
- package/esm/LinearGenomeView/initKeys.d.ts +0 -12
- package/esm/LinearGenomeView/initKeys.js +0 -68
- package/esm/MultiLevelRubberband/VerticalGuide.d.ts +0 -6
- package/esm/MultiLevelRubberband/VerticalGuide.js +0 -21
- package/esm/lazyPluginExports.d.ts +0 -23
- package/esm/lazyPluginExports.js +0 -36
|
@@ -1,36 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getConf } from '@jbrowse/core/configuration';
|
|
3
|
-
import { BaseViewModel
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { canonicalLocString } from '@jbrowse/core/TextSearch/places';
|
|
2
|
+
import { getConf, readConfObject } from '@jbrowse/core/configuration';
|
|
3
|
+
import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
|
|
4
|
+
import { exportViewSvg } from '@jbrowse/core/svg/exportViewSvg';
|
|
5
|
+
import { highlightListMenuItems } from '@jbrowse/core/ui/menuItems';
|
|
6
|
+
import { assembleLocString, clamp, getBpDisplayStr, getEnv, getNotificationSink, getSession, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, localStorageSetBoolean, localStorageSetItem, springAnimate, sum, } from '@jbrowse/core/util';
|
|
6
7
|
import { bpToLinearBp, bpToPx, computeMoveToLayout, createOverviewLayout, getLayoutHighlightCoords, getOverviewRegionPxSpan, moveTo, pxToBp, } from '@jbrowse/core/util/Base1DUtils';
|
|
8
|
+
import { scaleBandHeights } from '@jbrowse/core/util/bandHeight';
|
|
7
9
|
import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
|
|
8
10
|
import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
|
|
9
11
|
import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
|
|
10
|
-
import {
|
|
12
|
+
import { coerceHighlight } from '@jbrowse/core/util/highlights';
|
|
11
13
|
import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
|
|
12
14
|
import { ElementId } from '@jbrowse/core/util/types/mst';
|
|
13
|
-
import {
|
|
15
|
+
import { highlightsOnAssemblies } from '@jbrowse/core/util/viewHighlights';
|
|
16
|
+
import { assemblyErrorMessage, computeViewStatus, viewLoading, } from '@jbrowse/core/util/viewStatus';
|
|
14
17
|
import { pendingLaunch, withLaunchInput, } from '@jbrowse/core/util/withLaunchInput';
|
|
18
|
+
import { MIN_DISPLAY_HEIGHT } from '@jbrowse/display-kit/const';
|
|
15
19
|
import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
20
|
+
import { VIEW_TRACK_LABEL_MODES } from '@jbrowse/display-kit/types';
|
|
21
|
+
import { cast, getParent, getSnapshot, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
22
|
+
import { observable, when } from 'mobx';
|
|
23
|
+
import { fetchResults, handleSelectedRegion, isOpenInView, navToOption, } from '../searchUtils.js';
|
|
19
24
|
import { doAfterAttach } from './afterAttach.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
25
|
+
import { linearGenomeViewAxisConfigSchema } from './axisConfigSchema.js';
|
|
26
|
+
import { getCytobands, shouldSwapTracks } from './components/util.js';
|
|
27
|
+
import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, MIN_BP_PER_PX, MINIMIZED_TRACK_HEIGHT, MINIMUM_BLOCK_WIDTH, RESIZE_ALL_HANDLE_HEIGHT, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, SHOW_ALL_REGIONS_FILL, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from './consts.js';
|
|
22
28
|
import { planFlight } from './flyTo.js';
|
|
23
29
|
import { setupKeyboardHandler } from './keyboardHandler.js';
|
|
24
30
|
import { lgvLaunchKeys } from './launchKeys.js';
|
|
25
|
-
import { buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
|
|
31
|
+
import { buildHighlightsSubMenuItems, buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
|
|
26
32
|
import { sharedScaleContainerOf } from './sharedScaleContainer.js';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
import { showFiles } from './showFiles.js';
|
|
34
|
+
import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getGridlineTicks, getPaddingSpans, getScalebarLabels, getScalebarRefNameLabels, regionsOrientation, setDisplayedRegionsKeepingCenter, } from './util.js';
|
|
35
|
+
function searchHitsOf(model, results, query, assemblyName, land) {
|
|
36
|
+
const session = getSession(model);
|
|
37
|
+
const assembly = session.assemblyManager.get(assemblyName);
|
|
38
|
+
const ordered = [...results].sort((a, b) => Number(isOpenInView(b, model)) - Number(isOpenInView(a, model)));
|
|
39
|
+
return {
|
|
40
|
+
query,
|
|
41
|
+
assemblyName,
|
|
42
|
+
rows: ordered.map(result => {
|
|
43
|
+
const location = result.getLocation() ?? '';
|
|
44
|
+
const trackId = result.getTrackId();
|
|
45
|
+
return {
|
|
46
|
+
id: result.getId(),
|
|
47
|
+
label: result.getLabel(),
|
|
48
|
+
location: assembly && location
|
|
49
|
+
? canonicalLocString(location, assembly)
|
|
50
|
+
: location,
|
|
51
|
+
trackName: trackId ? (session.getTrackById(trackId)?.name ?? '') : '',
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
pick: async (id) => {
|
|
55
|
+
const picked = ordered.find(result => result.getId() === id);
|
|
56
|
+
try {
|
|
57
|
+
if (picked) {
|
|
58
|
+
await land(picked);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(e);
|
|
63
|
+
if (isAlive(model)) {
|
|
64
|
+
getNotificationSink(model).notifyError(`${e}`, e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
34
70
|
/**
|
|
35
71
|
* Calculate the offsetPx needed to center content within a viewport.
|
|
36
72
|
* Returns a negative offset when content is smaller than viewport (padding on left).
|
|
@@ -39,42 +75,46 @@ function getCenteredOffsetPx(contentPx, viewportPx) {
|
|
|
39
75
|
return Math.round(contentPx / 2 - viewportPx / 2);
|
|
40
76
|
}
|
|
41
77
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
78
|
+
* A refName in the named assembly's canonical spelling, or unchanged when that
|
|
79
|
+
* assembly is missing or unknown, so a highlight authored without one still
|
|
80
|
+
* lands in a single-assembly view.
|
|
45
81
|
*/
|
|
46
|
-
function
|
|
82
|
+
function canonicalRefName(assemblyManager, assemblyName, refName) {
|
|
83
|
+
const asm = assemblyName ? assemblyManager.get(assemblyName) : undefined;
|
|
84
|
+
return asm?.getCanonicalRefName2(refName) ?? refName;
|
|
85
|
+
}
|
|
86
|
+
function canonicalHighlight(self, region) {
|
|
47
87
|
const { assemblyManager } = getSession(self);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
:
|
|
51
|
-
|
|
88
|
+
return {
|
|
89
|
+
...region,
|
|
90
|
+
refName: canonicalRefName(assemblyManager, region.assemblyName, region.refName),
|
|
91
|
+
};
|
|
52
92
|
}
|
|
53
93
|
// bpPerPx deltas smaller than this are treated as no zoom change, avoiding
|
|
54
94
|
// pointless offset re-anchoring on micro-steps
|
|
95
|
+
// a stored mode this build does not know reads as unset, the plugin default
|
|
96
|
+
function storedTrackLabels() {
|
|
97
|
+
const stored = localStorageGetItem('lgv-trackLabels');
|
|
98
|
+
return VIEW_TRACK_LABEL_MODES.find(mode => mode === stored) ?? '';
|
|
99
|
+
}
|
|
55
100
|
const BP_PER_PX_EPSILON = 0.000001;
|
|
56
|
-
// px of
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
101
|
+
// px of blank space a view may scroll past either end of its regions, enough to
|
|
102
|
+
// read as an edge and too little to lose the content
|
|
103
|
+
const OVERSCROLL_PX = 100;
|
|
104
|
+
// The offsets a view can scroll to: the content edge to edge plus a little
|
|
105
|
+
// overscroll. Content narrower than the view collapses the range to the
|
|
106
|
+
// centered offset, which is what `showAllRegions` and `fitAllRegions` ask for.
|
|
107
|
+
function offsetBounds(contentPx, viewportPx) {
|
|
108
|
+
const centered = getCenteredOffsetPx(contentPx, viewportPx);
|
|
109
|
+
return contentPx > viewportPx
|
|
110
|
+
? { min: -OVERSCROLL_PX, max: contentPx - viewportPx + OVERSCROLL_PX }
|
|
111
|
+
: { min: centered, max: centered };
|
|
112
|
+
}
|
|
63
113
|
// whether two BlockSets cover the same blocks, by key and in order
|
|
64
114
|
function sameBlockKeys(a, b) {
|
|
65
115
|
return (a.blocks.length === b.blocks.length &&
|
|
66
116
|
a.blocks.every((block, i) => block.key === b.blocks[i].key));
|
|
67
117
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolve a NavLocation's refName to the assembly's canonical name, falling
|
|
70
|
-
* back to the raw refName (and the view's default assembly) when the assembly
|
|
71
|
-
* is missing or unknown.
|
|
72
|
-
*/
|
|
73
|
-
function navLocationRefName(assemblyManager, defaultAssemblyName, location) {
|
|
74
|
-
return (assemblyManager
|
|
75
|
-
.get(location.assemblyName || defaultAssemblyName)
|
|
76
|
-
?.getCanonicalRefName2(location.refName) || location.refName);
|
|
77
|
-
}
|
|
78
118
|
/**
|
|
79
119
|
* Resolve one end of a navigation range to the displayedRegion index that
|
|
80
120
|
* contains it plus the bp offset into that region. `side` selects which edge of
|
|
@@ -84,7 +124,7 @@ function navLocationRefName(assemblyManager, defaultAssemblyName, location) {
|
|
|
84
124
|
* resolved against those raw coords, and only the region it picks bounds `grow`.
|
|
85
125
|
*/
|
|
86
126
|
function resolveNavEndpoint({ location, assemblyManager, defaultAssemblyName, side, displayedRegions, grow, }) {
|
|
87
|
-
const refName =
|
|
127
|
+
const refName = canonicalRefName(assemblyManager, location.assemblyName || defaultAssemblyName, location.refName);
|
|
88
128
|
const first = displayedRegions.find(r => r.refName === refName);
|
|
89
129
|
if (!first) {
|
|
90
130
|
throw new Error(`could not find a region with refName "${refName}"`);
|
|
@@ -131,21 +171,24 @@ function openTrackSelectorWidget(self) {
|
|
|
131
171
|
throw new Error('session does not support widgets');
|
|
132
172
|
}
|
|
133
173
|
/**
|
|
134
|
-
* Whether the
|
|
174
|
+
* Whether the drawer is showing this view's track selector right now. One
|
|
135
175
|
* spelling for the `isTrackSelectorOpen` getter (which lights up the header
|
|
136
|
-
* button) and for `toggleTrackSelector` (which decides hide-vs-show)
|
|
137
|
-
*
|
|
138
|
-
*
|
|
176
|
+
* button) and for `toggleTrackSelector` (which decides hide-vs-show). The
|
|
177
|
+
* widget id is shared by every view, so the selector's `view` is what says
|
|
178
|
+
* whose it is.
|
|
139
179
|
*/
|
|
140
180
|
function isTrackSelectorVisible(self) {
|
|
141
181
|
const session = getSession(self);
|
|
142
182
|
return (isSessionModelWithWidgets(session) &&
|
|
143
183
|
session.visibleWidget?.id === TRACK_SELECTOR_WIDGET_ID &&
|
|
184
|
+
session.visibleWidget.view?.id === self.id &&
|
|
144
185
|
!session.minimized);
|
|
145
186
|
}
|
|
146
187
|
/**
|
|
147
188
|
* #stateModel LinearGenomeView
|
|
148
189
|
* #category view
|
|
190
|
+
* Stacks tracks under a horizontal axis of one or more genomic regions laid
|
|
191
|
+
* end to end, which the user scrolls, zooms and searches.
|
|
149
192
|
*
|
|
150
193
|
* #example
|
|
151
194
|
* A `LinearGenomeView` is what you hand-author under `defaultSession.views`, and
|
|
@@ -157,7 +200,7 @@ function isTrackSelectorVisible(self) {
|
|
|
157
200
|
* views: [
|
|
158
201
|
* {
|
|
159
202
|
* type: 'LinearGenomeView',
|
|
160
|
-
*
|
|
203
|
+
* colorByFrame: true,
|
|
161
204
|
* assembly: 'hg38',
|
|
162
205
|
* loc: 'chr1:1,000,000-1,100,000',
|
|
163
206
|
* tracks: ['genes', 'alignments'],
|
|
@@ -167,9 +210,8 @@ function isTrackSelectorVisible(self) {
|
|
|
167
210
|
* ```
|
|
168
211
|
* `assembly`, `loc`, `tracks`, `tracklist`, `nav` and `highlight` need
|
|
169
212
|
* on-attach resolution and are captured into the `launch` property below;
|
|
170
|
-
* `
|
|
171
|
-
*
|
|
172
|
-
* written the same way.
|
|
213
|
+
* `colorByFrame`, `showAminoAcids`, `showCenterLine` and `trackLabels` are plain
|
|
214
|
+
* view props MST restores natively. Both are written the same way.
|
|
173
215
|
* At runtime the same model is driven imperatively — every property and action
|
|
174
216
|
* below is reachable on `viewState.session.views[0]`:
|
|
175
217
|
* ```js
|
|
@@ -181,7 +223,7 @@ function isTrackSelectorVisible(self) {
|
|
|
181
223
|
*/
|
|
182
224
|
export function stateModelFactory(pluginManager) {
|
|
183
225
|
const model = types
|
|
184
|
-
.compose('LinearGenomeView', BaseViewModel,
|
|
226
|
+
.compose('LinearGenomeView', BaseViewModel, types.model({
|
|
185
227
|
/**
|
|
186
228
|
* #property
|
|
187
229
|
*/
|
|
@@ -196,17 +238,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
196
238
|
/**
|
|
197
239
|
* #property
|
|
198
240
|
* Left edge of the viewport, in linearized bp — the concatenated
|
|
199
|
-
* `displayedRegions` space that `offsetPx` indexes, which
|
|
241
|
+
* `displayedRegions` space that `offsetPx` indexes, which has no
|
|
200
242
|
* inter-region padding, so the two differ only by `bpPerPx`. May be
|
|
201
|
-
* negative
|
|
243
|
+
* negative when the view is scrolled past the left end.
|
|
202
244
|
*
|
|
203
|
-
* The
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* at 500px
|
|
208
|
-
*
|
|
209
|
-
* view disagreed, and only the one that stores intent was right.
|
|
245
|
+
* The view stores the genomic window it frames rather than the pixels
|
|
246
|
+
* that framed it, because pixels mean nothing without the width they
|
|
247
|
+
* were measured at and a snapshot does not include one. A session
|
|
248
|
+
* authored in a 1000px window therefore shows the same region when
|
|
249
|
+
* opened at 500px, as the same location given as a `&loc=` does: both
|
|
250
|
+
* ways to share a view store intent.
|
|
210
251
|
*/
|
|
211
252
|
windowStartBp: types.stripDefault(types.number, 0),
|
|
212
253
|
/**
|
|
@@ -219,14 +260,15 @@ export function stateModelFactory(pluginManager) {
|
|
|
219
260
|
windowWidthBp: types.stripDefault(types.number, 0),
|
|
220
261
|
/**
|
|
221
262
|
* #property
|
|
222
|
-
*
|
|
223
|
-
*
|
|
263
|
+
* Used for migration only, and permanent: share links written before the
|
|
264
|
+
* window was stored contain `bpPerPx`/`offsetPx`, and there are many of
|
|
265
|
+
* them.
|
|
224
266
|
*
|
|
225
|
-
* A snapshot written before the window was stored
|
|
267
|
+
* A snapshot written before the window was stored has `offsetPx` and
|
|
226
268
|
* `bpPerPx` but not the width they were measured at, so the window they
|
|
227
269
|
* framed cannot be recovered. `windowStartBp` can (it is
|
|
228
270
|
* `offsetPx * bpPerPx`, no width needed); the width in bp cannot. This
|
|
229
|
-
*
|
|
271
|
+
* property holds the old `bpPerPx` until the first measure, which adopts it at
|
|
230
272
|
* whatever width arrives — exactly what the old code did — and clears
|
|
231
273
|
* this. So an old link keeps its old behavior rather than being
|
|
232
274
|
* reinterpreted, and everything authored since restores its window.
|
|
@@ -235,9 +277,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
235
277
|
/**
|
|
236
278
|
* #property
|
|
237
279
|
* currently displayed regions, can be a single chromosome, arbitrary
|
|
238
|
-
* subsections, or the entire
|
|
239
|
-
*
|
|
240
|
-
* fragmented
|
|
280
|
+
* subsections, or the entire set of chromosomes in the genome. The entire
|
|
281
|
+
* set is not advised if your assembly is very fragmented
|
|
241
282
|
*/
|
|
242
283
|
displayedRegions: types.stripDefault(types.frozen(), []),
|
|
243
284
|
/**
|
|
@@ -264,14 +305,17 @@ export function stateModelFactory(pluginManager) {
|
|
|
264
305
|
hideNoTracksActive: types.stripDefault(types.boolean, false),
|
|
265
306
|
/**
|
|
266
307
|
* #property
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
308
|
+
* ignored: the hierarchical selector is the only one. Declared because
|
|
309
|
+
* saved sessions and share links carry it.
|
|
310
|
+
* @deprecated
|
|
270
311
|
*/
|
|
271
|
-
trackSelectorType: types.stripDefault(types.
|
|
312
|
+
trackSelectorType: types.stripDefault(types.string, 'hierarchical'),
|
|
272
313
|
/**
|
|
273
314
|
* #property
|
|
274
|
-
* show the "center line"
|
|
315
|
+
* show the "center line". A personal preference: never in a session
|
|
316
|
+
* snapshot. It, showCytobands, trackLabels, colorByFrame, showAminoAcids
|
|
317
|
+
* and showTrackOutlines start from this browser's last choice, which
|
|
318
|
+
* their setters record.
|
|
275
319
|
*/
|
|
276
320
|
showCenterLine: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCenterLine', false)),
|
|
277
321
|
/**
|
|
@@ -284,34 +328,30 @@ export function stateModelFactory(pluginManager) {
|
|
|
284
328
|
/**
|
|
285
329
|
* #property
|
|
286
330
|
* how to display the track labels, can be "overlapping", "offset", or
|
|
287
|
-
* "hidden", or empty string "" (which
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* https://jbrowse.org/jb2/docs/config/
|
|
291
|
-
* how
|
|
292
|
-
*/
|
|
293
|
-
trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') ?? ''),
|
|
294
|
-
/**
|
|
295
|
-
* #property
|
|
296
|
-
* show the "gridlines" in the track area
|
|
331
|
+
* "hidden", or empty string "" (which uses the LinearGenomeViewPlugin
|
|
332
|
+
* config default). The resolved value is the `effectiveTrackLabels`
|
|
333
|
+
* getter. See the LinearGenomeViewPlugin docs at
|
|
334
|
+
* https://jbrowse.org/jb2/docs/config/lineargenomeviewconfigschema/ for
|
|
335
|
+
* how the config is used
|
|
297
336
|
*/
|
|
298
|
-
|
|
337
|
+
trackLabels: types.optional(types.enumeration('ViewTrackLabels', ['', ...VIEW_TRACK_LABEL_MODES]), () => storedTrackLabels()),
|
|
299
338
|
/**
|
|
300
339
|
* #property
|
|
301
|
-
*
|
|
340
|
+
* The ruler's guide, a [](/docs/config/lineargenomeviewaxis) object:
|
|
341
|
+
* `{ grid: false }` stops its ticks running down through the tracks.
|
|
302
342
|
*/
|
|
303
|
-
|
|
343
|
+
axis: types.stripDefault(linearGenomeViewAxisConfigSchema, {}),
|
|
304
344
|
/**
|
|
305
345
|
* #property
|
|
306
346
|
* color CDS segments by reading frame
|
|
307
347
|
*/
|
|
308
|
-
|
|
348
|
+
colorByFrame: types.optional(types.boolean, () => localStorageGetBoolean('lgv-colorByFrame', false)),
|
|
309
349
|
/**
|
|
310
350
|
* #property
|
|
311
351
|
* draw translated codons on coding features once zoomed in far enough:
|
|
312
352
|
* an alternating per-codon shading, and the amino acid letters on top of
|
|
313
|
-
* it at base-level zoom. Independent of `
|
|
314
|
-
* the segments by frame.
|
|
353
|
+
* it at base-level zoom. Independent of `colorByFrame`, which only
|
|
354
|
+
* recolors the segments by frame.
|
|
315
355
|
*/
|
|
316
356
|
showAminoAcids: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showAminoAcids', true)),
|
|
317
357
|
/**
|
|
@@ -335,36 +375,49 @@ export function stateModelFactory(pluginManager) {
|
|
|
335
375
|
*/
|
|
336
376
|
launch: types.frozen(),
|
|
337
377
|
}))
|
|
338
|
-
.volatile(
|
|
378
|
+
.volatile(() => {
|
|
339
379
|
// typed locals so `unknown`/`Record` aren't narrowed to `undefined`/`{}`; inline
|
|
340
380
|
// type assertions here get stripped by no-unnecessary-type-assertion
|
|
341
381
|
const volatileError = undefined;
|
|
342
|
-
const trackRefs = {};
|
|
343
382
|
return {
|
|
344
383
|
/**
|
|
345
384
|
* #volatile
|
|
385
|
+
* Height of each track's in-flow label band, measured by
|
|
386
|
+
* TrackContainer as the rendering container's offset inside its Paper.
|
|
387
|
+
* Absent (0) while the label is hidden, overlapping, or not yet
|
|
388
|
+
* measured. Observable, so the offset and height getters re-derive
|
|
389
|
+
* when a band changes.
|
|
346
390
|
*/
|
|
347
|
-
|
|
391
|
+
trackLabelBands: observable.map(),
|
|
348
392
|
/**
|
|
349
393
|
* #volatile
|
|
394
|
+
* How much taller the view's box is than the scroll port it sits in,
|
|
395
|
+
* in px, negative when it is shorter. Measured by the view component
|
|
396
|
+
* for a top-level view; undefined until then, and for a nested one.
|
|
350
397
|
*/
|
|
351
|
-
|
|
398
|
+
scrollPortExcess: undefined,
|
|
399
|
+
/**
|
|
400
|
+
* #volatile
|
|
401
|
+
*/
|
|
402
|
+
volatileWidth: undefined,
|
|
352
403
|
/**
|
|
353
404
|
* #volatile
|
|
354
405
|
*/
|
|
355
406
|
draggingTrackId: undefined,
|
|
356
407
|
/**
|
|
357
408
|
* #volatile
|
|
409
|
+
* Set by a search whose hits name more than one place, until the user
|
|
410
|
+
* picks one or closes it.
|
|
358
411
|
*/
|
|
359
|
-
|
|
412
|
+
searchPicker: undefined,
|
|
360
413
|
/**
|
|
361
414
|
* #volatile
|
|
362
415
|
*/
|
|
363
|
-
|
|
416
|
+
lastTrackDragY: undefined,
|
|
364
417
|
/**
|
|
365
418
|
* #volatile
|
|
366
419
|
*/
|
|
367
|
-
|
|
420
|
+
volatileError,
|
|
368
421
|
/**
|
|
369
422
|
* #volatile
|
|
370
423
|
*/
|
|
@@ -376,30 +429,29 @@ export function stateModelFactory(pluginManager) {
|
|
|
376
429
|
/**
|
|
377
430
|
* #volatile
|
|
378
431
|
*/
|
|
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,
|
|
432
|
+
// A real scale rather than the not-yet-measured 0, for the canvas
|
|
433
|
+
// displays that lay out against it; `setWidth` seeds the true one at
|
|
434
|
+
// the first measure.
|
|
435
|
+
coarseBpPerPx: 1,
|
|
387
436
|
/**
|
|
388
437
|
* #volatile
|
|
438
|
+
* the scroll offset the coarse blocks were laid out at, in px at
|
|
439
|
+
* `coarseBpPerPx`: the settled viewport's left edge, which the first
|
|
440
|
+
* coarse block is not while the view is scrolled before the first region
|
|
389
441
|
*/
|
|
390
|
-
|
|
442
|
+
coarseOffsetPx: 0,
|
|
391
443
|
/**
|
|
392
444
|
* #volatile
|
|
393
445
|
*/
|
|
394
|
-
|
|
446
|
+
leftOffset: undefined,
|
|
395
447
|
/**
|
|
396
448
|
* #volatile
|
|
397
449
|
*/
|
|
398
|
-
|
|
450
|
+
rightOffset: undefined,
|
|
399
451
|
/**
|
|
400
452
|
* #volatile
|
|
401
453
|
*/
|
|
402
|
-
|
|
454
|
+
isScalebarRefNameMenuOpen: false,
|
|
403
455
|
/**
|
|
404
456
|
* #volatile
|
|
405
457
|
* temporary vertical guides that can be set by displays (e.g., LD display hover)
|
|
@@ -408,6 +460,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
408
460
|
};
|
|
409
461
|
})
|
|
410
462
|
.views(self => ({
|
|
463
|
+
/**
|
|
464
|
+
* #getter
|
|
465
|
+
* `axis.grid`: whether the ruler's ticks run down through the tracks.
|
|
466
|
+
*/
|
|
467
|
+
get axisGrid() {
|
|
468
|
+
return readConfObject(self.axis, 'grid');
|
|
469
|
+
},
|
|
411
470
|
// The viewport in pixels, derived from the window it frames — declared
|
|
412
471
|
// ahead of every other getter so that the ~300 reads of `self.bpPerPx` /
|
|
413
472
|
// `self.offsetPx` across the codebase go on resolving by name. Only the
|
|
@@ -444,6 +503,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
444
503
|
const { bpPerPx } = this;
|
|
445
504
|
return bpPerPx <= 0 ? 0 : self.windowStartBp / bpPerPx;
|
|
446
505
|
},
|
|
506
|
+
/**
|
|
507
|
+
* #getter
|
|
508
|
+
* a displayed region narrower than this many pixels draws as an elided
|
|
509
|
+
* block
|
|
510
|
+
*/
|
|
511
|
+
get minimumBlockWidth() {
|
|
512
|
+
return MINIMUM_BLOCK_WIDTH;
|
|
513
|
+
},
|
|
447
514
|
/**
|
|
448
515
|
* #getter
|
|
449
516
|
* scroll-to-zoom is a global, personal preference resolved from the
|
|
@@ -505,6 +572,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
505
572
|
const { assemblyManager } = getSession(self);
|
|
506
573
|
return this.assemblyNames.map(a => assemblyManager.getDisplayName(a));
|
|
507
574
|
},
|
|
575
|
+
/**
|
|
576
|
+
* #getter
|
|
577
|
+
* the session's highlights on this view's assemblies
|
|
578
|
+
*/
|
|
579
|
+
get highlights() {
|
|
580
|
+
const { highlights, assemblyManager } = getSession(self);
|
|
581
|
+
return highlightsOnAssemblies(highlights, this.assemblyNames, assemblyManager);
|
|
582
|
+
},
|
|
508
583
|
/**
|
|
509
584
|
* #getter
|
|
510
585
|
* checking if lgv is a 'top-level' view is used for toggling pin track
|
|
@@ -531,8 +606,23 @@ export function stateModelFactory(pluginManager) {
|
|
|
531
606
|
.views(self => ({
|
|
532
607
|
/**
|
|
533
608
|
* #getter
|
|
534
|
-
*
|
|
535
|
-
*
|
|
609
|
+
* The census entry for this view: the tracks it holds itself. Declared
|
|
610
|
+
* rather than derived — see `BaseViewModel.ownTracks`.
|
|
611
|
+
*/
|
|
612
|
+
get ownTracks() {
|
|
613
|
+
return [...self.tracks];
|
|
614
|
+
},
|
|
615
|
+
/**
|
|
616
|
+
* #getter
|
|
617
|
+
* See `BaseViewModel.rendersDisplays`.
|
|
618
|
+
*/
|
|
619
|
+
get rendersDisplays() {
|
|
620
|
+
return !self.scalebarOnly;
|
|
621
|
+
},
|
|
622
|
+
/**
|
|
623
|
+
* #getter
|
|
624
|
+
* Assembly name captioning the scalebar ahead of its refName labels, or
|
|
625
|
+
* undefined for none. A container view (e.g. LinearSyntenyView) opts its sub-views in by
|
|
536
626
|
* exposing showAssemblyNameInSubviewScalebar; duck-typed rather than
|
|
537
627
|
* matching a concrete view type so no upward plugin dependency is needed
|
|
538
628
|
* and any container can opt in. A wrong nesting depth simply yields no
|
|
@@ -579,10 +669,23 @@ export function stateModelFactory(pluginManager) {
|
|
|
579
669
|
* and error checks resolve it directly through here.
|
|
580
670
|
*/
|
|
581
671
|
get initAssembly() {
|
|
582
|
-
|
|
583
|
-
|
|
672
|
+
const name = this.launchAssemblyName;
|
|
673
|
+
return self.pendingLaunch && name !== undefined
|
|
674
|
+
? getSession(self).assemblyManager.get(name)
|
|
584
675
|
: undefined;
|
|
585
676
|
},
|
|
677
|
+
/**
|
|
678
|
+
* #getter
|
|
679
|
+
* The assembly a pending launch is for: the one it names, else the one
|
|
680
|
+
* the view already shows. A re-launch of a materialized view (a
|
|
681
|
+
* `jb.setSession` document writing `loc` beside the built state) need
|
|
682
|
+
* not restate it; without the default the readiness gate waited on
|
|
683
|
+
* `assemblyManager.get(undefined)` for good. Undefined on a fresh view
|
|
684
|
+
* whose blob names none, which `error` reports.
|
|
685
|
+
*/
|
|
686
|
+
get launchAssemblyName() {
|
|
687
|
+
return self.pendingLaunch?.assembly ?? self.assemblyNames[0];
|
|
688
|
+
},
|
|
586
689
|
/**
|
|
587
690
|
* #getter
|
|
588
691
|
*/
|
|
@@ -616,35 +719,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
616
719
|
},
|
|
617
720
|
/**
|
|
618
721
|
* #getter
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
* "Loading" for the part of startup that actually takes time. Falls back
|
|
622
|
-
* once the assembly is loaded and the remaining wait is the init autorun's
|
|
623
|
-
* own navigation, which is local.
|
|
624
|
-
*/
|
|
625
|
-
get loadingMessage() {
|
|
626
|
-
return this.showLoading
|
|
627
|
-
? this.loadingAssembly?.statusMessage || 'Loading'
|
|
628
|
-
: undefined;
|
|
629
|
-
},
|
|
630
|
-
/**
|
|
631
|
-
* #getter
|
|
632
|
-
* Determinate fraction for the spinner's bar, when the assembly load
|
|
633
|
-
* reports one (a whole-file download with a Content-Length)
|
|
722
|
+
* The loading screen's text while `showLoading`, taken from the
|
|
723
|
+
* assembly being waited on; undefined otherwise.
|
|
634
724
|
*/
|
|
635
|
-
get
|
|
636
|
-
return this.showLoading
|
|
637
|
-
? this.loadingAssembly?.statusProgress
|
|
638
|
-
: undefined;
|
|
639
|
-
},
|
|
640
|
-
/**
|
|
641
|
-
* #getter
|
|
642
|
-
* The URL the assembly load is currently fetching, when the phase named
|
|
643
|
-
* one. Only the stalled-load notice reads it — see
|
|
644
|
-
* {@link ViewLoadingScreen}.
|
|
645
|
-
*/
|
|
646
|
-
get loadingSource() {
|
|
647
|
-
return this.showLoading ? this.loadingAssembly?.statusSource : undefined;
|
|
725
|
+
get loading() {
|
|
726
|
+
return viewLoading(this.showLoading, () => this.loadingAssembly);
|
|
648
727
|
},
|
|
649
728
|
/**
|
|
650
729
|
* #getter
|
|
@@ -695,12 +774,12 @@ export function stateModelFactory(pluginManager) {
|
|
|
695
774
|
* `width`/`staticBlocks` throw by design before the view has been
|
|
696
775
|
* measured and navigated.
|
|
697
776
|
*
|
|
698
|
-
*
|
|
777
|
+
* Use this getter rather than `initialized`.
|
|
699
778
|
* That one answers "have the assembly's regions loaded", which is only
|
|
700
779
|
* the first of two async steps: navigating then populates
|
|
701
780
|
* `displayedRegions`, and in the window between the two `initialized` is
|
|
702
781
|
* already true while there is still nothing on screen. `showLoading`
|
|
703
|
-
* folds in `awaitingInitNavigation`, the getter
|
|
782
|
+
* folds in `awaitingInitNavigation`, the getter for exactly
|
|
704
783
|
* that gap.
|
|
705
784
|
*
|
|
706
785
|
* `error` is the third outcome and is why this is not a bare
|
|
@@ -723,12 +802,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
723
802
|
return computeViewStatus({
|
|
724
803
|
error: this.error,
|
|
725
804
|
hasSomethingToShow: this.hasSomethingToShow,
|
|
726
|
-
loading: () => this.
|
|
727
|
-
? {
|
|
728
|
-
message: this.loadingMessage ?? 'Loading',
|
|
729
|
-
progress: this.loadingProgress,
|
|
730
|
-
}
|
|
731
|
-
: undefined,
|
|
805
|
+
loading: () => this.loading,
|
|
732
806
|
});
|
|
733
807
|
},
|
|
734
808
|
/**
|
|
@@ -758,14 +832,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
758
832
|
* #getter
|
|
759
833
|
* A track's full cost beyond its display height.
|
|
760
834
|
*
|
|
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.
|
|
835
|
+
* The track *label* is not counted here: an offset label is an in-flow
|
|
836
|
+
* box whose height is whatever the theme renders a Paper of icon buttons
|
|
837
|
+
* at, so it is measured into `trackLabelBands` per track and added by
|
|
838
|
+
* `trackLabelBand` rather than derived.
|
|
769
839
|
*/
|
|
770
840
|
get trackChromeHeight() {
|
|
771
841
|
return this.trackLeadingChrome + this.trackTrailingChrome;
|
|
@@ -784,28 +854,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
784
854
|
return HEADER_BAR_HEIGHT + HEADER_OVERVIEW_HEIGHT;
|
|
785
855
|
}
|
|
786
856
|
},
|
|
787
|
-
/**
|
|
788
|
-
* #getter
|
|
789
|
-
* Where the scalebar pins when the view's chrome is sticky: everything
|
|
790
|
-
* stacked above it, which is the view's own title bar plus this view's
|
|
791
|
-
* header. Expressed from `headerHeight` rather than re-summing its
|
|
792
|
-
* constants — the two are the same box, and a second spelling of the sum
|
|
793
|
-
* is free to drift from the first while both typecheck.
|
|
794
|
-
*/
|
|
795
|
-
get rubberbandTop() {
|
|
796
|
-
return self.stickyViewHeaders
|
|
797
|
-
? VIEW_HEADER_HEIGHT + this.headerHeight
|
|
798
|
-
: 0;
|
|
799
|
-
},
|
|
800
|
-
/**
|
|
801
|
-
* #getter
|
|
802
|
-
*/
|
|
803
|
-
get pinnedTracksTop() {
|
|
804
|
-
return this.rubberbandTop + this.scalebarHeight;
|
|
805
|
-
},
|
|
806
857
|
/**
|
|
807
858
|
* #method
|
|
808
|
-
* rendered height of a single track,
|
|
859
|
+
* rendered height of a single track, using a fixed height when
|
|
809
860
|
* minimized. Shared by trackHeights and getTrackYOffset so the two can't
|
|
810
861
|
* disagree. Reads `activeDisplay` — the display TrackContainer actually
|
|
811
862
|
* mounts — rather than re-picking `displays[0]`, so the view's height math
|
|
@@ -816,6 +867,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
816
867
|
? MINIMIZED_TRACK_HEIGHT
|
|
817
868
|
: track.activeDisplay.height;
|
|
818
869
|
},
|
|
870
|
+
/**
|
|
871
|
+
* #method
|
|
872
|
+
* the measured in-flow label band above a track's rendering container,
|
|
873
|
+
* 0 until TrackContainer has measured one or when no label is in flow
|
|
874
|
+
*/
|
|
875
|
+
trackLabelBand(track) {
|
|
876
|
+
return self.trackLabelBands.get(track.configuration.trackId) ?? 0;
|
|
877
|
+
},
|
|
819
878
|
/**
|
|
820
879
|
* #getter
|
|
821
880
|
*/
|
|
@@ -826,7 +885,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
826
885
|
* #getter
|
|
827
886
|
*/
|
|
828
887
|
get trackHeightsWithChrome() {
|
|
829
|
-
return this.trackHeights +
|
|
888
|
+
return (this.trackHeights +
|
|
889
|
+
self.tracks.length * this.trackChromeHeight +
|
|
890
|
+
sum(self.tracks.map(t => this.trackLabelBand(t))));
|
|
830
891
|
},
|
|
831
892
|
/**
|
|
832
893
|
* #getter
|
|
@@ -835,7 +896,22 @@ export function stateModelFactory(pluginManager) {
|
|
|
835
896
|
if (self.scalebarOnly) {
|
|
836
897
|
return this.headerHeight + this.scalebarHeight;
|
|
837
898
|
}
|
|
838
|
-
return (this.trackHeightsWithChrome +
|
|
899
|
+
return (this.trackHeightsWithChrome +
|
|
900
|
+
this.headerHeight +
|
|
901
|
+
this.scalebarHeight +
|
|
902
|
+
(self.tracks.length && self.isTopLevelView
|
|
903
|
+
? RESIZE_ALL_HANDLE_HEIGHT
|
|
904
|
+
: 0));
|
|
905
|
+
},
|
|
906
|
+
/**
|
|
907
|
+
* #getter
|
|
908
|
+
* The display height of each track a whole-stack resize moves, by track
|
|
909
|
+
* instance id: every shown track that is not minimized.
|
|
910
|
+
*/
|
|
911
|
+
get resizableTrackHeights() {
|
|
912
|
+
return new Map(self.tracks
|
|
913
|
+
.filter(t => !t.minimized)
|
|
914
|
+
.map(t => [t.id, t.activeDisplay.height]));
|
|
839
915
|
},
|
|
840
916
|
/**
|
|
841
917
|
* #method
|
|
@@ -844,20 +920,27 @@ export function stateModelFactory(pluginManager) {
|
|
|
844
920
|
* first, then unpinned), from the same constants TrackContainer lays its
|
|
845
921
|
* Paper out with. Returns `undefined` if the track is not present.
|
|
846
922
|
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
923
|
+
* Includes each track's measured label band, this track's own included,
|
|
924
|
+
* since an in-flow label sits above the rendering container inside the
|
|
925
|
+
* same Paper. Exact once TrackContainer has measured; before that it is
|
|
926
|
+
* short by the unmeasured bands.
|
|
851
927
|
*/
|
|
852
928
|
getTrackYOffset(trackId) {
|
|
929
|
+
return this.trackYOffsets.get(trackId);
|
|
930
|
+
},
|
|
931
|
+
/**
|
|
932
|
+
* #getter
|
|
933
|
+
* `getTrackYOffset` for every shown track, by trackId, from one walk
|
|
934
|
+
*/
|
|
935
|
+
get trackYOffsets() {
|
|
936
|
+
const offsets = new Map();
|
|
853
937
|
let y = this.headerHeight + this.scalebarHeight + this.trackLeadingChrome;
|
|
854
938
|
for (const t of [...self.pinnedTracks, ...self.unpinnedTracks]) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
939
|
+
y += this.trackLabelBand(t);
|
|
940
|
+
offsets.set(t.configuration.trackId, y);
|
|
858
941
|
y += this.trackHeight(t) + this.trackChromeHeight;
|
|
859
942
|
}
|
|
860
|
-
return
|
|
943
|
+
return offsets;
|
|
861
944
|
},
|
|
862
945
|
/**
|
|
863
946
|
* #method
|
|
@@ -883,24 +966,22 @@ export function stateModelFactory(pluginManager) {
|
|
|
883
966
|
if (this.totalBp === 0 || self.width === 0) {
|
|
884
967
|
return 1;
|
|
885
968
|
}
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
// and the clamp() range in zoomTo.
|
|
890
|
-
return Math.max(MIN_BP_PER_PX, this.totalBp / (self.width * SHOW_ALL_REGIONS_FILL));
|
|
969
|
+
// floored so a tiny region set cannot put maxBpPerPx under
|
|
970
|
+
// minBpPerPx, which would invert the zoom slider and zoomTo's clamp
|
|
971
|
+
return Math.max(this.minBpPerPx, this.totalBp / (self.width * SHOW_ALL_REGIONS_FILL));
|
|
891
972
|
},
|
|
892
973
|
/**
|
|
893
974
|
* #getter
|
|
894
|
-
* The zoom-out limit.
|
|
895
|
-
* it on a shared scale
|
|
896
|
-
* one, drawn short so the two compare by length.
|
|
975
|
+
* The zoom-out limit. Equal to the fit for this view's regions, except
|
|
976
|
+
* while a container holds it on a coarser shared scale, such as a small
|
|
977
|
+
* genome next to a large one, drawn short so the two compare by length.
|
|
897
978
|
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
979
|
+
* The shared scale raises the limit. A bpPerPx written past the limit
|
|
980
|
+
* would be undone by the first operation that clamps (a wheel tick, a
|
|
981
|
+
* rubberband, a `setDisplayedRegions`), as with the dotplot's locked
|
|
982
|
+
* aspect ratio turning "zoom out" into a zoom in (`axisMaxBpPerPx`).
|
|
983
|
+
* Every zoom clamps against the same ceiling, so full zoom-out stops at
|
|
984
|
+
* the shared scale.
|
|
904
985
|
*/
|
|
905
986
|
get maxBpPerPx() {
|
|
906
987
|
return Math.max(this.fitBpPerPx, this.sharedFitBpPerPx);
|
|
@@ -911,8 +992,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
911
992
|
* or 0 for a standalone view, a mode that is off, and a stack that cannot
|
|
912
993
|
* answer yet.
|
|
913
994
|
*
|
|
914
|
-
*
|
|
915
|
-
* function of every row's fit, all of which
|
|
995
|
+
* A getter rather than a volatile the container writes: the number is a
|
|
996
|
+
* function of every row's fit, all of which change on a resize, and a
|
|
916
997
|
* stored copy would be stale by the next layout.
|
|
917
998
|
*/
|
|
918
999
|
get sharedFitBpPerPx() {
|
|
@@ -944,12 +1025,12 @@ export function stateModelFactory(pluginManager) {
|
|
|
944
1025
|
// hand-authored JSON is what fills it, so the type is not a guarantee.
|
|
945
1026
|
// Naming the authoring mistake beats the downstream symptom, which is
|
|
946
1027
|
// the literal string "Assembly undefined not found".
|
|
947
|
-
if (!
|
|
1028
|
+
if (!this.launchAssemblyName) {
|
|
948
1029
|
return 'LinearGenomeView init needs an "assembly"';
|
|
949
1030
|
}
|
|
950
1031
|
const asm = this.initAssembly;
|
|
951
1032
|
if (!asm) {
|
|
952
|
-
return `Assembly ${
|
|
1033
|
+
return `Assembly ${this.launchAssemblyName} not found`;
|
|
953
1034
|
}
|
|
954
1035
|
if (asm.error) {
|
|
955
1036
|
return asm.error;
|
|
@@ -961,15 +1042,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
961
1042
|
* #getter
|
|
962
1043
|
*/
|
|
963
1044
|
get maxOffset() {
|
|
964
|
-
|
|
965
|
-
return this.displayedRegionsTotalPx - MAX_OFFSET_PADDING_PX;
|
|
1045
|
+
return offsetBounds(this.displayedRegionsTotalPx, self.width).max;
|
|
966
1046
|
},
|
|
967
1047
|
/**
|
|
968
1048
|
* #getter
|
|
969
1049
|
*/
|
|
970
1050
|
get minOffset() {
|
|
971
|
-
|
|
972
|
-
return -self.width + MIN_OFFSET_PADDING_PX;
|
|
1051
|
+
return offsetBounds(this.displayedRegionsTotalPx, self.width).min;
|
|
973
1052
|
},
|
|
974
1053
|
/**
|
|
975
1054
|
* #getter
|
|
@@ -1014,6 +1093,15 @@ export function stateModelFactory(pluginManager) {
|
|
|
1014
1093
|
getActiveDisplayId(trackId) {
|
|
1015
1094
|
return this.getTrack(trackId)?.activeDisplay.configuration.displayId;
|
|
1016
1095
|
},
|
|
1096
|
+
}))
|
|
1097
|
+
.actions(self => ({
|
|
1098
|
+
/**
|
|
1099
|
+
* #action
|
|
1100
|
+
* Dismiss the search picker without choosing a place.
|
|
1101
|
+
*/
|
|
1102
|
+
closeSearchPicker() {
|
|
1103
|
+
self.searchPicker = undefined;
|
|
1104
|
+
},
|
|
1017
1105
|
}))
|
|
1018
1106
|
.actions(self => ({
|
|
1019
1107
|
/**
|
|
@@ -1021,6 +1109,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1021
1109
|
*/
|
|
1022
1110
|
setShowTrackOutlines(arg) {
|
|
1023
1111
|
self.showTrackOutlines = arg;
|
|
1112
|
+
localStorageSetBoolean('lgv-showTrackOutlines', arg);
|
|
1024
1113
|
},
|
|
1025
1114
|
/**
|
|
1026
1115
|
* #action
|
|
@@ -1031,38 +1120,69 @@ export function stateModelFactory(pluginManager) {
|
|
|
1031
1120
|
/**
|
|
1032
1121
|
* #action
|
|
1033
1122
|
*/
|
|
1034
|
-
|
|
1035
|
-
self.
|
|
1123
|
+
setColorByFrame(flag) {
|
|
1124
|
+
self.colorByFrame = flag;
|
|
1125
|
+
localStorageSetBoolean('lgv-colorByFrame', flag);
|
|
1036
1126
|
},
|
|
1037
1127
|
/**
|
|
1038
1128
|
* #action
|
|
1039
1129
|
*/
|
|
1040
1130
|
setShowAminoAcids(flag) {
|
|
1041
1131
|
self.showAminoAcids = flag;
|
|
1132
|
+
localStorageSetBoolean('lgv-showAminoAcids', flag);
|
|
1042
1133
|
},
|
|
1043
1134
|
/**
|
|
1044
1135
|
* #action
|
|
1045
1136
|
*/
|
|
1046
1137
|
setShowCytobands(flag) {
|
|
1047
1138
|
self.showCytobands = flag;
|
|
1139
|
+
localStorageSetBoolean('lgv-showCytobands', flag);
|
|
1140
|
+
},
|
|
1141
|
+
/**
|
|
1142
|
+
* #action
|
|
1143
|
+
*/
|
|
1144
|
+
setTrackLabelBand(trackId, band) {
|
|
1145
|
+
if (band === 0) {
|
|
1146
|
+
self.trackLabelBands.delete(trackId);
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
self.trackLabelBands.set(trackId, band);
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
/**
|
|
1153
|
+
* #action
|
|
1154
|
+
*/
|
|
1155
|
+
setScrollPortExcess(px) {
|
|
1156
|
+
self.scrollPortExcess = px;
|
|
1157
|
+
},
|
|
1158
|
+
/**
|
|
1159
|
+
* #action
|
|
1160
|
+
* Grow or shrink every unminimized track by `distance` px in all, each
|
|
1161
|
+
* by its share of the height it has above the display minimum. `from` is
|
|
1162
|
+
* the heights to scale, by track instance id; a drag passes the heights
|
|
1163
|
+
* it started with, so whole-pixel rounding does not compound frame by
|
|
1164
|
+
* frame. Returns how far the stack moved.
|
|
1165
|
+
*/
|
|
1166
|
+
resizeTracks(distance, from = self.resizableTrackHeights) {
|
|
1167
|
+
const tracks = self.tracks.filter(t => !t.minimized && from.has(t.id));
|
|
1168
|
+
const start = tracks.map(t => from.get(t.id));
|
|
1169
|
+
const next = scaleBandHeights(start, distance, MIN_DISPLAY_HEIGHT);
|
|
1170
|
+
for (const [i, track] of tracks.entries()) {
|
|
1171
|
+
const display = track.activeDisplay;
|
|
1172
|
+
const delta = next[i] - display.height;
|
|
1173
|
+
if (delta !== 0) {
|
|
1174
|
+
display.resizeHeight(delta);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return sum(next) - sum(start);
|
|
1048
1178
|
},
|
|
1049
1179
|
/**
|
|
1050
1180
|
* #action
|
|
1181
|
+
* A resize keeps the window in bp, so `bpPerPx` follows the new width
|
|
1182
|
+
* and the view keeps framing the same sequence (ADR-070).
|
|
1051
1183
|
*/
|
|
1052
|
-
// A resize needs no arithmetic: the window is what is stored, so a new
|
|
1053
|
-
// width simply divides into it and `bpPerPx` follows. The view keeps the
|
|
1054
|
-
// sequence it was framing rather than letting the right edge eat into it.
|
|
1055
|
-
//
|
|
1056
|
-
// Keeping the scale instead — which is what storing pixels amounted to —
|
|
1057
|
-
// was a block-cache optimization: block boundaries are
|
|
1058
|
-
// `blockNum * ceil(800 * bpPerPx)` and block keys embed start/end, so
|
|
1059
|
-
// holding bpPerPx across a resize kept every block key, and so every
|
|
1060
|
-
// fetched region, identical. Rescaling makes a resize a zoom, and a zoom
|
|
1061
|
-
// is already a solved case: FetchVisibleRegions' 300ms debounce coalesces
|
|
1062
|
-
// the gesture into one refetch, its in-flight guard caps concurrent
|
|
1063
|
-
// batches at one, and rpcDataMap is overwritten in place rather than
|
|
1064
|
-
// cleared, so nothing blanks (ADR-008, ADR-006).
|
|
1065
1184
|
setWidth(newWidth) {
|
|
1185
|
+
const unmeasured = self.bpPerPx <= 0;
|
|
1066
1186
|
self.volatileWidth = newWidth;
|
|
1067
1187
|
if (newWidth > 0 && self.windowWidthBp <= 0) {
|
|
1068
1188
|
// First measure, and nothing has established a window yet: a migrated
|
|
@@ -1073,6 +1193,12 @@ export function stateModelFactory(pluginManager) {
|
|
|
1073
1193
|
self.windowWidthBp = (self.legacyBpPerPx || 1) * newWidth;
|
|
1074
1194
|
self.legacyBpPerPx = 0;
|
|
1075
1195
|
}
|
|
1196
|
+
if (unmeasured && self.bpPerPx > 0) {
|
|
1197
|
+
// The debounced coarse autorun is 500ms out; a restored snapshot's
|
|
1198
|
+
// consumers would lay out and fetch at 1 bp/px until then.
|
|
1199
|
+
self.coarseBpPerPx = self.bpPerPx;
|
|
1200
|
+
self.coarseOffsetPx = self.offsetPx;
|
|
1201
|
+
}
|
|
1076
1202
|
},
|
|
1077
1203
|
/**
|
|
1078
1204
|
* #action
|
|
@@ -1086,12 +1212,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
1086
1212
|
setIsScalebarRefNameMenuOpen(isOpen) {
|
|
1087
1213
|
self.isScalebarRefNameMenuOpen = isOpen;
|
|
1088
1214
|
},
|
|
1089
|
-
/**
|
|
1090
|
-
* #action
|
|
1091
|
-
*/
|
|
1092
|
-
setScalebarRefNameClickPending(pending) {
|
|
1093
|
-
self.scalebarRefNameClickPending = pending;
|
|
1094
|
-
},
|
|
1095
1215
|
/**
|
|
1096
1216
|
* #action
|
|
1097
1217
|
*/
|
|
@@ -1119,14 +1239,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
1119
1239
|
/**
|
|
1120
1240
|
* #action
|
|
1121
1241
|
*/
|
|
1122
|
-
|
|
1123
|
-
self.
|
|
1124
|
-
},
|
|
1125
|
-
/**
|
|
1126
|
-
* #action
|
|
1127
|
-
*/
|
|
1128
|
-
setLabelsVisible(arg) {
|
|
1129
|
-
self.labelsVisible = arg;
|
|
1242
|
+
setAxisGrid(grid) {
|
|
1243
|
+
self.axis = cast({ ...getSnapshot(self.axis), grid });
|
|
1130
1244
|
},
|
|
1131
1245
|
/**
|
|
1132
1246
|
* #action
|
|
@@ -1200,18 +1314,49 @@ export function stateModelFactory(pluginManager) {
|
|
|
1200
1314
|
},
|
|
1201
1315
|
/**
|
|
1202
1316
|
* #action
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1317
|
+
* Raise the picker over search results that name more than one place.
|
|
1318
|
+
* @param onPick - where a picked result lands; by default it navigates
|
|
1319
|
+
* there and shows the track its index came from
|
|
1320
|
+
*/
|
|
1321
|
+
setSearchResults(searchResults, searchQuery, assemblyName, onPick = result => navToOption({
|
|
1322
|
+
option: result,
|
|
1323
|
+
model: self,
|
|
1324
|
+
assemblyName,
|
|
1325
|
+
})) {
|
|
1326
|
+
const hits = searchHitsOf(self, searchResults, searchQuery, assemblyName, onPick);
|
|
1327
|
+
const picker = {
|
|
1328
|
+
...hits,
|
|
1329
|
+
pick: async (id) => {
|
|
1330
|
+
await hits.pick(id);
|
|
1331
|
+
if (isAlive(self) && self.searchPicker === picker) {
|
|
1332
|
+
self.closeSearchPicker();
|
|
1333
|
+
}
|
|
1213
1334
|
},
|
|
1214
|
-
|
|
1335
|
+
};
|
|
1336
|
+
self.searchPicker = picker;
|
|
1337
|
+
},
|
|
1338
|
+
/**
|
|
1339
|
+
* #action
|
|
1340
|
+
* Search the view's assembly by name, for a host listing the hits
|
|
1341
|
+
* itself. A row's `pick` lands as the search box does: it navigates
|
|
1342
|
+
* there and shows the track the hit's index came from.
|
|
1343
|
+
*/
|
|
1344
|
+
async search(query, opts) {
|
|
1345
|
+
const assemblyName = self.assemblyNames[0] ?? self.launchAssemblyName;
|
|
1346
|
+
if (!assemblyName) {
|
|
1347
|
+
throw new Error('the view has no assembly to search yet');
|
|
1348
|
+
}
|
|
1349
|
+
const results = await fetchResults({
|
|
1350
|
+
session: getSession(self),
|
|
1351
|
+
queryString: query,
|
|
1352
|
+
assemblyName,
|
|
1353
|
+
signal: opts?.signal,
|
|
1354
|
+
});
|
|
1355
|
+
return searchHitsOf(self, results, query, assemblyName, result => navToOption({
|
|
1356
|
+
option: result,
|
|
1357
|
+
model: self,
|
|
1358
|
+
assemblyName,
|
|
1359
|
+
}));
|
|
1215
1360
|
},
|
|
1216
1361
|
/**
|
|
1217
1362
|
* #action
|
|
@@ -1227,6 +1372,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
1227
1372
|
},
|
|
1228
1373
|
}))
|
|
1229
1374
|
.actions(self => ({
|
|
1375
|
+
/**
|
|
1376
|
+
* #action
|
|
1377
|
+
* Resize every unminimized track so the view is exactly as tall as the
|
|
1378
|
+
* scroll port it sits in. Does nothing until the view has measured one.
|
|
1379
|
+
*/
|
|
1380
|
+
fitTracksToWindow() {
|
|
1381
|
+
if (self.scrollPortExcess) {
|
|
1382
|
+
self.resizeTracks(-self.scrollPortExcess);
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1230
1385
|
/**
|
|
1231
1386
|
* #action
|
|
1232
1387
|
* showTrack for a track whose display state model may be lazily
|
|
@@ -1235,68 +1390,108 @@ export function stateModelFactory(pluginManager) {
|
|
|
1235
1390
|
async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
|
|
1236
1391
|
return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
|
|
1237
1392
|
},
|
|
1238
|
-
}))
|
|
1239
|
-
.actions(self => ({
|
|
1240
1393
|
/**
|
|
1241
1394
|
* #action
|
|
1395
|
+
* Replace the highlights `highlights` reads, each written as the launch
|
|
1396
|
+
* spec's `highlight` writes one: a locstring, or an object whose
|
|
1397
|
+
* `assemblyName` defaults to the view's. Throws on one that names no
|
|
1398
|
+
* region, writing none.
|
|
1242
1399
|
*/
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1400
|
+
setHighlights(highlights) {
|
|
1401
|
+
const session = getSession(self);
|
|
1402
|
+
const assemblyName = self.assemblyNames[0] ?? self.launchAssemblyName ?? '';
|
|
1403
|
+
session.setHighlights(highlights.map(h => {
|
|
1404
|
+
const highlight = coerceHighlight(h, assemblyName, (ref, asm) => session.assemblyManager.isValidRefName(ref, asm ?? assemblyName));
|
|
1405
|
+
if (!highlight) {
|
|
1406
|
+
throw new Error(`not a region to highlight: ${JSON.stringify(h)}`);
|
|
1407
|
+
}
|
|
1408
|
+
return highlight;
|
|
1409
|
+
}));
|
|
1249
1410
|
},
|
|
1250
1411
|
/**
|
|
1251
1412
|
* #action
|
|
1413
|
+
* Show files from the user's disk as session tracks: one per data file,
|
|
1414
|
+
* beside the index among them that names it, its type guessed from its
|
|
1415
|
+
* name. Adds nothing when one file has no known type. Resolves the new
|
|
1416
|
+
* tracks' ids.
|
|
1252
1417
|
*/
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
const idx = section.findIndex(t => t.id === id);
|
|
1256
|
-
if (idx > 0) {
|
|
1257
|
-
this.moveTrack(id, section[idx - 1].id);
|
|
1258
|
-
}
|
|
1418
|
+
async showFiles(files) {
|
|
1419
|
+
return showFiles(self, files);
|
|
1259
1420
|
},
|
|
1421
|
+
}))
|
|
1422
|
+
.views(self => ({
|
|
1260
1423
|
/**
|
|
1261
|
-
* #
|
|
1424
|
+
* #method
|
|
1425
|
+
* The shown track a move action means: the menu passes a track model's
|
|
1426
|
+
* own `id`, an agent passes the `trackId` it knows the track by, and
|
|
1427
|
+
* both name one shown track. Answers the model id, or the argument
|
|
1428
|
+
* unchanged when nothing shown matches, so the move reports it.
|
|
1262
1429
|
*/
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
this.moveTrack(id, section[0].id);
|
|
1267
|
-
}
|
|
1430
|
+
trackModelId(idOrTrackId) {
|
|
1431
|
+
return ((self.tracks.find(t => t.id === idOrTrackId) ??
|
|
1432
|
+
self.tracks.find(t => t.configuration.trackId === idOrTrackId))?.id ?? idOrTrackId);
|
|
1268
1433
|
},
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.moveTrack(id, last.id);
|
|
1434
|
+
}))
|
|
1435
|
+
.actions(self => {
|
|
1436
|
+
function shownIndex(idOrTrackId) {
|
|
1437
|
+
const id = self.trackModelId(idOrTrackId);
|
|
1438
|
+
const index = self.tracks.findIndex(track => track.id === id);
|
|
1439
|
+
if (index === -1) {
|
|
1440
|
+
throw new Error(`No shown track has id or trackId "${idOrTrackId}" — the view's tracks are ${self.tracks.map(t => t.configuration.trackId).join(', ') || 'none'}`);
|
|
1277
1441
|
}
|
|
1278
|
-
|
|
1442
|
+
return index;
|
|
1443
|
+
}
|
|
1279
1444
|
/**
|
|
1280
1445
|
* #action
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
throw new Error(`Track ID ${targetId} not found`);
|
|
1290
|
-
}
|
|
1291
|
-
// direction-aware placement: filtering out oldIndex shifts the target
|
|
1292
|
-
// left by one when dragging down (oldIndex < newIndex), so splicing at
|
|
1293
|
-
// newIndex lands the track *after* the target; dragging up leaves the
|
|
1294
|
-
// target's index intact, landing *before* it. This matches the side
|
|
1295
|
-
// the dragged track approaches from.
|
|
1446
|
+
* Move one shown track to another's position; each is a track model id
|
|
1447
|
+
* or a trackId.
|
|
1448
|
+
*/
|
|
1449
|
+
function moveTrack(moving, target) {
|
|
1450
|
+
const oldIndex = shownIndex(moving);
|
|
1451
|
+
const newIndex = shownIndex(target);
|
|
1452
|
+
// splicing at newIndex after removing oldIndex lands a track dragged
|
|
1453
|
+
// down after its target and one dragged up before it
|
|
1296
1454
|
const tracks = self.tracks.filter((_, idx) => idx !== oldIndex);
|
|
1297
1455
|
tracks.splice(newIndex, 0, self.tracks[oldIndex]);
|
|
1298
1456
|
self.tracks = cast(tracks);
|
|
1299
|
-
}
|
|
1457
|
+
}
|
|
1458
|
+
function moveWithinSection(idOrTrackId, pick) {
|
|
1459
|
+
const moving = self.tracks[shownIndex(idOrTrackId)];
|
|
1460
|
+
const section = self.trackSection(moving.id);
|
|
1461
|
+
const target = pick(section, section.indexOf(moving));
|
|
1462
|
+
if (target && target !== moving) {
|
|
1463
|
+
moveTrack(moving.id, target.id);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
moveTrack,
|
|
1468
|
+
/**
|
|
1469
|
+
* #action
|
|
1470
|
+
*/
|
|
1471
|
+
moveTrackDown(idOrTrackId) {
|
|
1472
|
+
moveWithinSection(idOrTrackId, (section, i) => section[i + 1]);
|
|
1473
|
+
},
|
|
1474
|
+
/**
|
|
1475
|
+
* #action
|
|
1476
|
+
*/
|
|
1477
|
+
moveTrackUp(idOrTrackId) {
|
|
1478
|
+
moveWithinSection(idOrTrackId, (section, i) => section[i - 1]);
|
|
1479
|
+
},
|
|
1480
|
+
/**
|
|
1481
|
+
* #action
|
|
1482
|
+
*/
|
|
1483
|
+
moveTrackToTop(idOrTrackId) {
|
|
1484
|
+
moveWithinSection(idOrTrackId, section => section[0]);
|
|
1485
|
+
},
|
|
1486
|
+
/**
|
|
1487
|
+
* #action
|
|
1488
|
+
*/
|
|
1489
|
+
moveTrackToBottom(idOrTrackId) {
|
|
1490
|
+
moveWithinSection(idOrTrackId, section => section.at(-1));
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1493
|
+
})
|
|
1494
|
+
.actions(self => ({
|
|
1300
1495
|
/**
|
|
1301
1496
|
* #action
|
|
1302
1497
|
*/
|
|
@@ -1315,12 +1510,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
1315
1510
|
*/
|
|
1316
1511
|
setTrackLabels(setting) {
|
|
1317
1512
|
self.trackLabels = setting;
|
|
1513
|
+
localStorageSetItem('lgv-trackLabels', setting);
|
|
1318
1514
|
},
|
|
1319
1515
|
/**
|
|
1320
1516
|
* #action
|
|
1321
1517
|
*/
|
|
1322
1518
|
setShowCenterLine(b) {
|
|
1323
1519
|
self.showCenterLine = b;
|
|
1520
|
+
localStorageSetBoolean('lgv-showCenterLine', b);
|
|
1324
1521
|
},
|
|
1325
1522
|
/**
|
|
1326
1523
|
* #action
|
|
@@ -1334,8 +1531,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
1334
1531
|
* #action
|
|
1335
1532
|
*/
|
|
1336
1533
|
toggleTrackSelector() {
|
|
1534
|
+
const wasVisible = isTrackSelectorVisible(self);
|
|
1337
1535
|
const { session, selector } = openTrackSelectorWidget(self);
|
|
1338
|
-
if (
|
|
1536
|
+
if (wasVisible) {
|
|
1339
1537
|
session.hideWidget(selector);
|
|
1340
1538
|
}
|
|
1341
1539
|
else {
|
|
@@ -1343,39 +1541,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
1343
1541
|
}
|
|
1344
1542
|
return selector;
|
|
1345
1543
|
},
|
|
1346
|
-
/**
|
|
1347
|
-
* #method
|
|
1348
|
-
* Helper method for the fetchSequence.
|
|
1349
|
-
* Retrieves the corresponding regions that were selected by the
|
|
1350
|
-
* rubberband
|
|
1351
|
-
*
|
|
1352
|
-
* @param leftOffset - `object as {start, end, index, offset}`, offset = start
|
|
1353
|
-
* of user drag
|
|
1354
|
-
* @param rightOffset - `object as {start, end, index, offset}`,
|
|
1355
|
-
* offset = end of user drag
|
|
1356
|
-
* @returns array of Region[]
|
|
1357
|
-
*/
|
|
1358
|
-
getSelectedRegions(leftOffset, rightOffset) {
|
|
1359
|
-
if (!leftOffset || !rightOffset) {
|
|
1360
|
-
return [];
|
|
1361
|
-
}
|
|
1362
|
-
const layout = {
|
|
1363
|
-
displayedRegions: self.displayedRegions,
|
|
1364
|
-
bpPerPx: self.bpPerPx,
|
|
1365
|
-
offsetPx: self.offsetPx,
|
|
1366
|
-
width: self.width,
|
|
1367
|
-
minimumBlockWidth: self.minimumBlockWidth,
|
|
1368
|
-
};
|
|
1369
|
-
const { bpPerPx, offsetPx: rawOffsetPx } = computeMoveToLayout(layout, leftOffset, rightOffset);
|
|
1370
|
-
// mirror Base1DView.scrollTo clamping: raw offsetPx can be far outside
|
|
1371
|
-
// the valid range when both offsets are oob on the same side
|
|
1372
|
-
const offsetPx = clamp(rawOffsetPx, self.minOffset, self.totalBp / bpPerPx - MAX_OFFSET_PADDING_PX);
|
|
1373
|
-
return wholeBaseRegions(calculateDynamicBlocks({
|
|
1374
|
-
...layout,
|
|
1375
|
-
bpPerPx,
|
|
1376
|
-
offsetPx,
|
|
1377
|
-
}).contentBlocks);
|
|
1378
|
-
},
|
|
1379
1544
|
/**
|
|
1380
1545
|
* #action
|
|
1381
1546
|
*/
|
|
@@ -1394,12 +1559,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
1394
1559
|
self.lastTrackDragY = undefined;
|
|
1395
1560
|
}
|
|
1396
1561
|
},
|
|
1397
|
-
/**
|
|
1398
|
-
* #action
|
|
1399
|
-
*/
|
|
1400
|
-
setLastTrackDragY(y) {
|
|
1401
|
-
self.lastTrackDragY = y;
|
|
1402
|
-
},
|
|
1403
1562
|
/**
|
|
1404
1563
|
* #action
|
|
1405
1564
|
* called while dragging a track over the track at `targetId`; reorders
|
|
@@ -1415,7 +1574,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1415
1574
|
const movingDown = targetIdx > draggingIdx;
|
|
1416
1575
|
if (shouldSwapTracks(self.lastTrackDragY, currentY, movingDown)) {
|
|
1417
1576
|
self.lastTrackDragY = currentY;
|
|
1418
|
-
|
|
1577
|
+
self.moveTrack(draggingTrackId, targetId);
|
|
1419
1578
|
}
|
|
1420
1579
|
}
|
|
1421
1580
|
}
|
|
@@ -1426,15 +1585,41 @@ export function stateModelFactory(pluginManager) {
|
|
|
1426
1585
|
setLaunch(launch) {
|
|
1427
1586
|
self.launch = launch;
|
|
1428
1587
|
},
|
|
1588
|
+
}))
|
|
1589
|
+
.views(self => ({
|
|
1429
1590
|
/**
|
|
1430
1591
|
* #method
|
|
1431
|
-
*
|
|
1592
|
+
* Helper method for the fetchSequence.
|
|
1593
|
+
* Returns the regions the user selected with the
|
|
1594
|
+
* rubberband
|
|
1595
|
+
*
|
|
1596
|
+
* @param leftOffset - `object as {start, end, index, offset}`, offset = start
|
|
1597
|
+
* of user drag
|
|
1598
|
+
* @param rightOffset - `object as {start, end, index, offset}`,
|
|
1599
|
+
* offset = end of user drag
|
|
1600
|
+
* @returns array of Region[]
|
|
1432
1601
|
*/
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1602
|
+
getSelectedRegions(leftOffset, rightOffset) {
|
|
1603
|
+
if (!leftOffset || !rightOffset) {
|
|
1604
|
+
return [];
|
|
1605
|
+
}
|
|
1606
|
+
const layout = {
|
|
1607
|
+
displayedRegions: self.displayedRegions,
|
|
1608
|
+
bpPerPx: self.bpPerPx,
|
|
1609
|
+
offsetPx: self.offsetPx,
|
|
1610
|
+
width: self.width,
|
|
1611
|
+
minimumBlockWidth: self.minimumBlockWidth,
|
|
1612
|
+
};
|
|
1613
|
+
const { bpPerPx, offsetPx: rawOffsetPx } = computeMoveToLayout(layout, leftOffset, rightOffset);
|
|
1614
|
+
// mirror Base1DView.scrollTo clamping: raw offsetPx can be far outside
|
|
1615
|
+
// the valid range when both offsets are oob on the same side
|
|
1616
|
+
const { min, max } = offsetBounds(self.totalBp / bpPerPx, self.width);
|
|
1617
|
+
const offsetPx = clamp(rawOffsetPx, min, max);
|
|
1618
|
+
return wholeBaseRegions(calculateDynamicBlocks({
|
|
1619
|
+
...layout,
|
|
1620
|
+
bpPerPx,
|
|
1621
|
+
offsetPx,
|
|
1622
|
+
}).contentBlocks);
|
|
1438
1623
|
},
|
|
1439
1624
|
}))
|
|
1440
1625
|
.actions(self => {
|
|
@@ -1491,8 +1676,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
1491
1676
|
}
|
|
1492
1677
|
/**
|
|
1493
1678
|
* #action
|
|
1494
|
-
* cancel an in-flight animated zoom.
|
|
1495
|
-
*
|
|
1679
|
+
* cancel an in-flight animated zoom. Any other zoom already stops the
|
|
1680
|
+
* animation, so this is for stopping it *without* changing the
|
|
1496
1681
|
* zoom — the slider grabbing the thumb, before it has a value to commit.
|
|
1497
1682
|
*/
|
|
1498
1683
|
function cancelZoomAnimation() {
|
|
@@ -1520,25 +1705,26 @@ export function stateModelFactory(pluginManager) {
|
|
|
1520
1705
|
* #getter
|
|
1521
1706
|
* an ideogram only reads correctly against an entire chromosome: on a
|
|
1522
1707
|
* sub-region it is a meaningless slice of bands, and the centromere shows
|
|
1523
|
-
* up as a lone half-triangle
|
|
1708
|
+
* up as a lone half-triangle. The chromosome also needs bands: a cytoband
|
|
1709
|
+
* file often lists only the main chromosomes, not chrM or the
|
|
1710
|
+
* unplaced scaffolds
|
|
1524
1711
|
*/
|
|
1525
1712
|
get canShowCytobands() {
|
|
1526
|
-
|
|
1713
|
+
const region = self.displayedRegions[0];
|
|
1714
|
+
return (this.showsWholeChromosome &&
|
|
1715
|
+
!!region &&
|
|
1716
|
+
getCytobands(getSession(self).assemblyManager.get(region.assemblyName), region.refName).length > 0);
|
|
1527
1717
|
},
|
|
1528
1718
|
/**
|
|
1529
1719
|
* #getter
|
|
1530
1720
|
* the `showCytobands` setting gated by whether cytobands can be shown at
|
|
1531
|
-
* all (whole chromosome
|
|
1721
|
+
* all (whole chromosome with bands in the cytoband file) and by the header
|
|
1722
|
+
* overview they draw in being on, which the SVG export follows too
|
|
1532
1723
|
*/
|
|
1533
1724
|
get effectiveShowCytobands() {
|
|
1534
|
-
return this.canShowCytobands &&
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
* #getter
|
|
1538
|
-
*/
|
|
1539
|
-
get anyCytobandsExist() {
|
|
1540
|
-
const { assemblyManager } = getSession(self);
|
|
1541
|
-
return self.assemblyNames.some(a => assemblyManager.get(a)?.cytobands?.length);
|
|
1725
|
+
return (this.canShowCytobands &&
|
|
1726
|
+
self.showCytobands &&
|
|
1727
|
+
!self.hideHeaderOverview);
|
|
1542
1728
|
},
|
|
1543
1729
|
/**
|
|
1544
1730
|
* #getter
|
|
@@ -1565,6 +1751,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
1565
1751
|
menuItems() {
|
|
1566
1752
|
return buildMenuItems(self);
|
|
1567
1753
|
},
|
|
1754
|
+
/**
|
|
1755
|
+
* #method
|
|
1756
|
+
* the view menu's Highlights submenu; extend this to add a row there
|
|
1757
|
+
*/
|
|
1758
|
+
highlightsSubMenuItems() {
|
|
1759
|
+
return buildHighlightsSubMenuItems(self);
|
|
1760
|
+
},
|
|
1568
1761
|
/**
|
|
1569
1762
|
* #method
|
|
1570
1763
|
* what a plugin can start from the selected region — a synteny view, a
|
|
@@ -1595,7 +1788,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
1595
1788
|
let coverageRightPx = 0;
|
|
1596
1789
|
let prevBpPerPx;
|
|
1597
1790
|
let prevWidth;
|
|
1598
|
-
let prevMinimumBlockWidth;
|
|
1599
1791
|
let prevDisplayedRegions;
|
|
1600
1792
|
return {
|
|
1601
1793
|
/**
|
|
@@ -1607,16 +1799,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
1607
1799
|
* blocks to render their data for the region represented by the block
|
|
1608
1800
|
*/
|
|
1609
1801
|
get staticBlocks() {
|
|
1610
|
-
const { offsetPx, bpPerPx, width,
|
|
1802
|
+
const { offsetPx, bpPerPx, width, displayedRegions } = self;
|
|
1611
1803
|
// Fast path: if only offsetPx changed and viewport is still within
|
|
1612
1804
|
// the coverage range of existing blocks, skip the expensive
|
|
1613
|
-
// calculateStaticBlocks call entirely.
|
|
1614
|
-
// (not just in calculateStaticBlocks) so MobX still invalidates this
|
|
1615
|
-
// computed if it changes while the viewport stays within coverage.
|
|
1805
|
+
// calculateStaticBlocks call entirely.
|
|
1616
1806
|
if (currentlyCalculatedStaticBlocks !== undefined &&
|
|
1617
1807
|
bpPerPx === prevBpPerPx &&
|
|
1618
1808
|
width === prevWidth &&
|
|
1619
|
-
minimumBlockWidth === prevMinimumBlockWidth &&
|
|
1620
1809
|
displayedRegions === prevDisplayedRegions &&
|
|
1621
1810
|
offsetPx >= coverageLeftPx &&
|
|
1622
1811
|
offsetPx + width <= coverageRightPx) {
|
|
@@ -1625,16 +1814,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
1625
1814
|
const newBlocks = calculateStaticBlocks(self);
|
|
1626
1815
|
// Hand back the previous BlockSet when the recompute produced the same
|
|
1627
1816
|
// blocks, so a sideways scroll past the coverage edge doesn't re-render
|
|
1628
|
-
// every track.
|
|
1629
|
-
//
|
|
1630
|
-
//
|
|
1631
|
-
//
|
|
1632
|
-
// view has one block per refName, so joining those keys into a string
|
|
1633
|
-
// (as this used to) allocated an array and a string per zoom frame.
|
|
1817
|
+
// every track. Geometry is compared first: on a zoom it always
|
|
1818
|
+
// differs, which skips the key walk entirely — the whole-genome view
|
|
1819
|
+
// has one block per refName, so joining those keys into a string (as
|
|
1820
|
+
// this used to) allocated an array and a string per zoom frame.
|
|
1634
1821
|
if (currentlyCalculatedStaticBlocks === undefined ||
|
|
1635
1822
|
bpPerPx !== prevBpPerPx ||
|
|
1636
1823
|
width !== prevWidth ||
|
|
1637
|
-
minimumBlockWidth !== prevMinimumBlockWidth ||
|
|
1638
1824
|
!sameBlockKeys(currentlyCalculatedStaticBlocks, newBlocks)) {
|
|
1639
1825
|
currentlyCalculatedStaticBlocks = newBlocks;
|
|
1640
1826
|
}
|
|
@@ -1650,16 +1836,14 @@ export function stateModelFactory(pluginManager) {
|
|
|
1650
1836
|
}
|
|
1651
1837
|
prevBpPerPx = bpPerPx;
|
|
1652
1838
|
prevWidth = width;
|
|
1653
|
-
prevMinimumBlockWidth = minimumBlockWidth;
|
|
1654
1839
|
prevDisplayedRegions = displayedRegions;
|
|
1655
1840
|
return currentlyCalculatedStaticBlocks;
|
|
1656
1841
|
},
|
|
1657
1842
|
/**
|
|
1658
1843
|
* #getter
|
|
1659
1844
|
* dynamic blocks represent the exact coordinates of the currently
|
|
1660
|
-
* visible genome regions on the screen.
|
|
1661
|
-
*
|
|
1662
|
-
* represent exactly what is on screen
|
|
1845
|
+
* visible genome regions on the screen. Static blocks can go
|
|
1846
|
+
* offscreen, while dynamic blocks represent exactly what is on screen
|
|
1663
1847
|
*/
|
|
1664
1848
|
get dynamicBlocks() {
|
|
1665
1849
|
return calculateDynamicBlocks(self);
|
|
@@ -1701,12 +1885,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
1701
1885
|
* The frame exists because those three are laid out across every
|
|
1702
1886
|
* displayed region rather than across the viewport — it overhangs on
|
|
1703
1887
|
* both sides — so their `x` values are stable under a scroll and only
|
|
1704
|
-
* this number moves. `scalebarRefNameLabels` is the exception
|
|
1705
|
-
* so: its `transform` is already a screen x, because a sticky label's
|
|
1888
|
+
* this number moves. `scalebarRefNameLabels` is the exception: its `transform` is already a screen x, because a sticky label's
|
|
1706
1889
|
* position is a function of the scroll rather than of block geometry.
|
|
1707
1890
|
*
|
|
1708
|
-
* **The subtraction must happen here, in float64
|
|
1709
|
-
* load-bearing part.** `offsetPx` is a whole-genome pixel coordinate —
|
|
1891
|
+
* **The subtraction must happen here, in float64.** `offsetPx` is a whole-genome pixel coordinate —
|
|
1710
1892
|
* hg38 chr1 at base resolution is already past 1e10 — and a length that
|
|
1711
1893
|
* size does not survive the trip through CSS: the transform matrix is
|
|
1712
1894
|
* float32 by the time it reaches the compositor, where consecutive
|
|
@@ -1718,13 +1900,12 @@ export function stateModelFactory(pluginManager) {
|
|
|
1718
1900
|
* assemblies at high zoom, which is not where anyone tests. Both
|
|
1719
1901
|
* operands here are large and their difference is bounded by the
|
|
1720
1902
|
* overhang (a block or two), so what reaches CSS is small and exact.
|
|
1721
|
-
*
|
|
1903
|
+
* This applies the GPU side's rule one layer up; see
|
|
1722
1904
|
* `agent-docs/reference/BP_PRECISION.md`.
|
|
1723
1905
|
*
|
|
1724
1906
|
* Published because it is the one piece of coordinate arithmetic a host
|
|
1725
1907
|
* drawing its own chrome would otherwise have to know, and because
|
|
1726
|
-
*
|
|
1727
|
-
* came to disagree about rounding. Round it where the content is text
|
|
1908
|
+
* copies written out at each call site disagree about rounding. Round it where the content is text
|
|
1728
1909
|
* (a fractional offset blurs a label); leave it alone for paths and
|
|
1729
1910
|
* boxes.
|
|
1730
1911
|
*/
|
|
@@ -1739,21 +1920,12 @@ export function stateModelFactory(pluginManager) {
|
|
|
1739
1920
|
* than recomputing the makeTicks loop per component.
|
|
1740
1921
|
*/
|
|
1741
1922
|
get gridlineTicks() {
|
|
1742
|
-
const { bpPerPx } = self;
|
|
1743
1923
|
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const runLeft = run.offsetPx - firstBlockOffset;
|
|
1750
|
-
for (const { type, x } of makeBlockTicks(run, bpPerPx)) {
|
|
1751
|
-
if (x >= 0 && x <= run.widthPx) {
|
|
1752
|
-
ticks.push({ x: runLeft + x, major: type === 'major' });
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return ticks;
|
|
1924
|
+
return getGridlineTicks({
|
|
1925
|
+
blocks,
|
|
1926
|
+
firstBlockOffset,
|
|
1927
|
+
bpPerPx: self.bpPerPx,
|
|
1928
|
+
});
|
|
1757
1929
|
},
|
|
1758
1930
|
/**
|
|
1759
1931
|
* #getter
|
|
@@ -1771,33 +1943,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
1771
1943
|
* reads as the same number repeated across the row.
|
|
1772
1944
|
*/
|
|
1773
1945
|
get scalebarLabels() {
|
|
1774
|
-
const { bpPerPx } = self;
|
|
1775
1946
|
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
for (const { base, x } of makeBlockTicks(run, bpPerPx, true, false)) {
|
|
1783
|
-
const label = getTickDisplayStr(base + 1, bpPerPx);
|
|
1784
|
-
const w = tickLabelWidth(label);
|
|
1785
|
-
// the bold refName label pinned at the run's left edge takes
|
|
1786
|
-
// precedence, as it does on the overview scalebar
|
|
1787
|
-
if (x - w / 2 >= refNameLabelPx[i] &&
|
|
1788
|
-
labelFitsInBlock(x - w / 2, w, run.widthPx)) {
|
|
1789
|
-
runLabels.push({
|
|
1790
|
-
x: runLeft + x,
|
|
1791
|
-
label,
|
|
1792
|
-
key: `${run.offsetPx}-${base}`,
|
|
1793
|
-
});
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
if (tickLabelsWorthDrawing(runLabels.length)) {
|
|
1797
|
-
labels.push(...runLabels);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
return labels;
|
|
1947
|
+
return getScalebarLabels({
|
|
1948
|
+
blocks,
|
|
1949
|
+
firstBlockOffset,
|
|
1950
|
+
bpPerPx: self.bpPerPx,
|
|
1951
|
+
orientation: self.displayedRegionsOrientation,
|
|
1952
|
+
});
|
|
1801
1953
|
},
|
|
1802
1954
|
/**
|
|
1803
1955
|
* #getter
|
|
@@ -1806,11 +1958,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
1806
1958
|
* sticky, transform, maxWidth, paddingLeft, text}` each. One per run of
|
|
1807
1959
|
* same-refName regions, plus a "sticky" one pinned to the viewport's
|
|
1808
1960
|
* left edge naming the refName under it, so panning into a chromosome
|
|
1809
|
-
* does not scroll
|
|
1961
|
+
* does not scroll the chromosome's name off the screen.
|
|
1810
1962
|
*
|
|
1811
|
-
*
|
|
1812
|
-
* otherwise
|
|
1813
|
-
*
|
|
1963
|
+
* This getter applies three rules that a host drawing region names would
|
|
1964
|
+
* otherwise have to rediscover, and two of them are invisible until the
|
|
1965
|
+
* data is awkward:
|
|
1814
1966
|
*
|
|
1815
1967
|
* - the sticky label rides the rightmost run that has scrolled off the
|
|
1816
1968
|
* left, not the region's first block — that one is gone from
|
|
@@ -1822,7 +1974,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1822
1974
|
* is fitted to the whole run, and `displayedRegionIndex` /
|
|
1823
1975
|
* `lastDisplayedRegionIndex` bracket what it stands for.
|
|
1824
1976
|
* - a name is drawn whole or not at all, measured against the space its
|
|
1825
|
-
* run leaves. Clipped to
|
|
1977
|
+
* run leaves. Clipped to that space, `chr16` reads as `chr1` —
|
|
1826
1978
|
* a different chromosome rather than a shortened name, which is why
|
|
1827
1979
|
* this is a fit test rather than an ellipsis.
|
|
1828
1980
|
*
|
|
@@ -1838,10 +1990,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
1838
1990
|
* into that through `scalebarDisplayPrefix`) and whether it is flipped.
|
|
1839
1991
|
* A ` [rev]` there is a fact about the ROW; the same marker on a
|
|
1840
1992
|
* chromosome name means that one region, which only happens under mixed
|
|
1841
|
-
* orientation. `captionSpanPx` is the width it takes
|
|
1842
|
-
*
|
|
1993
|
+
* orientation. `captionSpanPx` is the width it takes: the sticky label
|
|
1994
|
+
* pins to its right edge rather than the viewport's, and the coordinate
|
|
1995
|
+
* numbers stay out from under it. The SVG export deliberately
|
|
1843
1996
|
* calls `getScalebarRefNameLabels` itself with no prefix rather than
|
|
1844
|
-
* reading this, since it draws
|
|
1997
|
+
* reading this, since it draws the assembly name itself above the ruler.
|
|
1845
1998
|
*/
|
|
1846
1999
|
get scalebarRefNameLabels() {
|
|
1847
2000
|
return getScalebarRefNameLabels({
|
|
@@ -1856,36 +2009,36 @@ export function stateModelFactory(pluginManager) {
|
|
|
1856
2009
|
* Every span along the row that is not track data, as plain geometry in
|
|
1857
2010
|
* the staticBlocks frame — the same frame as gridlineTicks and
|
|
1858
2011
|
* scalebarLabels, so one `translateX(staticBlocksTranslateX)` places all
|
|
1859
|
-
* three.
|
|
1860
|
-
* them:
|
|
2012
|
+
* three. A host that draws the row's chrome itself needs all three kinds:
|
|
1861
2013
|
*
|
|
1862
2014
|
* - `seam`: the 3px bar at a region's right edge. Displayed regions are
|
|
1863
|
-
* laid out **contiguously**
|
|
1864
|
-
* padding only before the first region and after the last
|
|
2015
|
+
* laid out **contiguously** (calculateStaticBlocks emits boundary
|
|
2016
|
+
* padding only before the first region and after the last), so this
|
|
1865
2017
|
* bar is the only thing separating two of them. Without it a
|
|
1866
|
-
* two-region view
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
2018
|
+
* two-region view looks like a single region.
|
|
2019
|
+
* `isRightEndOfDisplayedRegion` marks it; the right edge of the
|
|
2020
|
+
* currently *loaded* blocks, which block geometry gives you, is a
|
|
2021
|
+
* different set.
|
|
1870
2022
|
* - `elided`: a region too narrow to draw at this zoom. Whole-genome on
|
|
1871
|
-
* a real assembly is mostly these
|
|
1872
|
-
* but the 24 chromosomes
|
|
1873
|
-
*
|
|
2023
|
+
* a real assembly is mostly these (hg38 has 455 sequences, and all
|
|
2024
|
+
* but the 24 chromosomes are under a pixel wide), so a host that
|
|
2025
|
+
* skips them leaves that part of the row empty.
|
|
1874
2026
|
* - `boundary`: past the start of the first region or the end of the
|
|
1875
|
-
* last. Greying it
|
|
1876
|
-
*
|
|
2027
|
+
* last. Greying it makes the last region's seam read as the edge of a
|
|
2028
|
+
* filled area, where it would otherwise look like a line in the
|
|
2029
|
+
* middle of the track.
|
|
1877
2030
|
*
|
|
1878
|
-
* Elided blocks get no seam even though they
|
|
1879
|
-
* zoom where regions elide, one bar per region
|
|
2031
|
+
* Elided blocks get no seam even though they have the flag: at the
|
|
2032
|
+
* zoom where regions elide, one bar per region would fill the row solid
|
|
2033
|
+
* grey.
|
|
1880
2034
|
*
|
|
1881
|
-
* PaddingBlocks is the in-tree consumer. The SVG export
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
* interactive row
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
* so it is all that separates two of them.
|
|
2035
|
+
* PaddingBlocks is the in-tree consumer. The SVG export does not use
|
|
2036
|
+
* it: `SVGRegionSeparators` walks dynamicBlocks itself and draws only
|
|
2037
|
+
* the seam, because `elided` and `boundary` mark state for an
|
|
2038
|
+
* interactive row. In a static figure the striped grey would cover most
|
|
2039
|
+
* of the row at whole-genome zoom. The export keeps the seam because
|
|
2040
|
+
* regions lay out contiguously and the seam is all that separates two
|
|
2041
|
+
* of them.
|
|
1889
2042
|
*/
|
|
1890
2043
|
// Annotated with the shape spelled out rather than as `PaddingSpan`,
|
|
1891
2044
|
// because the API doc's type column is where a host drawing its own
|
|
@@ -1894,38 +2047,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1894
2047
|
// cannot drift without failing here.
|
|
1895
2048
|
get paddingSpans() {
|
|
1896
2049
|
const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
|
|
1897
|
-
|
|
1898
|
-
for (const block of blocks) {
|
|
1899
|
-
const x = block.offsetPx - firstBlockOffset;
|
|
1900
|
-
if (block.type === 'ContentBlock') {
|
|
1901
|
-
if (block.isRightEndOfDisplayedRegion) {
|
|
1902
|
-
spans.push({
|
|
1903
|
-
key: `${block.key}-sep`,
|
|
1904
|
-
x: x + block.widthPx - 1,
|
|
1905
|
-
width: 3,
|
|
1906
|
-
kind: 'seam',
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
else {
|
|
1911
|
-
spans.push({
|
|
1912
|
-
key: block.key,
|
|
1913
|
-
x,
|
|
1914
|
-
width: block.widthPx,
|
|
1915
|
-
kind: block.type === 'ElidedBlock'
|
|
1916
|
-
? 'elided'
|
|
1917
|
-
: 'boundary',
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
// One shared array when there is nothing to draw, so the computed's
|
|
1922
|
-
// value repeats and MobX's `===` stops the chain here. This is the
|
|
1923
|
-
// COMMON case, not an edge one: a view sitting inside one contig has
|
|
1924
|
-
// no region seam, no elision and no boundary, and that is where a
|
|
1925
|
-
// reader spends nearly all of a session. A fresh `[]` each frame
|
|
1926
|
-
// instead re-rendered a PaddingBlocks per track plus one for the
|
|
1927
|
-
// container, every frame of every gesture, to draw nothing.
|
|
1928
|
-
return spans.length > 0 ? spans : NO_PADDING_SPANS;
|
|
2050
|
+
return getPaddingSpans(blocks, firstBlockOffset);
|
|
1929
2051
|
},
|
|
1930
2052
|
/**
|
|
1931
2053
|
* #getter
|
|
@@ -1957,8 +2079,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
1957
2079
|
* viewport. False both when the view holds no regions at all and when
|
|
1958
2080
|
* it holds some but is scrolled entirely off them; either way there is
|
|
1959
2081
|
* no visible span for the scalebar, ruler and refName labels to
|
|
1960
|
-
* describe
|
|
1961
|
-
*
|
|
2082
|
+
* describe. The SVG export checks it before drawing its header.
|
|
2083
|
+
* Distinct from `hasDisplayedRegions`, which only asks whether the
|
|
1962
2084
|
* view has been given regions, not whether any are on screen.
|
|
1963
2085
|
*/
|
|
1964
2086
|
get hasVisibleContent() {
|
|
@@ -1998,7 +2120,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
1998
2120
|
/**
|
|
1999
2121
|
* #getter
|
|
2000
2122
|
* Right edge (px, viewport-relative) of the on-screen content, clamped
|
|
2001
|
-
* to the track — where a right-pinned overlay such as a wiggle's
|
|
2123
|
+
* to the track — where a right-pinned overlay such as a wiggle's color
|
|
2002
2124
|
* or score legend belongs, rather than out in the empty gutter the
|
|
2003
2125
|
* regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
|
|
2004
2126
|
* `contentRightEdgePx` states the rule; an SVG export applies the same
|
|
@@ -2008,7 +2130,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2008
2130
|
* array of fresh objects on every pan and zoom frame, so a component
|
|
2009
2131
|
* that derived this number itself re-rendered on every frame of every
|
|
2010
2132
|
* gesture — to produce, whenever content fills the track, the unchanged
|
|
2011
|
-
* `trackWidthPx`.
|
|
2133
|
+
* `trackWidthPx`. A component that reads the scalar lets MobX's `===` stop the chain at
|
|
2012
2134
|
* this computed instead. Measured over 20 zoom frames: the wiggle
|
|
2013
2135
|
* family's three body instances fell from 66 renders to 7.
|
|
2014
2136
|
*/
|
|
@@ -2060,23 +2182,22 @@ export function stateModelFactory(pluginManager) {
|
|
|
2060
2182
|
},
|
|
2061
2183
|
/**
|
|
2062
2184
|
* #getter
|
|
2063
|
-
* **
|
|
2064
|
-
* view has settled at least once, the live ones before that.
|
|
2185
|
+
* **The blocks a debounced consumer clips to**: the coarse blocks once
|
|
2186
|
+
* the view has settled at least once, the live ones before that.
|
|
2065
2187
|
*
|
|
2066
|
-
* The coarse blocks
|
|
2067
|
-
* animation frame during a pan or zoom
|
|
2068
|
-
* alignments coverage scale and MAF's coverage band
|
|
2069
|
-
*
|
|
2070
|
-
*
|
|
2071
|
-
*
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2076
|
-
* fetch used to be trailing-edge at 600ms, so it never did.
|
|
2188
|
+
* The coarse blocks stop a per-bp scan from recomputing on every
|
|
2189
|
+
* animation frame during a pan or zoom. Wiggle's autoscale domain, the
|
|
2190
|
+
* alignments coverage scale and MAF's coverage band use them. While the
|
|
2191
|
+
* coarse blocks are stale the result is a frame or two old, as the
|
|
2192
|
+
* debounce intends. **Empty coarse blocks are a different case.** A scan
|
|
2193
|
+
* over no blocks yields no entries, and the consumer falls back to the
|
|
2194
|
+
* domain `[0,1]`, which draws a line plot blank and a density plot
|
|
2195
|
+
* saturated. The coarse blocks are empty for the 500ms between a view
|
|
2196
|
+
* initializing and the coarse autorun's first run, and data can load
|
|
2197
|
+
* within that window.
|
|
2077
2198
|
*
|
|
2078
|
-
*
|
|
2079
|
-
*
|
|
2199
|
+
* Falling back to the live blocks costs one extra recompute at that
|
|
2200
|
+
* transition, compared with N per frame without the debounce.
|
|
2080
2201
|
*/
|
|
2081
2202
|
get settledDynamicBlocks() {
|
|
2082
2203
|
return self.coarseDynamicBlocks.length
|
|
@@ -2100,7 +2221,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2100
2221
|
* #getter
|
|
2101
2222
|
*/
|
|
2102
2223
|
get effectiveTotalBp() {
|
|
2103
|
-
return self.
|
|
2224
|
+
return self.windowWidthBp;
|
|
2104
2225
|
},
|
|
2105
2226
|
/**
|
|
2106
2227
|
* #getter
|
|
@@ -2130,6 +2251,9 @@ export function stateModelFactory(pluginManager) {
|
|
|
2130
2251
|
self.coarseTotalBp = blocks.totalBp;
|
|
2131
2252
|
self.coarseBpPerPx = bpPerPx;
|
|
2132
2253
|
}
|
|
2254
|
+
// on every call, since a scroll through the padding before the first
|
|
2255
|
+
// region moves the viewport and leaves the blocks the same
|
|
2256
|
+
self.coarseOffsetPx = self.offsetPx;
|
|
2133
2257
|
},
|
|
2134
2258
|
}))
|
|
2135
2259
|
.actions(self => ({
|
|
@@ -2140,7 +2264,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2140
2264
|
*
|
|
2141
2265
|
* The coarse blocks are a 500ms throttle, and every consumer trades
|
|
2142
2266
|
* freshness for not recomputing per animation frame — the location box,
|
|
2143
|
-
*
|
|
2267
|
+
* the feature display's on-screen feature set, and the two per-bp scans behind
|
|
2144
2268
|
* `settledDynamicBlocks`. That trade is only sound while the answer is a
|
|
2145
2269
|
* few frames old. **A jump makes it unrelated instead**, because there is
|
|
2146
2270
|
* nothing to coalesce: the viewport left behind is not an approximation of
|
|
@@ -2148,17 +2272,16 @@ export function stateModelFactory(pluginManager) {
|
|
|
2148
2272
|
* as a navigation that didn't happen, and wiggle's autoscale domain is
|
|
2149
2273
|
* computed over the window the user just left — measured at `[0,200]`
|
|
2150
2274
|
* against a correct `[0,300]` when a 40bp coarse window survived a jump to
|
|
2151
|
-
* 4040bp on screen.
|
|
2152
|
-
* made that reachable: at 600ms the data never landed inside the hole.
|
|
2275
|
+
* 4040bp on screen.
|
|
2153
2276
|
*
|
|
2154
|
-
* **The continuous paths
|
|
2277
|
+
* **The continuous paths do not call this.** The
|
|
2155
2278
|
* spring zoom writes through `zoomTo` per frame and a drag through
|
|
2156
2279
|
* `scrollTo`, so this adds no per-frame work; the settle is idempotent
|
|
2157
2280
|
* (`setCoarseDynamicBlocks` compares block keys), so the debounced
|
|
2158
|
-
* autorun's
|
|
2281
|
+
* autorun's follow-up run costs nothing.
|
|
2159
2282
|
*
|
|
2160
|
-
* Every placer calls it, and `placersSettleCoarseBlocks.test.ts`
|
|
2161
|
-
*
|
|
2283
|
+
* Every placer calls it, and `placersSettleCoarseBlocks.test.ts` enforces
|
|
2284
|
+
* that for the next one: it scans this file for the writes and
|
|
2162
2285
|
* fails on a placer that reaches neither the settle nor its named list of
|
|
2163
2286
|
* continuous paths. Two passes by hand missed four.
|
|
2164
2287
|
*/
|
|
@@ -2168,6 +2291,42 @@ export function stateModelFactory(pluginManager) {
|
|
|
2168
2291
|
}
|
|
2169
2292
|
},
|
|
2170
2293
|
}))
|
|
2294
|
+
.actions(self => {
|
|
2295
|
+
function layOutAt(width) {
|
|
2296
|
+
self.setWidth(width);
|
|
2297
|
+
self.settleCoarseBlocks();
|
|
2298
|
+
}
|
|
2299
|
+
return {
|
|
2300
|
+
/**
|
|
2301
|
+
* #method
|
|
2302
|
+
* renders the view to SVG markup, which it returns; saves it through
|
|
2303
|
+
* FileSaver unless `save: false`. A `viewWidth` lays the view out at
|
|
2304
|
+
* that many pixels for the export, as a window that wide would, and
|
|
2305
|
+
* puts the on-screen width back after.
|
|
2306
|
+
*/
|
|
2307
|
+
// Promise<string> is stated: renderToSvg is typed against this very
|
|
2308
|
+
// model, so an inferred return makes the type reference itself (TS2456)
|
|
2309
|
+
async exportSvg(opts = {}) {
|
|
2310
|
+
const { width } = self;
|
|
2311
|
+
const { viewWidth = width } = opts;
|
|
2312
|
+
// The on-screen tracks redraw at `viewWidth` until the export ends. A
|
|
2313
|
+
// hidden copy of the view would keep them still, but it needs a
|
|
2314
|
+
// session slot that autosave and undo skip, and new ids for the view,
|
|
2315
|
+
// its tracks and displays, since widgets find them by id.
|
|
2316
|
+
if (viewWidth !== width) {
|
|
2317
|
+
layOutAt(viewWidth);
|
|
2318
|
+
}
|
|
2319
|
+
try {
|
|
2320
|
+
return await exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
|
|
2321
|
+
}
|
|
2322
|
+
finally {
|
|
2323
|
+
if (viewWidth !== width) {
|
|
2324
|
+
layOutAt(width);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2328
|
+
};
|
|
2329
|
+
})
|
|
2171
2330
|
.actions(self => ({
|
|
2172
2331
|
/**
|
|
2173
2332
|
* #action
|
|
@@ -2183,11 +2342,11 @@ export function stateModelFactory(pluginManager) {
|
|
|
2183
2342
|
},
|
|
2184
2343
|
/**
|
|
2185
2344
|
* #action
|
|
2186
|
-
* Place the viewport by
|
|
2187
|
-
*
|
|
2345
|
+
* Place the viewport by `bpPerPx` and `offsetPx`, the pixel pair the view
|
|
2346
|
+
* stored before it stored the window. Prefer `setWindow`: pixels mean nothing without the width they were
|
|
2188
2347
|
* measured at, so a round trip through here is only exact while the width
|
|
2189
2348
|
* holds still. Kept for callers that genuinely have pixels — a wheel
|
|
2190
|
-
* gesture, a rubberband
|
|
2349
|
+
* gesture, a rubberband.
|
|
2191
2350
|
*/
|
|
2192
2351
|
setNewView(bpPerPx, offsetPx) {
|
|
2193
2352
|
self.zoomTo(bpPerPx);
|
|
@@ -2197,7 +2356,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2197
2356
|
/**
|
|
2198
2357
|
* #action
|
|
2199
2358
|
* Place the viewport by the pair it is actually STORED as: the window's
|
|
2200
|
-
* width and left edge, both in bp.
|
|
2359
|
+
* width and left edge, both in bp. The bp-space counterpart of `setNewView`, and the
|
|
2201
2360
|
* right one for anything that captures a viewport and puts it back later
|
|
2202
2361
|
* (an Undo, a saved location) — those two moments can be a window resize
|
|
2203
2362
|
* apart, and this pair survives one where a pixel pair does not.
|
|
@@ -2227,8 +2386,8 @@ export function stateModelFactory(pluginManager) {
|
|
|
2227
2386
|
},
|
|
2228
2387
|
/**
|
|
2229
2388
|
* #action
|
|
2230
|
-
*
|
|
2231
|
-
* by `displayedRegionIndex`, so a set that outlives the region list has a
|
|
2389
|
+
* Settles the coarse blocks, since this is the worst jump to leave
|
|
2390
|
+
* unsettled: they are keyed by `displayedRegionIndex`, so a set that outlives the region list has a
|
|
2232
2391
|
* consumer reading one contig's data against another's blocks.
|
|
2233
2392
|
*/
|
|
2234
2393
|
setDisplayedRegions(regions) {
|
|
@@ -2238,9 +2397,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
2238
2397
|
// [minOffset, maxOffset]. scrollTo is called explicitly rather than
|
|
2239
2398
|
// left to zoomTo's internal scrollTo, which is skipped when bpPerPx
|
|
2240
2399
|
// is already in range — shrinking the region set would otherwise
|
|
2241
|
-
// strand the view scrolled past the end, on blank space.
|
|
2242
|
-
|
|
2243
|
-
|
|
2400
|
+
// strand the view scrolled past the end, on blank space. An unmeasured
|
|
2401
|
+
// view has no bounds to clamp to, and keeps its window as a restored
|
|
2402
|
+
// snapshot does.
|
|
2403
|
+
if (self.bpPerPx > 0) {
|
|
2404
|
+
self.zoomTo(self.bpPerPx);
|
|
2405
|
+
self.scrollTo(self.offsetPx);
|
|
2406
|
+
}
|
|
2244
2407
|
self.settleCoarseBlocks();
|
|
2245
2408
|
},
|
|
2246
2409
|
/**
|
|
@@ -2339,14 +2502,6 @@ export function stateModelFactory(pluginManager) {
|
|
|
2339
2502
|
clearView() {
|
|
2340
2503
|
self.setDisplayedRegions([]);
|
|
2341
2504
|
self.tracks.clear();
|
|
2342
|
-
// it is necessary to run these after setting displayed regions empty
|
|
2343
|
-
// or else model.offsetPx gets set to Infinity and breaks
|
|
2344
|
-
// @jbrowse/mobx-state-tree snapshot
|
|
2345
|
-
self.scrollTo(0);
|
|
2346
|
-
self.zoomTo(10);
|
|
2347
|
-
// after them, not through setDisplayedRegions, whose own settle ran at
|
|
2348
|
-
// the intermediate viewport these two lines exist to leave
|
|
2349
|
-
self.settleCoarseBlocks();
|
|
2350
2505
|
},
|
|
2351
2506
|
/**
|
|
2352
2507
|
* #action
|
|
@@ -2455,8 +2610,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2455
2610
|
// unrelated files lose their parameter types to it.
|
|
2456
2611
|
async navToLocString(input, optAssemblyName, grow, opts) {
|
|
2457
2612
|
const { assemblyNames } = self;
|
|
2458
|
-
const
|
|
2459
|
-
const { assemblyManager } = session;
|
|
2613
|
+
const { assemblyManager } = getSession(self);
|
|
2460
2614
|
const assemblyName = optAssemblyName || assemblyNames[0];
|
|
2461
2615
|
if (assemblyName) {
|
|
2462
2616
|
await assemblyManager.waitForAssembly(assemblyName);
|
|
@@ -2491,7 +2645,18 @@ export function stateModelFactory(pluginManager) {
|
|
|
2491
2645
|
return;
|
|
2492
2646
|
}
|
|
2493
2647
|
const { assemblyManager } = getSession(self);
|
|
2494
|
-
|
|
2648
|
+
// A one-sided `volatileWidth !== undefined` parks forever on a view
|
|
2649
|
+
// that never mounts, and `navToMultiLevelBreak` awaits a Promise.all of
|
|
2650
|
+
// these one line before the `whenViewSettled` that was written to cure
|
|
2651
|
+
// exactly that hang. `isAlive` is the second terminal state and the
|
|
2652
|
+
// guard below is what it means. NOT `self.error`, which is a transient
|
|
2653
|
+
// truth (`assembliesNotFound` while a session assembly is registering)
|
|
2654
|
+
// independent of mounting, so it would return without navigating where
|
|
2655
|
+
// waiting would have navigated correctly.
|
|
2656
|
+
await when(() => self.volatileWidth !== undefined || !isAlive(self));
|
|
2657
|
+
if (!isAlive(self) || self.volatileWidth === undefined) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2495
2660
|
// Generate locations from the parsed regions
|
|
2496
2661
|
const locations = await generateLocations({
|
|
2497
2662
|
regions,
|
|
@@ -2554,6 +2719,41 @@ export function stateModelFactory(pluginManager) {
|
|
|
2554
2719
|
async navToLocation(parsedLocString, assemblyName, grow) {
|
|
2555
2720
|
return self.navToLocations([parsedLocString], assemblyName, grow);
|
|
2556
2721
|
},
|
|
2722
|
+
/**
|
|
2723
|
+
* #action
|
|
2724
|
+
* adds the region in view to the session's highlights
|
|
2725
|
+
*/
|
|
2726
|
+
highlightCurrentRegion() {
|
|
2727
|
+
const [region] = self.visibleWholeBaseRegions;
|
|
2728
|
+
if (region) {
|
|
2729
|
+
getSession(self).addHighlight(region);
|
|
2730
|
+
}
|
|
2731
|
+
},
|
|
2732
|
+
/**
|
|
2733
|
+
* #action
|
|
2734
|
+
* navigates to a highlight, zoomed out a little for context. Goes through
|
|
2735
|
+
* a locstring so it reaches a refName the view is not displaying
|
|
2736
|
+
*/
|
|
2737
|
+
navToHighlight(highlight) {
|
|
2738
|
+
const { assemblyName, ...region } = highlight;
|
|
2739
|
+
return self.navToLocString(assembleLocString(region), assemblyName, 0.2);
|
|
2740
|
+
},
|
|
2741
|
+
}))
|
|
2742
|
+
.actions(self => ({
|
|
2743
|
+
/**
|
|
2744
|
+
* #action
|
|
2745
|
+
* navigates to the most recently added highlight on this view's
|
|
2746
|
+
* assemblies
|
|
2747
|
+
*/
|
|
2748
|
+
async navigateNewestHighlight() {
|
|
2749
|
+
const newest = self.highlights.at(-1);
|
|
2750
|
+
if (newest) {
|
|
2751
|
+
await self.navToHighlight(newest);
|
|
2752
|
+
}
|
|
2753
|
+
else {
|
|
2754
|
+
getSession(self).notify('There are no highlights to navigate to.', 'info');
|
|
2755
|
+
}
|
|
2756
|
+
},
|
|
2557
2757
|
}))
|
|
2558
2758
|
.views(self => ({
|
|
2559
2759
|
/**
|
|
@@ -2570,14 +2770,29 @@ export function stateModelFactory(pluginManager) {
|
|
|
2570
2770
|
},
|
|
2571
2771
|
/**
|
|
2572
2772
|
* #method
|
|
2573
|
-
* Map a highlight
|
|
2773
|
+
* Map a highlight region to its pixel position+width inside
|
|
2574
2774
|
* the tracks container. Falls back to the raw refName if the region's
|
|
2575
2775
|
* assemblyName is missing or unknown so highlights authored without an
|
|
2576
2776
|
* assembly still render in single-assembly views.
|
|
2577
2777
|
*/
|
|
2578
2778
|
getHighlightCoords(region) {
|
|
2579
|
-
|
|
2580
|
-
|
|
2779
|
+
return getLayoutHighlightCoords(self, canonicalHighlight(self, region));
|
|
2780
|
+
},
|
|
2781
|
+
/**
|
|
2782
|
+
* #method
|
|
2783
|
+
* y ranges, from the top of the highlight bands, of tracks whose display
|
|
2784
|
+
* sets `drawsGenomicCoordinates` to false; the bands skip them
|
|
2785
|
+
*/
|
|
2786
|
+
nonGenomicTrackBands() {
|
|
2787
|
+
const y0 = self.headerHeight + self.scalebarHeight;
|
|
2788
|
+
const { trackYOffsets } = self;
|
|
2789
|
+
return [...self.pinnedTracks, ...self.unpinnedTracks].flatMap(track => {
|
|
2790
|
+
const top = trackYOffsets.get(track.configuration.trackId);
|
|
2791
|
+
return track.activeDisplay.drawsGenomicCoordinates === false &&
|
|
2792
|
+
top !== undefined
|
|
2793
|
+
? [{ top: top - y0, height: self.trackHeight(track) }]
|
|
2794
|
+
: [];
|
|
2795
|
+
});
|
|
2581
2796
|
},
|
|
2582
2797
|
/**
|
|
2583
2798
|
* #method
|
|
@@ -2585,11 +2800,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2585
2800
|
* shifted by the cytoband offset
|
|
2586
2801
|
*/
|
|
2587
2802
|
getOverviewHighlightCoords(region) {
|
|
2588
|
-
const
|
|
2589
|
-
const coords = getLayoutHighlightCoords(self.overviewLayout, {
|
|
2590
|
-
...region,
|
|
2591
|
-
refName,
|
|
2592
|
-
});
|
|
2803
|
+
const coords = getLayoutHighlightCoords(self.overviewLayout, canonicalHighlight(self, region));
|
|
2593
2804
|
return coords
|
|
2594
2805
|
? { ...coords, left: coords.left + self.cytobandOffset }
|
|
2595
2806
|
: undefined;
|
|
@@ -2628,6 +2839,17 @@ export function stateModelFactory(pluginManager) {
|
|
|
2628
2839
|
? this.pxToBp(self.width / 2)
|
|
2629
2840
|
: undefined;
|
|
2630
2841
|
},
|
|
2842
|
+
}))
|
|
2843
|
+
.actions(self => ({
|
|
2844
|
+
/**
|
|
2845
|
+
* #action
|
|
2846
|
+
* a region edit from the ruler's menu as one action, which a synteny
|
|
2847
|
+
* stack's follow reads as the reader's gesture on this row; see
|
|
2848
|
+
* `setDisplayedRegionsKeepingCenter`
|
|
2849
|
+
*/
|
|
2850
|
+
editDisplayedRegions(edit) {
|
|
2851
|
+
setDisplayedRegionsKeepingCenter(self, edit);
|
|
2852
|
+
},
|
|
2631
2853
|
}))
|
|
2632
2854
|
.views(self => ({
|
|
2633
2855
|
/**
|
|
@@ -2638,11 +2860,10 @@ export function stateModelFactory(pluginManager) {
|
|
|
2638
2860
|
},
|
|
2639
2861
|
/**
|
|
2640
2862
|
* #method
|
|
2641
|
-
*
|
|
2642
|
-
* this via Core-extendPluggableElement to add their own items
|
|
2863
|
+
* a highlight chip's menu rows after Dismiss; extend this to add one
|
|
2643
2864
|
*/
|
|
2644
2865
|
highlightMenuItems(_highlight) {
|
|
2645
|
-
return
|
|
2866
|
+
return highlightListMenuItems(getSession(self), getEnv(self).pluginManager);
|
|
2646
2867
|
},
|
|
2647
2868
|
}))
|
|
2648
2869
|
.actions(self => {
|
|
@@ -2656,21 +2877,21 @@ export function stateModelFactory(pluginManager) {
|
|
|
2656
2877
|
let heading;
|
|
2657
2878
|
/**
|
|
2658
2879
|
* #action
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
2880
|
+
* Animate to a window along the Van Wijk arc from the current window to
|
|
2881
|
+
* the target, over a duration derived from the arc.
|
|
2661
2882
|
*
|
|
2662
|
-
*
|
|
2663
|
-
* between
|
|
2664
|
-
*
|
|
2883
|
+
* The final state is the one `setWindow` would produce; only the path in
|
|
2884
|
+
* between differs. A caller can therefore offer an Undo, a snackbar or a
|
|
2885
|
+
* follow anchor around this exactly as around the instant move.
|
|
2665
2886
|
*
|
|
2666
|
-
*
|
|
2667
|
-
* wrote and
|
|
2668
|
-
* zoom, a drag, a locstring nav, or the Undo on the
|
|
2669
|
-
* flight was launched with.
|
|
2670
|
-
*
|
|
2671
|
-
* past `maxBpPerPx` reads its
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2887
|
+
* The flight stops when anything else moves the view: it reads back what
|
|
2888
|
+
* it wrote and stops as soon as the view holds something else, such as a
|
|
2889
|
+
* wheel zoom, a drag, a locstring nav, or the Undo on the snackbar the
|
|
2890
|
+
* flight was launched with. It compares against the written value, not
|
|
2891
|
+
* the requested one, because the write clamps: an arc that pulls back
|
|
2892
|
+
* past `maxBpPerPx` reads back its clamped result, and treating that as
|
|
2893
|
+
* interference would end the flight after one frame. `springAnimate`
|
|
2894
|
+
* handles the same value the same way.
|
|
2674
2895
|
*/
|
|
2675
2896
|
function flyTo(centerBp, windowWidthBp) {
|
|
2676
2897
|
cancelLastFlight();
|
|
@@ -2737,7 +2958,7 @@ export function stateModelFactory(pluginManager) {
|
|
|
2737
2958
|
* `centerAt`'s animated twin: the same destination, reached along the
|
|
2738
2959
|
* arc instead of jumped to, at the zoom the view is already on.
|
|
2739
2960
|
*/
|
|
2740
|
-
flyToCenter(coord, refName) {
|
|
2961
|
+
flyToCenter(coord, refName, displayedRegionIndex) {
|
|
2741
2962
|
// `bpToLinearBp` rather than `bpToPx`, which rounds to a whole pixel
|
|
2742
2963
|
// at the current zoom: mid-flight that zoom is a point on somebody
|
|
2743
2964
|
// else's arc, and a destination quantized against it moves with it.
|
|
@@ -2745,11 +2966,22 @@ export function stateModelFactory(pluginManager) {
|
|
|
2745
2966
|
refName,
|
|
2746
2967
|
coord,
|
|
2747
2968
|
displayedRegions: self.displayedRegions,
|
|
2969
|
+
displayedRegionIndex,
|
|
2748
2970
|
});
|
|
2749
2971
|
if (centerBp !== undefined) {
|
|
2750
2972
|
flyTo(centerBp, heading ? heading.windowWidthBp : self.windowWidthBp);
|
|
2751
2973
|
}
|
|
2752
2974
|
},
|
|
2975
|
+
/**
|
|
2976
|
+
* #action
|
|
2977
|
+
* A flight to a linear bp centre at the zoom the view is on, or is
|
|
2978
|
+
* already flying to, widened where that is narrower than `fitWidthBp`.
|
|
2979
|
+
* The current width mid-flight is a point on the arc, so a second
|
|
2980
|
+
* flight read from it would land pulled back.
|
|
2981
|
+
*/
|
|
2982
|
+
flyToFit(centerBp, fitWidthBp) {
|
|
2983
|
+
flyTo(centerBp, Math.max(heading ? heading.windowWidthBp : self.windowWidthBp, fitWidthBp));
|
|
2984
|
+
},
|
|
2753
2985
|
};
|
|
2754
2986
|
})
|
|
2755
2987
|
.actions(self => ({
|
|
@@ -2760,23 +2992,23 @@ export function stateModelFactory(pluginManager) {
|
|
|
2760
2992
|
doAfterAttach(self);
|
|
2761
2993
|
},
|
|
2762
2994
|
}));
|
|
2763
|
-
return withLaunchInput(model, lgvLaunchKeys
|
|
2995
|
+
return withLaunchInput(model, lgvLaunchKeys, {
|
|
2996
|
+
registry: pluginManager,
|
|
2997
|
+
materialized: snap => !!snap.displayedRegions?.length,
|
|
2998
|
+
})
|
|
2764
2999
|
.preProcessSnapshot((snap) => {
|
|
2765
3000
|
if (!snap) {
|
|
2766
3001
|
return snap;
|
|
2767
3002
|
}
|
|
2768
|
-
//
|
|
2769
|
-
//
|
|
2770
|
-
//
|
|
2771
|
-
const {
|
|
2772
|
-
const legacyShowCytobands = showCytobandsSetting ?? cytobandsVisible;
|
|
3003
|
+
// v4's `showCytobandsSetting` persists as the bare `showCytobands` prop
|
|
3004
|
+
// (the capability-gated getter is `effectiveShowCytobands`), and v4's
|
|
3005
|
+
// `colorByCDS` as `colorByFrame`.
|
|
3006
|
+
const { showCytobandsSetting, colorByCDS, offsetPx, bpPerPx, ...rest } = snap;
|
|
2773
3007
|
return {
|
|
2774
|
-
|
|
2775
|
-
?
|
|
2776
|
-
: [highlight],
|
|
2777
|
-
...(legacyShowCytobands !== undefined
|
|
2778
|
-
? { showCytobands: legacyShowCytobands }
|
|
3008
|
+
...(showCytobandsSetting !== undefined
|
|
3009
|
+
? { showCytobands: showCytobandsSetting }
|
|
2779
3010
|
: {}),
|
|
3011
|
+
...(colorByCDS !== undefined ? { colorByFrame: colorByCDS } : {}),
|
|
2780
3012
|
// The viewport used to persist as pixels. Half of that converts
|
|
2781
3013
|
// exactly and needs no width — the left edge in bp is offsetPx *
|
|
2782
3014
|
// bpPerPx — and half of it cannot convert at all, because the width
|
|
@@ -2802,30 +3034,13 @@ export function stateModelFactory(pluginManager) {
|
|
|
2802
3034
|
};
|
|
2803
3035
|
})
|
|
2804
3036
|
.postProcessSnapshot(snap => {
|
|
2805
|
-
//
|
|
2806
|
-
//
|
|
2807
|
-
//
|
|
2808
|
-
//
|
|
2809
|
-
//
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
const { launch, showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
|
|
2813
|
-
return {
|
|
2814
|
-
...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
|
-
...(!showCytobands ? { showCytobands } : {}),
|
|
2824
|
-
...(trackLabels ? { trackLabels } : {}),
|
|
2825
|
-
...(colorByCDS ? { colorByCDS } : {}),
|
|
2826
|
-
...(!showAminoAcids ? { showAminoAcids } : {}),
|
|
2827
|
-
...(!showTrackOutlines ? { showTrackOutlines } : {}),
|
|
2828
|
-
};
|
|
3037
|
+
// showCenterLine is a personal preference and stays out of the session.
|
|
3038
|
+
// The other localStorage-seeded settings are always written: the
|
|
3039
|
+
// receiving browser's localStorage fills an absent one, so stripping a
|
|
3040
|
+
// default would show a share link's reader their own setting instead
|
|
3041
|
+
// of the author's.
|
|
3042
|
+
const { showCenterLine, ...rest } = snap;
|
|
3043
|
+
return rest;
|
|
2829
3044
|
});
|
|
2830
3045
|
}
|
|
2831
3046
|
// #endregion
|