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

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 (32) hide show
  1. package/esm/LaunchLinearGenomeView/index.js +2 -13
  2. package/esm/LinearGenomeView/afterAttach.js +16 -18
  3. package/esm/LinearGenomeView/components/PaddingBlocks.d.ts +1 -1
  4. package/esm/LinearGenomeView/components/PaddingBlocks.js +11 -1
  5. package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +1 -1
  6. package/esm/LinearGenomeView/components/ZoomButton.js +6 -1
  7. package/esm/LinearGenomeView/components/ZoomTransform.js +4 -0
  8. package/esm/LinearGenomeView/components/searchModes.d.ts +1 -1
  9. package/esm/LinearGenomeView/components/searchModes.js +1 -1
  10. package/esm/LinearGenomeView/index.d.ts +1 -0
  11. package/esm/LinearGenomeView/index.js +8 -1
  12. package/esm/LinearGenomeView/initKeys.d.ts +0 -1
  13. package/esm/LinearGenomeView/initKeys.js +9 -45
  14. package/esm/LinearGenomeView/launchKeys.d.ts +2 -0
  15. package/esm/LinearGenomeView/launchKeys.js +23 -0
  16. package/esm/LinearGenomeView/lazyDialogs.d.ts +1 -1
  17. package/esm/LinearGenomeView/linkedViewSync.d.ts +1 -1
  18. package/esm/LinearGenomeView/linkedViewSync.js +6 -0
  19. package/esm/LinearGenomeView/model.d.ts +1734 -22
  20. package/esm/LinearGenomeView/model.js +121 -57
  21. package/esm/LinearGenomeView/svgcomponents/figureContract.js +14 -14
  22. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +2 -2
  23. package/esm/LinearGenomeView/types.d.ts +1 -1
  24. package/esm/displayKitTests/perRegionTestEnv.d.ts +51 -28
  25. package/esm/displayKitTests/perRegionTestEnv.js +0 -3
  26. package/esm/index.d.ts +14 -2
  27. package/esm/index.js +1 -2
  28. package/esm/searchUtils.d.ts +6 -3
  29. package/esm/searchUtils.js +37 -11
  30. package/package.json +8 -8
  31. package/esm/LinearGenomeView/FloatingSvgOverlay.d.ts +0 -5
  32. package/esm/LinearGenomeView/FloatingSvgOverlay.js +0 -25
@@ -8,9 +8,11 @@ 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
17
  import { when } from 'mobx';
16
18
  import { handleSelectedRegion } from '../searchUtils.js';
