@jbrowse/plugin-linear-genome-view 5.0.0-beta.11 → 5.0.0-beta.15

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 (69) 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/HeaderSearchBoxes.js +4 -1
  9. package/esm/LinearGenomeView/components/HighlightBand.d.ts +3 -2
  10. package/esm/LinearGenomeView/components/HighlightBand.js +4 -2
  11. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -2
  12. package/esm/LinearGenomeView/components/HighlightChip.js +4 -2
  13. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  14. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +7 -9
  15. package/esm/LinearGenomeView/components/MiniControls.js +8 -3
  16. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  17. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +4 -2
  18. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  19. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  20. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +4 -4
  21. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  22. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  23. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -9
  24. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  25. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +3 -8
  26. package/esm/LinearGenomeView/components/SearchResultsTable.js +4 -33
  27. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  28. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  29. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  30. package/esm/LinearGenomeView/components/TrackLabel.js +4 -1
  31. package/esm/LinearGenomeView/components/TrackLabelMenu.js +8 -12
  32. package/esm/LinearGenomeView/components/useRangeSelect.js +3 -2
  33. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  34. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  35. package/esm/LinearGenomeView/components/useSideScroll.js +11 -12
  36. package/esm/LinearGenomeView/lazyDialogs.d.ts +17 -2
  37. package/esm/LinearGenomeView/menuItems.js +32 -24
  38. package/esm/LinearGenomeView/model.d.ts +121 -65
  39. package/esm/LinearGenomeView/model.js +143 -135
  40. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +13 -0
  41. package/esm/LinearGenomeView/rExport/exportLinearView.js +98 -0
  42. package/esm/LinearGenomeView/rExport/rScript.d.ts +75 -0
  43. package/esm/LinearGenomeView/rExport/rScript.js +464 -0
  44. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +4 -7
  45. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +1 -2
  46. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +2 -2
  47. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.d.ts +1 -2
  48. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +5 -10
  49. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +1 -2
  50. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +5 -5
  51. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +1 -2
  52. package/esm/LinearGenomeView/svgcomponents/SVGView.js +3 -3
  53. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -21
  54. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  55. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +32 -17
  56. package/esm/LinearGenomeView/svgcomponents/util.d.ts +16 -13
  57. package/esm/LinearGenomeView/svgcomponents/util.js +10 -19
  58. package/esm/LinearGenomeView/util.d.ts +42 -0
  59. package/esm/LinearGenomeView/util.js +81 -0
  60. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +2 -2
  61. package/esm/displayKitTests/perRegionTestEnv.d.ts +45 -12
  62. package/esm/index.d.ts +4 -2
  63. package/esm/index.js +3 -1
  64. package/esm/searchUtils.js +1 -0
  65. package/esm/shared/RubberbandSpan.d.ts +3 -2
  66. package/esm/shared/RubberbandSpan.js +4 -2
  67. package/esm/shared/coordLabels.d.ts +6 -6
  68. package/esm/shared/coordLabels.js +7 -6
  69. package/package.json +10 -10
