@jbrowse/plugin-linear-genome-view 5.0.0-beta.13 → 5.0.0-beta.16

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.
Files changed (63) hide show
  1. package/esm/FeatureTrack/configSchema.d.ts +2 -1
  2. package/esm/FeatureTrack/configSchema.js +2 -1
  3. package/esm/LinearGenomeView/afterAttach.js +1 -23
  4. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +6 -2
  5. package/esm/LinearGenomeView/components/ExportSvgDialog.js +31 -7
  6. package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
  7. package/esm/LinearGenomeView/components/HeaderPanControls.js +4 -2
  8. package/esm/LinearGenomeView/components/HighlightBand.d.ts +3 -2
  9. package/esm/LinearGenomeView/components/HighlightBand.js +4 -2
  10. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -2
  11. package/esm/LinearGenomeView/components/HighlightChip.js +4 -2
  12. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  13. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +4 -5
  14. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  15. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +4 -2
  16. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  17. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  18. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +4 -4
  19. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  20. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  21. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -9
  22. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  23. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +3 -8
  24. package/esm/LinearGenomeView/components/SearchResultsTable.js +4 -32
  25. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  26. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  27. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  28. package/esm/LinearGenomeView/components/TrackLabelMenu.js +8 -12
  29. package/esm/LinearGenomeView/components/useRangeSelect.js +3 -2
  30. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  31. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  32. package/esm/LinearGenomeView/components/useSideScroll.js +11 -12
  33. package/esm/LinearGenomeView/lazyDialogs.d.ts +17 -2
  34. package/esm/LinearGenomeView/menuItems.js +32 -24
  35. package/esm/LinearGenomeView/model.d.ts +197 -151
  36. package/esm/LinearGenomeView/model.js +203 -201
  37. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +13 -0
  38. package/esm/LinearGenomeView/rExport/exportLinearView.js +98 -0
  39. package/esm/LinearGenomeView/rExport/rScript.d.ts +75 -0
  40. package/esm/LinearGenomeView/rExport/rScript.js +465 -0
  41. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +3 -6
  42. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +1 -2
  43. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +2 -2
  44. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +1 -2
  45. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +4 -4
  46. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +1 -2
  47. package/esm/LinearGenomeView/svgcomponents/SVGView.js +3 -3
  48. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +22 -20
  49. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  50. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +6 -11
  51. package/esm/LinearGenomeView/svgcomponents/util.d.ts +2 -1
  52. package/esm/LinearGenomeView/util.d.ts +42 -0
  53. package/esm/LinearGenomeView/util.js +81 -0
  54. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +2 -2
  55. package/esm/displayKitTests/perRegionTestEnv.d.ts +45 -12
  56. package/esm/index.d.ts +4 -2
  57. package/esm/index.js +3 -1
  58. package/esm/searchUtils.js +1 -0
  59. package/esm/shared/RubberbandSpan.d.ts +3 -2
  60. package/esm/shared/RubberbandSpan.js +4 -2
  61. package/esm/shared/coordLabels.d.ts +6 -6
  62. package/esm/shared/coordLabels.js +7 -6
  63. package/package.json +9 -9
@@ -1,14 +1,13 @@
1
- import { lazy } from 'react';
1
+ import { canonicalLocString } from '@jbrowse/core/TextSearch/places';
2
2
  import { getConf } from '@jbrowse/core/configuration';
3
3
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
4
4
  import { exportViewSvg } from '@jbrowse/core/svg/exportViewSvg';
5
- import { assembleLocString, clamp, getBpDisplayStr, getDialogHost, getSession, getTickDisplayStr, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, springAnimate, sum, } from '@jbrowse/core/util';
5
+ import { assembleLocString, clamp, getBpDisplayStr, getNotificationSink, getSession, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, localStorageSetBoolean, localStorageSetItem, springAnimate, sum, } from '@jbrowse/core/util';
6
6
  import { bpToLinearBp, bpToPx, computeMoveToLayout, createOverviewLayout, getLayoutHighlightCoords, getOverviewRegionPxSpan, moveTo, pxToBp, } from '@jbrowse/core/util/Base1DUtils';
7
7
  import { scaleBandHeights } from '@jbrowse/core/util/bandHeight';
8
8
  import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
9
9
  import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
10
10
  import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
11
- import { tickLabelsWorthDrawing } from '@jbrowse/core/util/tickLabels';
12
11
  import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
13
12
  import { ElementId } from '@jbrowse/core/util/types/mst';
14
13
  import { highlightsOnAssemblies } from '@jbrowse/core/util/viewHighlights';
@@ -18,7 +17,7 @@ import { MIN_DISPLAY_HEIGHT } from '@jbrowse/display-kit/const';
18
17
  import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
