@jbrowse/plugin-linear-genome-view 5.0.0-beta.19 → 5.0.0-beta.20

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 (47) hide show
  1. package/esm/LinearGenomeView/afterAttach.d.ts +17 -0
  2. package/esm/LinearGenomeView/afterAttach.js +71 -43
  3. package/esm/LinearGenomeView/components/CenterLine.js +6 -3
  4. package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
  5. package/esm/LinearGenomeView/components/ExportSvgDialog.js +12 -3
  6. package/esm/LinearGenomeView/components/Header.js +1 -1
  7. package/esm/LinearGenomeView/components/HeaderZoomControls.js +3 -2
  8. package/esm/LinearGenomeView/components/HighlightBand.js +7 -7
  9. package/esm/LinearGenomeView/components/ImportForm.js +3 -3
  10. package/esm/LinearGenomeView/components/MiniControls.js +1 -1
  11. package/esm/LinearGenomeView/components/OverviewRubberband.js +5 -3
  12. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +7 -4
  13. package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
  14. package/esm/LinearGenomeView/components/SVGHighlightBand.js +15 -3
  15. package/esm/LinearGenomeView/components/SearchBox.js +3 -2
  16. package/esm/LinearGenomeView/components/SearchResultsTable.js +2 -2
  17. package/esm/LinearGenomeView/components/TrackContainer.js +1 -1
  18. package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +2 -3
  19. package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
  20. package/esm/LinearGenomeView/components/useRangeSelect.js +5 -2
  21. package/esm/LinearGenomeView/menuItems.js +5 -5
  22. package/esm/LinearGenomeView/model.d.ts +52 -40
  23. package/esm/LinearGenomeView/model.js +71 -50
  24. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +4 -0
  25. package/esm/LinearGenomeView/rExport/exportLinearView.js +26 -6
  26. package/esm/LinearGenomeView/rExport/rScript.d.ts +19 -9
  27. package/esm/LinearGenomeView/rExport/rScript.js +91 -53
  28. package/esm/LinearGenomeView/showRegionsWithUndo.js +5 -3
  29. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
  30. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +2 -2
  31. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
  32. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +2 -2
  33. package/esm/LinearGenomeView/svgcomponents/SVGView.js +15 -1
  34. package/esm/LinearGenomeView/svgcomponents/util.d.ts +1 -0
  35. package/esm/LinearGenomeView/types.d.ts +6 -0
  36. package/esm/LinearGenomeView/types.js +0 -1
  37. package/esm/LinearGenomeView/util.js +9 -9
  38. package/esm/MultiLevelRubberband/useRangeSelect.js +14 -12
  39. package/esm/TrackModifiers/index.d.ts +4 -1
  40. package/esm/TrackModifiers/index.js +26 -7
  41. package/esm/displayKitTests/perRegionTestEnv.d.ts +2 -0
  42. package/esm/index.d.ts +2 -2
  43. package/esm/index.js +1 -1
  44. package/esm/searchUtils.d.ts +1 -3
  45. package/esm/searchUtils.js +33 -44
  46. package/esm/shared/useWindowDrag.js +11 -9
  47. package/package.json +7 -7
@@ -1,36 +1,43 @@
1
1
  import { expandAssemblySequence, expandSidecarShorthand, refNameAliasAdapterType, } from '@jbrowse/core/assemblyManager/expandAssemblyConfigShorthand';
2
2
  import { resolveUri } from '@jbrowse/core/util';
3
+ const VCF_ADAPTERS = {
4
+ VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/, index: '.tbi' },
5
+ VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
6
+ };
3
7
  /**
4
8
  * Each display ggjbrowse reads, by the reader that reads it, and the adapters
5
- * it opens (the slot holding the file and the extension the reader knows it
6
- * by).
9
+ * it opens: the slot holding the file, the extension the reader knows it by,
10
+ * and the suffix of the index the reader looks for beside it.
7
11
  */
