jbrowse-plugin-graphgenomeviewer 4.0.0 → 4.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbrowse-plugin-graphgenomeviewer",
3
- "version": "4.0.0",
3
+ "version": "4.0.3",
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": {
@@ -13,6 +13,7 @@ import { types } from '@jbrowse/mobx-state-tree'
13
13
  import { linearGenomeViewStateModelFactory } from '@jbrowse/plugin-linear-genome-view'
14
14
 
15
15
  import LinearGraphDisplayF from './index'
16
+ import GbzBaseSyntenyAdapterF from '../GbzBaseSyntenyAdapter/index'
16
17
  import RgfaTabixAdapterF from '../RgfaTabixAdapter/index'
17
18
 
18
19
  import type { LinearGraphDisplayModel } from './model'
@@ -97,25 +98,28 @@ function createEnvironment({ tiered = true } = {}) {
97
98
  console.warn = vi.fn()
98
99
  const pluginManager = new PluginManager()
99
100
  RgfaTabixAdapterF(pluginManager)
100
- pluginManager.addTrackType(() => {
101
- const trackConfigSchema = ConfigurationSchema(
102
- 'FeatureTrack',
103
- {},
104
- {
105
- baseConfiguration: createBaseTrackConfig(pluginManager),
106
- explicitIdentifier: 'trackId',
107
- },
108
- )
109
- return new TrackType({
110
- name: 'FeatureTrack',
111
- configSchema: trackConfigSchema,
112
- stateModel: createBaseTrackModel(
113
- pluginManager,
114
- 'FeatureTrack',
115
- trackConfigSchema,
116
- ),
101
+ GbzBaseSyntenyAdapterF(pluginManager)
102
+ for (const name of ['FeatureTrack', 'SyntenyTrack']) {
103
+ pluginManager.addTrackType(() => {
104
+ const trackConfigSchema = ConfigurationSchema(
105
+ name,
106
+ {},
107
+ {
108
+ baseConfiguration: createBaseTrackConfig(pluginManager),
109
+ explicitIdentifier: 'trackId',
110
+ },
111
+ )
112
+ return new TrackType({
113
+ name,
114
+ configSchema: trackConfigSchema,
115
+ stateModel: createBaseTrackModel(
116
+ pluginManager,
117
+ name,
118
+ trackConfigSchema,
119
+ ),
120
+ })
117
121
  })
118
- })
122
+ }
119
123
  LinearGraphDisplayF(pluginManager)
120
124
  pluginManager.addViewType(
121
125
  () =>
@@ -155,7 +159,20 @@ function createEnvironment({ tiered = true } = {}) {
155
159
  },
156
160
  { pluginManager },
157
161
  )
158
- const trackConfigs = [trackConfig]
162
+ const gbzTrackConfig = trackSchema.create(
163
+ {
164
+ type: 'SyntenyTrack',
165
+ trackId: 'walks',
166
+ name: 'walks',
167
+ assemblyNames: [ASM, 'HG1.1', 'HG2.1'],
168
+ adapter: { type: 'GbzBaseSyntenyAdapter', assemblyNames: [ASM] },
169
+ displays: [
170
+ { type: 'LinearGraphDisplay', displayId: 'walks-LinearGraphDisplay' },
171
+ ],
172
+ },
173
+ { pluginManager },
174
+ )
175
+ const trackConfigs = [trackConfig, gbzTrackConfig]
159
176
 
160
177
  const assemblyRegions = [
161
178
  { refName: REF, start: 0, end: CONTIG, assemblyName: ASM },
@@ -343,6 +360,58 @@ test('the track is as tall as its rows, up to the configured height', async () =
343
360
  expect(display.height).toBe(pane.canvasHeight)
344
361
  })
345
362
 
363
+ test("a launch names the pane's props without its type, and opens in that layout", async () => {
364
+ const { view, cuts } = createEnvironment()
365
+ view.zoomTo(60_000 / WIDTH_PX)
366
+ view.scrollTo(1_000_000 / view.bpPerPx)
367
+ view.showTrack(
368
+ 'graph',
369
+ {},
370
+ {
371
+ type: 'LinearGraphDisplay',
372
+ pane: { layoutMode: 'force', colorScheme: 'uniform' },
373
+ },
374
+ )
375
+ const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
376
+ display.pane.startRenderingBackend(fakeRenderer())
377
+ await wait(SETTLE_MS)
378
+ expect(cuts).toHaveLength(1)
379
+ expect(display.pane.layoutMode).toBe('force')
380
+ expect(display.pane.colorScheme).toBe('uniform')
381
+ expect(display.pane.hostPlacesX).toBe(false)
382
+ })
383
+
384
+ test('a launch that states one pane prop takes the rest from the config', async () => {
385
+ const { view, cuts } = createEnvironment()
386
+ view.zoomTo(60_000 / WIDTH_PX)
387
+ view.scrollTo(1_000_000 / view.bpPerPx)
388
+ view.showTrack(
389
+ 'graph',
390
+ {},
391
+ {
392
+ type: 'LinearGraphDisplay',
393
+ pane: { colorScheme: 'uniform' },
394
+ },
395
+ )
396
+ const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
397
+ display.pane.startRenderingBackend(fakeRenderer())
398
+ await wait(SETTLE_MS)
399
+ expect(cuts).toHaveLength(1)
400
+ expect(display.pane.colorScheme).toBe('uniform')
401
+ expect(display.pane.layoutMode).toBe('auto')
402
+ expect(display.pane.hostPlacesX).toBe(true)
403
+ })
404
+
405
+ test('a GBZ track cuts for the lanes it names', async () => {
406
+ const { view } = createEnvironment()
407
+ view.zoomTo(60_000 / WIDTH_PX)
408
+ view.scrollTo(1_000_000 / view.bpPerPx)
409
+ view.showTrack('walks')
410
+ const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
411
+ expect(display.type).toBe('LinearGraphDisplay')
412
+ expect(display.pane.subgraphHaplotypes).toEqual(['HG1.1', 'HG2.1'])
413
+ })
414
+
346
415
  test('the track menu offers the layouts, colours and the settings dialog', async () => {
347
416
  const { display } = await shownGraph()
348
417
  const labels = display
@@ -25,6 +25,10 @@ const LinearGraphDisplay = observer(function LinearGraphDisplay({
25
25
  return (
26
26
  <div
27
27
  data-testid="linear-graph-display"
28
+ data-layout={pane.layoutMode}
29
+ data-cut-tier={pane.cutTier}
30
+ data-recuts={pane.recuts}
31
+ data-node-count={pane.hasGraph ? pane.nodeCount : undefined}
28
32
  style={{
29
33
  position: 'relative',
30
34
  width: pane.width,
@@ -12,6 +12,7 @@ import { COLOR_SCHEMES } from '../GraphGenomeView/colorSchemes'
12
12
  import { isLinearHost } from '../GraphGenomeView/host'
13
13
  import { LAYOUT_MODES } from '../GraphGenomeView/layoutModes'
14
14
  import paneModelFactory from '../GraphGenomeView/model'
15
+ import { trackLanes } from '../launchSubgraph/subgraphTracks'
15
16
 
16
17
  import type { LinearGraphDisplayConfigModel } from './configSchema'
17
18
  import type { MenuItem } from '@jbrowse/core/ui'
@@ -21,206 +22,282 @@ const GraphSettingsDialog = lazy(
21
22
  () => import('../GraphGenomeView/components/GraphSettingsDialog'),
22
23
  )
23
24
 
25
+ type ConfiguredSlot = 'layoutMode' | 'colorScheme' | 'height'
26
+
24
27
  export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
25
28
  const Pane = paneModelFactory()
26
- return types
27
- .compose(
28
- 'LinearGraphDisplay',
29
- BaseDisplay,
30
- TrackHeightMixin(),
31
- types.model({
32
- type: types.literal('LinearGraphDisplay'),
33
- configuration: ConfigurationReference(configSchema),
34
- // The graph pane, the same model the standalone view is. Here the
35
- // linear view above is its host: x is that view's window for a layout
36
- // whose x is reference bp, and the cut is re-made when the window
37
- // leaves it (pane.host).
38
- pane: types.optional(Pane, () => ({
39
- type: 'GraphGenomeView' as const,
40
- })),
41
- // The config's layout and colour reach the pane once, so a choice made
42
- // in the track menu survives a reload.
43
- configured: types.optional(types.boolean, false),
44
- }),
45
- )
46
- .views(self => ({
47
- get host() {
48
- let view: unknown
49
- try {
50
- view = getContainingView(self)
51
- } catch {
52
- return undefined
29
+ return (
30
+ types
31
+ .compose(
32
+ 'LinearGraphDisplay',
33
+ BaseDisplay,
34
+ TrackHeightMixin(),
35
+ types.model({
36
+ type: types.literal('LinearGraphDisplay'),
37
+ configuration: ConfigurationReference(configSchema),
38
+ // The graph pane, the same model the standalone view is. Here the
39
+ // linear view above is its host: x is that view's window for a layout
40
+ // whose x is reference bp, and the cut is re-made when the window
41
+ // leaves it (pane.host).
42
+ pane: types.optional(Pane, () => ({
43
+ type: 'GraphGenomeView' as const,
44
+ })),
45
+ // Which of the config's slots have reached the pane. Each reaches it
46
+ // once, so a choice made in the track menu survives a reload, and a
47
+ // launch that states a pane prop keeps it over the config's.
48
+ configured: types.optional(
49
+ types.frozen<Partial<Record<ConfiguredSlot, boolean>>>(),
50
+ {},
51
+ ),
52
+ }),
53
+ )
54
+ // A launch names the pane's props without the pane's own type, and what
55
+ // it states is what the config does not set.
56
+ .preProcessSnapshot(snapshot => {
57
+ const { pane, configured } = snapshot as {
58
+ pane?: Record<string, unknown>
59
+ configured?: Partial<Record<ConfiguredSlot, boolean>>
53
60
  }
54
- return isLinearHost(view) ? view : undefined
55
- },
56
- get trackId(): string {
57
- return getConf(self.parentTrack, 'trackId')
58
- },
59
- }))
60
- .views(self => ({
61
- trackMenuItems(): MenuItem[] {
62
- const { pane } = self
63
- const { graph } = pane
64
- const walks = pane.walkChoices
65
- return [
66
- {
67
- label: 'Layout',
68
- subMenu: LAYOUT_MODES.map(mode => ({
69
- type: 'radio' as const,
70
- label: mode.label,
71
- checked: pane.layoutMode === mode.value,
72
- disabled: graph ? !mode.available(graph) : false,
73
- onClick: () => {
74
- void pane.switchLayout(mode.value)
75
- },
76
- })),
77
- },
78
- {
79
- label: 'Color',
80
- subMenu: COLOR_SCHEMES.map(scheme => ({
81
- type: 'radio' as const,
82
- label: scheme.label,
83
- checked: pane.colorScheme === scheme.value,
84
- onClick: () => {
85
- pane.setColorScheme(scheme.value)
61
+ return pane
62
+ ? ({
63
+ ...snapshot,
64
+ configured: {
65
+ layoutMode: pane.layoutMode !== undefined,
66
+ colorScheme: pane.colorScheme !== undefined,
67
+ height: pane.paneHeight !== undefined,
68
+ ...configured,
86
69
  },
87
- })),
88
- },
89
- ...(walks.length > 0
90
- ? [
91
- {
92
- label: 'Walk',
93
- subMenu: [
94
- {
95
- type: 'radio' as const,
96
- label: 'None',
97
- checked: pane.highlightedPath === '',
98
- onClick: () => {
99
- pane.setHighlightedPath('')
70
+ pane:
71
+ pane.type === undefined
72
+ ? { type: 'GraphGenomeView', ...pane }
73
+ : pane,
74
+ } as typeof snapshot)
75
+ : snapshot
76
+ })
77
+ .views(self => ({
78
+ get host() {
79
+ let view: unknown
80
+ try {
81
+ view = getContainingView(self)
82
+ } catch {
83
+ return undefined
84
+ }
85
+ return isLinearHost(view) ? view : undefined
86
+ },
87
+ get trackId(): string {
88
+ return getConf(self.parentTrack, 'trackId')
89
+ },
90
+ }))
91
+ .views(self => ({
92
+ trackMenuItems(): MenuItem[] {
93
+ const { pane } = self
94
+ const { graph } = pane
95
+ const walks = pane.walkChoices
96
+ return [
97
+ {
98
+ label: 'Layout',
99
+ subMenu: LAYOUT_MODES.map(mode => ({
100
+ type: 'radio' as const,
101
+ label: mode.label,
102
+ checked: pane.layoutMode === mode.value,
103
+ disabled: graph ? !mode.available(graph) : false,
104
+ onClick: () => {
105
+ void pane.switchLayout(mode.value)
106
+ },
107
+ })),
108
+ },
109
+ {
110
+ label: 'Color',
111
+ subMenu: COLOR_SCHEMES.map(scheme => ({
112
+ type: 'radio' as const,
113
+ label: scheme.label,
114
+ checked: pane.colorScheme === scheme.value,
115
+ onClick: () => {
116
+ pane.setColorScheme(scheme.value)
117
+ },
118
+ })),
119
+ },
120
+ ...(walks.length > 0
121
+ ? [
122
+ {
123
+ label: 'Walk',
124
+ subMenu: [
125
+ {
126
+ type: 'radio' as const,
127
+ label: 'None',
128
+ checked: pane.highlightedPath === '',
129
+ onClick: () => {
130
+ pane.setHighlightedPath('')
131
+ },
100
132
  },
101
- },
102
- ...walks.map(walk => ({
103
- type: 'radio' as const,
104
- label: walk.label,
105
- checked: pane.highlightedPath === walk.name,
106
- onClick: () => {
107
- pane.setHighlightedPath(walk.name)
133
+ ...walks.map(walk => ({
134
+ type: 'radio' as const,
135
+ label: walk.label,
136
+ checked: pane.highlightedPath === walk.name,
137
+ onClick: () => {
138
+ pane.setHighlightedPath(walk.name)
139
+ },
140
+ })),
141
+ ],
142
+ },
143
+ ]
144
+ : []),
145
+ ...(pane.layoutMode === 'walkrows' && pane.repeatChoices.length > 0
146
+ ? [
147
+ {
148
+ label: 'Repeat',
149
+ subMenu: [
150
+ {
151
+ type: 'radio' as const,
152
+ label: 'Whole window',
153
+ checked: pane.repeatKey === '',
154
+ onClick: () => {
155
+ pane.setRepeatKey('')
156
+ },
108
157
  },
109
- })),
110
- ],
111
- },
112
- ]
113
- : []),
114
- ...(pane.hostPlacesX
115
- ? []
116
- : [
117
- {
118
- label: 'Zoom in',
119
- onClick: () => {
120
- pane.zoom(1.5, pane.width / 2, pane.canvasHeight / 2)
158
+ ...pane.repeatChoices.map(({ key, name, unit }) => ({
159
+ type: 'radio' as const,
160
+ label: `${name} · ${unit.toLocaleString()} bp unit`,
161
+ checked: pane.repeatKey === key,
162
+ onClick: () => {
163
+ pane.setRepeatKey(key)
164
+ },
165
+ })),
166
+ ],
121
167
  },
122
- },
123
- {
124
- label: 'Zoom out',
125
- onClick: () => {
126
- pane.zoom(1 / 1.5, pane.width / 2, pane.canvasHeight / 2)
168
+ ]
169
+ : []),
170
+ ...(pane.hostPlacesX
171
+ ? []
172
+ : [
173
+ {
174
+ label: 'Zoom in',
175
+ onClick: () => {
176
+ pane.zoom(1.5, pane.width / 2, pane.canvasHeight / 2)
177
+ },
127
178
  },
128
- },
129
- {
130
- label: 'Zoom to fit',
131
- onClick: () => {
132
- pane.zoomToFit()
179
+ {
180
+ label: 'Zoom out',
181
+ onClick: () => {
182
+ pane.zoom(1 / 1.5, pane.width / 2, pane.canvasHeight / 2)
183
+ },
133
184
  },
134
- },
135
- ]),
136
- {
137
- type: 'checkbox',
138
- label: 'Mark bubbles',
139
- checked: pane.showBubbles,
140
- onClick: () => {
141
- pane.setShowBubbles(!pane.showBubbles)
142
- },
143
- },
144
- {
145
- type: 'checkbox',
146
- label: 'Show deletion edges',
147
- checked: pane.showDeletionEdges,
148
- onClick: () => {
149
- pane.setShowDeletionEdges(!pane.showDeletionEdges)
150
- },
151
- },
152
- {
153
- type: 'checkbox',
154
- label: 'Genes on the backbone',
155
- checked: pane.showGenes,
156
- onClick: () => {
157
- pane.setShowGenes(!pane.showGenes)
185
+ {
186
+ label: 'Zoom to fit',
187
+ onClick: () => {
188
+ pane.zoomToFit()
189
+ },
190
+ },
191
+ ]),
192
+ {
193
+ type: 'checkbox',
194
+ label: 'Mark bubbles',
195
+ checked: pane.showBubbles,
196
+ onClick: () => {
197
+ pane.setShowBubbles(!pane.showBubbles)
198
+ },
158
199
  },
159
- },
160
- {
161
- label: 'Settings',
162
- icon: SettingsIcon,
163
- onClick: () => {
164
- getSession(self).queueDialog(onClose => [
165
- GraphSettingsDialog,
166
- { model: pane, open: true, onClose },
167
- ])
200
+ {
201
+ type: 'checkbox',
202
+ label: 'Show deletion edges',
203
+ checked: pane.showDeletionEdges,
204
+ onClick: () => {
205
+ pane.setShowDeletionEdges(!pane.showDeletionEdges)
206
+ },
168
207
  },
169
- },
170
- ]
171
- },
172
- }))
173
- .actions(self => ({
174
- setConfigured() {
175
- self.configured = true
176
- },
177
- // The track's height is the pane's ceiling: a layout shorter than it
178
- // takes only what it needs, and a drag on the handle moves the ceiling.
179
- resizeHeight(distance: number) {
180
- const before = self.height
181
- const target = Math.max(before + distance, 40)
182
- self.pane.setPaneHeight(target)
183
- return target - before
184
- },
185
- }))
186
- .actions(self => ({
187
- afterAttach() {
188
- const { pane } = self
189
- if (!self.configured) {
190
- pane.setLayoutMode(getConf(self, 'layoutMode'))
191
- pane.setColorScheme(getConf(self, 'colorScheme'))
192
- pane.setPaneHeight(getConf(self, 'height'))
193
- self.setConfigured()
194
- }
195
- pane.adoptTrack(self.trackId)
196
- pane.startHosting()
197
- addDisposer(
198
- self,
199
- autorun(
200
- () => {
201
- const { host } = self
202
- if (host) {
203
- pane.setWidth(host.width)
204
- pane.pairWithLinearView(host.id)
205
- }
208
+ {
209
+ type: 'checkbox',
210
+ label: 'Genes on the backbone',
211
+ checked: pane.showGenes,
212
+ onClick: () => {
213
+ pane.setShowGenes(!pane.showGenes)
214
+ },
206
215
  },
207
- { name: 'LinearGraphDisplayHost' },
208
- ),
209
- )
210
- addDisposer(
211
- self,
212
- autorun(
213
- () => {
214
- const height = pane.canvasHeight
215
- if (self.height !== height) {
216
- self.setHeight(height)
217
- }
216
+ {
217
+ label: 'Settings',
218
+ icon: SettingsIcon,
219
+ onClick: () => {
220
+ getSession(self).queueDialog(onClose => [
221
+ GraphSettingsDialog,
222
+ { model: pane, open: true, onClose },
223
+ ])
224
+ },
218
225
  },
219
- { name: 'LinearGraphDisplayHeight' },
220
- ),
221
- )
222
- },
223
- }))
226
+ ]
227
+ },
228
+ }))
229
+ .actions(self => ({
230
+ // The config's slots the pane has not yet taken, then all of them.
231
+ applyConfig() {
232
+ const { pane, configured } = self
233
+ if (!configured.layoutMode) {
234
+ pane.setLayoutMode(getConf(self, 'layoutMode'))
235
+ }
236
+ if (!configured.colorScheme) {
237
+ pane.setColorScheme(getConf(self, 'colorScheme'))
238
+ }
239
+ if (!configured.height) {
240
+ pane.setPaneHeight(getConf(self, 'height'))
241
+ }
242
+ self.configured = {
243
+ layoutMode: true,
244
+ colorScheme: true,
245
+ height: true,
246
+ }
247
+ },
248
+ // The track's height is the pane's ceiling: a layout shorter than it
249
+ // takes only what it needs, and a drag on the handle moves the ceiling.
250
+ resizeHeight(distance: number) {
251
+ const before = self.height
252
+ const target = Math.max(before + distance, 40)
253
+ self.pane.setPaneHeight(target)
254
+ return target - before
255
+ },
256
+ }))
257
+ .actions(self => ({
258
+ afterAttach() {
259
+ const { pane } = self
260
+ self.applyConfig()
261
+ pane.adoptTrack(self.trackId)
262
+ // A GBZ track cuts for the lanes it names, as its lanes display
263
+ // draws them, unless a launch or a reader picked others.
264
+ if (
265
+ pane.subgraphHaplotypes === undefined &&
266
+ self.adapterConfig.type === 'GbzBaseSyntenyAdapter'
267
+ ) {
268
+ pane.setSubgraphHaplotypes(
269
+ trackLanes(self.parentTrack.configuration),
270
+ )
271
+ }
272
+ pane.startHosting()
273
+ addDisposer(
274
+ self,
275
+ autorun(
276
+ () => {
277
+ const { host } = self
278
+ if (host) {
279
+ pane.setWidth(host.width)
280
+ pane.pairWithLinearView(host.id)
281
+ }
282
+ },
283
+ { name: 'LinearGraphDisplayHost' },
284
+ ),
285
+ )
286
+ addDisposer(
287
+ self,
288
+ autorun(
289
+ () => {
290
+ const height = pane.canvasHeight
291
+ if (self.height !== height) {
292
+ self.setHeight(height)
293
+ }
294
+ },
295
+ { name: 'LinearGraphDisplayHeight' },
296
+ ),
297
+ )
298
+ },
299
+ }))
300
+ )
224
301
  }
225
302
 
226
303
  export type LinearGraphDisplayStateModel = ReturnType<typeof stateModelFactory>
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '4.0.0'
1
+ export const version = '4.0.3'
@@ -1,2 +0,0 @@
1
- import{a as e,b as i}from"./chunk-SDTCVLC3.js";import"./chunk-A4ZWFDQY.js";import"./chunk-ZNGDWZ6Z.js";import"./chunk-OHT6UX4C.js";import"./chunk-OP7IRVG6.js";import"./chunk-F4C3BEPV.js";import{a as r,b as s,c as h}from"./chunk-XEJ47DLD.js";import{b as t}from"./chunk-FY3BTKY4.js";var p=t(h(),1),n=t(s(),1);var o=t(r(),1),m={position:"absolute",left:8,top:4,zIndex:5,background:"rgba(255,255,255,0.82)",padding:"0 4px",borderRadius:3},y=(0,n.observer)(function({model:l}){let{pane:a,height:d}=l;return(0,o.jsxs)("div",{"data-testid":"linear-graph-display",style:{position:"relative",width:a.width,height:d,overflow:"hidden"},children:[(0,o.jsx)(e,{model:a,toolbar:!1}),a.hasGraph?null:(0,o.jsx)(i,{model:a}),a.cutNote?(0,o.jsx)(p.Typography,{variant:"caption",color:"warning.main",style:m,"data-testid":"graph-cut-note",children:a.cutNote}):null]})}),g=y;export{g as default};
2
- //# sourceMappingURL=LinearGraphDisplay-P7U6NCZF.js.map