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.
- package/README.md +29 -2
- package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js +8 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js.map +7 -0
- package/dist/chunks/GraphGenomeView-TM7WYDLH.js +14 -0
- package/dist/chunks/GraphGenomeView-TM7WYDLH.js.map +7 -0
- package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js +2 -0
- package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js.map +7 -0
- package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js +3 -0
- package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js.map +7 -0
- package/dist/chunks/{chunk-X3X74OGB.js → chunk-7T2HTRP3.js} +2 -2
- package/dist/chunks/{chunk-X3X74OGB.js.map → chunk-7T2HTRP3.js.map} +3 -3
- package/dist/chunks/{chunk-JCG7RLMB.js → chunk-HOM2Y6XS.js} +2 -2
- package/dist/chunks/chunk-HOM2Y6XS.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +21 -22
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +69 -0
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +13 -10
- package/src/GetSubgraph.ts +5 -1
- package/src/GraphGenomeView/components/FollowControl.test.tsx +69 -0
- package/src/GraphGenomeView/components/FollowControl.tsx +42 -0
- package/src/GraphGenomeView/components/GraphStats.tsx +1 -0
- package/src/GraphGenomeView/components/GraphToolbar.tsx +2 -0
- package/src/GraphGenomeView/follow.test.ts +624 -0
- package/src/GraphGenomeView/follow.ts +129 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.test.ts +16 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.ts +13 -4
- package/src/GraphGenomeView/layoutModes.ts +3 -1
- package/src/GraphGenomeView/model.ts +361 -55
- package/src/GraphGenomeView/types.ts +4 -0
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +1 -1
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +73 -0
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +48 -16
- package/src/RgfaTabixAdapter/configSchema.ts +70 -3
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz.tbi +0 -0
- package/src/launchSubgraph/index.ts +12 -6
- package/src/launchSubgraph/launchSubgraph.test.ts +89 -3
- package/src/launchSubgraph/launchSubgraphView.ts +14 -6
- package/src/launchSubgraph/linearViewMenuItems.ts +3 -0
- package/src/launchSubgraph/subgraphMenuItems.ts +7 -0
- package/src/launchSubgraph/testEnv.ts +2 -0
- package/src/panSNTabix.ts +6 -5
- package/src/synteny/ComparativeAdapterBase.ts +13 -10
- package/src/synteny/clipFeatureToRegion.test.ts +41 -1
- package/src/synteny/clipFeatureToRegion.ts +22 -5
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js +0 -8
- package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js.map +0 -7
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js +0 -14
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js.map +0 -7
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js +0 -2
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js.map +0 -7
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js +0 -3
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js.map +0 -7
- 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) =>
|
|
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
|