jbrowse-plugin-mafviewer 1.3.1 → 1.3.2
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 +1 -1
- package/dist/BigMafAdapter/BigMafAdapter.js +39 -28
- package/dist/BigMafAdapter/BigMafAdapter.js.map +1 -1
- package/dist/LinearMafDisplay/components/Crosshairs.js +1 -1
- package/dist/LinearMafDisplay/components/Crosshairs.js.map +1 -1
- package/dist/LinearMafDisplay/components/MAFTooltip.d.ts +2 -3
- package/dist/LinearMafDisplay/components/MAFTooltip.js +6 -19
- package/dist/LinearMafDisplay/components/MAFTooltip.js.map +1 -1
- package/dist/LinearMafDisplay/stateModel.d.ts +8 -0
- package/dist/LinearMafDisplay/stateModel.js +10 -0
- package/dist/LinearMafDisplay/stateModel.js.map +1 -1
- package/dist/LinearMafDisplay/util.d.ts +20 -0
- package/dist/LinearMafDisplay/util.js +29 -0
- package/dist/LinearMafDisplay/util.js.map +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +3 -0
- package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -2
- package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
- package/dist/LinearMafRenderer/components/ReactComponent.d.ts +3 -0
- package/dist/LinearMafRenderer/components/ReactComponent.js +41 -2
- package/dist/LinearMafRenderer/components/ReactComponent.js.map +1 -1
- package/dist/LinearMafRenderer/components/util.d.ts +1 -0
- package/dist/LinearMafRenderer/components/util.js +13 -0
- package/dist/LinearMafRenderer/components/util.js.map +1 -0
- package/dist/LinearMafRenderer/makeImageData.d.ts +4 -5
- package/dist/LinearMafRenderer/makeImageData.js +28 -146
- package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/features.d.ts +21 -0
- package/dist/LinearMafRenderer/rendering/features.js +58 -0
- package/dist/LinearMafRenderer/rendering/features.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/gaps.d.ts +12 -0
- package/dist/LinearMafRenderer/rendering/gaps.js +35 -0
- package/dist/LinearMafRenderer/rendering/gaps.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/index.d.ts +8 -0
- package/dist/LinearMafRenderer/rendering/index.js +10 -0
- package/dist/LinearMafRenderer/rendering/index.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/insertions.d.ts +14 -0
- package/dist/LinearMafRenderer/rendering/insertions.js +84 -0
- package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/matches.d.ts +13 -0
- package/dist/LinearMafRenderer/rendering/matches.js +41 -0
- package/dist/LinearMafRenderer/rendering/matches.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/mismatches.d.ts +13 -0
- package/dist/LinearMafRenderer/rendering/mismatches.js +47 -0
- package/dist/LinearMafRenderer/rendering/mismatches.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.d.ts +60 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.js +99 -0
- package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/text.d.ts +12 -0
- package/dist/LinearMafRenderer/rendering/text.js +42 -0
- package/dist/LinearMafRenderer/rendering/text.js.map +1 -0
- package/dist/LinearMafRenderer/rendering/types.d.ts +67 -0
- package/dist/LinearMafRenderer/rendering/types.js +15 -0
- package/dist/LinearMafRenderer/rendering/types.js.map +1 -0
- package/dist/MafTabixAdapter/MafTabixAdapter.js +48 -22
- package/dist/MafTabixAdapter/MafTabixAdapter.js.map +1 -1
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -8
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
- package/dist/out.js +34520 -0
- package/dist/out.js.map +7 -0
- package/dist/util/fastaUtils.js.map +1 -1
- package/package.json +5 -3
- package/src/BigMafAdapter/BigMafAdapter.ts +49 -28
- package/src/LinearMafDisplay/components/Crosshairs.tsx +1 -7
- package/src/LinearMafDisplay/components/MAFTooltip.tsx +14 -33
- package/src/LinearMafDisplay/stateModel.ts +10 -0
- package/src/LinearMafDisplay/util.ts +57 -0
- package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -2
- package/src/LinearMafRenderer/components/ReactComponent.tsx +70 -2
- package/src/LinearMafRenderer/components/util.ts +13 -0
- package/src/LinearMafRenderer/makeImageData.ts +49 -196
- package/src/LinearMafRenderer/rendering/features.ts +138 -0
- package/src/LinearMafRenderer/rendering/gaps.ts +71 -0
- package/src/LinearMafRenderer/rendering/index.ts +9 -0
- package/src/LinearMafRenderer/rendering/insertions.ts +170 -0
- package/src/LinearMafRenderer/rendering/matches.ts +79 -0
- package/src/LinearMafRenderer/rendering/mismatches.ts +125 -0
- package/src/LinearMafRenderer/rendering/spatialIndex.ts +136 -0
- package/src/LinearMafRenderer/rendering/text.ts +72 -0
- package/src/LinearMafRenderer/rendering/types.ts +81 -0
- package/src/MafTabixAdapter/MafTabixAdapter.ts +77 -22
- package/src/util/fastaUtils.ts +2 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
|
|
2
2
|
import { createJBrowseTheme } from '@jbrowse/core/ui'
|
|
3
|
-
import { Feature
|
|
3
|
+
import { Feature } from '@jbrowse/core/util'
|
|
4
|
+
import RBush from 'rbush'
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
FONT_CONFIG,
|
|
8
|
+
RenderedBase,
|
|
9
|
+
RenderingContext,
|
|
10
|
+
Sample,
|
|
11
|
+
processFeatureAlignment,
|
|
12
|
+
processFeatureInsertions,
|
|
13
|
+
} from './rendering'
|
|
14
|
+
import { getColorBaseMap, getContrastBaseMap } from './util'
|
|
11
15
|
|
|
12
|
-
interface Sample {
|
|
13
|
-
id: string
|
|
14
|
-
color?: string
|
|
15
|
-
}
|
|
16
16
|
interface RenderArgs extends RenderArgsDeserialized {
|
|
17
17
|
samples: Sample[]
|
|
18
18
|
rowHeight: number
|
|
@@ -24,10 +24,6 @@ interface RenderArgs extends RenderArgsDeserialized {
|
|
|
24
24
|
showAsUpperCase: boolean
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
function getLetter(a: string, showAsUpperCase: boolean) {
|
|
28
|
-
return showAsUpperCase ? a.toUpperCase() : a
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
export function makeImageData({
|
|
32
28
|
ctx,
|
|
33
29
|
renderArgs,
|
|
@@ -47,204 +43,61 @@ export function makeImageData({
|
|
|
47
43
|
features,
|
|
48
44
|
showAsUpperCase,
|
|
49
45
|
} = renderArgs
|
|
46
|
+
|
|
50
47
|
const region = regions[0]!
|
|
51
48
|
const canvasWidth = (region.end - region.start) / bpPerPx
|
|
52
49
|
const h = rowHeight * rowProportion
|
|
53
50
|
const theme = createJBrowseTheme(configTheme)
|
|
54
51
|
const colorForBase = getColorBaseMap(theme)
|
|
55
52
|
const contrastForBase = getContrastBaseMap(theme)
|
|
56
|
-
|
|
57
|
-
const { charHeight } = getCharWidthHeight()
|
|
58
53
|
const sampleToRowMap = new Map(samples.map((s, i) => [s.id, i]))
|
|
59
54
|
const scale = 1 / bpPerPx
|
|
60
|
-
const f = 0.4
|
|
61
|
-
const h2 = rowHeight / 2
|
|
62
55
|
const hp2 = h / 2
|
|
63
56
|
const offset = (rowHeight - h) / 2
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
ctx.font = 'bold 10px Courier New,monospace'
|
|
67
|
-
|
|
68
|
-
for (const feature of features.values()) {
|
|
69
|
-
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
70
|
-
const vals = feature.get('alignments') as Record<string, { seq: string }>
|
|
71
|
-
const seq = feature.get('seq').toLowerCase()
|
|
72
|
-
const r = Object.entries(vals)
|
|
73
|
-
for (const [sample, val] of r) {
|
|
74
|
-
const origAlignment = val.seq
|
|
75
|
-
const alignment = origAlignment.toLowerCase()
|
|
76
|
-
|
|
77
|
-
const row = sampleToRowMap.get(sample)
|
|
78
|
-
if (row === undefined) {
|
|
79
|
-
continue
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const t = rowHeight * row
|
|
83
|
-
const t2 = offset + t
|
|
84
|
-
|
|
85
|
-
// gaps
|
|
86
|
-
ctx.beginPath()
|
|
87
|
-
ctx.fillStyle = 'black'
|
|
88
|
-
for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
|
|
89
|
-
if (seq[i] !== '-') {
|
|
90
|
-
if (alignment[i] === '-') {
|
|
91
|
-
const l = leftPx + scale * o
|
|
92
|
-
ctx.moveTo(l, t + h2)
|
|
93
|
-
ctx.lineTo(l + scale + f, t + h2)
|
|
94
|
-
}
|
|
95
|
-
o++
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
ctx.stroke()
|
|
58
|
+
ctx.font = FONT_CONFIG
|
|
99
59
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// mismatches
|
|
116
|
-
for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
|
|
117
|
-
const c = alignment[i]
|
|
118
|
-
if (seq[i] !== '-') {
|
|
119
|
-
if (c !== '-') {
|
|
120
|
-
const l = leftPx + scale * o
|
|
121
|
-
if (seq[i] !== c && c !== ' ') {
|
|
122
|
-
fillRect(
|
|
123
|
-
ctx,
|
|
124
|
-
l,
|
|
125
|
-
t2,
|
|
126
|
-
scale + f,
|
|
127
|
-
h,
|
|
128
|
-
canvasWidth,
|
|
129
|
-
mismatchRendering
|
|
130
|
-
? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
131
|
-
(colorForBase[c as keyof typeof colorForBase] ?? 'black')
|
|
132
|
-
: 'orange',
|
|
133
|
-
)
|
|
134
|
-
} else if (showAllLetters) {
|
|
135
|
-
fillRect(
|
|
136
|
-
ctx,
|
|
137
|
-
l,
|
|
138
|
-
t2,
|
|
139
|
-
scale + f,
|
|
140
|
-
h,
|
|
141
|
-
canvasWidth,
|
|
142
|
-
mismatchRendering
|
|
143
|
-
? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
144
|
-
(colorForBase[c as keyof typeof colorForBase] ?? 'black')
|
|
145
|
-
: 'lightblue',
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
o++
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// font
|
|
154
|
-
const charSizeW = 10
|
|
155
|
-
if (scale >= charSizeW) {
|
|
156
|
-
for (let i = 0, o = 0, l = alignment.length; i < l; i++) {
|
|
157
|
-
if (seq[i] !== '-') {
|
|
158
|
-
const l = leftPx + scale * o
|
|
159
|
-
const offset = (scale - charSizeW) / 2 + 1
|
|
160
|
-
const c = alignment[i]!
|
|
161
|
-
if ((showAllLetters || seq[i] !== c) && c !== '-') {
|
|
162
|
-
ctx.fillStyle = mismatchRendering
|
|
163
|
-
? (contrastForBase[c] ?? 'white')
|
|
164
|
-
: 'black'
|
|
165
|
-
if (rowHeight > charHeight) {
|
|
166
|
-
ctx.fillText(
|
|
167
|
-
getLetter(origAlignment[i] || '', showAsUpperCase),
|
|
168
|
-
l + offset,
|
|
169
|
-
hp2 + t + 3,
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
o++
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
60
|
+
const renderingContext: RenderingContext = {
|
|
61
|
+
ctx,
|
|
62
|
+
scale,
|
|
63
|
+
canvasWidth,
|
|
64
|
+
rowHeight,
|
|
65
|
+
h,
|
|
66
|
+
hp2,
|
|
67
|
+
offset,
|
|
68
|
+
colorForBase,
|
|
69
|
+
contrastForBase,
|
|
70
|
+
showAllLetters,
|
|
71
|
+
mismatchRendering,
|
|
72
|
+
showAsUpperCase,
|
|
73
|
+
spatialIndex: new RBush<RenderedBase>(),
|
|
74
|
+
lastInsertedX: -Infinity, // Start with -Infinity so first item is always inserted
|
|
178
75
|
}
|
|
179
76
|
|
|
180
|
-
//
|
|
181
|
-
// insertions are always 'on top' of the other features
|
|
77
|
+
// First pass: render alignments (gaps, matches, mismatches, text)
|
|
182
78
|
for (const feature of features.values()) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (row === undefined) {
|
|
192
|
-
continue
|
|
193
|
-
}
|
|
79
|
+
processFeatureAlignment(
|
|
80
|
+
feature,
|
|
81
|
+
region,
|
|
82
|
+
bpPerPx,
|
|
83
|
+
sampleToRowMap,
|
|
84
|
+
renderingContext,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
194
87
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (ins.length > 0) {
|
|
206
|
-
const l = leftPx + scale * o - 1
|
|
88
|
+
// Second pass: render insertions on top
|
|
89
|
+
for (const feature of features.values()) {
|
|
90
|
+
processFeatureInsertions(
|
|
91
|
+
feature,
|
|
92
|
+
region,
|
|
93
|
+
bpPerPx,
|
|
94
|
+
sampleToRowMap,
|
|
95
|
+
renderingContext,
|
|
96
|
+
)
|
|
97
|
+
}
|
|
207
98
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
fillRect(ctx, l - 1, t2, 2, h, canvasWidth, 'purple')
|
|
212
|
-
} else if (h > charHeight) {
|
|
213
|
-
const rwidth = measureText(txt, 10)
|
|
214
|
-
const padding = 2
|
|
215
|
-
fillRect(
|
|
216
|
-
ctx,
|
|
217
|
-
l - rwidth / 2 - padding,
|
|
218
|
-
t2,
|
|
219
|
-
rwidth + 2 * padding,
|
|
220
|
-
h,
|
|
221
|
-
canvasWidth,
|
|
222
|
-
'purple',
|
|
223
|
-
)
|
|
224
|
-
ctx.fillStyle = 'white'
|
|
225
|
-
ctx.fillText(txt, l - rwidth / 2, t + h)
|
|
226
|
-
} else {
|
|
227
|
-
const padding = 2
|
|
228
|
-
fillRect(
|
|
229
|
-
ctx,
|
|
230
|
-
l - padding,
|
|
231
|
-
t2,
|
|
232
|
-
2 * padding,
|
|
233
|
-
h,
|
|
234
|
-
canvasWidth,
|
|
235
|
-
'purple',
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
fillRect(ctx, l, t2, 1, h, canvasWidth, 'purple')
|
|
240
|
-
if (bpPerPx < 0.2 && rowHeight > 5) {
|
|
241
|
-
fillRect(ctx, l - 2, t2, 5, 1, canvasWidth)
|
|
242
|
-
fillRect(ctx, l - 2, t2 + h - 1, 5, 1, canvasWidth)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
o++
|
|
247
|
-
}
|
|
248
|
-
}
|
|
99
|
+
// Return serialized RBush spatial index
|
|
100
|
+
return {
|
|
101
|
+
rbush: renderingContext.spatialIndex.toJSON(),
|
|
249
102
|
}
|
|
250
103
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Feature, featureSpanPx } from '@jbrowse/core/util'
|
|
2
|
+
|
|
3
|
+
import { renderGaps } from './gaps'
|
|
4
|
+
import { renderInsertions } from './insertions'
|
|
5
|
+
import { renderMatches } from './matches'
|
|
6
|
+
import { renderMismatches } from './mismatches'
|
|
7
|
+
import { renderText } from './text'
|
|
8
|
+
|
|
9
|
+
import type { AlignmentRecord, GenomicRegion, RenderingContext } from './types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Processes alignment data for a single feature, rendering gaps, matches, mismatches, and text
|
|
13
|
+
* @param feature - JBrowse feature containing alignment data
|
|
14
|
+
* @param region - Genomic region being rendered
|
|
15
|
+
* @param bpPerPx - Base pairs per pixel (zoom level)
|
|
16
|
+
* @param sampleToRowMap - Maps sample IDs to row indices
|
|
17
|
+
* @param renderingContext - Shared rendering parameters
|
|
18
|
+
*/
|
|
19
|
+
export function processFeatureAlignment(
|
|
20
|
+
feature: Feature,
|
|
21
|
+
region: GenomicRegion,
|
|
22
|
+
bpPerPx: number,
|
|
23
|
+
sampleToRowMap: Map<string, number>,
|
|
24
|
+
renderingContext: RenderingContext,
|
|
25
|
+
) {
|
|
26
|
+
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
27
|
+
const alignments = feature.get('alignments') as Record<
|
|
28
|
+
string,
|
|
29
|
+
AlignmentRecord
|
|
30
|
+
>
|
|
31
|
+
const referenceSeq = feature.get('seq').toLowerCase()
|
|
32
|
+
const featureId =
|
|
33
|
+
feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
|
|
34
|
+
|
|
35
|
+
for (const [sampleId, alignmentData] of Object.entries(alignments)) {
|
|
36
|
+
const row = sampleToRowMap.get(sampleId)
|
|
37
|
+
if (row === undefined) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const originalAlignment = alignmentData.seq
|
|
42
|
+
const alignment = originalAlignment.toLowerCase()
|
|
43
|
+
const rowTop = renderingContext.offset + renderingContext.rowHeight * row
|
|
44
|
+
|
|
45
|
+
renderGaps(
|
|
46
|
+
renderingContext,
|
|
47
|
+
alignment,
|
|
48
|
+
referenceSeq,
|
|
49
|
+
leftPx,
|
|
50
|
+
rowTop,
|
|
51
|
+
sampleId,
|
|
52
|
+
featureId,
|
|
53
|
+
alignmentData.start,
|
|
54
|
+
alignmentData.chr,
|
|
55
|
+
)
|
|
56
|
+
renderMatches(
|
|
57
|
+
renderingContext,
|
|
58
|
+
alignment,
|
|
59
|
+
referenceSeq,
|
|
60
|
+
leftPx,
|
|
61
|
+
rowTop,
|
|
62
|
+
sampleId,
|
|
63
|
+
featureId,
|
|
64
|
+
alignmentData.start,
|
|
65
|
+
alignmentData.chr,
|
|
66
|
+
)
|
|
67
|
+
renderMismatches(
|
|
68
|
+
renderingContext,
|
|
69
|
+
alignment,
|
|
70
|
+
referenceSeq,
|
|
71
|
+
leftPx,
|
|
72
|
+
rowTop,
|
|
73
|
+
sampleId,
|
|
74
|
+
featureId,
|
|
75
|
+
alignmentData.start,
|
|
76
|
+
alignmentData.chr,
|
|
77
|
+
)
|
|
78
|
+
renderText(
|
|
79
|
+
renderingContext,
|
|
80
|
+
alignment,
|
|
81
|
+
originalAlignment,
|
|
82
|
+
referenceSeq,
|
|
83
|
+
leftPx,
|
|
84
|
+
rowTop,
|
|
85
|
+
sampleId,
|
|
86
|
+
featureId,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Processes insertion data for a single feature in a separate pass
|
|
93
|
+
* Insertions are rendered on top to ensure visibility
|
|
94
|
+
* @param feature - JBrowse feature containing alignment data
|
|
95
|
+
* @param region - Genomic region being rendered
|
|
96
|
+
* @param bpPerPx - Base pairs per pixel (zoom level)
|
|
97
|
+
* @param sampleToRowMap - Maps sample IDs to row indices
|
|
98
|
+
* @param renderingContext - Shared rendering parameters
|
|
99
|
+
*/
|
|
100
|
+
export function processFeatureInsertions(
|
|
101
|
+
feature: Feature,
|
|
102
|
+
region: GenomicRegion,
|
|
103
|
+
bpPerPx: number,
|
|
104
|
+
sampleToRowMap: Map<string, number>,
|
|
105
|
+
renderingContext: RenderingContext,
|
|
106
|
+
) {
|
|
107
|
+
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
108
|
+
const alignments = feature.get('alignments') as Record<
|
|
109
|
+
string,
|
|
110
|
+
AlignmentRecord
|
|
111
|
+
>
|
|
112
|
+
const referenceSeq = feature.get('seq').toLowerCase()
|
|
113
|
+
const featureId =
|
|
114
|
+
feature.id() || `feature_${feature.get('start')}_${feature.get('end')}`
|
|
115
|
+
|
|
116
|
+
for (const [sampleId, alignmentData] of Object.entries(alignments)) {
|
|
117
|
+
const row = sampleToRowMap.get(sampleId)
|
|
118
|
+
if (row === undefined) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const alignment = alignmentData.seq.toLowerCase()
|
|
123
|
+
const rowTop = renderingContext.offset + renderingContext.rowHeight * row
|
|
124
|
+
|
|
125
|
+
renderInsertions(
|
|
126
|
+
renderingContext,
|
|
127
|
+
alignment,
|
|
128
|
+
referenceSeq,
|
|
129
|
+
leftPx,
|
|
130
|
+
rowTop,
|
|
131
|
+
bpPerPx,
|
|
132
|
+
sampleId,
|
|
133
|
+
featureId,
|
|
134
|
+
alignmentData.start,
|
|
135
|
+
alignmentData.chr,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addToSpatialIndex,
|
|
3
|
+
createRenderedBase,
|
|
4
|
+
shouldAddToSpatialIndex,
|
|
5
|
+
} from './spatialIndex'
|
|
6
|
+
import { GAP_STROKE_OFFSET } from './types'
|
|
7
|
+
|
|
8
|
+
import type { RenderingContext } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders gap indicators (horizontal lines) where the alignment has deletions relative to reference
|
|
12
|
+
* @param context - Rendering context with canvas and styling info
|
|
13
|
+
* @param alignment - The aligned sequence for this sample
|
|
14
|
+
* @param seq - The reference sequence
|
|
15
|
+
* @param leftPx - Left pixel position of the feature
|
|
16
|
+
* @param rowTop - Top pixel position of the row
|
|
17
|
+
* @param alignmentStart - Start position of the alignment
|
|
18
|
+
* @param chr - Chromosome/sequence name
|
|
19
|
+
*/
|
|
20
|
+
export function renderGaps(
|
|
21
|
+
context: RenderingContext,
|
|
22
|
+
alignment: string,
|
|
23
|
+
seq: string,
|
|
24
|
+
leftPx: number,
|
|
25
|
+
rowTop: number,
|
|
26
|
+
sampleId: string,
|
|
27
|
+
featureId: string,
|
|
28
|
+
alignmentStart: number,
|
|
29
|
+
chr: string,
|
|
30
|
+
) {
|
|
31
|
+
const { ctx, scale } = context
|
|
32
|
+
const h2 = context.rowHeight / 2
|
|
33
|
+
|
|
34
|
+
ctx.beginPath()
|
|
35
|
+
ctx.fillStyle = 'black'
|
|
36
|
+
|
|
37
|
+
for (
|
|
38
|
+
let i = 0, genomicOffset = 0, seqLength = alignment.length;
|
|
39
|
+
i < seqLength;
|
|
40
|
+
i++
|
|
41
|
+
) {
|
|
42
|
+
if (seq[i] !== '-') {
|
|
43
|
+
if (alignment[i] === '-') {
|
|
44
|
+
const xPos = leftPx + scale * genomicOffset
|
|
45
|
+
ctx.moveTo(xPos, rowTop + h2)
|
|
46
|
+
ctx.lineTo(xPos + scale + GAP_STROKE_OFFSET, rowTop + h2)
|
|
47
|
+
|
|
48
|
+
// Add to spatial index if distance filter allows
|
|
49
|
+
if (shouldAddToSpatialIndex(xPos, context)) {
|
|
50
|
+
const renderedBase = createRenderedBase(
|
|
51
|
+
xPos,
|
|
52
|
+
rowTop,
|
|
53
|
+
context,
|
|
54
|
+
genomicOffset + alignmentStart,
|
|
55
|
+
chr,
|
|
56
|
+
sampleId,
|
|
57
|
+
'-',
|
|
58
|
+
false,
|
|
59
|
+
false,
|
|
60
|
+
true,
|
|
61
|
+
false,
|
|
62
|
+
featureId,
|
|
63
|
+
)
|
|
64
|
+
addToSpatialIndex(context, renderedBase)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
genomicOffset++
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
ctx.stroke()
|
|
71
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { measureText } from '@jbrowse/core/util'
|
|
2
|
+
|
|
3
|
+
import { fillRect, getCharWidthHeight } from '../util'
|
|
4
|
+
import {
|
|
5
|
+
addToSpatialIndex,
|
|
6
|
+
createRenderedInsertion,
|
|
7
|
+
shouldAddToSpatialIndex,
|
|
8
|
+
} from './spatialIndex'
|
|
9
|
+
import {
|
|
10
|
+
CHAR_SIZE_WIDTH,
|
|
11
|
+
HIGH_BP_PER_PX_THRESHOLD,
|
|
12
|
+
HIGH_ZOOM_THRESHOLD,
|
|
13
|
+
INSERTION_BORDER_HEIGHT,
|
|
14
|
+
INSERTION_BORDER_WIDTH,
|
|
15
|
+
INSERTION_LINE_WIDTH,
|
|
16
|
+
INSERTION_PADDING,
|
|
17
|
+
LARGE_INSERTION_THRESHOLD,
|
|
18
|
+
MIN_ROW_HEIGHT_FOR_BORDERS,
|
|
19
|
+
} from './types'
|
|
20
|
+
|
|
21
|
+
import type { RenderingContext } from './types'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Renders insertion markers where the alignment has bases not present in reference
|
|
25
|
+
* Large insertions show count, small ones show as colored bars with optional borders
|
|
26
|
+
* @param context - Rendering context with canvas and styling info
|
|
27
|
+
* @param alignment - The aligned sequence for this sample
|
|
28
|
+
* @param seq - The reference sequence
|
|
29
|
+
* @param leftPx - Left pixel position of the feature
|
|
30
|
+
* @param rowTop - Top pixel position of the row
|
|
31
|
+
* @param bpPerPx - Base pairs per pixel (zoom level)
|
|
32
|
+
* @param alignmentStart - Start position of the alignment
|
|
33
|
+
* @param chr - Chromosome/sequence name
|
|
34
|
+
*/
|
|
35
|
+
export function renderInsertions(
|
|
36
|
+
context: RenderingContext,
|
|
37
|
+
alignment: string,
|
|
38
|
+
seq: string,
|
|
39
|
+
leftPx: number,
|
|
40
|
+
rowTop: number,
|
|
41
|
+
bpPerPx: number,
|
|
42
|
+
sampleId: string,
|
|
43
|
+
featureId: string,
|
|
44
|
+
alignmentStart: number,
|
|
45
|
+
chr: string,
|
|
46
|
+
) {
|
|
47
|
+
const { ctx, scale, h, canvasWidth, rowHeight } = context
|
|
48
|
+
const { charHeight } = getCharWidthHeight()
|
|
49
|
+
|
|
50
|
+
for (
|
|
51
|
+
let i = 0, genomicOffset = 0, seqLength = alignment.length;
|
|
52
|
+
i < seqLength;
|
|
53
|
+
i++
|
|
54
|
+
) {
|
|
55
|
+
let insertionSequence = ''
|
|
56
|
+
while (seq[i] === '-') {
|
|
57
|
+
if (alignment[i] !== '-' && alignment[i] !== ' ') {
|
|
58
|
+
insertionSequence += alignment[i]
|
|
59
|
+
}
|
|
60
|
+
i++
|
|
61
|
+
}
|
|
62
|
+
if (insertionSequence.length > 0) {
|
|
63
|
+
// Found an insertion
|
|
64
|
+
const xPos = leftPx + scale * genomicOffset - INSERTION_LINE_WIDTH
|
|
65
|
+
|
|
66
|
+
// Determine actual rendered width and position for spatial index
|
|
67
|
+
let actualXPos: number
|
|
68
|
+
let actualWidth: number
|
|
69
|
+
|
|
70
|
+
// Large insertions: show count instead of individual bases
|
|
71
|
+
if (insertionSequence.length > LARGE_INSERTION_THRESHOLD) {
|
|
72
|
+
const lengthText = `${insertionSequence.length}`
|
|
73
|
+
if (bpPerPx > HIGH_BP_PER_PX_THRESHOLD) {
|
|
74
|
+
// Very zoomed out: simple line
|
|
75
|
+
actualXPos = xPos - INSERTION_LINE_WIDTH
|
|
76
|
+
actualWidth = INSERTION_BORDER_WIDTH
|
|
77
|
+
fillRect(
|
|
78
|
+
ctx,
|
|
79
|
+
actualXPos,
|
|
80
|
+
rowTop,
|
|
81
|
+
actualWidth,
|
|
82
|
+
h,
|
|
83
|
+
canvasWidth,
|
|
84
|
+
'purple',
|
|
85
|
+
)
|
|
86
|
+
} else if (h > charHeight) {
|
|
87
|
+
// Medium zoom: show count in colored box
|
|
88
|
+
const textWidth = measureText(lengthText, CHAR_SIZE_WIDTH)
|
|
89
|
+
const padding = INSERTION_PADDING
|
|
90
|
+
actualXPos = xPos - textWidth / 2 - padding
|
|
91
|
+
actualWidth = textWidth + 2 * padding
|
|
92
|
+
fillRect(
|
|
93
|
+
ctx,
|
|
94
|
+
actualXPos,
|
|
95
|
+
rowTop,
|
|
96
|
+
actualWidth,
|
|
97
|
+
h,
|
|
98
|
+
canvasWidth,
|
|
99
|
+
'purple',
|
|
100
|
+
)
|
|
101
|
+
ctx.fillStyle = 'white'
|
|
102
|
+
ctx.fillText(lengthText, xPos - textWidth / 2, rowTop + (h * 7) / 8)
|
|
103
|
+
} else {
|
|
104
|
+
const padding = INSERTION_PADDING
|
|
105
|
+
actualXPos = xPos - padding
|
|
106
|
+
actualWidth = 2 * padding
|
|
107
|
+
fillRect(
|
|
108
|
+
ctx,
|
|
109
|
+
actualXPos,
|
|
110
|
+
rowTop,
|
|
111
|
+
actualWidth,
|
|
112
|
+
h,
|
|
113
|
+
canvasWidth,
|
|
114
|
+
'purple',
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Small insertions: vertical line with optional border at high zoom
|
|
119
|
+
actualXPos = xPos
|
|
120
|
+
actualWidth = INSERTION_LINE_WIDTH
|
|
121
|
+
fillRect(ctx, actualXPos, rowTop, actualWidth, h, canvasWidth, 'purple')
|
|
122
|
+
if (
|
|
123
|
+
bpPerPx < HIGH_ZOOM_THRESHOLD &&
|
|
124
|
+
rowHeight > MIN_ROW_HEIGHT_FOR_BORDERS
|
|
125
|
+
) {
|
|
126
|
+
// Add horizontal borders for visibility at high zoom
|
|
127
|
+
// Note: borders extend the effective clickable area
|
|
128
|
+
actualXPos = xPos - INSERTION_BORDER_WIDTH
|
|
129
|
+
actualWidth = INSERTION_BORDER_HEIGHT
|
|
130
|
+
fillRect(
|
|
131
|
+
ctx,
|
|
132
|
+
xPos - INSERTION_BORDER_WIDTH,
|
|
133
|
+
rowTop,
|
|
134
|
+
INSERTION_BORDER_HEIGHT,
|
|
135
|
+
INSERTION_LINE_WIDTH,
|
|
136
|
+
canvasWidth,
|
|
137
|
+
)
|
|
138
|
+
fillRect(
|
|
139
|
+
ctx,
|
|
140
|
+
xPos - INSERTION_BORDER_WIDTH,
|
|
141
|
+
rowTop + h - INSERTION_LINE_WIDTH,
|
|
142
|
+
INSERTION_BORDER_HEIGHT,
|
|
143
|
+
INSERTION_LINE_WIDTH,
|
|
144
|
+
canvasWidth,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add insertion to spatial index with actual rendered dimensions
|
|
150
|
+
// Insertions always bypass distance filter
|
|
151
|
+
if (shouldAddToSpatialIndex(actualXPos, context, true)) {
|
|
152
|
+
addToSpatialIndex(
|
|
153
|
+
context,
|
|
154
|
+
createRenderedInsertion(
|
|
155
|
+
actualXPos,
|
|
156
|
+
rowTop,
|
|
157
|
+
actualWidth,
|
|
158
|
+
context,
|
|
159
|
+
genomicOffset + alignmentStart,
|
|
160
|
+
chr,
|
|
161
|
+
sampleId,
|
|
162
|
+
insertionSequence,
|
|
163
|
+
featureId,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
genomicOffset++
|
|
169
|
+
}
|
|
170
|
+
}
|