jbrowse-plugin-graphgenomeviewer 3.0.4 → 3.1.0

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 (58) hide show
  1. package/README.md +29 -2
  2. package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js +8 -0
  3. package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js.map +7 -0
  4. package/dist/chunks/GraphGenomeView-TM7WYDLH.js +14 -0
  5. package/dist/chunks/GraphGenomeView-TM7WYDLH.js.map +7 -0
  6. package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js +2 -0
  7. package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js.map +7 -0
  8. package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js +3 -0
  9. package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js.map +7 -0
  10. package/dist/chunks/{chunk-X3X74OGB.js → chunk-7T2HTRP3.js} +2 -2
  11. package/dist/chunks/{chunk-X3X74OGB.js.map → chunk-7T2HTRP3.js.map} +3 -3
  12. package/dist/chunks/{chunk-JCG7RLMB.js → chunk-HOM2Y6XS.js} +2 -2
  13. package/dist/chunks/chunk-HOM2Y6XS.js.map +7 -0
  14. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
  15. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  16. package/package.json +21 -22
  17. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +69 -0
  18. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +13 -10
  19. package/src/GetSubgraph.ts +5 -1
  20. package/src/GraphGenomeView/components/FollowControl.test.tsx +69 -0
  21. package/src/GraphGenomeView/components/FollowControl.tsx +42 -0
  22. package/src/GraphGenomeView/components/GraphStats.tsx +1 -0
  23. package/src/GraphGenomeView/components/GraphToolbar.tsx +2 -0
  24. package/src/GraphGenomeView/follow.test.ts +624 -0
  25. package/src/GraphGenomeView/follow.ts +129 -0
  26. package/src/GraphGenomeView/layout/sampleRowLayout.test.ts +16 -0
  27. package/src/GraphGenomeView/layout/sampleRowLayout.ts +13 -4
  28. package/src/GraphGenomeView/layoutModes.ts +3 -1
  29. package/src/GraphGenomeView/model.ts +361 -55
  30. package/src/GraphGenomeView/types.ts +4 -0
  31. package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +1 -1
  32. package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +73 -0
  33. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +48 -16
  34. package/src/RgfaTabixAdapter/configSchema.ts +70 -3
  35. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz +0 -0
  36. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz.tbi +0 -0
  37. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz +0 -0
  38. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz.tbi +0 -0
  39. package/src/launchSubgraph/index.ts +12 -6
  40. package/src/launchSubgraph/launchSubgraph.test.ts +89 -3
  41. package/src/launchSubgraph/launchSubgraphView.ts +14 -6
  42. package/src/launchSubgraph/linearViewMenuItems.ts +3 -0
  43. package/src/launchSubgraph/subgraphMenuItems.ts +7 -0
  44. package/src/launchSubgraph/testEnv.ts +2 -0
  45. package/src/panSNTabix.ts +6 -5
  46. package/src/synteny/ComparativeAdapterBase.ts +13 -10
  47. package/src/synteny/clipFeatureToRegion.test.ts +41 -1
  48. package/src/synteny/clipFeatureToRegion.ts +22 -5
  49. package/src/version.ts +1 -1
  50. package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js +0 -8
  51. package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js.map +0 -7
  52. package/dist/chunks/GraphGenomeView-UD5WCV3Q.js +0 -14
  53. package/dist/chunks/GraphGenomeView-UD5WCV3Q.js.map +0 -7
  54. package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js +0 -2
  55. package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js.map +0 -7
  56. package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js +0 -3
  57. package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js.map +0 -7
  58. package/dist/chunks/chunk-JCG7RLMB.js.map +0 -7
