@jbrowse/plugin-linear-genome-view 5.0.0-beta.1 → 5.0.0-beta.3

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