19
18
  import { cast, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
20
19
  import { observable, when } from 'mobx';
21
- import { handleSelectedRegion, navToOption } from '../searchUtils.js';
20
+ import { handleSelectedRegion, isOpenInView, navToOption, } from '../searchUtils.js';
22
21
  import { doAfterAttach } from './afterAttach.js';
23
22
  import { getCytobands, shouldSwapTracks } from './components/util.js';
24
23
  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';
@@ -27,13 +26,7 @@ import { setupKeyboardHandler } from './keyboardHandler.js';
27
26
  import { lgvLaunchKeys } from './launchKeys.js';
28
27
  import { buildHighlightsSubMenuItems, buildMenuItems, buildRubberBandMenuItems, buildRubberbandClickMenuItems, } from './menuItems.js';
29
28
  import { sharedScaleContainerOf } from './sharedScaleContainer.js';
30
- import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getScalebarRefNameLabels, regionsOrientation, groupContiguousBlocks, labelFitsInBlock, makeBlockTicks, runRefNameLabelPx, setDisplayedRegionsKeepingCenter, tickLabelWidth, } from './util.js';
31
- // lazies
32
- const SearchResultsDialog = lazy(() => import('./components/SearchResultsDialog.js'));
33
- // The one array `paddingSpans` returns when a view has nothing to mask, so that
34
- // computed's value repeats by identity. Frozen because every consumer maps over
35
- // it and a caller that sorted in place would do so for the whole session.
36
- const NO_PADDING_SPANS = Object.freeze([]);
29
+ import { calculateVisibleLocStrings, cytobandLabelGutterWidth, expandRegion, fitAllRegionsWindow, generateLocations, getGridlineTicks, getPaddingSpans, getScalebarLabels, getScalebarRefNameLabels, regionsOrientation, setDisplayedRegionsKeepingCenter, } from './util.js';
37
30
  /**
38
31
  * Calculate the offsetPx needed to center content within a viewport.
39
32
  * Returns a negative offset when content is smaller than viewport (padding on left).
@@ -60,13 +53,18 @@ function canonicalHighlight(self, region) {
60
53
  // bpPerPx deltas smaller than this are treated as no zoom change, avoiding
61
54
  // pointless offset re-anchoring on micro-steps
62
55
  const BP_PER_PX_EPSILON = 0.000001;
63
- // px of the rightmost content kept on-screen at max scroll-right, so the genome
64
- // can't be scrolled entirely off the left edge. Shared by maxOffset and
65
- // getSelectedRegions' clamp so the two bounds can't drift apart.
66
- const MAX_OFFSET_PADDING_PX = 10;
67
- // px of the leftmost content kept on-screen at max scroll-left, mirroring
68
- // MAX_OFFSET_PADDING_PX at the other end
69
- const MIN_OFFSET_PADDING_PX = 30;
56
+ // px of blank space a view may scroll past either end of its regions, enough to
57
+ // read as an edge and too little to lose the content
58
+ const OVERSCROLL_PX = 100;
59
+ // The offsets a view can scroll to: the content edge to edge plus a little
60
+ // overscroll. Content narrower than the view collapses the range to the
61
+ // centered offset, which is what `showAllRegions` and `fitAllRegions` ask for.
62
+ function offsetBounds(contentPx, viewportPx) {
63
+ const centered = getCenteredOffsetPx(contentPx, viewportPx);
64
+ return contentPx > viewportPx
65
+ ? { min: -OVERSCROLL_PX, max: contentPx - viewportPx + OVERSCROLL_PX }
66
+ : { min: centered, max: centered };
67
+ }
70
68
  // whether two BlockSets cover the same blocks, by key and in order
71
69
  function sameBlockKeys(a, b) {
72
70
  return (a.blocks.length === b.blocks.length &&
@@ -143,6 +141,8 @@ function isTrackSelectorVisible(self) {
143
141
  /**
144
142
  * #stateModel LinearGenomeView
145
143
  * #category view
144
+ * Stacks tracks under a horizontal axis of one or more genomic regions laid
145
+ * end to end, which the user scrolls, zooms and searches.
146
146
  *
147
147
  * #example
148
148
  * A `LinearGenomeView` is what you hand-author under `defaultSession.views`, and
@@ -192,17 +192,16 @@ export function stateModelFactory(pluginManager) {
192
192
  /**
193
193
  * #property
194
194
  * Left edge of the viewport, in linearized bp — the concatenated
195
- * `displayedRegions` space that `offsetPx` indexes, which carries no
195
+ * `displayedRegions` space that `offsetPx` indexes, which has no
196
196
  * inter-region padding, so the two differ only by `bpPerPx`. May be
197
- * negative, which is the view scrolled past the left end.
197
+ * negative when the view is scrolled past the left end.
198
198
  *
199
- * The viewport is stored as the genomic WINDOW it frames rather than as
200
- * the pixels that framed it, because pixels mean nothing without the
201
- * width they were measured at and a snapshot does not carry one. Storing
202
- * them anyway is why a session authored in a 1000px window used to open
203
- * at 500px showing half the region its author was looking at, while the
204
- * same location as a `&loc=` opened correctly — the two ways to share a
205
- * view disagreed, and only the one that stores intent was right.
199
+ * The view stores the genomic window it frames rather than the pixels
200
+ * that framed it, because pixels mean nothing without the width they
201
+ * were measured at and a snapshot does not include one. A session
202
+ * authored in a 1000px window therefore shows the same region when
203
+ * opened at 500px, as the same location given as a `&loc=` does: both
204
+ * ways to share a view store intent.
206
205
  */
207
206
  windowStartBp: types.stripDefault(types.number, 0),
208
207
  /**
@@ -215,14 +214,15 @@ export function stateModelFactory(pluginManager) {
215
214
  windowWidthBp: types.stripDefault(types.number, 0),
216
215
  /**
217
216
  * #property
218
- * MIGRATION ONLY, and permanent: share links written before the window
219
- * was stored carry `bpPerPx`/`offsetPx`, and there are many of them.
217
+ * Used for migration only, and permanent: share links written before the
218
+ * window was stored contain `bpPerPx`/`offsetPx`, and there are many of
219
+ * them.
220
220
  *
221
- * A snapshot written before the window was stored carries `offsetPx` and
221
+ * A snapshot written before the window was stored has `offsetPx` and
222
222
  * `bpPerPx` but not the width they were measured at, so the window they
223
223
  * framed cannot be recovered. `windowStartBp` can (it is
224
224
  * `offsetPx * bpPerPx`, no width needed); the width in bp cannot. This
225
- * carries the old `bpPerPx` to the first measure, which adopts it at
225
+ * property holds the old `bpPerPx` until the first measure, which adopts it at
226
226
  * whatever width arrives — exactly what the old code did — and clears
227
227
  * this. So an old link keeps its old behavior rather than being
228
228
  * reinterpreted, and everything authored since restores its window.
@@ -231,9 +231,8 @@ export function stateModelFactory(pluginManager) {
231
231
  /**
232
232
  * #property
233
233
  * currently displayed regions, can be a single chromosome, arbitrary
234
- * subsections, or the entire set of chromosomes in the genome, but it not
235
- * advised to use the entire set of chromosomes if your assembly is very
236
- * fragmented
234
+ * subsections, or the entire set of chromosomes in the genome. The entire
235
+ * set is not advised if your assembly is very fragmented
237
236
  */
