@jbrowse/plugin-linear-genome-view 5.0.0-beta.8 → 5.0.0-beta.9

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 (76) hide show
  1. package/esm/FeatureTrack/configSchema.d.ts +140 -48
  2. package/esm/LinearGenomeView/afterAttach.d.ts +5 -5
  3. package/esm/LinearGenomeView/afterAttach.js +18 -24
  4. package/esm/LinearGenomeView/closeUps.d.ts +83 -0
  5. package/esm/LinearGenomeView/closeUps.js +211 -0
  6. package/esm/LinearGenomeView/components/AddCloseUpDialog.d.ts +19 -0
  7. package/esm/LinearGenomeView/components/AddCloseUpDialog.js +29 -0
  8. package/esm/LinearGenomeView/components/CloseUps.d.ts +5 -0
  9. package/esm/LinearGenomeView/components/CloseUps.js +75 -0
  10. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +2 -1
  11. package/esm/LinearGenomeView/components/ExportSvgDialog.js +6 -2
  12. package/esm/LinearGenomeView/components/GetSequenceDialog.js +2 -2
  13. package/esm/LinearGenomeView/components/Header.js +6 -1
  14. package/esm/LinearGenomeView/components/HeaderZoomControls.js +4 -34
  15. package/esm/LinearGenomeView/components/Highlight.js +2 -4
  16. package/esm/LinearGenomeView/components/HighlightBand.js +2 -2
  17. package/esm/LinearGenomeView/components/HighlightChip.d.ts +1 -2
  18. package/esm/LinearGenomeView/components/HighlightChip.js +5 -7
  19. package/esm/LinearGenomeView/components/ImportForm.js +2 -2
  20. package/esm/LinearGenomeView/components/LinearGenomeView.js +3 -3
  21. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +3 -2
  22. package/esm/LinearGenomeView/components/OverviewHighlight.js +1 -1
  23. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +2 -3
  24. package/esm/LinearGenomeView/components/OverviewScalebarPolygon.d.ts +7 -1
  25. package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +22 -7
  26. package/esm/LinearGenomeView/components/SVGHighlightBand.js +2 -3
  27. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +6 -21
  28. package/esm/LinearGenomeView/components/SearchBox.js +2 -2
  29. package/esm/LinearGenomeView/components/TrackContainer.js +12 -5
  30. package/esm/LinearGenomeView/components/TrackLabel.js +1 -1
  31. package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +3 -1
  32. package/esm/LinearGenomeView/components/TrackRenderingContainer.js +3 -7
  33. package/esm/LinearGenomeView/components/TracksContainer.js +7 -15
  34. package/esm/LinearGenomeView/components/fetchSequence.d.ts +1 -2
  35. package/esm/LinearGenomeView/components/useSideScroll.js +1 -1
  36. package/esm/LinearGenomeView/consts.d.ts +2 -0
  37. package/esm/LinearGenomeView/consts.js +5 -0
  38. package/esm/LinearGenomeView/headerFit.d.ts +2 -1
  39. package/esm/LinearGenomeView/headerFit.js +7 -2
  40. package/esm/LinearGenomeView/index.d.ts +1 -1
  41. package/esm/LinearGenomeView/keyboardHandler.d.ts +2 -0
  42. package/esm/LinearGenomeView/keyboardHandler.js +11 -0
  43. package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
  44. package/esm/LinearGenomeView/launchKeys.js +15 -5
  45. package/esm/LinearGenomeView/lazyDialogs.d.ts +7 -0
  46. package/esm/LinearGenomeView/lazyDialogs.js +2 -0
  47. package/esm/LinearGenomeView/menuItems.d.ts +28 -16
  48. package/esm/LinearGenomeView/menuItems.js +213 -85
  49. package/esm/LinearGenomeView/model.d.ts +367 -199
  50. package/esm/LinearGenomeView/model.js +302 -143
  51. package/esm/LinearGenomeView/sharedScaleContainer.d.ts +1 -1
  52. package/esm/LinearGenomeView/showRegionsWithUndo.js +7 -1
  53. package/esm/LinearGenomeView/svgcomponents/SVGCloseUpFrame.d.ts +5 -0
  54. package/esm/LinearGenomeView/svgcomponents/SVGCloseUpFrame.js +8 -0
  55. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +3 -4
  56. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +3 -3
  57. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +95 -24
  58. package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +4 -5
  59. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +13 -4
  60. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +4 -4
  61. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +1 -1
  62. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +52 -63
  63. package/esm/LinearGenomeView/svgcomponents/util.d.ts +8 -6
  64. package/esm/LinearGenomeView/svgcomponents/util.js +37 -16
  65. package/esm/LinearGenomeView/types.d.ts +5 -4
  66. package/esm/LinearGenomeView/util.d.ts +20 -37
  67. package/esm/LinearGenomeView/util.js +58 -141
  68. package/esm/displayKitTests/perRegionTestEnv.d.ts +64 -45
  69. package/esm/displayKitTests/perRegionTestEnv.js +6 -6
  70. package/esm/index.d.ts +9 -67
  71. package/esm/index.js +6 -22
  72. package/esm/searchUtils.d.ts +2 -3
  73. package/esm/searchUtils.js +2 -2
  74. package/package.json +7 -7
  75. package/esm/lazyPluginExports.d.ts +0 -23
  76. package/esm/lazyPluginExports.js +0 -36
@@ -1,8 +1,9 @@
1
1
  import { lazy } from 'react';
2
2
  import { getConf } from '@jbrowse/core/configuration';
