jbrowse-plugin-graphgenomeviewer 4.0.4 → 4.0.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbrowse-plugin-graphgenomeviewer",
3
- "version": "4.0.4",
3
+ "version": "4.0.6",
4
4
  "description": "JBrowse 2 graph genome (pangenome) view: rGFA reference-anchored layouts, the Bandage force-directed engine, and haplotype lanes read from a gbz-base database",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -0,0 +1,41 @@
1
+ import { cutHolds, hostCut } from './host'
2
+
3
+ import type { HostWindow } from './host'
4
+
5
+ const window: HostWindow = {
6
+ refName: 'chr1',
7
+ assemblyName: 'hg38',
8
+ start: 1_000_000.4,
9
+ end: 1_060_000.4,
10
+ bpPerPx: 60,
11
+ span: 60_000,
12
+ regionStart: 0,
13
+ regionEnd: 10_000_000,
14
+ }
15
+
16
+ test('a cut with margins is the window plus a window-width each side, under the cap', () => {
17
+ expect(hostCut(window, Infinity)).toEqual({
18
+ refName: 'chr1',
19
+ assemblyName: 'hg38',
20
+ start: 940_000,
21
+ end: 1_120_001,
22
+ })
23
+ const capped = hostCut(window, 66_000)
24
+ expect(capped.end - capped.start).toBeLessThanOrEqual(66_000)
25
+ expect(cutHolds(capped, window)).toBe(true)
26
+ })
27
+
28
+ test('a cut without margins is the window alone, and holds for that window only', () => {
29
+ const cut = hostCut(window, Infinity, false)
30
+ expect(cut).toMatchObject({ start: 1_000_000, end: 1_060_001 })
31
+ expect(cutHolds(cut, window, false)).toBe(true)
32
+ expect(
33
+ cutHolds(cut, { ...window, start: 1_010_000, end: 1_070_000 }, false),
34
+ ).toBe(false)
35
+ })
36
+
37
+ test('a cut trimmed to a cap the window fills still holds that window', () => {
38
+ const cut = hostCut(window, 60_000)
39
+ expect(cut.end - cut.start).toBeLessThanOrEqual(60_000)
40
+ expect(cutHolds(cut, window)).toBe(true)
41
+ })
@@ -114,11 +114,20 @@ export function hostCut(
114
114
  const margin = margins
115
115
  ? Math.max(0, Math.min(window.span, (capBp - visible) / 2))
116
116
  : 0
117
+ let start = Math.max(window.regionStart, Math.floor(window.start - margin))
118
+ let end = Math.min(window.regionEnd, Math.ceil(window.end + margin))
119
+ // Rounding outward can put a cut a base or two past the cap, which the load
120
+ // refuses whole; trim it back rather than lose the cut.
121
+ const over = end - start - capBp
122
+ if (over > 0) {
123
+ start += Math.floor(over / 2)
124
+ end -= Math.ceil(over / 2)
125
+ }
117
126
  return {
118
127
  refName: window.refName,
119
128
  assemblyName: window.assemblyName,
120
- start: Math.max(window.regionStart, Math.floor(window.start - margin)),
121
- end: Math.min(window.regionEnd, Math.ceil(window.end + margin)),
129
+ start,
130
+ end,
122
131
  }
123
132
  }
124
133
 
@@ -135,8 +144,9 @@ export function cutHolds(
135
144
  ) {
136
145
  return false
137
146
  }
147
+ // to the base, since a cut trimmed to its cap can end a base inside the window
138
148
  if (margins) {
139
- return cut.start <= window.start && window.end <= cut.end
149
+ return cut.start <= window.start + 1 && window.end <= cut.end + 1
140
150
  }
141
151
  const exact = hostCut(window, Infinity, false)