238
237
  displayedRegions: types.stripDefault(types.frozen(), []),
239
238
  /**
@@ -267,7 +266,10 @@ export function stateModelFactory(pluginManager) {
267
266
  trackSelectorType: types.stripDefault(types.enumeration(['hierarchical']), 'hierarchical'),
268
267
  /**
269
268
  * #property
270
- * show the "center line"
269
+ * show the "center line". A personal preference: never in a session
270
+ * snapshot. It, showCytobands, trackLabels, colorByCDS, showAminoAcids
271
+ * and showTrackOutlines start from this browser's last choice, which
272
+ * their setters record.
271
273
  */
272
274
  showCenterLine: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCenterLine', false)),
273
275
  /**
@@ -280,11 +282,11 @@ export function stateModelFactory(pluginManager) {
280
282
  /**
281
283
  * #property
282
284
  * how to display the track labels, can be "overlapping", "offset", or
283
- * "hidden", or empty string "" (which results in the
284
- * LinearGenomeViewPlugin config default being used). the resolved value
285
- * is the `effectiveTrackLabels` getter. see LinearGenomeViewPlugin
286
- * https://jbrowse.org/jb2/docs/config/lineargenomeviewconfigschema/ docs for
287
- * how conf is used
285
+ * "hidden", or empty string "" (which uses the LinearGenomeViewPlugin
286
+ * config default). The resolved value is the `effectiveTrackLabels`
287
+ * getter. See the LinearGenomeViewPlugin docs at
288
+ * https://jbrowse.org/jb2/docs/config/lineargenomeviewconfigschema/ for
289
+ * how the config is used
288
290
  */
289
291
  trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') ?? ''),
290
292
  /**
@@ -355,6 +357,12 @@ export function stateModelFactory(pluginManager) {
355
357
  * #volatile
356
358
  */
357
359
  draggingTrackId: undefined,
360
+ /**
361
+ * #volatile
362
+ * Set by a search whose hits name more than one place, until the user
363
+ * picks one or closes it.
364
+ */
365
+ searchPicker: undefined,
358
366
  /**
359
367
  * #volatile
360
368
  */
@@ -650,8 +658,8 @@ export function stateModelFactory(pluginManager) {
650
658
  },
651
659
  /**
652
660
  * #getter
653
- * What the loading screen says while `showLoading`, read off the
654
- * assembly whose load is the wait; undefined otherwise.
661
+ * The loading screen's text while `showLoading`, taken from the
662
+ * assembly being waited on; undefined otherwise.
655
663
  */