@@ -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
@@ -267,7 +267,10 @@ export function stateModelFactory(pluginManager) {
267
267
  trackSelectorType: types.stripDefault(types.enumeration(['hierarchical']), 'hierarchical'),
268
268
  /**
269
269
  * #property
270
- * show the "center line"
270
+ * show the "center line". A personal preference: never in a session
271
+ * snapshot. It, showCytobands, trackLabels, colorByCDS, showAminoAcids
272
+ * and showTrackOutlines start from this browser's last choice, which
273
+ * their setters record.
271
274
  */
272
275
  showCenterLine: types.optional(types.boolean, () => localStorageGetBoolean('lgv-showCenterLine', false)),
273
276
  /**
@@ -355,6 +358,12 @@ export function stateModelFactory(pluginManager) {
355
358
  * #volatile
356
359
  */
357
360
  draggingTrackId: undefined,
361
+ /**
362
+ * #volatile
363
+ * Set by a search whose hits name more than one place, until the user
364
+ * picks one or closes it.
365
+ */
366
+ searchPicker: undefined,
358
367
  /**
359
368
  * #volatile
360
369
  */
@@ -967,15 +976,13 @@ export function stateModelFactory(pluginManager) {
967
976
  * #getter
968
977
  */
969
978
  get maxOffset() {
970
- // objectively determined to keep the linear genome on the main screen
971
- return this.displayedRegionsTotalPx - MAX_OFFSET_PADDING_PX;
979
+ return offsetBounds(this.displayedRegionsTotalPx, self.width).max;
972
980
  },
973
981
  /**
974
982
  * #getter
975
983
  */
976
984
  get minOffset() {
977
- // objectively determined to keep the linear genome on the main screen
978
- return -self.width + MIN_OFFSET_PADDING_PX;
985
+ return offsetBounds(this.displayedRegionsTotalPx, self.width).min;
979
986
  },
980
987
  /**
981
988
  * #getter
@@ -1020,6 +1027,15 @@ export function stateModelFactory(pluginManager) {
1020
1027
  getActiveDisplayId(trackId) {
1021
1028
  return this.getTrack(trackId)?.activeDisplay.configuration.displayId;
1022
1029
  },
1030
+ }))
1031
+ .actions(self => ({
1032
+ /**
1033
+ * #action
1034
+ * Dismiss the search picker without choosing a place.
1035
+ */
1036
+ closeSearchPicker() {
1037
+ self.searchPicker = undefined;
1038
+ },
1023
1039
  }))