142
152
  return (
@@ -39,10 +39,11 @@ export interface LayoutMode {
39
39
  // rest rather than hiding them, so the reason a mode is unavailable stays
40
40
  // visible instead of the menu silently changing shape between graphs
41
41
  available: (graph: Graph) => boolean
42
- // whether the drawing puts reference bp on x, so a host places it and a cut
43
- // carries margins to pan over. A mode without one draws its own picture of
44
- // the window, so its cut is the window alone.
45
- referenceAxis: boolean
42
+ // whether a cut for this mode carries a window-width margin each side, to
43
+ // pan over before the next cut. A mode that draws a picture of the window
44
+ // — force, ordered, and walk rows, whose bars are lengths through it — is
45
+ // cut to the window alone.
46
+ cutMargins: boolean
46
47
  // whether `run` draws this graph itself, as opposed to handing off to the
47
48
  // engine. Not the same question as `available` and not derivable from it:
48
49
  // 'force' is available for every graph and draws none of them itself. The two
@@ -67,7 +68,7 @@ export const LAYOUT_MODES = [
67
68
  description:
68
69
  'x is reference bp, one row per stable rank. Needs rGFA tags or a reference path.',
69
70
  run: anchoredLayout,
70
- referenceAxis: true,
71
+ cutMargins: true,
71
72
  available: hasBackbone,
72
73
  drawsLocally: hasBackbone,
73
74
  },
@@ -77,7 +78,7 @@ export const LAYOUT_MODES = [
77
78
  description:
78
79
  'x is reference bp, one row per contributing assembly. Needs rGFA tags or a reference path.',
79
80
  run: sampleRowLayout,
80
- referenceAxis: true,
81
+ cutMargins: true,
81
82
  available: hasAlleles,
82
83
  drawsLocally: hasAlleles,
83
84
  },
@@ -87,7 +88,7 @@ export const LAYOUT_MODES = [
87
88
  description:
88
89
  "x is each walk's own bp: one bar per haplotype, sequence the reference also carries in blue and sequence it does not in purple, so a repeat expansion reads as bar length. Needs W or P lines.",
89
90
  run: walkRowLayout,
90
- referenceAxis: true,
91
+ cutMargins: false,
91
92
  available: hasWalks,
92
93
  drawsLocally: hasWalks,
93
94
  },
@@ -97,7 +98,7 @@ export const LAYOUT_MODES = [
97
98
  description:
98
99
  'x is reference order, not bp: every node gets room, bubbles read as lenses. Needs rGFA tags or a reference path.',
99
100
  run: orderedLayout,
100
- referenceAxis: false,
101
+ cutMargins: false,
101
102
  available: hasBackbone,
102
103
  drawsLocally: hasBackbone,
103
104
  },
@@ -107,7 +108,7 @@ export const LAYOUT_MODES = [
107
108
  description:
108
109
  'The reference as a line with one typed glyph per bubble, from the bubble index or from the graph itself: SNP, indel, deletion, inversion, repeat array. Click a glyph to open the graph inside it.',
109
110
  run: variantMapLayout,
110
- referenceAxis: true,
111
+ cutMargins: true,
111
112
  available: hasBackbone,
112
113
  drawsLocally: hasBackbone,
113
114
  },
@@ -116,7 +117,7 @@ export const LAYOUT_MODES = [
116
117
  label: FORCE_LAYOUT_LABEL,
117
118
  description: 'OGDF FMMM, via the external Bandage engine.',
118
119
  run: () => undefined,
119
- referenceAxis: false,
120
+ cutMargins: false,
120
121
  available: () => true,
121
122
  drawsLocally: () => false,
122
123
  },
@@ -2508,7 +2508,7 @@ export default function stateModelFactory() {
2508
2508
  // window is cut to the window alone; one the host places carries
2509
2509
  // margins to pan over.
2510
2510
  settleOn(seen: HostWindow) {
2511
- const margins = layoutModeByValue(self.layoutMode).referenceAxis
2511
+ const margins = layoutModeByValue(self.layoutMode).cutMargins
2512
2512
  const above = self.coarseAboveBpPerPx
2513
2513
  const tier =
2514
2514
  above !== undefined && seen.bpPerPx > above ? 'coarse' : 'fine'
@@ -22,6 +22,27 @@ test('a graph view launched from this linear view contributes its highlight', ()
22
22
 
23
23
  const linearView = (id: string) => ({ id, type: 'LinearGenomeView' })
24
24
 
25
+ test("a graph pane inside this linear view's own track contributes its highlight", () => {
26
+ const view = {
27
+ ...linearView('lgv1'),
28
+ tracks: [
29
+ {
30
+ displays: [
31
+ {
32
+ id: 'display1',
33
+ type: 'LinearGraphDisplay',
34
+ pane: { type: 'GraphGenomeView', hoverHighlight: HIGHLIGHT },
35
+ },
36
+ ],
37
+ },
38
+ ],
39
+ }
40
+ expect(graphViewHighlights([view], 'lgv1')).toEqual([
41
+ { key: 'display1', region: HIGHLIGHT },
42
+ ])
43
+ expect(graphViewHighlights([view], 'lgv2')).toEqual([])
44
+ })
45
+
25
46
  test('a graph view launched from a different linear view is ignored', () => {
26
47
  expect(
27
48
  graphViewHighlights(
@@ -89,6 +89,21 @@ function isOnAssembly(
89
89
  // The highlights a linear view should draw for the graph views connected to it.
90
90
  // Reads `session.views` structurally: the members it needs are declared by
91
91
  // GraphGenomeView, not by the AbstractViewModel the session array is typed as.
92
+ // The graph panes inside a linear view's own tracks, connected to it by where
93
+ // they sit.
94
+ function trackPanes(view: Record<string, unknown>) {
95
+ const tracks = Array.isArray(view.tracks) ? view.tracks : []
96
+ return tracks.flatMap((track: unknown) => {
97
+ const displays =
98
+ isRecord(track) && Array.isArray(track.displays) ? track.displays : []
99
+ return displays.flatMap((display: unknown) =>
100
+ isRecord(display) && isRecord(display.pane)
101
+ ? [{ key: display.id, pane: display.pane }]
102
+ : [],
103
+ )
104
+ })
105
+ }
106
+
92
107
  export function graphViewHighlights(
93
108
  views: unknown[],
94
109
  linearViewId: string,
@@ -101,6 +116,17 @@ export function graphViewHighlights(
101
116
  withRows(views).map(view => (isRecord(view) ? view.id : undefined)),
102
117
  )
103
118
  for (const view of views) {
119
+ if (isRecord(view) && view.id === linearViewId) {
120
+ for (const { key, pane } of trackPanes(view)) {
121
+ const region = readRegion(pane.hoverHighlight)
122
+ if (region && isOnAssembly(region, linearAssemblyNames)) {
123
+ highlights.push({
124
+ key: typeof key === 'string' ? key : 'graph-track',
125
+ region,
126
+ })
127
+ }
128
+ }
129
+ }
104
130
  if (
105
131
  isRecord(view) &&
106
132
  view.type === 'GraphGenomeView' &&
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '4.0.4'
1
+ export const version = '4.0.6'