@@ -0,0 +1,129 @@
1
+ import type { SubgraphRegion } from '../launchSubgraph/launchSubgraphView'
2
+
3
+ // The members of a LinearGenomeView the follow reads and drives. Structural,
4
+ // because this plugin takes no runtime dependency on the LGV plugin (see
5
+ // hoverSync/index.tsx).
6
+ export interface FollowedBlock {
7
+ refName: string
8
+ assemblyName: string
9
+ start: number
10
+ end: number
11
+ offsetPx: number
12
+ reversed?: boolean
13
+ displayedRegionIndex?: number
14
+ }
15
+
16
+ export interface FollowedView {
17
+ id: string
18
+ initialized: boolean
19
+ bpPerPx: number
20
+ offsetPx: number
21
+ width: number
22
+ displayedRegions: readonly { start: number; end: number }[]
23
+ dynamicBlocks: { contentBlocks: readonly FollowedBlock[] }
24
+ coarseDynamicBlocks: readonly FollowedBlock[]
25
+ horizontalScroll: (distance: number) => number
26
+ zoomTo: (bpPerPx: number, offset?: number) => number
27
+ }
28
+
29
+ export function isFollowable(view: unknown): view is FollowedView {
30
+ if (typeof view !== 'object' || view === null) {
31
+ return false
32
+ }
33
+ const v = view as Record<string, unknown>
34
+ return (
35
+ typeof v.horizontalScroll === 'function' &&
36
+ typeof v.zoomTo === 'function' &&
37
+ 'coarseDynamicBlocks' in v &&
38
+ 'dynamicBlocks' in v
39
+ )
40
+ }
41
+
42
+ // launchSubgraphView's widestBlock, restated rather than imported: that module
43
+ // imports the model, which imports this.
44
+ function widest(blocks: readonly FollowedBlock[]) {
45
+ return blocks.reduce<FollowedBlock | undefined>(
46
+ (best, b) => (best && best.end - best.start >= b.end - b.start ? best : b),
47
+ undefined,
48
+ )
49
+ }
50
+
51
+ export interface FollowWindow {
52
+ refName: string
53
+ assemblyName: string
54
+ start: number
55
+ end: number
56
+ bpPerPx: number
57
+ // the view's width in bp, which the margin is sized by: a block at a contig
58
+ // end is clipped, the zoom is not
59
+ span: number
60
+ regionStart: number
61
+ regionEnd: number
62
+ }
63
+
64
+ // x is reference bp on an anchored layout, so the graph shows what the linear
65
+ // view shows when one bp is as many px in both and the cut's refName sits at
66
+ // the same screen x. Undefined when the linear view shows no block of that
67
+ // refName, which is a re-cut's job rather than the frame's.
68
+ export function followFrame(view: FollowedView, region: SubgraphRegion) {
69
+ const block = widest(
70
+ view.dynamicBlocks.contentBlocks.filter(
71
+ b =>
72
+ b.refName === region.refName && b.assemblyName === region.assemblyName,
73
+ ),
74
+ )
75
+ const { bpPerPx } = view
76
+ return block && bpPerPx > 0
77
+ ? {
78
+ scale: 1 / bpPerPx,
79
+ translateX: block.offsetPx - view.offsetPx - block.start / bpPerPx,
80
+ }
81
+ : undefined
82
+ }
83
+
84
+ // Read off the LIVE blocks even when the coarse ones woke the caller: those
85
+ // name where the window was half a second ago (SyntenyFollow/CLAUDE.md).
86
+ export function followWindow(view: FollowedView): FollowWindow | undefined {
87
+ const block = widest(view.dynamicBlocks.contentBlocks)
88
+ const displayed =
89
+ block?.displayedRegionIndex === undefined
90
+ ? undefined
91
+ : view.displayedRegions[block.displayedRegionIndex]
92
+ return block && view.bpPerPx > 0
93
+ ? {
94
+ refName: block.refName,
95
+ assemblyName: block.assemblyName,
96
+ start: block.start,
97
+ end: block.end,
98
+ bpPerPx: view.bpPerPx,
99
+ span: view.bpPerPx * view.width,
100
+ regionStart: displayed?.start ?? block.start,
101
+ regionEnd: displayed?.end ?? block.end,
102
+ }
103
+ : undefined
104
+ }
105
+
106
+ // The window plus one window-width each side, clamped to the displayed
107
+ // region, with the margins narrowed until the whole cut fits under `capBp`.
108
+ export function followCut(window: FollowWindow, capBp: number): SubgraphRegion {
109
+ const visible = window.end - window.start
110
+ const margin = Math.max(0, Math.min(window.span, (capBp - visible) / 2))
111
+ return {
112
+ refName: window.refName,
113
+ assemblyName: window.assemblyName,
114
+ start: Math.max(window.regionStart, Math.floor(window.start - margin)),
115
+ end: Math.min(window.regionEnd, Math.ceil(window.end + margin)),
116
+ }
117
+ }
118
+
119
+ export function cutHolds(
120
+ cut: SubgraphRegion | undefined,
121
+ window: FollowWindow,
122
+ ) {
123
+ return (
124
+ cut?.refName === window.refName &&
125
+ cut.assemblyName === window.assemblyName &&
126
+ cut.start <= window.start &&
127
+ window.end <= cut.end
128
+ )
129
+ }
@@ -34,6 +34,22 @@ test('rows are the backbone plus one per contributing assembly', () => {
34
34
  ])