3
- import { BaseViewModel, HighlightsMixin, } from '@jbrowse/core/pluggableElementTypes/models';
3
+ import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models';
4
+ import { exportViewSvg } from '@jbrowse/core/svg/exportViewSvg';
4
5
  import { VIEW_HEADER_HEIGHT } from '@jbrowse/core/ui';
5
- import { assembleLocString, clamp, getBpDisplayStr, getDialogHost, getSession, getTickDisplayStr, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, springAnimate, sum, } from '@jbrowse/core/util';
6
+ import { assembleLocString, clamp, getBpDisplayStr, getDialogHost, getSession, getTickDisplayStr, isSessionModelWithWidgets, localStorageGetBoolean, localStorageGetItem, scheduleDetachedDestroy, springAnimate, sum, } from '@jbrowse/core/util';
6
7
  import { bpToLinearBp, bpToPx, computeMoveToLayout, createOverviewLayout, getLayoutHighlightCoords, getOverviewRegionPxSpan, moveTo, pxToBp, } from '@jbrowse/core/util/Base1DUtils';
7
8
  import { wholeBaseRegions } from '@jbrowse/core/util/blockTypes';
8
9
  import calculateDynamicBlocks from '@jbrowse/core/util/calculateDynamicBlocks';
@@ -10,15 +11,17 @@ import calculateStaticBlocks from '@jbrowse/core/util/calculateStaticBlocks';
10
11
  import { tickLabelsWorthDrawing } from '@jbrowse/core/util/tickLabels';
11
12
  import { hideTrackGeneric, launchToggleTrackGeneric, launchTrackGeneric, showTrackGeneric, toggleTrackGeneric, } from '@jbrowse/core/util/tracks';
12
13
  import { ElementId } from '@jbrowse/core/util/types/mst';
13
- import { assemblyErrorMessage, computeViewStatus, } from '@jbrowse/core/util/viewStatus';
14
+ import { highlightsOnAssemblies } from '@jbrowse/core/util/viewHighlights';
15
+ import { assemblyErrorMessage, computeViewStatus, viewLoading, } from '@jbrowse/core/util/viewStatus';
14
16
  import { pendingLaunch, withLaunchInput, } from '@jbrowse/core/util/withLaunchInput';
15
17
  import { contentRightEdgePx } from '@jbrowse/display-kit/regionHost';