1024
1040
  .actions(self => ({
1025
1041
  /**
@@ -1027,6 +1043,7 @@ export function stateModelFactory(pluginManager) {
1027
1043
  */
1028
1044
  setShowTrackOutlines(arg) {
1029
1045
  self.showTrackOutlines = arg;
1046
+ localStorageSetBoolean('lgv-showTrackOutlines', arg);
1030
1047
  },
1031
1048
  /**
1032
1049
  * #action
@@ -1039,18 +1056,21 @@ export function stateModelFactory(pluginManager) {
1039
1056
  */
1040
1057
  setColorByCDS(flag) {
1041
1058
  self.colorByCDS = flag;
1059
+ localStorageSetBoolean('lgv-colorByCDS', flag);
1042
1060
  },
1043
1061
  /**
1044
1062
  * #action
1045
1063
  */
1046
1064
  setShowAminoAcids(flag) {
1047
1065
  self.showAminoAcids = flag;
1066
+ localStorageSetBoolean('lgv-showAminoAcids', flag);
1048
1067
  },
1049
1068
  /**
1050
1069
  * #action
1051
1070
  */
1052
1071
  setShowCytobands(flag) {
1053
1072
  self.showCytobands = flag;
1073
+ localStorageSetBoolean('lgv-showCytobands', flag);
1054
1074
  },
1055
1075
  /**
1056
1076
  * #action
@@ -1236,17 +1256,46 @@ export function stateModelFactory(pluginManager) {
1236
1256
  model: self,
1237
1257
  assemblyName,
1238
1258
  })) {
1239
- getDialogHost(self).queueDialog(handleClose => [
1240
- SearchResultsDialog,
1241
- {
1242
- model: self,
1243
- searchResults,
1244
- searchQuery,
1245
- handleClose,
1246
- assemblyName,
1247
- onPick,
1259
+ const session = getSession(self);
1260
+ const assembly = session.assemblyManager.get(assemblyName);
1261
+ const ordered = [...searchResults].sort((a, b) => Number(isOpenInView(b, self)) -
1262
+ Number(isOpenInView(a, self)));
1263
+ const picker = {
1264
+ query: searchQuery,
1265
+ assemblyName,
1266
+ rows: ordered.map(result => {
1267
+ const location = result.getLocation() ?? '';
1268
+ const trackId = result.getTrackId();
1269
+ return {
1270
+ id: result.getId(),
1271
+ label: result.getLabel(),
1272
+ location: assembly && location
1273
+ ? canonicalLocString(location, assembly)
1274
+ : location,
1275
+ trackName: trackId
1276
+ ? (session.getTrackById(trackId)?.name ?? '')
1277
+ : '',
1278
+ };
1279
+ }),
1280
+ pick: async (id) => {
1281
+ const picked = ordered.find(result => result.getId() === id);
1282
+ try {
1283
+ if (picked) {
1284
+ await onPick(picked);
1285
+ }
1286
+ }
1287
+ catch (e) {
1288
+ console.error(e);
1289
+ if (isAlive(self)) {
1290
+ getNotificationSink(self).notifyError(`${e}`, e);
1291
+ }
1292
+ }
1293
+ if (isAlive(self) && self.searchPicker === picker) {
1294
+ self.closeSearchPicker();
1295
+ }
1248
1296
  },
1249
- ]);
1297
+ };
1298
+ self.searchPicker = picker;
1250
1299
  },
1251
1300
  /**
1252
1301
  * #action
@@ -1372,12 +1421,14 @@ export function stateModelFactory(pluginManager) {
1372
1421
  */
1373
1422
  setTrackLabels(setting) {
1374
1423
  self.trackLabels = setting;
1424
+ localStorageSetItem('lgv-trackLabels', setting);
1375
1425
  },
1376
1426
  /**
1377
1427
  * #action
1378
1428
  */
1379
1429
  setShowCenterLine(b) {
1380
1430
  self.showCenterLine = b;
1431
+ localStorageSetBoolean('lgv-showCenterLine', b);
1381
1432
  },
1382
1433
  /**
1383
1434
  * #action
@@ -1426,7 +1477,8 @@ export function stateModelFactory(pluginManager) {
1426
1477
  const { bpPerPx, offsetPx: rawOffsetPx } = computeMoveToLayout(layout, leftOffset, rightOffset);
1427
1478
  // mirror Base1DView.scrollTo clamping: raw offsetPx can be far outside
1428
1479
  // 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);
1480
+ const { min, max } = offsetBounds(self.totalBp / bpPerPx, self.width);
1481
+ const offsetPx = clamp(rawOffsetPx, min, max);
1430
1482
  return wholeBaseRegions(calculateDynamicBlocks({
1431
1483
  ...layout,
1432
1484
  bpPerPx,
@@ -1477,16 +1529,6 @@ export function stateModelFactory(pluginManager) {
1477
1529
  setLaunch(launch) {
1478
1530
  self.launch = launch;
1479
1531
  },
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
1532
  }))
1491
1533
  .actions(self => {
1492
1534
  let cancelLastSlide = () => { };
@@ -1790,21 +1832,12 @@ export function stateModelFactory(pluginManager) {
1790
1832
  * than recomputing the makeTicks loop per component.
1791
1833
  */
1792
1834
  get gridlineTicks() {
1793
- const { bpPerPx } = self;
1794
1835
  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;
1836
+ return getGridlineTicks({
1837
+ blocks,
1838
+ firstBlockOffset,
1839
+ bpPerPx: self.bpPerPx,
1840
+ });
1808
1841
  },
1809
1842
  /**
1810
1843
  * #getter
@@ -1822,33 +1855,13 @@ export function stateModelFactory(pluginManager) {
1822
1855
  * reads as the same number repeated across the row.
1823
1856
  */
1824
1857
  get scalebarLabels() {
1825
- const { bpPerPx } = self;
1826
1858
  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;
1859
+ return getScalebarLabels({
1860
+ blocks,
1861
+ firstBlockOffset,
1862
+ bpPerPx: self.bpPerPx,
1863
+ orientation: self.displayedRegionsOrientation,
1864
+ });
1852
1865
  },
1853
1866
  /**
1854
1867
  * #getter
@@ -1946,38 +1959,7 @@ export function stateModelFactory(pluginManager) {
1946
1959
  // cannot drift without failing here.
1947
1960
  get paddingSpans() {
1948
1961
  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;
1962
+ return getPaddingSpans(blocks, firstBlockOffset);
1981
1963
  },
1982
1964
  /**
1983
1965
  * #getter
@@ -2050,7 +2032,7 @@ export function stateModelFactory(pluginManager) {
2050
2032
  /**
2051
2033
  * #getter
2052
2034
  * 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
2035
+ * to the track — where a right-pinned overlay such as a wiggle's color
2054
2036
  * or score legend belongs, rather than out in the empty gutter the
2055
2037
  * regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
2056
2038
  * `contentRightEdgePx` states the rule; an SVG export applies the same
@@ -2219,6 +2201,42 @@ export function stateModelFactory(pluginManager) {
2219
2201
  }
2220
2202
  },
2221
2203
  }))
2204
+ .actions(self => {
2205
+ function layOutAt(width) {
2206
+ self.setWidth(width);
2207
+ self.settleCoarseBlocks();
2208
+ }
2209
+ return {
2210
+ /**
2211
+ * #method
2212
+ * renders the view to SVG markup, which it returns; saves it through
2213
+ * FileSaver unless `save: false`. A `viewWidth` lays the view out at
2214
+ * that many pixels for the export, as a window that wide would, and
2215
+ * puts the on-screen width back after.
2216
+ */
2217
+ // Promise<string> is stated: renderToSvg is typed against this very
2218
+ // model, so an inferred return makes the type reference itself (TS2456)
2219
+ async exportSvg(opts = {}) {
2220
+ const { width } = self;
2221
+ const { viewWidth = width } = opts;
2222
+ // The on-screen tracks redraw at `viewWidth` until the export ends. A
2223
+ // hidden copy of the view would keep them still, but it needs a
2224
+ // session slot that autosave and undo skip, and new ids for the view,
2225
+ // its tracks and displays, since widgets find them by id.
2226
+ if (viewWidth !== width) {
2227
+ layOutAt(viewWidth);
2228
+ }
2229
+ try {
2230
+ return await exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
2231
+ }
2232
+ finally {
2233
+ if (viewWidth !== width) {
2234
+ layOutAt(width);
2235
+ }
2236
+ }
2237
+ },
2238
+ };
2239
+ })
2222
2240
  .actions(self => ({
2223
2241
  /**
2224
2242
  * #action
@@ -2498,8 +2516,7 @@ export function stateModelFactory(pluginManager) {
2498
2516
  // unrelated files lose their parameter types to it.
2499
2517
  async navToLocString(input, optAssemblyName, grow, opts) {
2500
2518
  const { assemblyNames } = self;
2501
- const session = getSession(self);
2502
- const { assemblyManager } = session;
2519
+ const { assemblyManager } = getSession(self);
2503
2520
  const assemblyName = optAssemblyName || assemblyNames[0];
2504
2521
  if (assemblyName) {
2505
2522
  await assemblyManager.waitForAssembly(assemblyName);
@@ -2911,22 +2928,13 @@ export function stateModelFactory(pluginManager) {
2911
2928
  };
2912
2929
  })
2913
2930
  .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
- };
2931
+ // showCenterLine is a personal preference and stays out of the session.
2932
+ // The other localStorage-seeded settings are always written: the
2933
+ // receiving browser's localStorage fills an absent one, so stripping a
2934
+ // default would show a share link's reader their own setting instead
2935
+ // of the author's.
2936
+ const { showCenterLine, ...rest } = snap;
2937
+ return rest;
2930
2938
  });
2931
2939
  }
2932
2940
  // #endregion
@@ -0,0 +1,13 @@
1
+ import type { LinearGenomeViewModel } from '../model.ts';
2
+ /** The R script that redraws the tracks open in the view, over its visible regions. */
3
+ export declare function exportLinearViewToR(view: LinearGenomeViewModel, out?: string): {
4
+ drawn: number;
5
+ script: undefined;
6
+ notes: string[];
7
+ } | {
8
+ drawn: number;
9
+ script: string;
10
+ notes: string[];
11
+ };
12
+ /** Downloads the view's R script, telling the user what it leaves out. */
13
+ export declare function downloadRScript(view: LinearGenomeViewModel): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { fullConfSnapshot, readConfObject } from '@jbrowse/core/configuration';
2
+ import { getSession } from '@jbrowse/core/util';
3
+ import { getSnapshot } from '@jbrowse/mobx-state-tree';
4
+ import { exportTracksToR } from './rScript.js';
5
+ function trackConf(track) {
6
+ return track.configuration;
7
+ }
8
+ /** The regions on screen, each displayed region cut to what the view shows of it. */
9
+ function viewRegions(view) {
10
+ const blocks = view.dynamicBlocks.contentBlocks;
11
+ if (blocks.length === 0) {
12
+ return view.displayedRegions.map(r => ({
13
+ refName: r.refName,
14
+ start: Math.floor(r.start),
15
+ end: Math.ceil(r.end),
16
+ }));
17
+ }
18
+ const byRegion = new Map();
19
+ for (const block of blocks) {
20
+ const key = block.displayedRegionIndex ?? -1;
21
+ const held = byRegion.get(key);
22
+ if (held?.refName === block.refName) {
23
+ held.start = Math.min(held.start, Math.floor(block.start));
24
+ held.end = Math.max(held.end, Math.ceil(block.end));
25
+ }
26
+ else {
27
+ byRegion.set(key, {
28
+ refName: block.refName,
29
+ start: Math.floor(block.start),
30
+ end: Math.ceil(block.end),
31
+ });
32
+ }
33
+ }
34
+ return [...byRegion.values()];
35
+ }
36
+ /** The view's assembly config as written. */
37
+ function assemblyOf(view) {
38
+ const name = view.assemblyNames[0];
39
+ const configuration = name
40
+ ? getSession(view).assemblyManager.get(name)?.configuration
41
+ : undefined;
42
+ return configuration
43
+ ? getSnapshot(configuration)
44
+ : undefined;
45
+ }
46
+ function tracksOf(view) {
47
+ const tracks = [];
48
+ for (const track of view.tracks) {
49
+ const display = track.displays[0];
50
+ if (display) {
51
+ const { type, displayId, ...written } = getSnapshot(display.configuration);
52
+ const { heightMode, height } = fullConfSnapshot(display.configuration);
53
+ tracks.push({
54
+ trackId: track.configuration.trackId,
55
+ trackName: readConfObject(trackConf(track), 'name') ||
56
+ track.configuration.trackId,
57
+ displayType: display.type,
58
+ adapter: readConfObject(trackConf(track), 'adapter'),
59
+ display: written,
60
+ heightMode,
61
+ height,
62
+ });
63
+ }
64
+ }
65
+ return tracks;
66
+ }
67
+ /** The R script that redraws the tracks open in the view, over its visible regions. */
68
+ export function exportLinearViewToR(view, out) {
69
+ return exportTracksToR({
70
+ tracks: tracksOf(view),
71
+ regions: viewRegions(view),
72
+ assembly: assemblyOf(view),
73
+ bpPerPx: view.bpPerPx,
74
+ out,
75
+ notes: view.displayedRegions.some(r => r.reversed)
76
+ ? ['view: shown right to left, where the figure runs left to right']
77
+ : [],
78
+ });
79
+ }
80
+ /** Downloads the view's R script, telling the user what it leaves out. */
81
+ export async function downloadRScript(view) {
82
+ const session = getSession(view);
83
+ try {
84
+ const { saveAs } = await import('@jbrowse/core/util/FileSaver');
85
+ const { script, notes } = exportLinearViewToR(view);
86
+ if (!script) {
87
+ session.notify(`No open track has an R export. ${notes.join('; ')}`, 'warning');
88
+ return;
89
+ }
90
+ saveAs(new Blob([script], { type: 'text/plain;charset=utf-8' }), 'jbrowse_view.R');
91
+ if (notes.length) {
92
+ session.notify(`Exported with ${notes.length} note${notes.length === 1 ? '' : 's'} in the script header`, 'info');
93
+ }
94
+ }
95
+ catch (e) {
96
+ session.notifyError(`${e}`, e);
97
+ }
98
+ }
@@ -0,0 +1,75 @@
1
+ /** A view's region, 0-based half-open, on the assembly's canonical contig. */
2
+ export interface Region {
3
+ refName: string;
4
+ start: number;
5
+ end: number;
6
+ }
7
+ export interface RExportTrack {
8
+ trackId: string;
9
+ trackName: string;
10
+ displayType: string;
11
+ adapter: Record<string, unknown>;
12
+ /** The display's config as written, its defaults left out, which the engine fills from the same schema. */
13
+ display: Record<string, unknown>;
14
+ /** The display's resolved sizing, which a feature track exports so the figure drops what the view drops. */
15
+ heightMode?: unknown;
16
+ height?: unknown;
17
+ }
18
+ /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
19
+ export declare const GGJBROWSE_VERSION = "0.10.0";
20
+ /**
21
+ * A resolved config writes an unset slot as `''`, `[]` or `{}`, which a reader
22
+ * would take for a setting. Keeps `false` and `0`, which are values.
23
+ */
24
+ export declare function withoutUnset(value: unknown): unknown;
25
+ export declare function rStr(s: string): string;
26
+ /**
27
+ * A config value as the R literal ggjbrowse hands the engine: a `list` per
28
+ * object, `c()` per vector of scalars, `list()` per vector of objects and per
29
+ * empty vector, which jsonlite would otherwise write as `{}`.
30
+ */
31
+ export declare function rValue(value: unknown): string;
32
+ /**
33
+ * The path or URL a JBrowse file location points at, as R opens it: a `uri`
34
+ * resolved against its `baseUri` as the browser's fetch resolved it, a
35
+ * `file://` URL as its path, or a `localPath`.
36
+ */
37
+ export declare function locationPath(location: unknown): string | undefined;
38
+ /**
39
+ * The view's assembly as the R value every reader takes: `jbrowse_assembly()`
40
+ * of its sequence and alias files where ggjbrowse builds the same config from
41
+ * their paths, and otherwise the config itself, its locations paths R opens.
42
+ */
43
+ export declare function assemblyArgument(snapshot: Record<string, unknown>): {
44
+ value: string;
45
+ } | {
46
+ skipped: string;
47
+ };
48
+ /** A region as JBrowse's location box writes it, 1-based and inclusive. */
49
+ export declare function locusOf(refName: string, start: number, end: number): string;
50
+ /**
51
+ * One R script for a view's tracks over its regions: a ggjbrowse reader per
52
+ * track, reading each as its display does, then `jbrowse_stack()` drawing
53
+ * them as the view stacks them. A track ggjbrowse cannot read lands in the
54
+ * header instead of stopping the figure: a script that quietly differs from
55
+ * its view says so.
56
+ */
57
+ export declare function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, bpPerPx, out, notes: viewNotes, }: {
58
+ tracks: readonly RExportTrack[];
59
+ regions: readonly Region[];
60
+ /** The view's assembly config as written, which names its sequence and its contigs' aliases. */
61
+ assembly?: Record<string, unknown>;
62
+ /** The view's zoom, which decides what a feature or quantitative display shows. */
63
+ bpPerPx?: number;
64
+ out?: string;
65
+ /** What the caller knows of the view that the figure does not show. */
66
+ notes?: readonly string[];
67
+ }): {
68
+ drawn: number;
69
+ script: undefined;
70
+ notes: string[];
71
+ } | {
72
+ drawn: number;
73
+ script: string;
74
+ notes: string[];
75
+ };