35
35
  })
36
36
 
37
+ // A follow re-cuts as the linear view pans, and a stack re-sorted per window
38
+ // moves every row under the reader. Handed the rows on screen, the layout keeps
39
+ // those in their order, drops one the window no longer holds, and puts a sample
40
+ // new to the window below them.
41
+ test('the rows on screen keep their order, and a new sample goes below', () => {
42
+ const graph = ecoliGraph()
43
+ const [first, second, third] = sampleRowLayout(graph)!.sampleRows!
44
+ const { sampleRows, rowLabels } = sampleRowLayout(graph, undefined, [
45
+ 'HG00099',
46
+ third!,
47
+ second!,
48
+ ])!
49
+ expect(sampleRows).toEqual([third, second, first])
50
+ expect(rowLabels!.slice(1).map(r => r.label)).toEqual(sampleRows)
51
+ })
52
+
37
53
  // Alphabetical put HG00099 above HG00280 for no reason a reader could see, so a
38
54
  // row's neighbours said nothing. Ordering by contributed sequence reads the way
39
55
  // a sorted pileup does — most divergent at the top, and how far down the stack
@@ -55,11 +55,14 @@ import type { Graph, LayoutResult, RowLabel } from '../types'
55
55
  // What that costs is comparing one window against another — the order is a fact
56
56
  // about the window, so a sample is not in the same place in two of them. That is
57
57
  // the case §8's "a row set cannot be requested" is for, and an explicit sample
58
- // list is the right answer to it rather than an order nobody can read.
58
+ // list is the right answer to it rather than an order nobody can read. A
59
+ // follow's re-cut is the exception it can answer already: `keep` is the rows on
60
+ // screen, those samples hold their order, and a sample new to the window goes
61
+ // below them by content.
59
62
  //
60
63
  // Name breaks ties, so the order is still deterministic and a pan that changes
61
64
  // nothing about the content reshuffles nothing.
62
- function contributingSamples(graph: Graph) {
65
+ function contributingSamples(graph: Graph, keep: readonly string[]) {
63
66
  const carried = new Map<string, number>()
64
67
  for (const node of graph.nodes) {
65
68
  if (isOffReference(node)) {
@@ -67,17 +70,22 @@ function contributingSamples(graph: Graph) {
67
70
  carried.set(sample, (carried.get(sample) ?? 0) + node.length)
68
71
  }
69
72
  }
73
+ const held = new Map(keep.map((sample, row) => [sample, row]))
70
74
  return [...carried.keys()].sort(
71
- (a, b) => carried.get(b)! - carried.get(a)! || a.localeCompare(b),
75
+ (a, b) =>
76
+ (held.get(a) ?? Infinity) - (held.get(b) ?? Infinity) ||
77
+ carried.get(b)! - carried.get(a)! ||
78
+ a.localeCompare(b),
72
79
  )
73
80
  }
74
81
 
75
82
  export function sampleRowLayout(
76
83
  graph: Graph,
77
84
  region?: { start: number; end: number },
85
+ keep: readonly string[] = [],
78
86
  ): LayoutResult | undefined {
79
87
  const backbone = backboneNodes(graph)
80
- const samples = contributingSamples(graph)
88
+ const samples = contributingSamples(graph, keep)
81
89
  if (backbone.length === 0 || samples.length === 0) {
82
90
  return undefined
83
91
  }
@@ -127,6 +135,7 @@ export function sampleRowLayout(
127
135
  return {
128
136
  nodePositions,
129
137
  rowLabels,
138
+ sampleRows: samples,
130
139
  referenceAxis: true,
131
140
  pixelRows: true,
132
141
  alleleDeletions,
@@ -28,10 +28,12 @@ export interface LayoutMode {
28
28
  // import. The reference-anchored modes scale their rows and their allele
29
29
  // floor against it rather than against the backbone they were handed, which
30
30
  // a long-range allele's far anchor stretches well past the window
31
- // (referenceSpan).
31
+ // (referenceSpan). `keepRows` is the sample rows on screen, which only the
32
+ // sample-row layout reads (LayoutResult.sampleRows).
32
33
  run: (
33
34
  graph: Graph,
34
35
  region?: { start: number; end: number },
36
+ keepRows?: readonly string[],
35
37
  ) => LayoutResult | undefined
36
38
  // whether this mode can draw this graph at all; the dropdown greys out the
37
39
  // rest rather than hiding them, so the reason a mode is unavailable stays