16
- import { cast, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
18
+ import { cast, detach, getParent, hasParent, isAlive, types, } from '@jbrowse/mobx-state-tree';
17
19
  import { observable, when } from 'mobx';
18
20
  import { handleSelectedRegion } from '../searchUtils.js';
19
21
  import { doAfterAttach } from './afterAttach.js';
22
+ import { closeUpHost, closeUpType } from './closeUps.js';
20
23
  import { shouldSwapTracks } from './components/util.js';
21
- import { HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, MIN_BP_PER_PX, MINIMIZED_TRACK_HEIGHT, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, SHOW_ALL_REGIONS_FILL, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from './consts.js';
24
+ import { CLOSE_UP_CONNECTOR_HEIGHT, HEADER_BAR_HEIGHT, HEADER_OVERVIEW_HEIGHT, MIN_BP_PER_PX, MIN_CLOSE_UP_CONNECTOR_HEIGHT, MINIMIZED_TRACK_HEIGHT, RESIZE_HANDLE_HEIGHT, SCALE_BAR_HEIGHT, SHOW_ALL_REGIONS_FILL, TRACK_OUTLINE_BORDER, TRACK_TOP_GAP, } from './consts.js';
22
25
  import { planFlight } from './flyTo.js';
23
26
  import { setupKeyboardHandler } from './keyboardHandler.js';
24
27
  import { lgvLaunchKeys } from './launchKeys.js';
@@ -167,9 +170,8 @@ function isTrackSelectorVisible(self) {
167
170
  * ```
168
171
  * `assembly`, `loc`, `tracks`, `tracklist`, `nav` and `highlight` need
169
172
  * on-attach resolution and are captured into the `launch` property below;
170
- * `colorByCDS`, `showAminoAcids`, `showCenterLine`, `trackLabels` and
171
- * `showHighlightChips` are plain view props MST restores natively. Both are
172
- * written the same way.
173
+ * `colorByCDS`, `showAminoAcids`, `showCenterLine` and `trackLabels` are plain
174
+ * view props MST restores natively. Both are written the same way.
173
175
  * At runtime the same model is driven imperatively — every property and action
174
176
  * below is reachable on `viewState.session.views[0]`:
175
177
  * ```js
@@ -181,7 +183,7 @@ function isTrackSelectorVisible(self) {
181
183
  */
182
184
  export function stateModelFactory(pluginManager) {
183
185
  const model = types
184
- .compose('LinearGenomeView', BaseViewModel, HighlightsMixin(), types.model({
186
+ .compose('LinearGenomeView', BaseViewModel, types.model({
185
187
  /**
186
188
  * #property
187
189
  */
@@ -256,6 +258,23 @@ export function stateModelFactory(pluginManager) {
256
258
  * keep the header, drop the whole-chromosome overview strip below it
257
259
  */
258
260
  hideHeaderOverview: types.stripDefault(types.boolean, false),
261
+ /**
262
+ * #property
263
+ * Closer views of the same locus stacked under the tracks, widest
264
+ * first, so the page zooms in as it reads down. Each is a
265
+ * LinearGenomeView with tracks of its own whose regions, width and
266
+ * centre this view drives; its window width is the one thing it keeps.
267
+ * See `closeUps.ts`.
268
+ */
269
+ closeUps: types.stripDefault(types.array(closeUpType(pluginManager)), []),
270
+ /**
271
+ * #property
272
+ * Height of the bands the trapezoids between this view's close-ups
273
+ * are drawn in, dragged by any one of them. One number for the stack:
274
+ * the bands are a ladder the eye reads down, and a rung of its own
275
+ * height reads as a difference in the data rather than in the drawing.
276
+ */
277
+ closeUpConnectorHeight: types.stripDefault(types.number, CLOSE_UP_CONNECTOR_HEIGHT),
259
278
  /**
260
279
  * #property
261
280
  * suppress the "No tracks active" placeholder, for an embed that opens
@@ -287,7 +306,7 @@ export function stateModelFactory(pluginManager) {
287
306
  * "hidden", or empty string "" (which results in the
288
307
  * LinearGenomeViewPlugin config default being used). the resolved value
289
308
  * is the `effectiveTrackLabels` getter. see LinearGenomeViewPlugin
290
- * https://jbrowse.org/jb2/docs/config/lineargenomeviewplugin/ docs for
309
+ * https://jbrowse.org/jb2/docs/config/lineargenomeviewconfigschema/ docs for
291
310
  * how conf is used
292
311
  */
293
312
  trackLabels: types.optional(types.string, () => localStorageGetItem('lgv-trackLabels') ?? ''),
@@ -298,7 +317,7 @@ export function stateModelFactory(pluginManager) {
298
317
  showGridlines: types.stripDefault(types.boolean, true),
299
318
  /**
300
319
  * #property
301
- * controls whether highlight/bookmark chip labels are shown inline
320
+ * controls whether highlight chip labels are shown inline
302
321
  */
303
322
  labelsVisible: types.stripDefault(types.boolean, true),
304
323
  /**
@@ -505,6 +524,14 @@ export function stateModelFactory(pluginManager) {
505
524
  const { assemblyManager } = getSession(self);
506
525
  return this.assemblyNames.map(a => assemblyManager.getDisplayName(a));
507
526
  },
527
+ /**
528
+ * #getter
529
+ * the session's highlights on this view's assemblies
530
+ */
531
+ get highlights() {
532
+ const { highlights, assemblyManager } = getSession(self);
533
+ return highlightsOnAssemblies(highlights, this.assemblyNames, assemblyManager);
534
+ },
508
535
  /**
509
536
  * #getter
510
537
  * checking if lgv is a 'top-level' view is used for toggling pin track
@@ -539,8 +566,38 @@ export function stateModelFactory(pluginManager) {
539
566
  },
540
567
  /**
541
568
  * #getter
542
- * Assembly-name prefix for the scalebar refName labels, or undefined for
543
- * none. A container view (e.g. LinearSyntenyView) opts its sub-views in by
569
+ * The census entry for this view's close-ups, which are views in
570
+ * their own right.
571
+ */
572
+ get ownViews() {
573
+ return [...self.closeUps];
574
+ },
575
+ /**
576
+ * #getter
577
+ * The close-ups as what the host reads off them. The array element
578
+ * is a late type back onto this view, so this is where it gets a shape.
579
+ */
580
+ get closeUpViews() {
581
+ return self.closeUps;
582
+ },
583
+ /**
584
+ * #getter
585
+ * Whether this view is itself a close-up of another
586
+ */
587
+ get isCloseUp() {
588
+ return !!closeUpHost(self);
589
+ },
590
+ /**
591
+ * #getter
592
+ * See `BaseViewModel.rendersDisplays`.
593
+ */
594
+ get rendersDisplays() {
595
+ return !self.scalebarOnly;
596
+ },
597
+ /**
598
+ * #getter
599
+ * Assembly name captioning the scalebar ahead of its refName labels, or
600
+ * undefined for none. A container view (e.g. LinearSyntenyView) opts its sub-views in by
544
601
  * exposing showAssemblyNameInSubviewScalebar; duck-typed rather than
545
602
  * matching a concrete view type so no upward plugin dependency is needed
546
603
  * and any container can opt in. A wrong nesting depth simply yields no
@@ -587,10 +644,23 @@ export function stateModelFactory(pluginManager) {
587
644
  * and error checks resolve it directly through here.
588
645
  */
589
646
  get initAssembly() {
590
- return self.pendingLaunch
591
- ? getSession(self).assemblyManager.get(self.pendingLaunch.assembly)
647
+ const name = this.launchAssemblyName;
648
+ return self.pendingLaunch && name !== undefined
649
+ ? getSession(self).assemblyManager.get(name)
592
650
  : undefined;
593
651
  },
652
+ /**
653
+ * #getter
654
+ * The assembly a pending launch is for: the one it names, else the one
655
+ * the view already shows. A re-launch of a materialized view (a
656
+ * `jb.setSession` document writing `loc` beside the built state) need
657
+ * not restate it; without the default the readiness gate waited on
658
+ * `assemblyManager.get(undefined)` for good. Undefined on a fresh view
659
+ * whose blob names none, which `error` reports.
660
+ */
661
+ get launchAssemblyName() {
662
+ return self.pendingLaunch?.assembly ?? self.assemblyNames[0];
663
+ },
594
664
  /**
595
665
  * #getter
596
666
  */
@@ -624,35 +694,11 @@ export function stateModelFactory(pluginManager) {
624
694
  },
625
695
  /**
626
696
  * #getter
627
- * What the spinner says. The assembly reports which of its files it is
628
- * downloading, so this is "Downloading chromosome aliases" rather than a bare
629
- * "Loading" for the part of startup that actually takes time. Falls back
630
- * once the assembly is loaded and the remaining wait is the init autorun's
631
- * own navigation, which is local.
632
- */
633
- get loadingMessage() {
634
- return this.showLoading
635
- ? this.loadingAssembly?.statusMessage || 'Loading'
636
- : undefined;
637
- },
638
- /**
639
- * #getter
640
- * Determinate fraction for the spinner's bar, when the assembly load
641
- * reports one (a whole-file download with a Content-Length)
697
+ * What the loading screen says while `showLoading`, read off the
698
+ * assembly whose load is the wait; undefined otherwise.
642
699
  */
643
- get loadingProgress() {
644
- return this.showLoading
645
- ? this.loadingAssembly?.statusProgress
646
- : undefined;
647
- },
648
- /**
649
- * #getter
650
- * The URL the assembly load is currently fetching, when the phase named
651
- * one. Only the stalled-load notice reads it — see
652
- * {@link ViewLoadingScreen}.
653
- */
654
- get loadingSource() {
655
- return this.showLoading ? this.loadingAssembly?.statusSource : undefined;
700
+ get loading() {
701
+ return viewLoading(this.showLoading, () => this.loadingAssembly);
656
702
  },
657
703
  /**
658
704
  * #getter
@@ -731,12 +777,7 @@ export function stateModelFactory(pluginManager) {
731
777
  return computeViewStatus({
732
778
  error: this.error,
733
779
  hasSomethingToShow: this.hasSomethingToShow,
734
- loading: () => this.showLoading
735
- ? {
736
- message: this.loadingMessage ?? 'Loading',
737
- progress: this.loadingProgress,
738
- }
739
- : undefined,
780
+ loading: () => this.loading,
740
781
  });
741
782
  },
742
783
  /**
@@ -791,15 +832,15 @@ export function stateModelFactory(pluginManager) {
791
832
  /**
792
833
  * #getter
793
834
  * Where the scalebar pins when the view's chrome is sticky: everything
794
- * stacked above it, which is the view's own title bar plus this view's
795
- * header. Expressed from `headerHeight` rather than re-summing its
796
- * constants — the two are the same box, and a second spelling of the sum
797
- * is free to drift from the first while both typecheck.
835
+ * stacked above it, which is the view's own title bar, where the session
836
+ * draws one, plus this view's header. Expressed from `headerHeight`
837
+ * rather than re-summing its constants — the two are the same box, and a
838
+ * second spelling of the sum is free to drift from the first while both
839
+ * typecheck.
798
840
  */
799
841
  get rubberbandTop() {
800
- return self.stickyViewHeaders
801
- ? VIEW_HEADER_HEIGHT + this.headerHeight
802
- : 0;
842
+ const titleBar = getSession(self).viewTitleBars === false ? 0 : VIEW_HEADER_HEIGHT;
843
+ return self.stickyViewHeaders ? titleBar + this.headerHeight : 0;
803
844
  },
804
845
  /**
805
846
  * #getter
@@ -906,16 +947,16 @@ export function stateModelFactory(pluginManager) {
906
947
  },
907
948
  /**
908
949
  * #getter
909
- * The zoom-out limit. This view's own fit, except while a container holds
910
- * it on a shared scale coarser than that — a small genome next to a large
911
- * one, drawn short so the two compare by length.
950
+ * The zoom-out limit. Equal to the fit for this view's regions, except
951
+ * while a container holds it on a coarser shared scale, such as a small
952
+ * genome next to a large one, drawn short so the two compare by length.
912
953
  *
913
- * Raising the LIMIT is what makes such a scale survive: written past the
914
- * limit instead, it is undone by the first thing that clamps — a wheel
915
- * tick, a rubberband, a `setDisplayedRegions` — which is how the dotplot's
916
- * locked aspect ratio once turned "zoom out" into a zoom in
917
- * (`axisMaxBpPerPx`). Here every route to a zoom clamps against the same
918
- * ceiling, so full zoom-out LANDS on the shared scale.
954
+ * The shared scale raises the limit. A bpPerPx written past the limit
955
+ * would be undone by the first operation that clamps (a wheel tick, a
956
+ * rubberband, a `setDisplayedRegions`), as with the dotplot's locked
957
+ * aspect ratio turning "zoom out" into a zoom in (`axisMaxBpPerPx`).
958
+ * Every zoom clamps against the same ceiling, so full zoom-out stops at
959
+ * the shared scale.
919
960
  */
920
961
  get maxBpPerPx() {
921
962
  return Math.max(this.fitBpPerPx, this.sharedFitBpPerPx);
@@ -959,12 +1000,12 @@ export function stateModelFactory(pluginManager) {
959
1000
  // hand-authored JSON is what fills it, so the type is not a guarantee.
960
1001
  // Naming the authoring mistake beats the downstream symptom, which is
961
1002
  // the literal string "Assembly undefined not found".
962
- if (!self.pendingLaunch.assembly) {
1003
+ if (!this.launchAssemblyName) {
963
1004
  return 'LinearGenomeView init needs an "assembly"';
964
1005
  }
965
1006
  const asm = this.initAssembly;
966
1007
  if (!asm) {
967
- return `Assembly ${self.pendingLaunch.assembly} not found`;
1008
+ return `Assembly ${this.launchAssemblyName} not found`;
968
1009
  }
969
1010
  if (asm.error) {
970
1011
  return asm.error;
@@ -1133,6 +1174,50 @@ export function stateModelFactory(pluginManager) {
1133
1174
  setHideHeaderOverview(b) {
1134
1175
  self.hideHeaderOverview = b;
1135
1176
  },
1177
+ /**
1178
+ * #action
1179
+ * Add a close-up showing `trackIds`, spanning `windowWidthBp` bases or a
1180
+ * tenth of the closest close-up there is. The stack stays widest first,
1181
+ * so a span between two existing close-ups lands between them, and one
1182
+ * wider than this view is pulled back in to it by the sync.
1183
+ *
1184
+ * Answers the close-up, which is a LinearGenomeView: its own actions
1185
+ * navigate it, name its tracks and take it away again.
1186
+ */
1187
+ addCloseUp({ windowWidthBp, trackIds = [], } = {}) {
1188
+ const closest = self.closeUpViews.at(-1);
1189
+ const width = windowWidthBp ?? (closest?.windowWidthBp ?? self.windowWidthBp) / 10;
1190
+ const centerBp = self.windowStartBp + self.windowWidthBp / 2;
1191
+ const at = self.closeUpViews.filter(closeUp => closeUp.windowWidthBp > width).length;
1192
+ self.closeUps.splice(at, 0, {
1193
+ type: 'LinearGenomeView',
1194
+ hideHeader: true,
1195
+ displayedRegions: self.displayedRegions,
1196
+ windowWidthBp: width,
1197
+ windowStartBp: centerBp - width / 2,
1198
+ });
1199
+ const closeUp = self.closeUpViews[at];
1200
+ for (const trackId of trackIds) {
1201
+ closeUp.showTrack(trackId);
1202
+ }
1203
+ return closeUp;
1204
+ },
1205
+ /**
1206
+ * #action
1207
+ */
1208
+ removeCloseUp(closeUp) {
1209
+ detach(closeUp);
1210
+ scheduleDetachedDestroy(closeUp);
1211
+ },
1212
+ /**
1213
+ * #action
1214
+ * Set the height of every band between this view's close-ups.
1215
+ * Floored at the drag surface's own height rather than at 0: the band IS
1216
+ * the handle, so a band dragged shut could never be dragged open again.
1217
+ */
1218
+ setCloseUpConnectorHeight(height) {
1219
+ self.closeUpConnectorHeight = Math.max(MIN_CLOSE_UP_CONNECTOR_HEIGHT, Math.round(height));
1220
+ },
1136
1221
  /**
1137
1222
  * #action
1138
1223
  */
@@ -1256,6 +1341,34 @@ export function stateModelFactory(pluginManager) {
1256
1341
  },
1257
1342
  }))
1258
1343
  .actions(self => ({
1344
+ /**
1345
+ * #action
1346
+ * Open a close-up over a rubberband selection, showing this view's
1347
+ * tracks. The view recentres on the span first, because every close-up
1348
+ * shares its host's centre — a close-up is a closer look at the middle of
1349
+ * this view, and a selection off to one side would otherwise open
1350
+ * somewhere else.
1351
+ *
1352
+ * The span is `computeMoveToLayout`'s, the pure half of the `moveTo` the
1353
+ * same selection's "Zoom to region" runs, so the close-up covers exactly
1354
+ * what zooming would have navigated to.
1355
+ */
1356
+ addCloseUpForSpan(start, end, { trackIds } = {}) {
1357
+ if (!start || !end) {
1358
+ return undefined;
1359
+ }
1360
+ const { bpPerPx, offsetPx } = computeMoveToLayout(self, start, end);
1361
+ const windowWidthBp = bpPerPx * self.width;
1362
+ const centerBp = offsetPx * bpPerPx + windowWidthBp / 2;
1363
+ self.scrollToBp(centerBp - self.windowWidthBp / 2);
1364
+ return self.addCloseUp({
1365
+ windowWidthBp,
1366
+ // the view's own tracks unless the caller named a list, so a close-up
1367
+ // is a closer look at what is on screen by default and an empty row
1368
+ // when someone asked for one
1369
+ trackIds: trackIds ?? self.tracks.map(track => track.configuration.trackId),
1370
+ });
1371
+ },
1259
1372
  /**
1260
1373
  * #action
1261
1374
  * showTrack for a track whose display state model may be lazily
@@ -1264,12 +1377,26 @@ export function stateModelFactory(pluginManager) {
1264
1377
  async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
1265
1378
  return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
1266
1379
  },
1380
+ }))
1381
+ .views(self => ({
1382
+ /**
1383
+ * #method
1384
+ * The shown track a move action means: the menu passes a track model's
1385
+ * own `id`, an agent passes the `trackId` it knows the track by, and
1386
+ * both name one shown track. Answers the model id, or the argument
1387
+ * unchanged when nothing shown matches, so the move reports it.
1388
+ */
1389
+ trackModelId(idOrTrackId) {
1390
+ return ((self.tracks.find(t => t.id === idOrTrackId) ??
1391
+ self.tracks.find(t => t.configuration.trackId === idOrTrackId))?.id ?? idOrTrackId);
1392
+ },
1267
1393
  }))
1268
1394
  .actions(self => ({
1269
1395
  /**
1270
1396
  * #action
1271
1397
  */
1272
- moveTrackDown(id) {
1398
+ moveTrackDown(idOrTrackId) {
1399
+ const id = self.trackModelId(idOrTrackId);
1273
1400
  const section = self.trackSection(id);
1274
1401
  const idx = section.findIndex(t => t.id === id);
1275
1402
  if (idx !== -1 && idx < section.length - 1) {
@@ -1279,7 +1406,8 @@ export function stateModelFactory(pluginManager) {
1279
1406
  /**
1280
1407
  * #action
1281
1408
  */
1282
- moveTrackUp(id) {
1409
+ moveTrackUp(idOrTrackId) {
1410
+ const id = self.trackModelId(idOrTrackId);
1283
1411
  const section = self.trackSection(id);
1284
1412
  const idx = section.findIndex(t => t.id === id);
1285
1413
  if (idx > 0) {
@@ -1289,7 +1417,8 @@ export function stateModelFactory(pluginManager) {
1289
1417
  /**
1290
1418
  * #action
1291
1419
  */
1292
- moveTrackToTop(id) {
1420
+ moveTrackToTop(idOrTrackId) {
1421
+ const id = self.trackModelId(idOrTrackId);
1293
1422
  const section = self.trackSection(id);
1294
1423
  if (section.length && section[0].id !== id) {
1295
1424
  this.moveTrack(id, section[0].id);
@@ -1298,7 +1427,8 @@ export function stateModelFactory(pluginManager) {
1298
1427
  /**
1299
1428
  * #action
1300
1429
  */
1301
- moveTrackToBottom(id) {
1430
+ moveTrackToBottom(idOrTrackId) {
1431
+ const id = self.trackModelId(idOrTrackId);
1302
1432
  const section = self.trackSection(id);
1303
1433
  const last = section[section.length - 1];
1304
1434
  if (last && last.id !== id) {
@@ -1307,15 +1437,19 @@ export function stateModelFactory(pluginManager) {
1307
1437
  },
1308
1438
  /**
1309
1439
  * #action
1440
+ * Move one shown track to another's position; each is a track model id
1441
+ * or a trackId.
1310
1442
  */
1311
- moveTrack(movingId, targetId) {
1443
+ moveTrack(moving, target) {
1444
+ const movingId = self.trackModelId(moving);
1445
+ const targetId = self.trackModelId(target);
1312
1446
  const oldIndex = self.tracks.findIndex(track => track.id === movingId);
1313
1447
  if (oldIndex === -1) {
1314
- throw new Error(`Track ID ${movingId} not found`);
1448
+ throw new Error(`No shown track has id or trackId "${moving}" — the view's tracks are ${self.tracks.map(t => t.configuration.trackId).join(', ') || 'none'}`);
1315
1449
  }
1316
1450
  const newIndex = self.tracks.findIndex(track => track.id === targetId);
1317
1451
  if (newIndex === -1) {
1318
- throw new Error(`Track ID ${targetId} not found`);
1452
+ throw new Error(`No shown track has id or trackId "${target}" — the view's tracks are ${self.tracks.map(t => t.configuration.trackId).join(', ') || 'none'}`);
1319
1453
  }
1320
1454
  // direction-aware placement: filtering out oldIndex shifts the target
1321
1455
  // left by one when dragging down (oldIndex < newIndex), so splicing at
@@ -1460,17 +1594,10 @@ export function stateModelFactory(pluginManager) {
1460
1594
  * renders the view to SVG markup, which it returns; saves it through
1461
1595
  * FileSaver unless `save: false`
1462
1596
  */
1463
- // Promise<string> is stated, against the house preference for inferred
1464
- // return types: renderToSvg is typed against this very model, so an
1465
- // inferred return makes the model type reference itself (TS2456).
1597
+ // Promise<string> is stated: renderToSvg is typed against this very
1598
+ // model, so an inferred return makes the type reference itself (TS2456)
1466
1599
  async exportSvg(opts = {}) {
1467
- const { renderToSvg } = await import('./svgcomponents/SVGLinearGenomeView.js');
1468
- const html = await renderToSvg(self, opts);
1469
- if (opts.save !== false) {
1470
- const { saveSvgAsImage } = await import('@jbrowse/core/svg/saveSvgAsImage');
1471
- await saveSvgAsImage(html, opts);
1472
- }
1473
- return html;
1600
+ return exportViewSvg(self, opts, () => import('./svgcomponents/SVGLinearGenomeView.js'));
1474
1601
  },
1475
1602
  }))
1476
1603
  .actions(self => {
@@ -1874,8 +2001,9 @@ export function stateModelFactory(pluginManager) {
1874
2001
  * into that through `scalebarDisplayPrefix`) and whether it is flipped.
1875
2002
  * A ` [rev]` there is a fact about the ROW; the same marker on a
1876
2003
  * chromosome name means that one region, which only happens under mixed
1877
- * orientation. `captionSpanPx` is the width it takes, which the
1878
- * coordinate numbers stay out from under. The SVG export deliberately
2004
+ * orientation. `captionSpanPx` is the width it takes: the sticky label
2005
+ * pins to its right edge rather than the viewport's, and the coordinate
2006
+ * numbers stay out from under it. The SVG export deliberately
1879
2007
  * calls `getScalebarRefNameLabels` itself with no prefix rather than
1880
2008
  * reading this, since it draws its own assembly name above the ruler.
1881
2009
  */
@@ -1892,36 +2020,36 @@ export function stateModelFactory(pluginManager) {
1892
2020
  * Every span along the row that is not track data, as plain geometry in
1893
2021
  * the staticBlocks frame — the same frame as gridlineTicks and
1894
2022
  * scalebarLabels, so one `translateX(staticBlocksTranslateX)` places all
1895
- * three. Three kinds, and a host drawing its own chrome needs all of
1896
- * them:
2023
+ * three. A host drawing its own chrome needs all three kinds:
1897
2024
  *
1898
2025
  * - `seam`: the 3px bar at a region's right edge. Displayed regions are
1899
- * laid out **contiguously** — calculateStaticBlocks emits boundary
1900
- * padding only before the first region and after the last — so this
2026
+ * laid out **contiguously** (calculateStaticBlocks emits boundary
2027
+ * padding only before the first region and after the last), so this
1901
2028
  * bar is the only thing separating two of them. Without it a
1902
- * two-region view reads as a one-region view scrolled somewhere
1903
- * strange. `isRightEndOfDisplayedRegion` is what marks it, and the
1904
- * right edge of the blocks currently *loaded* — which is what block
1905
- * geometry hands you — is not the same filter.
2029
+ * two-region view looks like a single region.
2030
+ * `isRightEndOfDisplayedRegion` marks it; the right edge of the
2031
+ * currently *loaded* blocks, which block geometry gives you, is a
2032
+ * different set.
1906
2033
  * - `elided`: a region too narrow to draw at this zoom. Whole-genome on
1907
- * a real assembly is mostly these — hg38 has 455 sequences and all
1908
- * but the 24 chromosomes land sub-pixel — so a host that skips them
1909
- * renders that tail as nothing at all.
2034
+ * a real assembly is mostly these (hg38 has 455 sequences, and all
2035
+ * but the 24 chromosomes are under a pixel wide), so a host that
2036
+ * skips them leaves that part of the row empty.
1910
2037
  * - `boundary`: past the start of the first region or the end of the
1911
- * last. Greying it is what makes the last region's seam read as the
1912
- * edge of a filled area rather than a rule floating in the track.
2038
+ * last. Greying it shows the last region's seam as the edge of a
2039
+ * filled area, where it would otherwise look like a line in the
2040
+ * middle of the track.
1913
2041
  *
1914
2042
  * Elided blocks get no seam even though they carry the flag: at the
1915
- * zoom where regions elide, one bar per region is a solid grey wall.
2043
+ * zoom where regions elide, one bar per region would fill the row solid
2044
+ * grey.
1916
2045
  *
1917
- * PaddingBlocks is the in-tree consumer. The SVG export is deliberately
1918
- * NOT one: `SVGRegionSeparators` walks dynamicBlocks itself and draws
1919
- * only the seam, because `elided` and `boundary` are chrome for an
1920
- * interactive row rather than information a figure carries — striped
1921
- * grey saying "regions here are too narrow to draw" is noise in a
1922
- * static image, and at whole-genome zoom it would be most of the row.
1923
- * The seam is the one that must survive: regions lay out contiguously,
1924
- * so it is all that separates two of them.
2046
+ * PaddingBlocks is the in-tree consumer. The SVG export does not use
2047
+ * it: `SVGRegionSeparators` walks dynamicBlocks itself and draws only
2048
+ * the seam, because `elided` and `boundary` mark state for an
2049
+ * interactive row. In a static figure the striped grey would cover most
2050
+ * of the row at whole-genome zoom. The export keeps the seam because
2051
+ * regions lay out contiguously and the seam is all that separates two
2052
+ * of them.
1925
2053
  */
1926
2054
  // Annotated with the shape spelled out rather than as `PaddingSpan`,
1927
2055
  // because the API doc's type column is where a host drawing its own
@@ -1993,8 +2121,8 @@ export function stateModelFactory(pluginManager) {
1993
2121
  * viewport. False both when the view holds no regions at all and when
1994
2122
  * it holds some but is scrolled entirely off them; either way there is
1995
2123
  * no visible span for the scalebar, ruler and refName labels to
1996
- * describe, which is what the SVG export's header checks before drawing
1997
- * one. Distinct from `hasDisplayedRegions`, which only asks whether the
2124
+ * describe. The SVG export checks it before drawing its header.
2125
+ * Distinct from `hasDisplayedRegions`, which only asks whether the
1998
2126
  * view has been given regions, not whether any are on screen.
1999
2127
  */
2000
2128
  get hasVisibleContent() {
@@ -2096,23 +2224,22 @@ export function stateModelFactory(pluginManager) {
2096
2224
  },
2097
2225
  /**
2098
2226
  * #getter
2099
- * **What a debounced consumer clips to**: the coarse blocks once the
2100
- * view has settled at least once, the live ones before that.
2227
+ * **The blocks a debounced consumer clips to**: the coarse blocks once
2228
+ * the view has settled at least once, the live ones before that.
2101
2229
  *
2102
- * The coarse blocks exist so a per-bp scan does not recompute on every
2103
- * animation frame during a pan or zoom — wiggle's autoscale domain, the
2104
- * alignments coverage scale and MAF's coverage band are the three — and
2105
- * while they are stale
2106
- * the answer is merely a frame or two old, which is what the debounce
2107
- * means. **Empty is different in kind.** A scan over no blocks yields no
2108
- * entries, and no entries is not a stale domain, it is the fallback one:
2109
- * `[0,1]`, which draws a line plot blank and a density plot saturated.
2110
- * That window is the 500ms between a view initializing and the coarse
2111
- * autorun's first run, and data can now land inside it — the per-region
2112
- * fetch used to be trailing-edge at 600ms, so it never did.
2230
+ * The coarse blocks stop a per-bp scan from recomputing on every
2231
+ * animation frame during a pan or zoom. Wiggle's autoscale domain, the
2232
+ * alignments coverage scale and MAF's coverage band use them. While the
2233
+ * coarse blocks are stale the result is a frame or two old, as the
2234
+ * debounce intends. **Empty coarse blocks are a different case.** A scan
2235
+ * over no blocks yields no entries, and the consumer falls back to the
2236
+ * domain `[0,1]`, which draws a line plot blank and a density plot
2237
+ * saturated. The coarse blocks are empty for the 500ms between a view
2238
+ * initializing and the coarse autorun's first run, and data can load
2239
+ * within that window.
2113
2240
  *
2114
- * One recompute at that transition, against N per frame, which is the
2115
- * trade the guard was making anyway.
2241
+ * Falling back to the live blocks costs one extra recompute at that
2242
+ * transition, compared with N per frame without the debounce.
2116
2243
  */
2117
2244
  get settledDynamicBlocks() {
2118
2245
  return self.coarseDynamicBlocks.length
@@ -2601,6 +2728,31 @@ export function stateModelFactory(pluginManager) {
2601
2728
  async navToLocation(parsedLocString, assemblyName, grow) {
2602
2729
  return self.navToLocations([parsedLocString], assemblyName, grow);
2603
2730
  },
2731
+ /**
2732
+ * #action
2733
+ * adds the region in view to the session's highlights
2734
+ */
2735
+ highlightCurrentRegion() {
2736
+ const [region] = self.visibleWholeBaseRegions;
2737
+ if (region) {
2738
+ getSession(self).addHighlight(region);
2739
+ }
2740
+ },
2741
+ /**
2742
+ * #action
2743
+ * navigates to the most recently added highlight on this view's
2744
+ * assemblies, zoomed out a little for context
2745
+ */
2746
+ async navigateNewestHighlight() {
2747
+ const newest = self.highlights.at(-1);
2748
+ if (newest) {
2749
+ const { assemblyName, ...region } = newest;
2750
+ await self.navToLocString(assembleLocString(region), assemblyName, 0.2);
2751
+ }
2752
+ else {
2753
+ getSession(self).notify('There are no highlights to navigate to.', 'info');
2754
+ }
2755
+ },
2604
2756
  }))
2605
2757
  .views(self => ({
2606
2758
  /**
@@ -2617,7 +2769,7 @@ export function stateModelFactory(pluginManager) {
2617
2769
  },
2618
2770
  /**
2619
2771
  * #method
2620
- * Map a highlight or bookmark region to its pixel position+width inside
2772
+ * Map a highlight region to its pixel position+width inside
2621
2773
  * the tracks container. Falls back to the raw refName if the region's
2622
2774
  * assemblyName is missing or unknown so highlights authored without an
2623
2775
  * assembly still render in single-assembly views.
@@ -2703,21 +2855,21 @@ export function stateModelFactory(pluginManager) {
2703
2855
  let heading;
2704
2856
  /**
2705
2857
  * #action
2706
- * Travel to a window rather than appear in it: the Van Wijk arc from
2707
- * where the view is to where it is going, played over its own duration.
2858
+ * Animate to a window along the Van Wijk arc from the current window to
2859
+ * the target, over a duration derived from the arc.
2708
2860
  *
2709
- * WHAT LANDS IS WHAT `setWindow` WOULD HAVE LANDED — only the path in
2710
- * between is new. That is what lets a caller offer an Undo, a snackbar or
2711
- * a follow anchor around this exactly as it did around the instant move.
2861
+ * The final state is the one `setWindow` would produce; only the path in
2862
+ * between differs. A caller can therefore offer an Undo, a snackbar or a
2863
+ * follow anchor around this exactly as around the instant move.
2712
2864
  *
2713
- * It YIELDS to anything else that moves the view, by reading back what it
2714
- * wrote and stopping the moment the view holds something else: a wheel
2715
- * zoom, a drag, a locstring nav, or the Undo on the very snackbar the
2716
- * flight was launched with. Compared against what was WRITTEN rather than
2717
- * what was asked for, because the write clamps — an arc that pulls back
2718
- * past `maxBpPerPx` reads its own clamped result back, and treating that
2719
- * as interference would end the flight one frame in. `springAnimate`
2720
- * defends the same value the same way.
2865
+ * The flight stops when anything else moves the view: it reads back what
2866
+ * it wrote and stops as soon as the view holds something else, such as a
2867
+ * wheel zoom, a drag, a locstring nav, or the Undo on the snackbar the
2868
+ * flight was launched with. It compares against the written value, not
2869
+ * the requested one, because the write clamps: an arc that pulls back
2870
+ * past `maxBpPerPx` reads back its clamped result, and treating that as
2871
+ * interference would end the flight after one frame. `springAnimate`
2872
+ * handles the same value the same way.
2721
2873
  */
2722
2874
  function flyTo(centerBp, windowWidthBp) {
2723
2875
  cancelLastFlight();
@@ -2798,6 +2950,16 @@ export function stateModelFactory(pluginManager) {
2798
2950
  flyTo(centerBp, heading ? heading.windowWidthBp : self.windowWidthBp);
2799
2951
  }
2800
2952
  },
2953
+ /**
2954
+ * #action
2955
+ * A flight to a linear bp centre at the zoom the view is on, or is
2956
+ * already flying to, widened where that is narrower than `fitWidthBp`.
2957
+ * The current width mid-flight is a point on the arc, so a second
2958
+ * flight read from it would land pulled back.
2959
+ */
2960
+ flyToFit(centerBp, fitWidthBp) {
2961
+ flyTo(centerBp, Math.max(heading ? heading.windowWidthBp : self.windowWidthBp, fitWidthBp));
2962
+ },
2801
2963
  };
2802
2964
  })
2803
2965
  .actions(self => ({
@@ -2819,12 +2981,9 @@ export function stateModelFactory(pluginManager) {
2819
2981
  // The cytobands setting has been `showCytobandsSetting` and (briefly)
2820
2982
  // `cytobandsVisible`; both now persist as the bare `showCytobands` prop
2821
2983
  // (the capability-gated getter is `effectiveShowCytobands`).
2822
- const { highlight, showCytobandsSetting, cytobandsVisible, offsetPx, bpPerPx, ...rest } = snap;
2984
+ const { showCytobandsSetting, cytobandsVisible, offsetPx, bpPerPx, ...rest } = snap;
2823
2985
  const legacyShowCytobands = showCytobandsSetting ?? cytobandsVisible;
2824
2986
  return {
2825
- highlight: Array.isArray(highlight) || highlight === undefined
2826
- ? highlight
2827
- : [highlight],
2828
2987
  ...(legacyShowCytobands !== undefined
2829
2988
  ? { showCytobands: legacyShowCytobands }
2830
2989
  : {}),