@@ -19,11 +21,16 @@ 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,20 +326,14 @@ 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
338
  .volatile(self => {
340
339
  // typed locals so `unknown`/`Record` aren't narrowed to `undefined`/`{}`; inline
@@ -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.
@@ -555,10 +562,7 @@ export function stateModelFactory(pluginManager) {
555
562
  */
556
563
  get assemblyErrors() {
557
564
  const { assemblyManager } = getSession(self);
558
- return self.assemblyNames
559
- .map(name => assemblyManager.get(name)?.error)
560
- .filter(e => !!e)
561
- .join(', ');
565
+ return assemblyErrorMessage(assemblyManager, self.assemblyNames);
562
566
  },
563
567
  /**
564
568
  * #getter
@@ -575,8 +579,8 @@ export function stateModelFactory(pluginManager) {
575
579
  * and error checks resolve it directly through here.
576
580
  */
577
581
  get initAssembly() {
578
- return self.init
579
- ? getSession(self).assemblyManager.get(self.init.assembly)
582
+ return self.pendingLaunch
583
+ ? getSession(self).assemblyManager.get(self.pendingLaunch.assembly)
580
584
  : undefined;
581
585
  },
582
586
  /**
@@ -587,7 +591,7 @@ export function stateModelFactory(pluginManager) {
587
591
  return false;
588
592
  }
589
593
  // if init is set, wait for that assembly to have regions loaded
590
- if (self.init) {
594
+ if (self.pendingLaunch) {
591
595
  const asm = this.initAssembly;
592
596
  return !!(asm?.initialized && asm.regions);
593
597
  }
@@ -606,7 +610,7 @@ export function stateModelFactory(pluginManager) {
606
610
  * displayed assemblies that hasn't finished loading.
607
611
  */
608
612
  get loadingAssembly() {
609
- return self.init
613
+ return self.pendingLaunch
610
614
  ? this.initAssembly
611
615
  : getSession(self).assemblyManager.loadingAssembly(self.assemblyNames);
612
616
  },
@@ -646,7 +650,7 @@ export function stateModelFactory(pluginManager) {
646
650
  * #getter
647
651
  */
648
652
  get hasSomethingToShow() {
649
- return this.hasDisplayedRegions || !!self.init;
653
+ return this.hasDisplayedRegions || !!self.pendingLaunch;
650
654
  },
651
655
  /**
652
656
  * #getter
@@ -656,7 +660,7 @@ export function stateModelFactory(pluginManager) {
656
660
  * would mount over empty regions and pxToBp/hover would throw.
657
661
  *
658
662
  * **Not dotplot's or synteny's `initPending`**, which is the bare
659
- * `!!self.init` — "an init blob has not been applied" — and which those
663
+ * `!!self.pendingLaunch` — "an init blob has not been applied" — and which those
660
664
  * views read from their `settled` gate rather than from `showLoading`.
661
665
  * This one is narrower on purpose: it closes only the window where there
662
666
  * is nothing on screen. Once navigation has produced regions the view is
@@ -666,7 +670,7 @@ export function stateModelFactory(pluginManager) {
666
670
  * theirs.
667
671
  */
668
672
  get awaitingInitNavigation() {
669
- return !!self.init && !this.hasDisplayedRegions;
673
+ return !!self.pendingLaunch && !this.hasDisplayedRegions;
670
674
  },
671
675
  /**
672
676
  * #getter
@@ -935,17 +939,17 @@ export function stateModelFactory(pluginManager) {
935
939
  return this.assembliesNotFound;
936
940
  }
937
941
  // Check init assembly for errors (displayedRegions may be empty during init)
938
- if (self.init) {
942
+ if (self.pendingLaunch) {
939
943
  // `assembly` is required by InitState, but init is a frozen blob and
940
944
  // hand-authored JSON is what fills it, so the type is not a guarantee.
941
945
  // Naming the authoring mistake beats the downstream symptom, which is
942
946
  // the literal string "Assembly undefined not found".
943
- if (!self.init.assembly) {
947
+ if (!self.pendingLaunch.assembly) {
944
948
  return 'LinearGenomeView init needs an "assembly"';
945
949
  }
946
950
  const asm = this.initAssembly;
947
951
  if (!asm) {
948
- return `Assembly ${self.init.assembly} not found`;
952
+ return `Assembly ${self.pendingLaunch.assembly} not found`;
949
953
  }
950
954
  if (asm.error) {
951
955
  return asm.error;
@@ -1221,6 +1225,16 @@ export function stateModelFactory(pluginManager) {
1221
1225
  hideTrack(trackId) {
1222
1226
  return hideTrackGeneric(self, trackId);
1223
1227
  },
1228
+ }))
1229
+ .actions(self => ({
1230
+ /**
1231
+ * #action
1232
+ * showTrack for a track whose display state model may be lazily
1233
+ * loaded: loads it, then shows
1234
+ */
1235
+ async launchTrack(trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
1236
+ return launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
1237
+ },
1224
1238
  }))
1225
1239
  .actions(self => ({
1226
1240
  /**
@@ -1289,6 +1303,13 @@ export function stateModelFactory(pluginManager) {
1289
1303
  toggleTrack(trackId) {
1290
1304
  return toggleTrackGeneric(self, trackId);
1291
1305
  },
1306
+ /**
1307
+ * #action
1308
+ * toggleTrack with launchTrack's loading behavior
1309
+ */
1310
+ async launchToggleTrack(trackId) {
1311
+ return launchToggleTrackGeneric(self, trackId);
1312
+ },
1292
1313
  /**
1293
1314
  * #action
1294
1315
  */
@@ -1402,8 +1423,8 @@ export function stateModelFactory(pluginManager) {
1402
1423
  /**
1403
1424
  * #action
1404
1425
  */
1405
- setInit(arg) {
1406
- self.init = arg;
1426
+ setLaunch(launch) {
1427
+ self.launch = launch;
1407
1428
  },
1408
1429
  /**
1409
1430
  * #method
@@ -1866,6 +1887,11 @@ export function stateModelFactory(pluginManager) {
1866
1887
  * The seam is the one that must survive: regions lay out contiguously,
1867
1888
  * so it is all that separates two of them.
1868
1889
  */
1890
+ // Annotated with the shape spelled out rather than as `PaddingSpan`,
1891
+ // because the API doc's type column is where a host drawing its own
1892
+ // chrome reads the field names, and a named type prints as its name.
1893
+ // `spans` and NO_PADDING_SPANS are both `PaddingSpan[]`, so the two
1894
+ // cannot drift without failing here.
1869
1895
  get paddingSpans() {
1870
1896
  const { blocks, offsetPx: firstBlockOffset } = this.staticBlocks;
1871
1897
  const spans = [];
@@ -1892,7 +1918,14 @@ export function stateModelFactory(pluginManager) {
1892
1918
  });
1893
1919
  }
1894
1920
  }
1895
- return spans;
1921
+ // One shared array when there is nothing to draw, so the computed's
1922
+ // value repeats and MobX's `===` stops the chain here. This is the
1923
+ // COMMON case, not an edge one: a view sitting inside one contig has
1924
+ // no region seam, no elision and no boundary, and that is where a
1925
+ // reader spends nearly all of a session. A fresh `[]` each frame
1926
+ // instead re-rendered a PaddingBlocks per track plus one for the
1927
+ // container, every frame of every gesture, to draw nothing.
1928
+ return spans.length > 0 ? spans : NO_PADDING_SPANS;
1896
1929
  },
1897
1930
  /**
1898
1931
  * #getter
@@ -1962,6 +1995,32 @@ export function stateModelFactory(pluginManager) {
1962
1995
  };
1963
1996
  });
1964
1997
  },
1998
+ /**
1999
+ * #getter
2000
+ * Right edge (px, viewport-relative) of the on-screen content, clamped
2001
+ * to the track — where a right-pinned overlay such as a wiggle's colour
2002
+ * or score legend belongs, rather than out in the empty gutter the
2003
+ * regions can leave at whole-genome zoom. `@jbrowse/display-kit`'s
2004
+ * `contentRightEdgePx` states the rule; an SVG export applies the same
2005
+ * one against the export's canvas width instead of this track's.
2006
+ *
2007
+ * Published as a SCALAR on purpose. `visibleRegions` rebuilds a fresh
2008
+ * array of fresh objects on every pan and zoom frame, so a component
2009
+ * that derived this number itself re-rendered on every frame of every
2010
+ * gesture — to produce, whenever content fills the track, the unchanged
2011
+ * `trackWidthPx`. Reading the scalar lets MobX's `===` stop the chain at
2012
+ * this computed instead. Measured over 20 zoom frames: the wiggle
2013
+ * family's three body instances fell from 66 renders to 7.
2014
+ */
2015
+ get contentRightEdgePx() {
2016
+ // The `trackWidthPx` rule sends a DISPLAY to `model.canvasWidthPx`
2017
+ // so it cannot pick the wrong one of four view getters. This is the
2018
+ // view, publishing its own geometry, and `canvasWidthPx` is defined
2019
+ // as exactly this host's `trackWidthPx` (MultiRegionDisplayMixin) —
2020
+ // the same source of truth, read from the side that owns it.
2021
+ // eslint-disable-next-line no-restricted-syntax
2022
+ return contentRightEdgePx(this.visibleRegions, self.trackWidthPx);
2023
+ },
1965
2024
  /**
1966
2025
  * #getter
1967
2026
  * visibleRegions expanded by a half-screen buffer on each side,
@@ -2386,13 +2445,15 @@ export function stateModelFactory(pluginManager) {
2386
2445
  * navigating to the locstring
2387
2446
  * @param grow - optional multiplier to expand the region by (e.g., 0.2
2388
2447
  * adds 20% padding on each side)
2448
+ * @param opts - `showHitTrack: false` keeps a text-search hit from also
2449
+ * showing the track its index was built from
2389
2450
  * @returns whether the view moved. A multi-hit search raises the picker
2390
2451
  * instead of navigating, and both resolve — see `handleSelectedRegion`.
2391
2452
  */
2392
2453
  // annotated, not inferred: the same type cycle `showTrack` names. Left to
2393
2454
  // infer, the `boolean` reaches this model's type through `self` and 180
2394
2455
  // unrelated files lose their parameter types to it.
2395
- async navToLocString(input, optAssemblyName, grow) {
2456
+ async navToLocString(input, optAssemblyName, grow, opts) {
2396
2457
  const { assemblyNames } = self;
2397
2458
  const session = getSession(self);
2398
2459
  const { assemblyManager } = session;
@@ -2407,6 +2468,7 @@ export function stateModelFactory(pluginManager) {
2407
2468
  input,
2408
2469
  assemblyName,
2409
2470
  grow,
2471
+ showHitTrack: opts?.showHitTrack,
2410
2472
  model: self,
2411
2473
  });
2412
2474
  },
@@ -2697,7 +2759,8 @@ export function stateModelFactory(pluginManager) {
2697
2759
  afterAttach() {
2698
2760
  doAfterAttach(self);
2699
2761
  },
2700
- }))
2762
+ }));
2763
+ return withLaunchInput(model, lgvLaunchKeys)
2701
2764
  .preProcessSnapshot((snap) => {
2702
2765
  if (!snap) {
2703
2766
  return snap;
@@ -2739,23 +2802,24 @@ export function stateModelFactory(pluginManager) {
2739
2802
  };
2740
2803
  })
2741
2804
  .postProcessSnapshot(snap => {
2742
- // init is transient launch state, never persisted. showCenterLine is
2805
+ // launch is transient, never persisted. showCenterLine is
2743
2806
  // purely a localStorage-backed preference (see setupLocalStorageAutorun
2744
2807
  // in afterAttach.ts) and is never persisted into session snapshots. The
2745
2808
  // remaining fields are also localStorage-backed, but still persist when
2746
2809
  // non-default: their strip baseline is the universal default (hardcoded
2747
2810
  // here), not the localStorage-derived creation default, so a
2748
2811
  // localStorage-set value stays portable across browsers.
2749
- const { init, showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
2812
+ const { launch, showCenterLine, showCytobands, trackLabels, colorByCDS, showAminoAcids, showTrackOutlines, ...rest } = snap;
2750
2813
  return {
2751
2814
  ...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.
2815
+ // keep the launch state until displayedRegions exist, so a snapshot
2816
+ // taken before the launch autorun navigates (e.g. autosave firing
2817
+ // mid-load) can still rebuild the view instead of dropping to the
2818
+ // import form. displayedRegions is stripDefault, so it's absent (not
2819
+ // []) when empty — the optional chain is runtime-necessary despite the
2820
+ // non-nullish type.
2757
2821
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2758
- ...(init && !snap.displayedRegions?.length ? { init } : {}),
2822
+ ...(launch && !snap.displayedRegions?.length ? { launch } : {}),
2759
2823
  ...(!showCytobands ? { showCytobands } : {}),
2760
2824
  ...(trackLabels ? { trackLabels } : {}),
2761
2825
  ...(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'>>;