656
664
  get loading() {
657
665
  return viewLoading(this.showLoading, () => this.loadingAssembly);
@@ -705,12 +713,12 @@ export function stateModelFactory(pluginManager) {
705
713
  * `width`/`staticBlocks` throw by design before the view has been
706
714
  * measured and navigated.
707
715
  *
708
- * **Not `initialized`**, which is the trap this getter exists to close.
716
+ * Use this getter rather than `initialized`.
709
717
  * That one answers "have the assembly's regions loaded", which is only
710
718
  * the first of two async steps: navigating then populates
711
719
  * `displayedRegions`, and in the window between the two `initialized` is
712
720
  * already true while there is still nothing on screen. `showLoading`
713
- * folds in `awaitingInitNavigation`, the getter that exists for exactly
721
+ * folds in `awaitingInitNavigation`, the getter for exactly
714
722
  * that gap.
715
723
  *
716
724
  * `error` is the third outcome and is why this is not a bare
@@ -787,7 +795,7 @@ export function stateModelFactory(pluginManager) {
787
795
  },
788
796
  /**
789
797
  * #method
790
- * rendered height of a single track, collapsing to a fixed height when
798
+ * rendered height of a single track, using a fixed height when
791
799
  * minimized. Shared by trackHeights and getTrackYOffset so the two can't
792
800
  * disagree. Reads `activeDisplay` — the display TrackContainer actually
793
801
  * mounts — rather than re-picking `displays[0]`, so the view's height math
@@ -917,8 +925,8 @@ export function stateModelFactory(pluginManager) {
917
925
  * or 0 for a standalone view, a mode that is off, and a stack that cannot
918
926
  * answer yet.
919
927
  *
920
- * Pulled rather than pushed down into a volatile: the number is a
921
- * function of every row's fit, all of which move on a resize, and a
928
+ * A getter rather than a volatile the container writes: the number is a
929
+ * function of every row's fit, all of which change on a resize, and a
922
930
  * stored copy would be stale by the next layout.
923
931
  */
924
932
  get sharedFitBpPerPx() {
@@ -967,15 +975,13 @@ export function stateModelFactory(pluginManager) {
967
975
  * #getter
968
976
  */
969
977
  get maxOffset() {
970
- // objectively determined to keep the linear genome on the main screen
971
- return this.displayedRegionsTotalPx - MAX_OFFSET_PADDING_PX;
978
+ return offsetBounds(this.displayedRegionsTotalPx, self.width).max;
972
979
  },
973
980
  /**
974
981
  * #getter
975
982
  */
976
983
  get minOffset() {
977
- // objectively determined to keep the linear genome on the main screen
978
- return -self.width + MIN_OFFSET_PADDING_PX;
984
+ return offsetBounds(this.displayedRegionsTotalPx, self.width).min;
979
985
  },
980
986
  /**
981
987
  * #getter
@@ -1020,6 +1026,15 @@ export function stateModelFactory(pluginManager) {
1020
1026
  getActiveDisplayId(trackId) {
1021
1027
  return this.getTrack(trackId)?.activeDisplay.configuration.displayId;
1022
1028
  },
1029
+ }))
1030
+ .actions(self => ({
1031
+ /**
1032
+ * #action
1033
+ * Dismiss the search picker without choosing a place.
1034
+ */
1035
+ closeSearchPicker() {
1036
+ self.searchPicker = undefined;
1037
+ },
1023
1038
  }))
1024
1039
  .actions(self => ({
1025
1040
  /**
@@ -1027,6 +1042,7 @@ export function stateModelFactory(pluginManager) {
1027
1042
  */
1028
1043
  setShowTrackOutlines(arg) {
1029
1044
  self.showTrackOutlines = arg;
1045
+ localStorageSetBoolean('lgv-showTrackOutlines', arg);
1030
1046
  },
1031
1047
  /**
1032
1048
  * #action
@@ -1039,18 +1055,21 @@ export function stateModelFactory(pluginManager) {
1039
1055
  */
1040
1056
  setColorByCDS(flag) {
1041
1057
  self.colorByCDS = flag;
1058
+ localStorageSetBoolean('lgv-colorByCDS', flag);
1042
1059
  },
1043
1060
  /**
1044
1061
  * #action
1045
1062
  */
1046
1063
  setShowAminoAcids(flag) {
1047
1064
  self.showAminoAcids = flag;
1065
+ localStorageSetBoolean('lgv-showAminoAcids', flag);
1048
1066
  },
1049
1067
  /**
1050
1068
  * #action
1051
1069
  */
1052
1070
  setShowCytobands(flag) {
1053
1071
  self.showCytobands = flag;
1072
+ localStorageSetBoolean('lgv-showCytobands', flag);
1054
1073
  },
1055
1074
  /**
1056
1075
  * #action
@@ -1236,17 +1255,46 @@ export function stateModelFactory(pluginManager) {
1236
1255
  model: self,
1237
1256
  assemblyName,
1238
1257
  })) {
1239
- getDialogHost(self).queueDialog(handleClose => [
1240
- SearchResultsDialog,
1241
- {
1242
- model: self,
1243
- searchResults,
1244
- searchQuery,
1245
- handleClose,
1246
- assemblyName,
1247
- onPick,
1258
+ const session = getSession(self);
1259
+ const assembly = session.assemblyManager.get(assemblyName);
1260
+ const ordered = [...searchResults].sort((a, b) => Number(isOpenInView(b, self)) -
1261
+ Number(isOpenInView(a, self)));
1262
+ const picker = {
1263
+ query: searchQuery,
1264
+ assemblyName,
1265
+ rows: ordered.map(result => {
1266
+ const location = result.getLocation() ?? '';
1267
+ const trackId = result.getTrackId();
1268
+ return {
1269
+ id: result.getId(),
1270
+ label: result.getLabel(),
1271
+ location: assembly && location
1272
+ ? canonicalLocString(location, assembly)
1273
+ : location,
1274
+ trackName: trackId
1275
+ ? (session.getTrackById(trackId)?.name ?? '')
1276
+ : '',
1277
+ };
1278
+ }),
1279
+ pick: async (id) => {
1280
+ const picked = ordered.find(result => result.getId() === id);
1281
+ try {
1282
+ if (picked) {
1283
+ await onPick(picked);
1284
+ }
1285
+ }
1286
+ catch (e) {
1287
+ console.error(e);
1288
+ if (isAlive(self)) {
1289
+ getNotificationSink(self).notifyError(`${e}`, e);
1290
+ }
1291
+ }
1292
+ if (isAlive(self) && self.searchPicker === picker) {
1293
+ self.closeSearchPicker();
1294
+ }
1248
1295
  },
1249
- ]);
1296
+ };
1297
+ self.searchPicker = picker;
1250
1298
  },
1251
1299
  /**
1252
1300
  * #action
@@ -1372,12 +1420,14 @@ export function stateModelFactory(pluginManager) {
1372
1420
  */
1373
1421
  setTrackLabels(setting) {
1374
1422
  self.trackLabels = setting;
1423
+ localStorageSetItem('lgv-trackLabels', setting);
1375
1424
  },
1376
1425
  /**
1377
1426
  * #action
1378
1427
  */
1379
1428
  setShowCenterLine(b) {
1380
1429
  self.showCenterLine = b;
1430
+ localStorageSetBoolean('lgv-showCenterLine', b);
1381
1431
  },
1382
1432
  /**
1383
1433
  * #action
@@ -1403,7 +1453,7 @@ export function stateModelFactory(pluginManager) {
1403
1453
  /**
1404
1454
  * #method
1405
1455
  * Helper method for the fetchSequence.
1406
- * Retrieves the corresponding regions that were selected by the
1456
+ * Returns the regions the user selected with the
1407
1457
  * rubberband
1408
1458
  *
1409
1459
  * @param leftOffset - `object as {start, end, index, offset}`, offset = start
@@ -1426,7 +1476,8 @@ export function stateModelFactory(pluginManager) {
1426
1476
  const { bpPerPx, offsetPx: rawOffsetPx } = computeMoveToLayout(layout, leftOffset, rightOffset);
1427
1477
  // mirror Base1DView.scrollTo clamping: raw offsetPx can be far outside
1428
1478
  // the valid range when both offsets are oob on the same side
1429
- const offsetPx = clamp(rawOffsetPx, self.minOffset, self.totalBp / bpPerPx - MAX_OFFSET_PADDING_PX);
1479
+ const { min, max } = offsetBounds(self.totalBp / bpPerPx, self.width);
1480
+ const offsetPx = clamp(rawOffsetPx, min, max);
1430
1481
  return wholeBaseRegions(calculateDynamicBlocks({
1431
1482
  ...layout,
1432
1483
  bpPerPx,
@@ -1477,16 +1528,6 @@ export function stateModelFactory(pluginManager) {
1477
1528
  setLaunch(launch) {
1478
1529
  self.launch = launch;
1479
1530
  },
1480
- /**
1481
- * #method
1482
- * renders the view to SVG markup, which it returns; saves it through
1483
- * FileSaver unless `save: false`
1484
- */
1485
- // Promise<string> is stated: renderToSvg is typed against this very
1486
- // model, so an inferred return makes the type reference itself (TS2456)
1487
- async exportSvg(opts = {}) {
1488
- return exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
1489
- },
1490
1531
  }))
1491
1532
  .actions(self => {
1492
1533
  let cancelLastSlide = () => { };
@@ -1542,8 +1583,8 @@ export function stateModelFactory(pluginManager) {
1542
1583
  }
1543
1584
  /**
1544
1585
  * #action
1545
- * cancel an in-flight animated zoom. The animation already yields to any
1546
- * other zoom on its own, so this is for stopping it *without* changing the
1586
+ * cancel an in-flight animated zoom. Any other zoom already stops the
1587
+ * animation, so this is for stopping it *without* changing the
1547
1588
  * zoom — the slider grabbing the thumb, before it has a value to commit.
1548
1589
  */
1549
1590
  function cancelZoomAnimation() {
@@ -1571,8 +1612,8 @@ export function stateModelFactory(pluginManager) {
1571
1612
  * #getter
1572
1613
  * an ideogram only reads correctly against an entire chromosome: on a
1573
1614
  * sub-region it is a meaningless slice of bands, and the centromere shows
1574
- * up as a lone half-triangle. The chromosome needs bands of its own too:
1575
- * a cytoband file often lists only the main chromosomes, not chrM or the
1615
+ * up as a lone half-triangle. The chromosome also needs bands: a cytoband
1616
+ * file often lists only the main chromosomes, not chrM or the
1576
1617
  * unplaced scaffolds
1577
1618
  */
1578
1619
  get canShowCytobands() {
@@ -1584,7 +1625,7 @@ export function stateModelFactory(pluginManager) {
1584
1625
  /**
1585
1626
  * #getter
1586
1627
  * the `showCytobands` setting gated by whether cytobands can be shown at
1587
- * all (whole chromosome with bands of its own) and by the header
1628
+ * all (whole chromosome with bands in the cytoband file) and by the header
1588
1629
  * overview they draw in being on, which the SVG export follows too
1589
1630
  */
1590
1631
  get effectiveShowCytobands() {
@@ -1708,9 +1749,8 @@ export function stateModelFactory(pluginManager) {
1708
1749
  /**
1709
1750
  * #getter
1710
1751
  * dynamic blocks represent the exact coordinates of the currently
1711
- * visible genome regions on the screen. they are similar to static
1712
- * blocks, but static blocks can go offscreen while dynamic blocks
1713
- * represent exactly what is on screen
1752
+ * visible genome regions on the screen. Static blocks can go
1753
+ * offscreen, while dynamic blocks represent exactly what is on screen
1714
1754
  */
1715
1755
  get dynamicBlocks() {
1716
1756
  return calculateDynamicBlocks(self);
@@ -1752,12 +1792,10 @@ export function stateModelFactory(pluginManager) {
1752
1792
  * The frame exists because those three are laid out across every
1753
1793
  * displayed region rather than across the viewport — it overhangs on
1754
1794
  * both sides — so their `x` values are stable under a scroll and only
1755
- * this number moves. `scalebarRefNameLabels` is the exception and says
1756
- * so: its `transform` is already a screen x, because a sticky label's
1795
+ * this number moves. `scalebarRefNameLabels` is the exception: its `transform` is already a screen x, because a sticky label's
1757
1796
  * position is a function of the scroll rather than of block geometry.
1758
1797
  *
1759
- * **The subtraction must happen here, in float64, and that is the
1760
- * load-bearing part.** `offsetPx` is a whole-genome pixel coordinate —
1798
+ * **The subtraction must happen here, in float64.** `offsetPx` is a whole-genome pixel coordinate —
1761
1799
  * hg38 chr1 at base resolution is already past 1e10 — and a length that
1762
1800
  * size does not survive the trip through CSS: the transform matrix is
1763
1801
  * float32 by the time it reaches the compositor, where consecutive
@@ -1769,13 +1807,12 @@ export function stateModelFactory(pluginManager) {
1769
1807
  * assemblies at high zoom, which is not where anyone tests. Both
1770
1808
  * operands here are large and their difference is bounded by the
1771
1809
  * overhang (a block or two), so what reaches CSS is small and exact.
1772
- * Same rule as the GPU side, one layer up —
1810
+ * This applies the GPU side's rule one layer up; see
1773
1811
  * `agent-docs/reference/BP_PRECISION.md`.
1774
1812
  *
1775
1813
  * Published because it is the one piece of coordinate arithmetic a host
1776
1814
  * drawing its own chrome would otherwise have to know, and because
1777
- * having it written out at each call site is how the two in-tree copies
1778
- * came to disagree about rounding. Round it where the content is text
1815
+ * copies written out at each call site disagree about rounding. Round it where the content is text
1779
1816
  * (a fractional offset blurs a label); leave it alone for paths and
1780
1817
  * boxes.
1781
1818
  */
@@ -1790,21 +1827,12 @@ export function stateModelFactory(pluginManager) {
1790
1827
  * than recomputing the makeTicks loop per component.
1791
1828
  */
1792
1829
  get gridlineTicks() {
1793
- const { bpPerPx } = self;
1794
1830
  const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
1795
- const ticks = [];
1796
- // iterate merged region runs (like scalebarLabels) so a tick on an
1797
- // internal ~800px chunk boundary isn't emitted twice (once as a
1798
- // chunk's right edge, once as the next chunk's left edge)
1799
- for (const run of groupContiguousBlocks(blocks)) {
1800
- const runLeft = run.offsetPx - firstBlockOffset;
1801
- for (const { type, x } of makeBlockTicks(run, bpPerPx)) {
1802
- if (x >= 0 && x <= run.widthPx) {
1803
- ticks.push({ x: runLeft + x, major: type === 'major' });
1804
- }
1805
- }
1806
- }
1807
- return ticks;
1831
+ return getGridlineTicks({
1832
+ blocks,
1833
+ firstBlockOffset,
1834
+ bpPerPx: self.bpPerPx,
1835
+ });
1808
1836
  },
1809
1837
  /**
1810
1838
  * #getter
@@ -1822,33 +1850,13 @@ export function stateModelFactory(pluginManager) {
1822
1850
  * reads as the same number repeated across the row.
1823
1851
  */
1824
1852
  get scalebarLabels() {
1825
- const { bpPerPx } = self;
1826
1853
  const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
1827
- const labels = [];
1828
- const runs = groupContiguousBlocks(blocks);
1829
- const refNameLabelPx = runRefNameLabelPx(runs, self.displayedRegionsOrientation);
1830
- for (const [i, run] of runs.entries()) {
1831
- const runLeft = run.offsetPx - firstBlockOffset;
1832
- const runLabels = [];
1833
- for (const { base, x } of makeBlockTicks(run, bpPerPx, false)) {
1834
- const label = getTickDisplayStr(base + 1, bpPerPx);
1835
- const w = tickLabelWidth(label);
1836
- // the bold refName label pinned at the run's left edge takes
1837
- // precedence, as it does on the overview scalebar
1838
- if (x - w / 2 >= refNameLabelPx[i] &&
1839
- labelFitsInBlock(x - w / 2, w, run.widthPx)) {
1840
- runLabels.push({
1841
- x: runLeft + x,
1842
- label,
1843
- key: `${run.offsetPx}-${base}`,
1844
- });
1845
- }
1846
- }
1847
- if (tickLabelsWorthDrawing(runLabels.length)) {
1848
- labels.push(...runLabels);
1849
- }
1850
- }
1851
- return labels;
1854
+ return getScalebarLabels({
1855
+ blocks,
1856
+ firstBlockOffset,
1857
+ bpPerPx: self.bpPerPx,
1858
+ orientation: self.displayedRegionsOrientation,
1859
+ });
1852
1860
  },
1853
1861
  /**
1854
1862
  * #getter
@@ -1857,11 +1865,11 @@ export function stateModelFactory(pluginManager) {
1857
1865
  * sticky, transform, maxWidth, paddingLeft, text}` each. One per run of
1858
1866
  * same-refName regions, plus a "sticky" one pinned to the viewport's
1859
1867
  * left edge naming the refName under it, so panning into a chromosome
1860
- * does not scroll its own name off the screen.
1868
+ * does not scroll the chromosome's name off the screen.
1861
1869
  *
1862
- * Three rules live in here that a host drawing region names will
1863
- * otherwise rediscover the hard way, and two of them are invisible until
1864
- * the data is awkward:
1870
+ * This getter applies three rules that a host drawing region names would
1871
+ * otherwise have to rediscover, and two of them are invisible until the
1872
+ * data is awkward:
1865
1873
  *
1866
1874
  * - the sticky label rides the rightmost run that has scrolled off the
1867
1875
  * left, not the region's first block — that one is gone from
@@ -1873,7 +1881,7 @@ export function stateModelFactory(pluginManager) {
1873
1881
  * is fitted to the whole run, and `displayedRegionIndex` /
1874
1882
  * `lastDisplayedRegionIndex` bracket what it stands for.
1875
1883
  * - a name is drawn whole or not at all, measured against the space its
1876
- * run leaves. Clipped to its own width, `chr16` reads as `chr1` —
1884
+ * run leaves. Clipped to that space, `chr16` reads as `chr1` —
1877
1885
  * a different chromosome rather than a shortened name, which is why
1878
1886
  * this is a fit test rather than an ellipsis.
1879
1887
  *
@@ -1893,7 +1901,7 @@ export function stateModelFactory(pluginManager) {
1893
1901
  * pins to its right edge rather than the viewport's, and the coordinate
1894
1902
  * numbers stay out from under it. The SVG export deliberately
1895
1903
  * calls `getScalebarRefNameLabels` itself with no prefix rather than
1896
- * reading this, since it draws its own assembly name above the ruler.
1904
+ * reading this, since it draws the assembly name itself above the ruler.
1897
1905
  */
1898
1906
  get scalebarRefNameLabels() {
1899
1907
  return getScalebarRefNameLabels({
@@ -1908,7 +1916,7 @@ export function stateModelFactory(pluginManager) {
1908
1916
  * Every span along the row that is not track data, as plain geometry in
1909
1917
  * the staticBlocks frame — the same frame as gridlineTicks and
1910
1918
  * scalebarLabels, so one `translateX(staticBlocksTranslateX)` places all
1911
- * three. A host drawing its own chrome needs all three kinds:
1919
+ * three. A host that draws the row's chrome itself needs all three kinds:
1912
1920
  *
1913
1921
  * - `seam`: the 3px bar at a region's right edge. Displayed regions are
1914
1922
  * laid out **contiguously** (calculateStaticBlocks emits boundary
@@ -1923,11 +1931,11 @@ export function stateModelFactory(pluginManager) {
1923
1931
  * but the 24 chromosomes are under a pixel wide), so a host that
1924
1932
  * skips them leaves that part of the row empty.
1925
1933
  * - `boundary`: past the start of the first region or the end of the
1926
- * last. Greying it shows the last region's seam as the edge of a
1934
+ * last. Greying it makes the last region's seam read as the edge of a
1927
1935
  * filled area, where it would otherwise look like a line in the
1928
1936
  * middle of the track.
1929
1937
  *
1930
- * Elided blocks get no seam even though they carry the flag: at the
1938
+ * Elided blocks get no seam even though they have the flag: at the
1931
1939
  * zoom where regions elide, one bar per region would fill the row solid
1932
1940
  * grey.
1933
1941
  *
@@ -1946,38 +1954,7 @@ export function stateModelFactory(pluginManager) {
1946
1954
  // cannot drift without failing here.
1947
1955
  get paddingSpans() {
1948
1956
  const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
1949
- const spans = [];
1950
- for (const block of blocks) {
1951
- const x = block.offsetPx - firstBlockOffset;
1952
- if (block.type === 'ContentBlock') {
1953
- if (block.isRightEndOfDisplayedRegion) {
1954
- spans.push({
1955
- key: `${block.key}-sep`,
1956
- x: x + block.widthPx - 1,
1957
- width: 3,
1958
- kind: 'seam',
1959
- });
1960
- }
1961
- }
1962
- else {
1963
- spans.push({
1964
- key: block.key,
1965
- x,
1966
- width: block.widthPx,
1967
- kind: block.type === 'ElidedBlock'
1968
- ? 'elided'
1969
- : 'boundary',
1970
- });
1971
- }
1972
- }
1973
- // One shared array when there is nothing to draw, so the computed's
1974
- // value repeats and MobX's `===` stops the chain here. This is the
1975
- // COMMON case, not an edge one: a view sitting inside one contig has
1976
- // no region seam, no elision and no boundary, and that is where a
1977
- // reader spends nearly all of a session. A fresh `[]` each frame
1978
- // instead re-rendered a PaddingBlocks per track plus one for the
1979
- // container, every frame of every gesture, to draw nothing.
1980
- return spans.length > 0 ? spans : NO_PADDING_SPANS;
1957
+ return getPaddingSpans(blocks, firstBlockOffset);
1981
1958
  },
1982
1959
  /**
1983
1960
  * #getter
@@ -2050,7 +2027,7 @@ export function stateModelFactory(pluginManager) {
2050
2027
  /**
2051
2028
  * #getter
2052
2029
  * Right edge (px, viewport-relative) of the on-screen content, clamped
2053
- * to the track — where a right-pinned overlay such as a wiggle's colour
2030
+ * to the track — where a right-pinned overlay such as a wiggle's color
2054
2031
  * or score legend belongs, rather than out in the empty gutter the
2055
2032
  * regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
2056
2033
  * `contentRightEdgePx` states the rule; an SVG export applies the same
@@ -2060,7 +2037,7 @@ export function stateModelFactory(pluginManager) {
2060
2037
  * array of fresh objects on every pan and zoom frame, so a component
2061
2038
  * that derived this number itself re-rendered on every frame of every
2062
2039
  * gesture — to produce, whenever content fills the track, the unchanged
2063
- * `trackWidthPx`. Reading the scalar lets MobX's `===` stop the chain at
2040
+ * `trackWidthPx`. A component that reads the scalar lets MobX's `===` stop the chain at
2064
2041
  * this computed instead. Measured over 20 zoom frames: the wiggle
2065
2042
  * family's three body instances fell from 66 renders to 7.
2066
2043
  */
@@ -2199,17 +2176,16 @@ export function stateModelFactory(pluginManager) {
2199
2176
  * as a navigation that didn't happen, and wiggle's autoscale domain is
2200
2177
  * computed over the window the user just left — measured at `[0,200]`
2201
2178
  * against a correct `[0,300]` when a 40bp coarse window survived a jump to
2202
- * 4040bp on screen. Only the per-region fetch moving to the leading edge
2203
- * made that reachable: at 600ms the data never landed inside the hole.
2179
+ * 4040bp on screen.
2204
2180
  *
2205
- * **The continuous paths deliberately do not come through here.** The
2181
+ * **The continuous paths do not call this.** The
2206
2182
  * spring zoom writes through `zoomTo` per frame and a drag through
2207
2183
  * `scrollTo`, so this adds no per-frame work; the settle is idempotent
2208
2184
  * (`setCoarseDynamicBlocks` compares block keys), so the debounced
2209
- * autorun's own follow-up run costs nothing.
2185
+ * autorun's follow-up run costs nothing.
2210
2186
  *
2211
- * Every placer calls it, and `placersSettleCoarseBlocks.test.ts` is what
2212
- * makes that true of the next one — it scans this file for the writes and
2187
+ * Every placer calls it, and `placersSettleCoarseBlocks.test.ts` enforces
2188
+ * that for the next one: it scans this file for the writes and
2213
2189
  * fails on a placer that reaches neither the settle nor its named list of
2214
2190
  * continuous paths. Two passes by hand missed four.
2215
2191
  */
@@ -2219,6 +2195,42 @@ export function stateModelFactory(pluginManager) {
2219
2195
  }
2220
2196
  },
2221
2197
  }))
2198
+ .actions(self => {
2199
+ function layOutAt(width) {
2200
+ self.setWidth(width);
2201
+ self.settleCoarseBlocks();
2202
+ }
2203
+ return {
2204
+ /**
2205
+ * #method
2206
+ * renders the view to SVG markup, which it returns; saves it through
2207
+ * FileSaver unless `save: false`. A `viewWidth` lays the view out at
2208
+ * that many pixels for the export, as a window that wide would, and
2209
+ * puts the on-screen width back after.
2210
+ */
2211
+ // Promise<string> is stated: renderToSvg is typed against this very
2212
+ // model, so an inferred return makes the type reference itself (TS2456)
2213
+ async exportSvg(opts = {}) {
2214
+ const { width } = self;
2215
+ const { viewWidth = width } = opts;
2216
+ // The on-screen tracks redraw at `viewWidth` until the export ends. A
2217
+ // hidden copy of the view would keep them still, but it needs a
2218
+ // session slot that autosave and undo skip, and new ids for the view,
2219
+ // its tracks and displays, since widgets find them by id.
2220
+ if (viewWidth !== width) {
2221
+ layOutAt(viewWidth);
2222
+ }
2223
+ try {
2224
+ return await exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
2225
+ }
2226
+ finally {
2227
+ if (viewWidth !== width) {
2228
+ layOutAt(width);
2229
+ }
2230
+ }
2231
+ },
2232
+ };
2233
+ })
2222
2234
  .actions(self => ({
2223
2235
  /**
2224
2236
  * #action
@@ -2234,8 +2246,8 @@ export function stateModelFactory(pluginManager) {
2234
2246
  },
2235
2247
  /**
2236
2248
  * #action
2237
- * Place the viewport by the pair of PIXEL quantities it used to be stored
2238
- * as. Prefer `setWindow`: pixels mean nothing without the width they were
2249
+ * Place the viewport by `bpPerPx` and `offsetPx`, the pixel pair the view
2250
+ * stored before it stored the window. Prefer `setWindow`: pixels mean nothing without the width they were
2239
2251
  * measured at, so a round trip through here is only exact while the width
2240
2252
  * holds still. Kept for callers that genuinely have pixels — a wheel
2241
2253
  * gesture, a rubberband.
@@ -2248,7 +2260,7 @@ export function stateModelFactory(pluginManager) {
2248
2260
  /**
2249
2261
  * #action
2250
2262
  * Place the viewport by the pair it is actually STORED as: the window's
2251
- * width and left edge, both in bp. `setNewView`'s bp-space twin, and the
2263
+ * width and left edge, both in bp. The bp-space counterpart of `setNewView`, and the
2252
2264
  * right one for anything that captures a viewport and puts it back later
2253
2265
  * (an Undo, a saved location) — those two moments can be a window resize
2254
2266
  * apart, and this pair survives one where a pixel pair does not.
@@ -2278,8 +2290,8 @@ export function stateModelFactory(pluginManager) {
2278
2290
  },
2279
2291
  /**
2280
2292
  * #action
2281
- * The worst jump of them to leave unsettled: the coarse blocks are keyed
2282
- * by `displayedRegionIndex`, so a set that outlives the region list has a
2293
+ * Settles the coarse blocks, since this is the worst jump to leave
2294
+ * unsettled: they are keyed by `displayedRegionIndex`, so a set that outlives the region list has a
2283
2295
  * consumer reading one contig's data against another's blocks.
2284
2296
  */
2285
2297
  setDisplayedRegions(regions) {
@@ -2498,8 +2510,7 @@ export function stateModelFactory(pluginManager) {
2498
2510
  // unrelated files lose their parameter types to it.
2499
2511
  async navToLocString(input, optAssemblyName, grow, opts) {
2500
2512
  const { assemblyNames } = self;
2501
- const session = getSession(self);
2502
- const { assemblyManager } = session;
2513
+ const { assemblyManager } = getSession(self);
2503
2514
  const assemblyName = optAssemblyName || assemblyNames[0];
2504
2515
  if (assemblyName) {
2505
2516
  await assemblyManager.waitForAssembly(assemblyName);
@@ -2739,7 +2750,7 @@ export function stateModelFactory(pluginManager) {
2739
2750
  /**
2740
2751
  * #method
2741
2752
  * returns menu items for a highlight context menu. plugins can extend
2742
- * this via Core-extendPluggableElement to add their own items
2753
+ * this via Core-extendPluggableElement to add items
2743
2754
  */
2744
2755
  highlightMenuItems(_highlight) {
2745
2756
  return [];
@@ -2911,22 +2922,13 @@ export function stateModelFactory(pluginManager) {
2911
2922
  };
2912
2923
  })
2913
2924
  .postProcessSnapshot(snap => {
2914
- // showCenterLine is purely a localStorage-backed preference (see
2915
- // setupLocalStorageAutorun in afterAttach.ts) and is never persisted
2916
- // into session snapshots. The remaining fields are also
2917
- // localStorage-backed, but still persist when non-default: their strip
2918
- // baseline is the universal default (hardcoded here), not the
2919
- // localStorage-derived creation default, so a localStorage-set value
2920
- // stays portable across browsers.
2921
- const { showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
2922
- return {
2923
- ...rest,
2924
- ...(!showCytobands ? { showCytobands } : {}),
2925
- ...(trackLabels ? { trackLabels } : {}),
2926
- ...(colorByCDS ? { colorByCDS } : {}),
2927
- ...(!showAminoAcids ? { showAminoAcids } : {}),
2928
- ...(!showTrackOutlines ? { showTrackOutlines } : {}),
2929
- };
2925
+ // showCenterLine is a personal preference and stays out of the session.
2926
+ // The other localStorage-seeded settings are always written: the
2927
+ // receiving browser's localStorage fills an absent one, so stripping a
2928
+ // default would show a share link's reader their own setting instead
2929
+ // of the author's.
2930
+ const { showCenterLine, ...rest } = snap;
2931
+ return rest;
2930
2932
  });
2931
2933
  }
2932
2934
  // #endregion