8
12
  export const READERS = {
9
13
  LinearBasicDisplay: {
10
14
  call: 'jbrowse_features',
11
15
  adapters: {
12
- Gff3TabixAdapter: { slot: 'gffGzLocation', ext: /\.gff3?\.gz$/ },
16
+ Gff3TabixAdapter: {
17
+ slot: 'gffGzLocation',
18
+ ext: /\.gff3?\.gz$/,
19
+ index: '.tbi',
20
+ },
13
21
  Gff3Adapter: { slot: 'gffLocation', ext: /\.gff3?$/ },
14
- GtfTabixAdapter: { slot: 'gtfGzLocation', ext: /\.gtf\.gz$/ },
22
+ GtfTabixAdapter: {
23
+ slot: 'gtfGzLocation',
24
+ ext: /\.gtf\.gz$/,
25
+ index: '.tbi',
26
+ },
15
27
  GtfAdapter: { slot: 'gtfLocation', ext: /\.gtf$/ },
16
- BedTabixAdapter: { slot: 'bedGzLocation', ext: /\.bed\.gz$/ },
28
+ BedTabixAdapter: {
29
+ slot: 'bedGzLocation',
30
+ ext: /\.bed\.gz$/,
31
+ index: '.tbi',
32
+ },
17
33
  BedAdapter: { slot: 'bedLocation', ext: /\.bed$/ },
18
34
  BigBedAdapter: { slot: 'bigBedLocation', ext: /\.(bb|bigbed)$/ },
19
35
  },
20
36
  },
21
37
  LinearVariantDisplay: {
22
- call: 'jbrowse_features',
23
- adapters: {
24
- VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
25
- VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
26
- },
27
- },
28
- LinearMultiSampleVariantDisplay: {
29
38
  call: 'jbrowse_variants',
30
- adapters: {
31
- VcfTabixAdapter: { slot: 'vcfGzLocation', ext: /\.vcf\.gz$/ },
32
- VcfAdapter: { slot: 'vcfLocation', ext: /\.vcf$/ },
33
- },
39
+ recordsOnlyCall: 'jbrowse_features',
40
+ adapters: VCF_ADAPTERS,
34
41
  },
35
42
  LinearWiggleDisplay: {
36
43
  call: 'jbrowse_wiggle',
@@ -42,14 +49,17 @@ export const READERS = {
42
49
  LinearAlignmentsDisplay: {
43
50
  call: 'jbrowse_alignments',
44
51
  adapters: {
45
- BamAdapter: { slot: 'bamLocation', ext: /(?<!\.cram)$/ },
46
- CramAdapter: { slot: 'cramLocation', ext: /\.cram$/ },
52
+ BamAdapter: { slot: 'bamLocation', ext: /(?<!\.cram)$/, index: '.bai' },
53
+ CramAdapter: { slot: 'cramLocation', ext: /\.cram$/, index: '.crai' },
47
54
  },
48
55
  },
49
56
  };
50
- /** The ggjbrowse release whose readers and `jbrowse_stack` the script calls. */
51
- export const GGJBROWSE_VERSION = '0.10.0';
52
- const INSTALL = 'remotes::install_github("GMOD/ggjbrowse")';
57
+ /**
58
+ * The ggjbrowse release the script is tested against: its floor, and the tag
59
+ * its install line pins. The R export workflow installs this tag on a push.
60
+ */
61
+ export const GGJBROWSE_VERSION = '0.11.0';
62
+ const INSTALL = `remotes::install_github("GMOD/ggjbrowse@v${GGJBROWSE_VERSION}")`;
53
63
  function isSet(value) {
54
64
  return !(value === undefined ||
55
65
  value === null ||
@@ -189,20 +199,10 @@ function fileArgument(adapters, adapter) {
189
199
  skipped: 'it reads a file opened in the browser, which R cannot reach',
190
200
  };
191
201
  }
192
- const file = known.slot
193
- ? (locationPath(adapter[known.slot]) ??
194
- (typeof adapter.uri === 'string' ? adapter.uri : undefined))
195
- : undefined;
202
+ const file = known.slot ? locationPath(adapter[known.slot]) : undefined;
196
203
  const index = adapter.index;
197
204
  const indexPath = locationPath(index?.location) ?? locationPath(adapter.craiLocation);
198
- const besideSuffix = type === 'CramAdapter' ? '.crai' : type === 'BamAdapter' ? '.bai' : '.tbi';
199
- const plainSlots = new Set([
200
- 'type',
201
- 'uri',
202
- 'index',
203
- 'craiLocation',
204
- known.slot,
205
- ]);
205
+ const plainSlots = new Set(['type', 'index', 'craiLocation', known.slot]);
206
206
  const plain = file !== undefined &&
207
207
  known.ext?.test(file.replace(/\?.*$/, '').toLowerCase()) &&
208
208
  Object.keys(located).every(k => plainSlots.has(k)) &&
@@ -211,7 +211,7 @@ function fileArgument(adapters, adapter) {
211
211
  return plain
212
212
  ? {
213
213
  file: rStr(file),
214
- index: indexPath && indexPath !== `${file}${besideSuffix}`
214
+ index: indexPath && indexPath !== `${file}${known.index}`
215
215
  ? indexPath
216
216
  : undefined,
217
217
  uris,
@@ -345,6 +345,44 @@ function stackArgument(title, name) {
345
345
  : `${isIdentifier(title) ? title : rStr(title)} = ${name}`;
346
346
  }
347
347
  /** What the figure cannot show of a track whose settings ggjbrowse reads. */
348
+ // The variant display's record settings, under `records`, as the feature
349
+ // track's top-level spelling `jbrowse_features` reads; the genotype rows'
350
+ // settings stay behind.
351
+ const RECORD_SETTINGS = [
352
+ 'displayMode',
353
+ 'mark',
354
+ 'labels',
355
+ 'facet',
356
+ 'maxFeatureScreenDensity',
357
+ ];
358
+ const SHARED_SETTINGS = [
359
+ 'filter',
360
+ 'encoding',
361
+ 'legend',
362
+ 'heightMode',
363
+ 'growMaxHeight',
364
+ ];
365
+ function isRecord(x) {
366
+ return typeof x === 'object' && x !== null && !Array.isArray(x);
367
+ }
368
+ function setEntries(source, keys) {
369
+ return keys.flatMap(k => {
370
+ const entry = [k, source[k]];
371
+ return entry[1] === undefined ? [] : [entry];
372
+ });
373
+ }
374
+ function recordsAsFeatureDisplay(display) {
375
+ const records = isRecord(display.records) ? display.records : {};
376
+ const tooltip = {
377
+ ...(isRecord(display.tooltip) ? display.tooltip : {}),
378
+ ...(isRecord(records.tooltip) ? records.tooltip : {}),
379
+ };
380
+ return Object.fromEntries([
381
+ ...setEntries(display, SHARED_SETTINGS),
382
+ ...setEntries(records, RECORD_SETTINGS),
383
+ ...setEntries({ tooltip }, Object.keys(tooltip).length > 0 ? ['tooltip'] : []),
384
+ ]);
385
+ }
348
386
  function settingNotes(track) {
349
387
  const { color } = track.display.encoding ?? {};
350
388
  return [
@@ -371,46 +409,50 @@ export function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, b
371
409
  const assemblyValue = assembly && 'value' in assembly ? assembly.value : undefined;
372
410
  const loci = regions.map(r => locusOf(r.refName, r.start, r.end));
373
411
  const totalBp = regions.reduce((sum, r) => sum + r.end - r.start, 0);
374
- const viewPx = bpPerPx ? Math.round(totalBp / bpPerPx) : undefined;
375
- const widthReaders = [];
412
+ const viewPx = Math.round(totalBp / bpPerPx);
376
413
  const names = trackNames(tracks);
377
414
  const reads = [];
378
415
  const drawn = [];
379
416
  const bands = [];
380
417
  let alignments;
381
418
  tracks.forEach((track, i) => {
382
- const reader = READERS[track.displayType];
383
- if (!reader) {
419
+ const known = READERS[track.displayType];
420
+ if (!known) {
384
421
  notes.push(`${track.trackName}: ${track.displayType} has no R reader in ggjbrowse yet`);
385
422
  return;
386
423
  }
424
+ const recordsCall = track.recordsOnly ? known.recordsOnlyCall : undefined;
425
+ const asRecords = recordsCall !== undefined;
426
+ const reader = {
427
+ adapters: known.adapters,
428
+ call: recordsCall ?? known.call,
429
+ };
387
430
  const source = fileArgument(reader.adapters, track.adapter);
388
431
  if ('skipped' in source) {
389
432
  notes.push(`${track.trackName}: ${source.skipped}`);
390
433
  return;
391
434
  }
392
435
  const { heightMode, height } = track;
436
+ const written = asRecords
437
+ ? recordsAsFeatureDisplay(track.display)
438
+ : track.display;
393
439
  const display = reader.call === 'jbrowse_features'
394
440
  ? {
395
- ...track.display,
441
+ ...written,
396
442
  ...(heightMode === undefined ? {} : { heightMode }),
397
443
  ...(height === undefined ? {} : { height }),
398
444
  }
399
- : track.display;
400
- const width = viewPx !== undefined;
445
+ : written;
401
446
  const geneTrack = track.displayType === 'LinearBasicDisplay';
402
447
  const args = [
403
448
  ...(Object.keys(display).length ? [`display = ${rValue(display)}`] : []),
404
- ...(width ? ['width = width'] : []),
449
+ 'width = width',
405
450
  ...(assemblyValue ? ['assembly = assembly'] : []),
406
451
  ...(geneTrack && colorByFrame ? ['color_by_cds = TRUE'] : []),
407
452
  ...(geneTrack && !showAminoAcids ? ['amino_acids = FALSE'] : []),
408
453
  ...(source.index ? [`index = ${rStr(source.index)}`] : []),
409
454
  ];
410
455
  const name = names[i];
411
- if (width) {
412
- widthReaders.push(name);
413
- }
414
456
  reads.push([
415
457
  `# ${track.trackName}`,
416
458
  `${name} <- ${reader.call}(${source.file},`,
@@ -466,14 +508,10 @@ export function exportTracksToR({ tracks, regions, assembly: assemblySnapshot, b
466
508
  `assembly <- ${assemblyValue}`,
467
509
  ]
468
510
  : []),
469
- ...(widthReaders.length
470
- ? [
471
- "# The view's pixels across its regions. A smaller number lays the tracks",
472
- '# out as a narrower browser window would, for a narrower figure with',
473
- '# fewer labels at the same size.',
474
- `width <- ${viewPx}`,
475
- ]
476
- : []),
511
+ "# The view's pixels across its regions. A smaller number lays the tracks",
512
+ '# out as a narrower browser window would, for a narrower figure with',
513
+ '# fewer labels at the same size.',
514
+ `width <- ${viewPx}`,
477
515
  '',
478
516
  reads.join('\n\n'),
479
517
  '',
@@ -42,9 +42,11 @@ export function showRegionsWithUndo({ view, regions, message, alsoUndo, }) {
42
42
  getNotificationSink(view).notify(message, 'info', {
43
43
  name: 'Undo',
44
44
  onClick: () => {
45
- view.setDisplayedRegions(previous.displayedRegions);
46
- view.setWindow(previous.windowWidthBp, previous.windowStartBp);
47
- alsoUndo?.();
45
+ transaction(() => {
46
+ view.setDisplayedRegions(previous.displayedRegions);
47
+ view.setWindow(previous.windowWidthBp, previous.windowStartBp);
48
+ alsoUndo?.();
49
+ });
48
50
  },
49
51
  });
50
52
  }
@@ -1,5 +1,7 @@
1
+ import type { BandGap } from '../components/SVGHighlightBand.tsx';
1
2
  import type { LinearGenomeViewModel } from '../index.ts';
2
- export default function SVGHighlights({ model, height, }: {
3
+ export default function SVGHighlights({ model, height, gaps, }: {
3
4
  model: LinearGenomeViewModel;
4
5
  height: number;
6
+ gaps: BandGap[];
5
7
  }): (import("react").JSX.Element | null)[] | null;
@@ -14,13 +14,13 @@ import { getHighlightColor, highlightKey } from '../components/util.js';
14
14
  // bands across track bodies that were still drawn where the snapshot left them.
15
15
  // The file export renders the whole document in one synchronous pass, so it
16
16
  // never needed the subscription either.
17
- export default function SVGHighlights({ model, height, }) {
17
+ export default function SVGHighlights({ model, height, gaps, }) {
18
18
  const theme = useStyleTheme();
19
19
  const session = getSession(model);
20
20
  return session.highlightsVisible
21
21
  ? model.highlights.map((h, idx) => {
22
22
  const coords = model.getHighlightCoords(h);
23
- return coords ? (_jsx(SVGHighlightBand, { coords: coords, height: height, color: getHighlightColor(h, theme).toRgbString(), label: session.highlightLabelsVisible ? h.label : undefined, labelColor: theme.palette.text.primary }, highlightKey(h, idx))) : null;
23
+ return coords ? (_jsx(SVGHighlightBand, { coords: coords, height: height, gaps: gaps, color: getHighlightColor(h, theme).toRgbString(), label: session.highlightLabelsVisible ? h.label : undefined, labelColor: theme.palette.text.primary }, highlightKey(h, idx))) : null;
24
24
  })
25
25
  : null;
26
26
  }
@@ -1,5 +1,7 @@
1
+ import type { BandGap } from '../components/SVGHighlightBand.tsx';
1
2
  import type { LinearGenomeViewModel } from '../index.ts';
2
- export default function SVGHighlightsOverlay({ model, tracksHeight, }: {
3
+ export default function SVGHighlightsOverlay({ model, tracksHeight, gaps, }: {
3
4
  model: LinearGenomeViewModel;
4
5
  tracksHeight: number;
6
+ gaps: BandGap[];
5
7
  }): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import SVGHighlights from './SVGHighlights.js';
7
7
  // The highlight layer over one view's tracks area: the view's highlights plus
8
8
  // whatever plugins contribute, clipped to the view width. Callers translate
9
9
  // this to the top-left of the tracks area.
10
- export default function SVGHighlightsOverlay({ model, tracksHeight, }) {
10
+ export default function SVGHighlightsOverlay({ model, tracksHeight, gaps, }) {
11
11
  const { pluginManager } = getEnv(model);
12
- return (_jsxs(SvgClipRect, { id: `highlight-clip-${svgNodeId(model)}`, width: model.width, height: tracksHeight, children: [_jsx(SVGHighlights, { model: model, height: tracksHeight }), _jsx(PluggableElements, { pluginManager: pluginManager, name: "LinearGenomeView-HighlightSVGComponent", props: { model, height: tracksHeight } })] }));
12
+ return (_jsxs(SvgClipRect, { id: `highlight-clip-${svgNodeId(model)}`, width: model.width, height: tracksHeight, children: [_jsx(SVGHighlights, { model: model, height: tracksHeight, gaps: gaps }), _jsx(PluggableElements, { pluginManager: pluginManager, name: "LinearGenomeView-HighlightSVGComponent", props: { model, height: tracksHeight } })] }));
13
13
  }
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import SVGGridlines from './SVGGridlines.js';
3
3
  import SVGHighlightsOverlay from './SVGHighlightsOverlay.js';
4
4
  import SVGTracks from './SVGTracks.js';
5
+ import { labelOffset, trackBoxOffsets } from './util.js';
5
6
  // One LGV's worth of exported SVG: a caller-supplied header on top, then
6
7
  // optional gridlines, the track bodies, and the highlight layer over them.
7
8
  // Shared by all three exports that stack an LGV — the standalone one, and the
@@ -9,5 +10,18 @@ import SVGTracks from './SVGTracks.js';
9
10
  // drift. It did drift while the standalone export kept its own copy of these
10
11
  // four groups.
11
12
  export default function SVGView({ view, displayResults, header, fontSize, textHeight, trackLabels, trackLabelOffset, contentTop, tracksHeight, showGridlines, leftBuffer, }) {
12
- return (_jsxs(_Fragment, { children: [_jsx("g", { transform: `translate(${trackLabelOffset})`, children: header }), showGridlines ? (_jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGGridlines, { model: view, height: tracksHeight }) })) : null, _jsx("g", { transform: `translate(0 ${contentTop})`, children: _jsx(SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: view, displayResults: displayResults, trackLabelOffset: trackLabelOffset, leftBuffer: leftBuffer }) }), _jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGHighlightsOverlay, { model: view, tracksHeight: tracksHeight }) })] }));
13
+ const tracks = displayResults.map(r => r.track);
14
+ const offsets = trackBoxOffsets(tracks, trackLabels, textHeight);
15
+ const nonGenomicBands = tracks.flatMap((track, i) => {
16
+ const display = track.displays[0];
17
+ return display.drawsGenomicCoordinates === false
18
+ ? [
19
+ {
20
+ top: offsets[i] + labelOffset(track, trackLabels, textHeight),
21
+ height: display.height,
22
+ },
23
+ ]
24
+ : [];
25
+ });
26
+ return (_jsxs(_Fragment, { children: [_jsx("g", { transform: `translate(${trackLabelOffset})`, children: header }), showGridlines ? (_jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGGridlines, { model: view, height: tracksHeight }) })) : null, _jsx("g", { transform: `translate(0 ${contentTop})`, children: _jsx(SVGTracks, { textHeight: textHeight, trackLabels: trackLabels, fontSize: fontSize, model: view, displayResults: displayResults, trackLabelOffset: trackLabelOffset, leftBuffer: leftBuffer }) }), _jsx("g", { transform: `translate(${trackLabelOffset} ${contentTop})`, children: _jsx(SVGHighlightsOverlay, { model: view, tracksHeight: tracksHeight, gaps: nonGenomicBands }) })] }));
13
27
  }
@@ -17,6 +17,7 @@ export interface SvgDisplayResult {
17
17
  displays: ({
18
18
  height: number;
19
19
  regionTooLarge?: boolean;
20
+ drawsGenomicCoordinates?: boolean;
20
21
  } & SidebarDisplay)[];
21
22
  };
22
23
  result: ReactNode;
@@ -39,3 +39,9 @@ export interface InitState {
39
39
  highlight?: (string | HighlightInput)[];
40
40
  }
41
41
  export type LinearGenomeViewLaunchProps = Partial<Omit<SnapshotIn<LinearGenomeViewStateModel>, keyof InitState | 'id' | 'type' | 'launch'>>;
42
+ /**
43
+ * One LinearGenomeView panel inside another view (a breakpoint split view's
44
+ * stack, a synteny view's rows): the LGV's launch keys and props, less `nav`
45
+ * and `tracklist`, the header and the track selector a panel does not show.
46
+ */
47
+ export type LinearGenomeViewPanelInit = Omit<InitState, 'nav' | 'tracklist'> & LinearGenomeViewLaunchProps;
@@ -1,2 +1 @@
1
1
  export {};
2
- // #endregion
@@ -1,4 +1,4 @@
1
- import { UnknownRefNameError, assembleLocStrings, getTickDisplayStr, measureText, parseBpString, parseLocString, } from '@jbrowse/core/util';
1
+ import { UnknownRefNameError, assembleLocString, assembleLocStrings, getTickDisplayStr, measureText, parseBpString, parseLocString, } from '@jbrowse/core/util';
2
2
  import { bpOffsetInRegion } from '@jbrowse/core/util/Base1DUtils';
3
3
  import { chooseGridPitch } from '@jbrowse/core/util/chooseGridPitch';
4
4
  import { tickLabelsWorthDrawing } from '@jbrowse/core/util/tickLabels';
@@ -558,6 +558,10 @@ export function makeBlockTicks({ start, end, reversed = false, }, bpPerPx, inclu
558
558
  * navigating to a feature
559
559
  */
560
560
  export async function generateLocations({ regions, assemblyManager, assemblyName, grow, }) {
561
+ const backwards = regions.find(({ start, end }) => start !== undefined && end !== undefined && start > end);
562
+ if (backwards) {
563
+ throw new Error(`found start greater than end in ${assembleLocString(backwards)}`);
564
+ }
561
565
  return Promise.all(regions.map(async (region) => {
562
566
  const asmName = region.assemblyName || assemblyName;
563
567
  if (!asmName) {
@@ -609,14 +613,12 @@ export async function generateLocations({ regions, assemblyManager, assemblyName
609
613
  * Used by navToLocString
610
614
  */
611
615
  export function parseLocStrings(input, assemblyName, isValidRefName) {
612
- const inputs = input
613
- .split(/(\s+)/)
614
- .map(f => f.trim())
615
- .filter(f => !!f);
616
+ const inputs = input.split(/\s+/).filter(Boolean);
617
+ const isRef = (ref, asm) => isValidRefName(ref, asm ?? assemblyName);
616
618
  // first try interpreting as a whitespace-separated sequence of
617
619
  // multiple locstrings
618
620
  try {
619
- return inputs.map(loc => parseLocString(loc, ref => isValidRefName(ref, assemblyName)));
621
+ return inputs.map(loc => parseLocString(loc, isRef));
620
622
  }
621
623
  catch (e) {
622
624
  // if this fails, try interpreting as a whitespace-separated refname,
@@ -629,9 +631,7 @@ export function parseLocStrings(input, assemblyName, isValidRefName) {
629
631
  inputs.length === 3 &&
630
632
  parseBpString(start) !== undefined &&
631
633
  parseBpString(end) !== undefined) {
632
- return [
633
- parseLocString(`${refName}:${start}..${end}`, ref => isValidRefName(ref, assemblyName)),
634
- ];
634
+ return [parseLocString(`${refName}:${start}..${end}`, isRef)];
635
635
  }
636
636
  throw e;
637
637
  }
@@ -57,18 +57,20 @@ export function useRangeSelect(ref, model) {
57
57
  onCancel: handleClose,
58
58
  });
59
59
  function mouseDown(event) {
60
- event.preventDefault();
61
- event.stopPropagation();
62
- // clear any leftover menu/selection so a fresh drag isn't blocked by a
63
- // stale anchorPosition keeping mouseDragging false
64
- setAnchorPosition(undefined);
65
- // the guide is what the pointer was showing up to now, and mouseMove won't
66
- // touch it during the drag — leaving it set keeps the selection span from
67
- // ever rendering, since the guide wins that either/or
68
- setGuideX(undefined);
69
- const relativeX = getRelativeX(event, ref.current);
70
- setStartX(relativeX);
71
- setCurrentX(relativeX);
60
+ if (event.button === 0) {
61
+ event.preventDefault();
62
+ event.stopPropagation();
63
+ // clear any leftover menu/selection so a fresh drag isn't blocked by a
64
+ // stale anchorPosition keeping mouseDragging false
65
+ setAnchorPosition(undefined);
66
+ // the guide is what the pointer was showing up to now, and mouseMove
67
+ // won't touch it during the drag — leaving it set keeps the selection
68
+ // span from ever rendering, since the guide wins that either/or
69
+ setGuideX(undefined);
70
+ const relativeX = getRelativeX(event, ref.current);
71
+ setStartX(relativeX);
72
+ setCurrentX(relativeX);
73
+ }
72
74
  }
73
75
  function mouseMove(event) {
74
76
  // Don't update guideX while a menu is open — the guide is what the click
@@ -74,7 +74,10 @@ export declare function launchTrackInit(view: ModifierView & IAnyStateTreeNode,
74
74
  * after every other modifier, so a member write keeps the rest of the setting
75
75
  * whatever order the command line gives them in. A display replaces a color
76
76
  * or facet object whole, which is why these cannot ride in on the launch
77
- * snapshot.
77
+ * snapshot. A keyed object (`rows`, ADR-257) seeds only its field and the
78
+ * objects written whole that the writes reach, such as `rows.color`: seeded
79
+ * whole it would restate the arrangement a field write must drop, and
80
+ * unseeded a spec holding the patch would lose the field and the channel.
78
81
  */
79
82
  export declare function writeMembers(view: Pick<ModifierView, 'tracks' | 'showTrack'>, trackId: string, members: string[]): Record<string, unknown>;
80
83
  export {};
@@ -1,4 +1,4 @@
1
- import { mergeConfSettings } from '@jbrowse/core/configuration';
1
+ import { keyedWritesAt, mergeConfSettings, writesWholeAt, } from '@jbrowse/core/configuration';
2
2
  import { getEnv, getSession } from '@jbrowse/core/util';
3
3
  import { basePaintedAt } from '@jbrowse/core/util/Base1DUtils';
4
4
  import { predicatesOf, testsSomething, withFields, } from '@jbrowse/core/util/fieldPredicate';
@@ -181,12 +181,24 @@ export async function launchTrackInit(view, t) {
181
181
  session.notify(notes.join('; '), 'warning');
182
182
  }
183
183
  }
184
+ // the named members of a keyed object as configured, undefined when it
185
+ // states none of them
186
+ function keyedSeed(member, names) {
187
+ const configured = isStateTreeNode(member)
188
+ ? getSnapshot(member)
189
+ : {};
190
+ const seed = Object.fromEntries(names.flatMap(name => configured[name] === undefined ? [] : [[name, configured[name]]]));
191
+ return Object.keys(seed).length > 0 ? seed : undefined;
192
+ }
184
193
  /**
185
194
  * Write a track's `memberWrites` onto what its display in `view` already has,
186
195
  * after every other modifier, so a member write keeps the rest of the setting
187
196
  * whatever order the command line gives them in. A display replaces a color
188
197
  * or facet object whole, which is why these cannot ride in on the launch
189
- * snapshot.
198
+ * snapshot. A keyed object (`rows`, ADR-257) seeds only its field and the
199
+ * objects written whole that the writes reach, such as `rows.color`: seeded
200
+ * whole it would restate the arrangement a field write must drop, and
201
+ * unseeded a spec holding the patch would lose the field and the channel.
190
202
  */
191
203
  export function writeMembers(view, trackId, members) {
192
204
  if (members.length === 0) {
@@ -198,14 +210,21 @@ export function writeMembers(view, trackId, members) {
198
210
  }
199
211
  const { configuration } = track.activeDisplay;
200
212
  const configured = getSnapshot(configuration);
201
- const patch = memberPatch(members, root => {
213
+ const type = getType(configuration);
214
+ const patch = memberPatch(members, (root, reached) => {
202
215
  // a list at a default the snapshot strips, Manhattan's marks, still seeds
203
216
  // the write, so `marks.0.mark=bar` edits the mark drawn
204
217
  const member = configuration[root];
205
- return (configured[root] ??
206
- (isStateTreeNode(member) && isArrayType(getType(member))
207
- ? getSnapshot(member)
208
- : undefined));
218
+ const keyed = keyedWritesAt(type, [root]);
219
+ return keyed
220
+ ? keyedSeed(member, [
221
+ keyed.keyedBy,
222
+ ...reached.filter(m => writesWholeAt(type, [root, m])),
223
+ ])
224
+ : (configured[root] ??
225
+ (isStateTreeNode(member) && isArrayType(getType(member))
226
+ ? getSnapshot(member)
227
+ : undefined));
209
228
  });
210
229
  view.showTrack(trackId, {}, patch);
211
230
  return patch;
@@ -537,6 +537,7 @@ export declare function createPerRegionTestEnvironment({ measuresBytes, gate, es
537
537
  notifyError(message: string): void;
538
538
  queueDialog(cb: (handleClose: () => void) => import("@jbrowse/display-test-utils").QueuedDialog): void;
539
539
  setSelection(thing: unknown): void;
540
+ clearSelection(): void;
540
541
  addWidget(type: string, id: string, initialState?: Record<string, unknown>): {
541
542
  type: string;
542
543
  id: string;
@@ -578,6 +579,7 @@ export declare function createPerRegionTestEnvironment({ measuresBytes, gate, es
578
579
  notifyError(message: string): void;
579
580
  queueDialog(cb: (handleClose: () => void) => import("@jbrowse/display-test-utils").QueuedDialog): void;
580
581
  setSelection(thing: unknown): void;
582
+ clearSelection(): void;
581
583
  addWidget(type: string, id: string, initialState?: Record<string, unknown>): {
582
584
  type: string;
583
585
  id: string;
package/esm/index.d.ts CHANGED
@@ -28,13 +28,13 @@ export { default as HeaderSearchBoxRow } from './LinearGenomeView/components/Hea
28
28
  export { searchBoxMenuItems, useSearchBoxPrefs, } from './LinearGenomeView/components/useSearchBoxPrefs.ts';
29
29
  export type { SearchBoxPrefs } from './LinearGenomeView/components/useSearchBoxPrefs.ts';
30
30
  export { normalizeTrackInit } from '@jbrowse/core/util/tracks';
31
- export { applyInitHighlights } from './LinearGenomeView/afterAttach.ts';
31
+ export { applyInitHighlights, navToInitLoc, showInitTracks, } from './LinearGenomeView/afterAttach.ts';
32
32
  export { launchTrackInit, launchTrackWithModifiers, openedDisplayType, writeFilter, writeMembers, writeReadFilter, } from './TrackModifiers/index.ts';
33
33
  export type { ModifierView } from './TrackModifiers/index.ts';
34
34
  export { MultiLevelRubberband, multiLevelRowClickMenuItems, multiLevelRowMenuItems, rowLabels, } from './MultiLevelRubberband/index.ts';
35
35
  export { fetchResults, SearchResultsNotFoundError } from './searchUtils.ts';
36
36
  export type { LaunchLinearGenomeViewArgs } from './LaunchLinearGenomeView/index.ts';
37
- export type { BpOffset, ExportSvgOptions, HighlightInput, HighlightType, InitState, LinearGenomeViewLaunchProps, NavLocation, TrackLabelMode, ViewTrackLabelMode, VolatileGuide, } from './LinearGenomeView/types.ts';
37
+ export type { BpOffset, ExportSvgOptions, HighlightInput, HighlightType, InitState, LinearGenomeViewLaunchProps, LinearGenomeViewPanelInit, NavLocation, TrackLabelMode, ViewTrackLabelMode, VolatileGuide, } from './LinearGenomeView/types.ts';
38
38
  export type { ScalebarRefNameLabel } from './LinearGenomeView/util.ts';
39
39
  export type { HighlightRegion, SearchHits, SearchPickerRow, SearchPickerState, } from './LinearGenomeView/model.ts';
40
40
  export type { RegionsOrientation } from './LinearGenomeView/util.ts';
package/esm/index.js CHANGED
@@ -68,7 +68,7 @@ export { default as SearchBox } from './LinearGenomeView/components/SearchBox.js
68
68
  export { default as HeaderSearchBoxRow } from './LinearGenomeView/components/HeaderSearchBoxRow.js';
69
69
  export { searchBoxMenuItems, useSearchBoxPrefs, } from './LinearGenomeView/components/useSearchBoxPrefs.js';
70
70
  export { normalizeTrackInit } from '@jbrowse/core/util/tracks';
71
- export { applyInitHighlights } from './LinearGenomeView/afterAttach.js';
71
+ export { applyInitHighlights, navToInitLoc, showInitTracks, } from './LinearGenomeView/afterAttach.js';
72
72
  export { launchTrackInit, launchTrackWithModifiers, openedDisplayType, writeFilter, writeMembers, writeReadFilter, } from './TrackModifiers/index.js';
73
73
  export { MultiLevelRubberband, multiLevelRowClickMenuItems, multiLevelRowMenuItems, rowLabels, } from './MultiLevelRubberband/index.js';
74
74
  export { fetchResults, SearchResultsNotFoundError } from './searchUtils.js';
@@ -23,7 +23,7 @@ export declare function navigateToSelectedOption({ option, model, assemblyName,
23
23
  option: BaseResult;
24
24
  model: LinearGenomeViewModel;
25
25
  assemblyName: string;
26
- }): Promise<void>;
26
+ }): Promise<boolean>;
27
27
  export declare function isOpenInView(result: BaseResult, model: LinearGenomeViewModel): boolean;
28
28
  export declare function distinctDestinations({ results, model, session, assembly, }: {
29
29
  results: BaseResult[];
@@ -58,7 +58,6 @@ export declare function handleSelectedRegion({ input, model, assemblyName, grow,
58
58
  grow?: number;
59
59
  showHitTrack?: boolean;
60
60
  }): Promise<boolean>;
61
- export declare function checkRef(str: string, isRef: (name: string) => boolean): boolean;
62
61
  interface SearchArgs {
63
62
  queryString: string;
64
63
  assemblyName: string;
@@ -84,5 +83,4 @@ export declare function nameNotFound(input: string, assemblyName: string, { inde
84
83
  indexCount: number;
85
84
  failures: unknown[];
86
85
  }): Error;
87
- export declare function splitLast(str: string, split: string): [string, string];
88
86
  export {};