jbrowse-plugin-graphgenomeviewer 5.0.1 → 5.2.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 +3 -4
- package/dist/chunks/GbzBaseSyntenyAdapter-TQVDD3OC.js +8 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-TQVDD3OC.js.map +7 -0
- package/dist/chunks/{GraphGenomeView-YYP4IFSI.js → GraphGenomeView-AY2FO4P2.js} +2 -2
- package/dist/chunks/{GraphTrackSettingsDialog-2WPJRAJC.js → GraphTrackSettingsDialog-QJXPMWYG.js} +2 -2
- package/dist/chunks/LinearGraphDisplay-GKVG72J4.js +2 -0
- package/dist/chunks/LinearGraphDisplay-GKVG72J4.js.map +7 -0
- package/dist/chunks/chunk-2V27PV25.js +3 -0
- package/dist/chunks/chunk-2V27PV25.js.map +7 -0
- package/dist/chunks/chunk-FNHKEAKB.js +5 -0
- package/dist/chunks/{chunk-LGFKHSVJ.js.map → chunk-FNHKEAKB.js.map} +3 -3
- package/dist/chunks/chunk-K5Z47YVI.js +2 -0
- package/dist/chunks/chunk-K5Z47YVI.js.map +7 -0
- package/dist/chunks/{chunk-SJNG2XK7.js → chunk-LJZDHT2C.js} +2 -2
- package/dist/chunks/chunk-NWJN4C4V.js +2 -0
- package/dist/chunks/chunk-NWJN4C4V.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +5 -5
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +20 -38
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +14 -65
- package/src/GraphGenomeView/components/GenePins.tsx +25 -80
- package/src/GraphGenomeView/components/usePaneGestures.ts +1 -0
- package/src/GraphGenomeView/genes/geneFeatures.ts +46 -7
- package/src/GraphGenomeView/genes/genes.test.ts +88 -1
- package/src/GraphGenomeView/model.test.ts +1 -1
- package/src/GraphGenomeView/pane/fitViews.ts +1 -0
- package/src/GraphGenomeView/pane/graphViews.ts +39 -5
- package/src/GraphGenomeView/pane/loadActions.ts +5 -5
- package/src/GraphGenomeView/pane/paneBase.ts +3 -2
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +160 -92
- package/src/LinearGraphDisplay/components/LinearGraphDisplay.tsx +2 -12
- package/src/LinearGraphDisplay/denseWindow.ts +40 -0
- package/src/LinearGraphDisplay/model.ts +34 -94
- package/src/index.ts +0 -2
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-CKKVHC2R.js +0 -4
- package/dist/chunks/GbzBaseSyntenyAdapter-CKKVHC2R.js.map +0 -7
- package/dist/chunks/LinearGraphDisplay-7YE624D5.js +0 -2
- package/dist/chunks/LinearGraphDisplay-7YE624D5.js.map +0 -7
- package/dist/chunks/chunk-2H7GDQ3R.js +0 -6
- package/dist/chunks/chunk-2H7GDQ3R.js.map +0 -7
- package/dist/chunks/chunk-BZOIENU5.js +0 -2
- package/dist/chunks/chunk-BZOIENU5.js.map +0 -7
- package/dist/chunks/chunk-H54W6OQQ.js +0 -2
- package/dist/chunks/chunk-H54W6OQQ.js.map +0 -7
- package/dist/chunks/chunk-LGFKHSVJ.js +0 -5
- package/dist/chunks/chunk-O4XV3K35.js +0 -2
- package/dist/chunks/chunk-O4XV3K35.js.map +0 -7
- package/dist/chunks/chunk-WKVVIDCS.js +0 -3
- package/dist/chunks/chunk-WKVVIDCS.js.map +0 -7
- package/src/GetHaplotypeOverview.ts +0 -77
- package/src/HaplotypeOverview/HaplotypeOverviewCanvas.tsx +0 -203
- package/src/HaplotypeOverview/draw.test.ts +0 -136
- package/src/HaplotypeOverview/draw.ts +0 -433
- package/src/HaplotypeOverview/model.ts +0 -163
- /package/dist/chunks/{GraphGenomeView-YYP4IFSI.js.map → GraphGenomeView-AY2FO4P2.js.map} +0 -0
- /package/dist/chunks/{GraphTrackSettingsDialog-2WPJRAJC.js.map → GraphTrackSettingsDialog-QJXPMWYG.js.map} +0 -0
- /package/dist/chunks/{chunk-SJNG2XK7.js.map → chunk-LJZDHT2C.js.map} +0 -0
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OVERVIEW_ABSENT,
|
|
3
|
-
OVERVIEW_PARTIAL,
|
|
4
|
-
OVERVIEW_REFERENCE,
|
|
5
|
-
OVERVIEW_VARIANT,
|
|
6
|
-
} from '@gmod/gbz-base'
|
|
7
|
-
|
|
8
|
-
import type { HaplotypeOverviewData } from '../GetHaplotypeOverview'
|
|
9
|
-
|
|
10
|
-
type Rgb = readonly [number, number, number]
|
|
11
|
-
|
|
12
|
-
// The variant track's colours: reference grey, so the two tracks read alike,
|
|
13
|
-
// and variant cells deepening with how many variant marks the bin holds
|
|
14
|
-
export const PALETTE = {
|
|
15
|
-
absent: [244, 244, 244],
|
|
16
|
-
reference: [200, 200, 200],
|
|
17
|
-
partial: [224, 204, 150],
|
|
18
|
-
variant: [
|
|
19
|
-
[132, 166, 200],
|
|
20
|
-
[64, 116, 168],
|
|
21
|
-
[33, 82, 132],
|
|
22
|
-
[14, 46, 86],
|
|
23
|
-
],
|
|
24
|
-
} as const satisfies Record<string, Rgb | readonly Rgb[]>
|
|
25
|
-
|
|
26
|
-
export const DENSITY_PX = 40
|
|
27
|
-
const GAP_PX = 4
|
|
28
|
-
const PIN_GAP_PX = 3
|
|
29
|
-
const PIN_ROW_PX = 14
|
|
30
|
-
const LABEL_MIN_PX = 9
|
|
31
|
-
// Past 16 kb bins nearly every bin holds a 50 bp excursion for most
|
|
32
|
-
// haplotypes, so the density row counts variant excursions instead of
|
|
33
|
-
// haplotypes per class
|
|
34
|
-
export const FINE_BIN_BP = 16_384
|
|
35
|
-
|
|
36
|
-
// "Diverges" rather than "variant": only excursions of 50 bp or more and
|
|
37
|
-
// rearrangements count, so a haplotype full of SNPs is reference-like
|
|
38
|
-
const CLASS_NAMES = [
|
|
39
|
-
'absent: no contig covers the bin',
|
|
40
|
-
'reference-like',
|
|
41
|
-
'partial: contigs cover under 90% of the bin',
|
|
42
|
-
'diverges',
|
|
43
|
-
]
|
|
44
|
-
const MARK_BUCKETS = [
|
|
45
|
-
'1 excursion',
|
|
46
|
-
'2–3 excursions',
|
|
47
|
-
'4–15 excursions',
|
|
48
|
-
'16+ excursions',
|
|
49
|
-
]
|
|
50
|
-
export const NOT_SHOWN = 'SNPs and indels under 50 bp are not shown'
|
|
51
|
-
|
|
52
|
-
export function cellColor(cell: number): Rgb {
|
|
53
|
-
const klass = cell & 3
|
|
54
|
-
return klass === OVERVIEW_ABSENT
|
|
55
|
-
? PALETTE.absent
|
|
56
|
-
: klass === OVERVIEW_REFERENCE
|
|
57
|
-
? PALETTE.reference
|
|
58
|
-
: klass === OVERVIEW_PARTIAL
|
|
59
|
-
? PALETTE.partial
|
|
60
|
-
: PALETTE.variant[cell >> 2]!
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function cellLabel(cell: number) {
|
|
64
|
-
const klass = cell & 3
|
|
65
|
-
return klass === OVERVIEW_VARIANT
|
|
66
|
-
? `diverges ≥50 bp, ${MARK_BUCKETS[cell >> 2]}`
|
|
67
|
-
: CLASS_NAMES[klass]!
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface OverviewLayout {
|
|
71
|
-
rows: number[]
|
|
72
|
-
pinnedCount: number
|
|
73
|
-
pinnedHeight: number
|
|
74
|
-
restHeight: number
|
|
75
|
-
rowsTop: number
|
|
76
|
-
restTop: number
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// The track's lanes first, at a height their labels fit when there is room,
|
|
80
|
-
// then every other haplotype unless only the lanes are wanted, sharing what
|
|
81
|
-
// is left. The reference sample's rows never draw.
|
|
82
|
-
export function overviewLayout(
|
|
83
|
-
data: Pick<HaplotypeOverviewData, 'rows' | 'pinned' | 'reference'>,
|
|
84
|
-
allRows: boolean,
|
|
85
|
-
height: number,
|
|
86
|
-
): OverviewLayout {
|
|
87
|
-
const skip = new Set([...data.reference, ...data.pinned])
|
|
88
|
-
const pinned = data.pinned.filter(row => !data.reference.includes(row))
|
|
89
|
-
const rest = allRows
|
|
90
|
-
? data.rows.flatMap((_, row) => (skip.has(row) ? [] : [row]))
|
|
91
|
-
: []
|
|
92
|
-
const rowsTop = DENSITY_PX + GAP_PX
|
|
93
|
-
const available = Math.max(0, height - rowsTop)
|
|
94
|
-
const gap = pinned.length > 0 && rest.length > 0 ? PIN_GAP_PX : 0
|
|
95
|
-
const pinnedHeight =
|
|
96
|
-
pinned.length === 0
|
|
97
|
-
? 0
|
|
98
|
-
: Math.min(
|
|
99
|
-
PIN_ROW_PX,
|
|
100
|
-
(rest.length > 0 ? available * 0.4 : available) / pinned.length,
|
|
101
|
-
)
|
|
102
|
-
const restTop = rowsTop + pinned.length * pinnedHeight + gap
|
|
103
|
-
return {
|
|
104
|
-
rows: [...pinned, ...rest],
|
|
105
|
-
pinnedCount: pinned.length,
|
|
106
|
-
pinnedHeight,
|
|
107
|
-
restHeight:
|
|
108
|
-
rest.length > 0 ? Math.max(0, height - restTop) / rest.length : 0,
|
|
109
|
-
rowsTop,
|
|
110
|
-
restTop,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function rowTop(layout: OverviewLayout, index: number) {
|
|
115
|
-
return index < layout.pinnedCount
|
|
116
|
-
? layout.rowsTop + index * layout.pinnedHeight
|
|
117
|
-
: layout.restTop + (index - layout.pinnedCount) * layout.restHeight
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// The haplotype row under y, or undefined over the density row or a gap
|
|
121
|
-
export function rowAt(layout: OverviewLayout, y: number) {
|
|
122
|
-
const { pinnedCount, pinnedHeight, restHeight, rowsTop, restTop } = layout
|
|
123
|
-
const pinnedEnd = rowsTop + pinnedCount * pinnedHeight
|
|
124
|
-
const index =
|
|
125
|
-
y >= rowsTop && y < pinnedEnd
|
|
126
|
-
? Math.floor((y - rowsTop) / pinnedHeight)
|
|
127
|
-
: y >= restTop && restHeight > 0
|
|
128
|
-
? pinnedCount + Math.floor((y - restTop) / restHeight)
|
|
129
|
-
: undefined
|
|
130
|
-
return index !== undefined && index < layout.rows.length
|
|
131
|
-
? layout.rows[index]
|
|
132
|
-
: undefined
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// The bin holding bp, or undefined between fragments and past either end
|
|
136
|
-
export function binAt(data: Pick<HaplotypeOverviewData, 'bins'>, bp: number) {
|
|
137
|
-
const { bins } = data
|
|
138
|
-
let lo = 0
|
|
139
|
-
let hi = bins.length - 1
|
|
140
|
-
while (lo < hi) {
|
|
141
|
-
const mid = (lo + hi + 1) >> 1
|
|
142
|
-
if (bins[mid]!.start <= bp) {
|
|
143
|
-
lo = mid
|
|
144
|
-
} else {
|
|
145
|
-
hi = mid - 1
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
const bin = bins[lo]
|
|
149
|
-
return bin && bin.start <= bp && bp < bin.end ? lo : undefined
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Per bin, the haplotypes that are not the reference counted by what the
|
|
153
|
-
// density row stacks: absent, partial, and variant in each mark bucket
|
|
154
|
-
export function densityCounts(data: HaplotypeOverviewData) {
|
|
155
|
-
const height = data.rows.length
|
|
156
|
-
const reference = new Set(data.reference)
|
|
157
|
-
return data.bins.map((_, b) => {
|
|
158
|
-
const counts = [0, 0, 0, 0, 0, 0]
|
|
159
|
-
for (let row = 0; row < height; row++) {
|
|
160
|
-
if (!reference.has(row)) {
|
|
161
|
-
const cell = data.cells[b * height + row]!
|
|
162
|
-
const klass = cell & 3
|
|
163
|
-
if (klass === OVERVIEW_VARIANT) {
|
|
164
|
-
counts[2 + (cell >> 2)]! += 1
|
|
165
|
-
} else if (klass === OVERVIEW_ABSENT) {
|
|
166
|
-
counts[0]! += 1
|
|
167
|
-
} else if (klass === OVERVIEW_PARTIAL) {
|
|
168
|
-
counts[1]! += 1
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return counts
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function describeBin(data: HaplotypeOverviewData, b: number) {
|
|
177
|
-
const bin = data.bins[b]!
|
|
178
|
-
const [absent, reference, partial, variant] = bin.classes
|
|
179
|
-
return {
|
|
180
|
-
span: `${bin.start.toLocaleString()}–${bin.end.toLocaleString()}`,
|
|
181
|
-
classes: `${reference} reference-like, ${variant} diverge ≥50 bp, ${partial} partial, ${absent} absent`,
|
|
182
|
-
excursions: `${bin.excursions.toLocaleString()} excursions, ${bin.variants.toLocaleString()} of 50 bp or more, longest ${bin.longestExcursion.toLocaleString()} bp`,
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
interface Frame {
|
|
187
|
-
// screen x of a bp on the overview's contig
|
|
188
|
-
xOf: (bp: number) => number
|
|
189
|
-
width: number
|
|
190
|
-
height: number
|
|
191
|
-
dpr: number
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function makeCanvas(width: number, height: number) {
|
|
195
|
-
const canvas = document.createElement('canvas')
|
|
196
|
-
canvas.width = Math.max(1, width)
|
|
197
|
-
canvas.height = Math.max(1, height)
|
|
198
|
-
return canvas
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Consecutive bins with no gap between them, which one image can stretch over
|
|
202
|
-
function contiguousRuns(data: HaplotypeOverviewData) {
|
|
203
|
-
const runs: [number, number][] = []
|
|
204
|
-
let first = 0
|
|
205
|
-
data.bins.forEach((bin, b) => {
|
|
206
|
-
if (b > 0 && bin.start !== data.bins[b - 1]!.end) {
|
|
207
|
-
runs.push([first, b])
|
|
208
|
-
first = b
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
if (data.bins.length > 0) {
|
|
212
|
-
runs.push([first, data.bins.length])
|
|
213
|
-
}
|
|
214
|
-
return runs
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface RowImage {
|
|
218
|
-
image: HTMLCanvasElement
|
|
219
|
-
top: number
|
|
220
|
-
height: number
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// One image per group of rows, built once per overview and layout: a pixel
|
|
224
|
-
// per cell, scaled to the group's device height with smoothing only when
|
|
225
|
-
// rows outnumber pixels
|
|
226
|
-
function rowImage(
|
|
227
|
-
data: HaplotypeOverviewData,
|
|
228
|
-
rows: number[],
|
|
229
|
-
top: number,
|
|
230
|
-
height: number,
|
|
231
|
-
dpr: number,
|
|
232
|
-
): RowImage | undefined {
|
|
233
|
-
const cols = data.bins.length
|
|
234
|
-
if (rows.length === 0 || cols === 0 || height <= 0) {
|
|
235
|
-
return undefined
|
|
236
|
-
}
|
|
237
|
-
const cells = makeCanvas(cols, rows.length)
|
|
238
|
-
const cellsCtx = cells.getContext('2d')!
|
|
239
|
-
const pixels = cellsCtx.createImageData(cols, rows.length)
|
|
240
|
-
const stride = data.rows.length
|
|
241
|
-
rows.forEach((row, y) => {
|
|
242
|
-
for (let b = 0; b < cols; b++) {
|
|
243
|
-
const [r, g, bl] = cellColor(data.cells[b * stride + row]!)
|
|
244
|
-
const o = (y * cols + b) * 4
|
|
245
|
-
pixels.data[o] = r
|
|
246
|
-
pixels.data[o + 1] = g
|
|
247
|
-
pixels.data[o + 2] = bl
|
|
248
|
-
pixels.data[o + 3] = 255
|
|
249
|
-
}
|
|
250
|
-
})
|
|
251
|
-
cellsCtx.putImageData(pixels, 0, 0)
|
|
252
|
-
const devHeight = Math.max(1, Math.round(height * dpr))
|
|
253
|
-
const image = makeCanvas(cols, devHeight)
|
|
254
|
-
const ctx = image.getContext('2d')!
|
|
255
|
-
ctx.imageSmoothingEnabled = rows.length > devHeight
|
|
256
|
-
ctx.imageSmoothingQuality = 'high'
|
|
257
|
-
ctx.drawImage(cells, 0, 0, cols, devHeight)
|
|
258
|
-
return { image, top, height }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function rowImages(
|
|
262
|
-
data: HaplotypeOverviewData,
|
|
263
|
-
layout: OverviewLayout,
|
|
264
|
-
dpr: number,
|
|
265
|
-
) {
|
|
266
|
-
const { rows, pinnedCount, pinnedHeight, restHeight } = layout
|
|
267
|
-
return [
|
|
268
|
-
rowImage(
|
|
269
|
-
data,
|
|
270
|
-
rows.slice(0, pinnedCount),
|
|
271
|
-
layout.rowsTop,
|
|
272
|
-
pinnedCount * pinnedHeight,
|
|
273
|
-
dpr,
|
|
274
|
-
),
|
|
275
|
-
rowImage(
|
|
276
|
-
data,
|
|
277
|
-
rows.slice(pinnedCount),
|
|
278
|
-
layout.restTop,
|
|
279
|
-
(rows.length - pinnedCount) * restHeight,
|
|
280
|
-
dpr,
|
|
281
|
-
),
|
|
282
|
-
].filter(image => image !== undefined)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// The screen span of bp [start, end), which runs right to left on a
|
|
286
|
-
// reversed block
|
|
287
|
-
function screenSpan(frame: Frame, start: number, end: number) {
|
|
288
|
-
const a = frame.xOf(start)
|
|
289
|
-
const b = frame.xOf(end)
|
|
290
|
-
return { left: Math.min(a, b), width: Math.abs(b - a), reversed: b < a }
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Each run of bins stretched to its bp, smoothed only when bins outnumber
|
|
294
|
-
// pixels, and mirrored on a reversed block
|
|
295
|
-
function blitRows(
|
|
296
|
-
ctx: CanvasRenderingContext2D,
|
|
297
|
-
data: HaplotypeOverviewData,
|
|
298
|
-
images: RowImage[],
|
|
299
|
-
frame: Frame,
|
|
300
|
-
) {
|
|
301
|
-
for (const [first, end] of contiguousRuns(data)) {
|
|
302
|
-
const { left, width, reversed } = screenSpan(
|
|
303
|
-
frame,
|
|
304
|
-
data.bins[first]!.start,
|
|
305
|
-
data.bins[end - 1]!.end,
|
|
306
|
-
)
|
|
307
|
-
if (left + width < 0 || left > frame.width) {
|
|
308
|
-
continue
|
|
309
|
-
}
|
|
310
|
-
ctx.imageSmoothingEnabled = end - first > width * frame.dpr
|
|
311
|
-
for (const { image, top, height } of images) {
|
|
312
|
-
ctx.save()
|
|
313
|
-
if (reversed) {
|
|
314
|
-
ctx.translate(2 * left + width, 0)
|
|
315
|
-
ctx.scale(-1, 1)
|
|
316
|
-
}
|
|
317
|
-
ctx.drawImage(
|
|
318
|
-
image,
|
|
319
|
-
first,
|
|
320
|
-
0,
|
|
321
|
-
end - first,
|
|
322
|
-
image.height,
|
|
323
|
-
left,
|
|
324
|
-
top,
|
|
325
|
-
width,
|
|
326
|
-
height,
|
|
327
|
-
)
|
|
328
|
-
ctx.restore()
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function drawDensity(
|
|
334
|
-
ctx: CanvasRenderingContext2D,
|
|
335
|
-
data: HaplotypeOverviewData,
|
|
336
|
-
counts: number[][] | undefined,
|
|
337
|
-
frame: Frame,
|
|
338
|
-
) {
|
|
339
|
-
const total = data.rows.length - data.reference.length
|
|
340
|
-
const most = Math.max(1, ...data.bins.map(bin => bin.variants))
|
|
341
|
-
const stack: Rgb[] = [[175, 175, 175], PALETTE.partial, ...PALETTE.variant]
|
|
342
|
-
data.bins.forEach((bin, b) => {
|
|
343
|
-
const span = screenSpan(frame, bin.start, bin.end)
|
|
344
|
-
const { left } = span
|
|
345
|
-
const width = Math.max(span.width, 1 / frame.dpr)
|
|
346
|
-
if (left + width < 0 || left > frame.width) {
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
if (counts) {
|
|
350
|
-
let y = DENSITY_PX
|
|
351
|
-
for (const i of [5, 4, 3, 2, 1, 0]) {
|
|
352
|
-
const h = (DENSITY_PX * counts[b]![i]!) / Math.max(1, total)
|
|
353
|
-
if (h > 0) {
|
|
354
|
-
const [r, g, bl] = stack[i]!
|
|
355
|
-
ctx.fillStyle = `rgb(${r},${g},${bl})`
|
|
356
|
-
ctx.fillRect(left, y - h, width, h)
|
|
357
|
-
y -= h
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
const h = (DENSITY_PX * bin.variants) / most
|
|
362
|
-
const [r, g, bl] = PALETTE.variant[1]
|
|
363
|
-
ctx.fillStyle = `rgb(${r},${g},${bl})`
|
|
364
|
-
ctx.fillRect(left, DENSITY_PX - h, width, h)
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function drawLabels(
|
|
370
|
-
ctx: CanvasRenderingContext2D,
|
|
371
|
-
data: HaplotypeOverviewData,
|
|
372
|
-
layout: OverviewLayout,
|
|
373
|
-
) {
|
|
374
|
-
if (layout.pinnedHeight < LABEL_MIN_PX) {
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
ctx.font = `${Math.min(11, layout.pinnedHeight - 2)}px sans-serif`
|
|
378
|
-
ctx.textBaseline = 'middle'
|
|
379
|
-
for (let i = 0; i < layout.pinnedCount; i++) {
|
|
380
|
-
const label = data.rows[layout.rows[i]!]!
|
|
381
|
-
const y = rowTop(layout, i)
|
|
382
|
-
const w = ctx.measureText(label).width
|
|
383
|
-
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
|
384
|
-
ctx.fillRect(2, y + 1, w + 6, layout.pinnedHeight - 2)
|
|
385
|
-
ctx.fillStyle = '#222'
|
|
386
|
-
ctx.fillText(label, 5, y + layout.pinnedHeight / 2)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function formatBp(bp: number) {
|
|
391
|
-
return bp >= 1_000_000
|
|
392
|
-
? `${+(bp / 1_000_000).toFixed(1)} Mb`
|
|
393
|
-
: `${+(bp / 1000).toFixed(1)} kb`
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// What the density band counts, which changes with the bin size
|
|
397
|
-
export function densityLabel(data: HaplotypeOverviewData) {
|
|
398
|
-
const haplotypes = data.rows.length - data.reference.length
|
|
399
|
-
return data.bin <= FINE_BIN_BP
|
|
400
|
-
? `haplotypes diverging ≥50 bp, of ${haplotypes} · ${formatBp(data.bin)} bins`
|
|
401
|
-
: `excursions ≥50 bp per ${formatBp(data.bin)} bin, all ${haplotypes} haplotypes`
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function drawDensityLabel(
|
|
405
|
-
ctx: CanvasRenderingContext2D,
|
|
406
|
-
data: HaplotypeOverviewData,
|
|
407
|
-
) {
|
|
408
|
-
const label = densityLabel(data)
|
|
409
|
-
ctx.font = '10px sans-serif'
|
|
410
|
-
ctx.textBaseline = 'top'
|
|
411
|
-
const w = ctx.measureText(label).width
|
|
412
|
-
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
|
413
|
-
ctx.fillRect(2, 1, w + 6, 13)
|
|
414
|
-
ctx.fillStyle = '#444'
|
|
415
|
-
ctx.fillText(label, 5, 2)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// The density band from per-class counts at bins up to FINE_BIN_BP, from
|
|
419
|
-
// excursion counts past them (undefined counts)
|
|
420
|
-
export function drawOverview(
|
|
421
|
-
ctx: CanvasRenderingContext2D,
|
|
422
|
-
data: HaplotypeOverviewData,
|
|
423
|
-
counts: number[][] | undefined,
|
|
424
|
-
layout: OverviewLayout,
|
|
425
|
-
images: RowImage[],
|
|
426
|
-
frame: Frame,
|
|
427
|
-
) {
|
|
428
|
-
ctx.clearRect(0, 0, frame.width, frame.height)
|
|
429
|
-
drawDensity(ctx, data, counts, frame)
|
|
430
|
-
blitRows(ctx, data, images, frame)
|
|
431
|
-
drawLabels(ctx, data, layout)
|
|
432
|
-
drawDensityLabel(ctx, data)
|
|
433
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { getRpcSessionId, getSession } from '@jbrowse/core/util'
|
|
2
|
-
import { flow, types } from '@jbrowse/mobx-state-tree'
|
|
3
|
-
|
|
4
|
-
import { FINE_BIN_BP, densityCounts } from './draw'
|
|
5
|
-
import { cutHolds, hostCut } from '../GraphGenomeView/host'
|
|
6
|
-
|
|
7
|
-
import type { HaplotypeOverviewData } from '../GetHaplotypeOverview'
|
|
8
|
-
import type { SubgraphRegion } from '../GetSubgraph'
|
|
9
|
-
import type { HostWindow } from '../GraphGenomeView/host'
|
|
10
|
-
|
|
11
|
-
// A cut refused for its node count: the GBZ adapter's own error, or
|
|
12
|
-
// gbz-base's when it reaches the display unwrapped
|
|
13
|
-
export function isNodeLimitError(error: unknown) {
|
|
14
|
-
return (
|
|
15
|
-
error instanceof Error &&
|
|
16
|
-
(error.name === 'NodeLimitError' ||
|
|
17
|
-
/reads more than nodeLimit|^Subgraph size limit of \d+ nodes exceeded/.test(
|
|
18
|
-
error.message,
|
|
19
|
-
))
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Where a cut came back over its node limit. The graph is as dense as that
|
|
24
|
-
// only near it, so it stands for windows as wide or wider on the same contig
|
|
25
|
-
// within one window-width of it.
|
|
26
|
-
export interface DenseWindow {
|
|
27
|
-
refName: string
|
|
28
|
-
assemblyName: string
|
|
29
|
-
start: number
|
|
30
|
-
end: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function denseCovers(dense: DenseWindow | undefined, seen: HostWindow) {
|
|
34
|
-
if (
|
|
35
|
-
dense?.refName !== seen.refName ||
|
|
36
|
-
dense.assemblyName !== seen.assemblyName
|
|
37
|
-
) {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
const span = dense.end - dense.start
|
|
41
|
-
return (
|
|
42
|
-
seen.end - seen.start >= span &&
|
|
43
|
-
seen.end > dense.start - span &&
|
|
44
|
-
seen.start < dense.end + span
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class NoOverviewError extends Error {
|
|
49
|
-
override name = 'NoOverviewError'
|
|
50
|
-
|
|
51
|
-
constructor() {
|
|
52
|
-
super(
|
|
53
|
-
'this window is too large to cut, and there is no overview to draw instead: either the haplotype index has none (rebuild it with gbz-haplotype-index 0.3 or later) or the reference has no path by this name. Zoom in to see the graph',
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// What a GBZ track draws once its window is too large to cut: the haplotype
|
|
59
|
-
// index's overview of the window plus one window-width each side, refetched
|
|
60
|
-
// once the window leaves it or the zoom moves a level.
|
|
61
|
-
export function HaplotypeOverviewMixin() {
|
|
62
|
-
return types
|
|
63
|
-
.model({
|
|
64
|
-
// every haplotype as a row, or only the lanes the track names; unset
|
|
65
|
-
// is every haplotype, and keeps the key out of snapshots
|
|
66
|
-
overviewRowsChoice: types.maybe(types.boolean),
|
|
67
|
-
})
|
|
68
|
-
.volatile(() => ({
|
|
69
|
-
overview: undefined as HaplotypeOverviewData | undefined,
|
|
70
|
-
overviewRegion: undefined as SubgraphRegion | undefined,
|
|
71
|
-
overviewBpPerPx: 0,
|
|
72
|
-
overviewLoading: false,
|
|
73
|
-
overviewError: undefined as Error | undefined,
|
|
74
|
-
overviewPainted: undefined as HaplotypeOverviewData | undefined,
|
|
75
|
-
overviewController: undefined as AbortController | undefined,
|
|
76
|
-
dense: undefined as DenseWindow | undefined,
|
|
77
|
-
}))
|
|
78
|
-
.views(self => ({
|
|
79
|
-
get overviewAllRows() {
|
|
80
|
-
return self.overviewRowsChoice ?? true
|
|
81
|
-
},
|
|
82
|
-
// per bin, the density band's class counts; past FINE_BIN_BP the band
|
|
83
|
-
// reads the bins' excursion counts instead
|
|
84
|
-
get overviewCounts() {
|
|
85
|
-
return self.overview && self.overview.bin <= FINE_BIN_BP
|
|
86
|
-
? densityCounts(self.overview)
|
|
87
|
-
: undefined
|
|
88
|
-
},
|
|
89
|
-
overviewHolds(seen: HostWindow) {
|
|
90
|
-
return (
|
|
91
|
-
self.overview !== undefined &&
|
|
92
|
-
self.overviewError === undefined &&
|
|
93
|
-
cutHolds(self.overviewRegion, seen) &&
|
|
94
|
-
seen.bpPerPx >= self.overviewBpPerPx / 2 &&
|
|
95
|
-
seen.bpPerPx <= self.overviewBpPerPx * 2
|
|
96
|
-
)
|
|
97
|
-
},
|
|
98
|
-
}))
|
|
99
|
-
.actions(self => ({
|
|
100
|
-
setOverviewAllRows(all: boolean) {
|
|
101
|
-
self.overviewRowsChoice = all
|
|
102
|
-
},
|
|
103
|
-
beforeDestroy() {
|
|
104
|
-
self.overviewController?.abort()
|
|
105
|
-
},
|
|
106
|
-
setOverviewPainted(data: HaplotypeOverviewData) {
|
|
107
|
-
self.overviewPainted = data
|
|
108
|
-
},
|
|
109
|
-
setDense(window: DenseWindow | undefined) {
|
|
110
|
-
self.dense = window
|
|
111
|
-
},
|
|
112
|
-
forgetOverview() {
|
|
113
|
-
self.overviewController?.abort()
|
|
114
|
-
self.overview = undefined
|
|
115
|
-
self.overviewRegion = undefined
|
|
116
|
-
self.overviewLoading = false
|
|
117
|
-
},
|
|
118
|
-
fetchOverview: flow(function* (
|
|
119
|
-
adapterConfig: Record<string, unknown>,
|
|
120
|
-
seen: HostWindow,
|
|
121
|
-
haplotypes: string[] | undefined,
|
|
122
|
-
) {
|
|
123
|
-
self.overviewController?.abort()
|
|
124
|
-
const controller = new AbortController()
|
|
125
|
-
self.overviewController = controller
|
|
126
|
-
self.overviewLoading = true
|
|
127
|
-
self.overviewError = undefined
|
|
128
|
-
const region = hostCut(seen, Infinity)
|
|
129
|
-
try {
|
|
130
|
-
const data = (yield getSession(self).rpcManager.call(
|
|
131
|
-
getRpcSessionId(self),
|
|
132
|
-
'GetHaplotypeOverview',
|
|
133
|
-
{
|
|
134
|
-
adapterConfig,
|
|
135
|
-
region,
|
|
136
|
-
// a level coarser than one bin per pixel, so a row image is
|
|
137
|
-
// at most a few times the window's width
|
|
138
|
-
bpPerPx: seen.bpPerPx * 2,
|
|
139
|
-
haplotypes,
|
|
140
|
-
signal: controller.signal,
|
|
141
|
-
},
|
|
142
|
-
)) as HaplotypeOverviewData | undefined
|
|
143
|
-
if (!controller.signal.aborted) {
|
|
144
|
-
self.overview = data
|
|
145
|
-
self.overviewRegion = region
|
|
146
|
-
self.overviewBpPerPx = seen.bpPerPx
|
|
147
|
-
if (!data) {
|
|
148
|
-
self.overviewError = new NoOverviewError()
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch (e) {
|
|
152
|
-
if (!controller.signal.aborted) {
|
|
153
|
-
console.error('[HaplotypeOverview]', e)
|
|
154
|
-
self.overviewError = e instanceof Error ? e : new Error(String(e))
|
|
155
|
-
}
|
|
156
|
-
} finally {
|
|
157
|
-
if (self.overviewController === controller) {
|
|
158
|
-
self.overviewLoading = false
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}),
|
|
162
|
-
}))
|
|
163
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|