jbrowse-plugin-mafviewer 1.4.3 → 1.4.5
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/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.d.ts +14 -0
- package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js +69 -0
- package/dist/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.js.map +1 -0
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js +4 -4
- package/dist/LinearMafDisplay/components/LinearMafDisplayComponent.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js +2 -2
- package/dist/LinearMafDisplay/components/Sidebar/ColorLegend.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.d.ts +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.js +2 -3
- package/dist/LinearMafDisplay/components/Sidebar/RectBg.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js +81 -11
- package/dist/LinearMafDisplay/components/Sidebar/SvgWrapper.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/Tree.js +30 -9
- package/dist/LinearMafDisplay/components/Sidebar/Tree.js.map +1 -1
- package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.d.ts +0 -1
- package/dist/LinearMafDisplay/components/Sidebar/YScaleBars.js.map +1 -1
- package/dist/LinearMafDisplay/renderSvg.js +1 -1
- package/dist/LinearMafDisplay/renderSvg.js.map +1 -1
- package/dist/LinearMafDisplay/stateModel.d.ts +69 -3
- package/dist/LinearMafDisplay/stateModel.js +96 -1
- package/dist/LinearMafDisplay/stateModel.js.map +1 -1
- package/dist/LinearMafDisplay/util.d.ts +1 -0
- package/dist/LinearMafDisplay/util.js +3 -2
- package/dist/LinearMafDisplay/util.js.map +1 -1
- package/dist/LinearMafRenderer/LinearMafRenderer.d.ts +4 -7
- package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
- package/dist/LinearMafRenderer/components/LinearMafRendering.js +27 -9
- package/dist/LinearMafRenderer/components/LinearMafRendering.js.map +1 -1
- package/dist/LinearMafRenderer/makeImageData.js +6 -7
- package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/features.d.ts +0 -1
- package/dist/LinearMafRenderer/rendering/features.js +1 -14
- package/dist/LinearMafRenderer/rendering/features.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/insertions.js +8 -6
- package/dist/LinearMafRenderer/rendering/insertions.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/matches.d.ts +1 -1
- package/dist/LinearMafRenderer/rendering/matches.js +3 -15
- package/dist/LinearMafRenderer/rendering/matches.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/spatialIndex.js +8 -2
- package/dist/LinearMafRenderer/rendering/spatialIndex.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/text.js +1 -3
- package/dist/LinearMafRenderer/rendering/text.js.map +1 -1
- package/dist/LinearMafRenderer/rendering/types.d.ts +5 -4
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +7 -7
- package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
- package/dist/util/fastaUtils.js +23 -50
- package/dist/util/fastaUtils.js.map +1 -1
- package/package.json +7 -7
- package/src/LinearMafDisplay/components/InsertionSequenceDialog/InsertionSequenceDialog.tsx +105 -0
- package/src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx +4 -4
- package/src/LinearMafDisplay/components/Sidebar/ColorLegend.tsx +2 -6
- package/src/LinearMafDisplay/components/Sidebar/RectBg.tsx +8 -3
- package/src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx +117 -15
- package/src/LinearMafDisplay/components/Sidebar/Tree.tsx +53 -8
- package/src/LinearMafDisplay/components/Sidebar/YScaleBars.tsx +0 -1
- package/src/LinearMafDisplay/renderSvg.tsx +1 -1
- package/src/LinearMafDisplay/stateModel.ts +109 -1
- package/src/LinearMafDisplay/util.ts +4 -2
- package/src/LinearMafRenderer/LinearMafRenderer.ts +2 -4
- package/src/LinearMafRenderer/components/LinearMafRendering.tsx +49 -29
- package/src/LinearMafRenderer/makeImageData.ts +5 -14
- package/src/LinearMafRenderer/rendering/features.ts +2 -36
- package/src/LinearMafRenderer/rendering/insertions.ts +11 -6
- package/src/LinearMafRenderer/rendering/matches.ts +2 -27
- package/src/LinearMafRenderer/rendering/spatialIndex.ts +9 -2
- package/src/LinearMafRenderer/rendering/text.ts +1 -2
- package/src/LinearMafRenderer/rendering/types.ts +7 -4
- package/src/util/fastaUtils.ts +28 -54
- package/dist/LinearMafRenderer/components/util.d.ts +0 -1
- package/dist/LinearMafRenderer/components/util.js +0 -13
- package/dist/LinearMafRenderer/components/util.js.map +0 -1
- package/src/LinearMafRenderer/components/util.ts +0 -13
|
@@ -26,6 +26,10 @@ const SetRowHeightDialog = lazy(
|
|
|
26
26
|
() => import('./components/SetRowHeightDialog/SetRowHeightDialog'),
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
const InsertionSequenceDialog = lazy(
|
|
30
|
+
() => import('./components/InsertionSequenceDialog/InsertionSequenceDialog'),
|
|
31
|
+
)
|
|
32
|
+
|
|
29
33
|
/**
|
|
30
34
|
* #stateModel LinearMafDisplay
|
|
31
35
|
* extends LinearBasicDisplay
|
|
@@ -82,6 +86,10 @@ export default function stateModelFactory(
|
|
|
82
86
|
* #property
|
|
83
87
|
*/
|
|
84
88
|
showAsUpperCase: true,
|
|
89
|
+
/**
|
|
90
|
+
* #property
|
|
91
|
+
*/
|
|
92
|
+
showSidebar: true,
|
|
85
93
|
}),
|
|
86
94
|
)
|
|
87
95
|
.volatile(() => ({
|
|
@@ -101,6 +109,14 @@ export default function stateModelFactory(
|
|
|
101
109
|
* #volatile
|
|
102
110
|
*/
|
|
103
111
|
volatileTree: undefined as any,
|
|
112
|
+
/**
|
|
113
|
+
* #volatile
|
|
114
|
+
*/
|
|
115
|
+
highlightedRowNames: undefined as string[] | undefined,
|
|
116
|
+
/**
|
|
117
|
+
* #volatile
|
|
118
|
+
*/
|
|
119
|
+
hoveredTreeNode: undefined as { x: number; y: number } | undefined,
|
|
104
120
|
}))
|
|
105
121
|
.actions(self => ({
|
|
106
122
|
/**
|
|
@@ -150,6 +166,46 @@ export default function stateModelFactory(
|
|
|
150
166
|
setShowAsUpperCase(arg: boolean) {
|
|
151
167
|
self.showAsUpperCase = arg
|
|
152
168
|
},
|
|
169
|
+
/**
|
|
170
|
+
* #action
|
|
171
|
+
*/
|
|
172
|
+
setTreeAreaWidth(width: number) {
|
|
173
|
+
self.treeAreaWidth = width
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* #action
|
|
177
|
+
*/
|
|
178
|
+
setShowSidebar(arg: boolean) {
|
|
179
|
+
self.showSidebar = arg
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* #action
|
|
183
|
+
*/
|
|
184
|
+
setHighlightedRowNames(
|
|
185
|
+
names?: string[],
|
|
186
|
+
nodePosition?: { x: number; y: number },
|
|
187
|
+
) {
|
|
188
|
+
self.highlightedRowNames = names
|
|
189
|
+
self.hoveredTreeNode = nodePosition
|
|
190
|
+
},
|
|
191
|
+
/**
|
|
192
|
+
* #action
|
|
193
|
+
*/
|
|
194
|
+
showInsertionSequenceDialog(insertionData: {
|
|
195
|
+
sequence: string
|
|
196
|
+
sampleLabel: string
|
|
197
|
+
chr: string
|
|
198
|
+
pos: number
|
|
199
|
+
}) {
|
|
200
|
+
getSession(self).queueDialog(handleClose => [
|
|
201
|
+
InsertionSequenceDialog,
|
|
202
|
+
{
|
|
203
|
+
model: self,
|
|
204
|
+
onClose: handleClose,
|
|
205
|
+
insertionData,
|
|
206
|
+
},
|
|
207
|
+
])
|
|
208
|
+
},
|
|
153
209
|
}))
|
|
154
210
|
.views(self => ({
|
|
155
211
|
/**
|
|
@@ -198,10 +254,16 @@ export default function stateModelFactory(
|
|
|
198
254
|
const r = self.root
|
|
199
255
|
if (r) {
|
|
200
256
|
const width = self.treeAreaWidth
|
|
257
|
+
// Use totalHeight - rowHeight so leaves are centered in rows
|
|
258
|
+
// (first leaf at rowHeight/2, last at totalHeight - rowHeight/2)
|
|
201
259
|
const clust = cluster<NodeWithIds>()
|
|
202
|
-
.size([this.totalHeight, width])
|
|
260
|
+
.size([this.totalHeight - self.rowHeight, width])
|
|
203
261
|
.separation(() => 1)
|
|
204
262
|
clust(r)
|
|
263
|
+
// Offset all nodes by rowHeight/2 to center in rows
|
|
264
|
+
for (const node of r.descendants()) {
|
|
265
|
+
node.x = node.x! + self.rowHeight / 2
|
|
266
|
+
}
|
|
205
267
|
setBrLength(r, (r.data.length = 0), width / maxLength(r))
|
|
206
268
|
return r as HierarchyNode<NodeWithIdsAndLength>
|
|
207
269
|
} else {
|
|
@@ -238,6 +300,38 @@ export default function stateModelFactory(
|
|
|
238
300
|
get leaves() {
|
|
239
301
|
return self.root?.leaves()
|
|
240
302
|
},
|
|
303
|
+
/**
|
|
304
|
+
* #getter
|
|
305
|
+
*/
|
|
306
|
+
get leafMap() {
|
|
307
|
+
return new Map(this.leaves?.map(leaf => [leaf.data.name, leaf]))
|
|
308
|
+
},
|
|
309
|
+
/**
|
|
310
|
+
* #getter
|
|
311
|
+
* Precomputed map from hierarchy node to its descendant leaf names
|
|
312
|
+
*/
|
|
313
|
+
get nodeDescendantNames() {
|
|
314
|
+
const map = new Map<unknown, string[]>()
|
|
315
|
+
function computeDescendants(
|
|
316
|
+
node: HierarchyNode<NodeWithIdsAndLength>,
|
|
317
|
+
): string[] {
|
|
318
|
+
if (!node.children || node.children.length === 0) {
|
|
319
|
+
const names = [node.data.name]
|
|
320
|
+
map.set(node, names)
|
|
321
|
+
return names
|
|
322
|
+
}
|
|
323
|
+
const names: string[] = []
|
|
324
|
+
for (const child of node.children) {
|
|
325
|
+
names.push(...computeDescendants(child))
|
|
326
|
+
}
|
|
327
|
+
map.set(node, names)
|
|
328
|
+
return names
|
|
329
|
+
}
|
|
330
|
+
if (this.hierarchy) {
|
|
331
|
+
computeDescendants(this.hierarchy)
|
|
332
|
+
}
|
|
333
|
+
return map
|
|
334
|
+
},
|
|
241
335
|
/**
|
|
242
336
|
* #getter
|
|
243
337
|
*/
|
|
@@ -348,6 +442,14 @@ export default function stateModelFactory(
|
|
|
348
442
|
self.setMismatchRendering(!self.mismatchRendering)
|
|
349
443
|
},
|
|
350
444
|
},
|
|
445
|
+
{
|
|
446
|
+
label: 'Show sidebar',
|
|
447
|
+
type: 'checkbox',
|
|
448
|
+
checked: self.showSidebar,
|
|
449
|
+
onClick: () => {
|
|
450
|
+
self.setShowSidebar(!self.showSidebar)
|
|
451
|
+
},
|
|
452
|
+
},
|
|
351
453
|
]
|
|
352
454
|
},
|
|
353
455
|
}
|
|
@@ -377,6 +479,12 @@ export default function stateModelFactory(
|
|
|
377
479
|
0,
|
|
378
480
|
)
|
|
379
481
|
},
|
|
482
|
+
/**
|
|
483
|
+
* #getter
|
|
484
|
+
*/
|
|
485
|
+
get sidebarWidth() {
|
|
486
|
+
return self.showSidebar ? this.labelWidth + 5 + self.treeWidth : 0
|
|
487
|
+
},
|
|
380
488
|
}))
|
|
381
489
|
.actions(self => ({
|
|
382
490
|
afterCreate() {
|
|
@@ -6,6 +6,7 @@ import type { HierarchyNode } from 'd3-hierarchy'
|
|
|
6
6
|
|
|
7
7
|
export interface HoveredInfo {
|
|
8
8
|
sampleId: string
|
|
9
|
+
sampleLabel: string
|
|
9
10
|
pos: number
|
|
10
11
|
base: string
|
|
11
12
|
chr: string
|
|
@@ -44,15 +45,16 @@ export function generateTooltipContent(
|
|
|
44
45
|
contentLines.push(`Ref: ${p2.refName}:${toLocale(p2.coord)}`)
|
|
45
46
|
|
|
46
47
|
if (hoveredInfo) {
|
|
47
|
-
const { base,
|
|
48
|
+
const { base, sampleLabel, pos, chr, isInsertion } = hoveredInfo
|
|
48
49
|
const thresh = 20
|
|
49
50
|
const len = base.length
|
|
50
51
|
const lengthSuffix = len > 1 ? ` ${len}bp` : ''
|
|
51
52
|
const baseDisplay =
|
|
52
53
|
base.length > thresh ? base.slice(0, thresh) + '...' : base
|
|
54
|
+
const insertionLabel = isInsertion ? ' Insertion' : ''
|
|
53
55
|
|
|
54
56
|
contentLines.push(
|
|
55
|
-
`Alt ${
|
|
57
|
+
`Alt ${sampleLabel}: ${chr}:${pos.toLocaleString('en-US')} (${baseDisplay}${lengthSuffix}${insertionLabel})`,
|
|
56
58
|
)
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -8,10 +8,8 @@ import {
|
|
|
8
8
|
|
|
9
9
|
import { makeImageData } from './makeImageData'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
color?: string
|
|
14
|
-
}
|
|
11
|
+
import type { Sample } from '../LinearMafDisplay/types'
|
|
12
|
+
|
|
15
13
|
interface RenderArgs extends RenderArgsDeserialized {
|
|
16
14
|
samples: Sample[]
|
|
17
15
|
rowHeight: number
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useRef } from 'react'
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { PrerenderedCanvas } from '@jbrowse/core/ui'
|
|
4
4
|
import Flatbush from 'flatbush'
|
|
@@ -19,48 +19,68 @@ const LinearMafRendering = observer(function (props: {
|
|
|
19
19
|
}) {
|
|
20
20
|
const { items, displayModel, height, samples, flatbush } = props
|
|
21
21
|
const ref = useRef<HTMLDivElement>(null)
|
|
22
|
-
const
|
|
22
|
+
const flatbush2 = useMemo(() => Flatbush.from(flatbush), [flatbush])
|
|
23
|
+
const [isOverLargeInsertion, setIsOverLargeInsertion] = useState(false)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
const getFeatureUnderMouse = useCallback(
|
|
26
|
+
(eventClientX: number, eventClientY: number) => {
|
|
27
|
+
let offsetX = 0
|
|
28
|
+
let offsetY = 0
|
|
29
|
+
if (ref.current) {
|
|
30
|
+
const r = ref.current.getBoundingClientRect()
|
|
31
|
+
offsetX = eventClientX - r.left
|
|
32
|
+
offsetY = eventClientY - r.top
|
|
33
|
+
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const x = flatbush2.search(offsetX, offsetY, offsetX + 1, offsetY + 1)
|
|
36
|
+
if (x.length) {
|
|
37
|
+
const elt = x.find(idx => items[idx]?.isInsertion)
|
|
38
|
+
const r = elt !== undefined ? items[elt]! : items[x[0]!]!
|
|
39
|
+
const s = samples[r.sampleId]
|
|
40
|
+
return {
|
|
41
|
+
...r,
|
|
42
|
+
sampleId: s?.id ?? 'unknown',
|
|
43
|
+
sampleLabel: s?.label || s?.id || 'unknown',
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
return undefined
|
|
41
47
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
},
|
|
49
|
+
[flatbush2, items, samples],
|
|
50
|
+
)
|
|
51
|
+
|
|
46
52
|
return (
|
|
47
53
|
<div
|
|
48
54
|
ref={ref}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
onClick={e => {
|
|
56
|
+
const feature = getFeatureUnderMouse(e.clientX, e.clientY)
|
|
57
|
+
if (feature?.isLargeInsertion) {
|
|
58
|
+
displayModel.showInsertionSequenceDialog?.({
|
|
59
|
+
sequence: feature.base,
|
|
60
|
+
sampleLabel: feature.sampleLabel,
|
|
61
|
+
chr: feature.chr,
|
|
62
|
+
pos: feature.pos,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
onMouseMove={e => {
|
|
67
|
+
const feature = getFeatureUnderMouse(e.clientX, e.clientY)
|
|
68
|
+
displayModel.setHoveredInfo?.(feature)
|
|
69
|
+
displayModel.setHighlightedRowNames?.(
|
|
70
|
+
feature?.sampleId ? [feature.sampleId] : undefined,
|
|
52
71
|
)
|
|
53
|
-
|
|
54
|
-
onMouseLeave={() => {
|
|
55
|
-
displayModel.setHoveredInfo?.(undefined)
|
|
72
|
+
setIsOverLargeInsertion(!!feature?.isLargeInsertion)
|
|
56
73
|
}}
|
|
57
|
-
|
|
74
|
+
onMouseLeave={() => {
|
|
58
75
|
displayModel.setHoveredInfo?.(undefined)
|
|
76
|
+
displayModel.setHighlightedRowNames?.(undefined)
|
|
77
|
+
setIsOverLargeInsertion(false)
|
|
59
78
|
}}
|
|
60
79
|
style={{
|
|
61
80
|
overflow: 'visible',
|
|
62
81
|
position: 'relative',
|
|
63
82
|
height,
|
|
83
|
+
cursor: isOverLargeInsertion ? 'pointer' : 'default',
|
|
64
84
|
}}
|
|
65
85
|
>
|
|
66
86
|
<PrerenderedCanvas
|
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
RenderingContext,
|
|
9
9
|
Sample,
|
|
10
10
|
processFeatureAlignment,
|
|
11
|
-
processFeatureInsertions,
|
|
12
11
|
} from './rendering'
|
|
13
|
-
import { getColorBaseMap, getContrastBaseMap } from './util'
|
|
12
|
+
import { getCharWidthHeight, getColorBaseMap, getContrastBaseMap } from './util'
|
|
14
13
|
|
|
15
14
|
interface RenderArgs extends RenderArgsDeserialized {
|
|
16
15
|
samples: Sample[]
|
|
@@ -53,12 +52,14 @@ export function makeImageData({
|
|
|
53
52
|
const scale = 1 / bpPerPx
|
|
54
53
|
const hp2 = h / 2
|
|
55
54
|
const offset = (rowHeight - h) / 2
|
|
55
|
+
const { charWidth, charHeight } = getCharWidthHeight()
|
|
56
56
|
|
|
57
57
|
ctx.font = FONT_CONFIG
|
|
58
58
|
|
|
59
59
|
const renderingContext: RenderingContext = {
|
|
60
60
|
ctx,
|
|
61
61
|
scale,
|
|
62
|
+
bpPerPx,
|
|
62
63
|
canvasWidth,
|
|
63
64
|
rowHeight,
|
|
64
65
|
h,
|
|
@@ -69,12 +70,13 @@ export function makeImageData({
|
|
|
69
70
|
showAllLetters,
|
|
70
71
|
mismatchRendering,
|
|
71
72
|
showAsUpperCase,
|
|
73
|
+
charWidth,
|
|
74
|
+
charHeight,
|
|
72
75
|
spatialIndex: [],
|
|
73
76
|
spatialIndexCoords: [],
|
|
74
77
|
lastInsertedX: -Infinity, // Start with -Infinity so first item is always inserted
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
// First pass: render alignments (gaps, matches, mismatches, text)
|
|
78
80
|
for (const feature of features.values()) {
|
|
79
81
|
processFeatureAlignment(
|
|
80
82
|
feature,
|
|
@@ -84,17 +86,6 @@ export function makeImageData({
|
|
|
84
86
|
renderingContext,
|
|
85
87
|
)
|
|
86
88
|
}
|
|
87
|
-
|
|
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
|
-
}
|
|
98
89
|
const flatbush = new Flatbush(renderingContext.spatialIndex.length || 1)
|
|
99
90
|
if (renderingContext.spatialIndex.length === 0) {
|
|
100
91
|
flatbush.add(0, 0, 1, 1)
|
|
@@ -20,7 +20,7 @@ export function processFeatureAlignment(
|
|
|
20
20
|
string,
|
|
21
21
|
AlignmentRecord
|
|
22
22
|
>
|
|
23
|
-
const referenceSeq = feature.get('seq').toLowerCase()
|
|
23
|
+
const referenceSeq = (feature.get('seq') as string).toLowerCase()
|
|
24
24
|
|
|
25
25
|
for (const [sampleId, alignmentData] of Object.entries(alignments)) {
|
|
26
26
|
const row = sampleToRowMap.get(sampleId)
|
|
@@ -33,16 +33,7 @@ export function processFeatureAlignment(
|
|
|
33
33
|
const rowTop = renderingContext.offset + renderingContext.rowHeight * row
|
|
34
34
|
|
|
35
35
|
renderGaps(renderingContext, alignment, referenceSeq, leftPx, rowTop)
|
|
36
|
-
renderMatches(
|
|
37
|
-
renderingContext,
|
|
38
|
-
alignment,
|
|
39
|
-
referenceSeq,
|
|
40
|
-
leftPx,
|
|
41
|
-
rowTop,
|
|
42
|
-
row,
|
|
43
|
-
alignmentData.start,
|
|
44
|
-
alignmentData.chr,
|
|
45
|
-
)
|
|
36
|
+
renderMatches(renderingContext, alignment, referenceSeq, leftPx, rowTop)
|
|
46
37
|
renderMismatches(
|
|
47
38
|
renderingContext,
|
|
48
39
|
alignment,
|
|
@@ -61,31 +52,6 @@ export function processFeatureAlignment(
|
|
|
61
52
|
leftPx,
|
|
62
53
|
rowTop,
|
|
63
54
|
)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function processFeatureInsertions(
|
|
68
|
-
feature: Feature,
|
|
69
|
-
region: GenomicRegion,
|
|
70
|
-
bpPerPx: number,
|
|
71
|
-
sampleToRowMap: Map<string, number>,
|
|
72
|
-
renderingContext: RenderingContext,
|
|
73
|
-
) {
|
|
74
|
-
const [leftPx] = featureSpanPx(feature, region, bpPerPx)
|
|
75
|
-
const alignments = feature.get('alignments') as Record<
|
|
76
|
-
string,
|
|
77
|
-
AlignmentRecord
|
|
78
|
-
>
|
|
79
|
-
const referenceSeq = feature.get('seq').toLowerCase()
|
|
80
|
-
for (const [sampleId, alignmentData] of Object.entries(alignments)) {
|
|
81
|
-
const row = sampleToRowMap.get(sampleId)
|
|
82
|
-
if (row === undefined) {
|
|
83
|
-
continue
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const alignment = alignmentData.seq.toLowerCase()
|
|
87
|
-
const rowTop = renderingContext.offset + renderingContext.rowHeight * row
|
|
88
|
-
|
|
89
55
|
renderInsertions(
|
|
90
56
|
renderingContext,
|
|
91
57
|
alignment,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { measureText } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
|
-
import { fillRect
|
|
3
|
+
import { fillRect } from '../util'
|
|
4
4
|
import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
|
|
5
5
|
import {
|
|
6
6
|
CHAR_SIZE_WIDTH,
|
|
@@ -27,8 +27,7 @@ export function renderInsertions(
|
|
|
27
27
|
alignmentStart: number,
|
|
28
28
|
chr: string,
|
|
29
29
|
) {
|
|
30
|
-
const { ctx, scale, h, canvasWidth, rowHeight } = context
|
|
31
|
-
const { charHeight } = getCharWidthHeight()
|
|
30
|
+
const { ctx, scale, h, canvasWidth, rowHeight, charHeight } = context
|
|
32
31
|
|
|
33
32
|
for (
|
|
34
33
|
let i = 0, genomicOffset = 0, seqLength = alignment.length;
|
|
@@ -102,14 +101,17 @@ export function renderInsertions(
|
|
|
102
101
|
actualXPos = xPos
|
|
103
102
|
actualWidth = INSERTION_LINE_WIDTH
|
|
104
103
|
fillRect(ctx, actualXPos, rowTop, actualWidth, h, canvasWidth, 'purple')
|
|
104
|
+
|
|
105
|
+
// Always use a wider hit box for spatial index, even if visual is 1px
|
|
106
|
+
const hitBoxPadding = 2
|
|
107
|
+
actualXPos = xPos - hitBoxPadding
|
|
108
|
+
actualWidth = INSERTION_LINE_WIDTH + 2 * hitBoxPadding
|
|
109
|
+
|
|
105
110
|
if (
|
|
106
111
|
bpPerPx < HIGH_ZOOM_THRESHOLD &&
|
|
107
112
|
rowHeight > MIN_ROW_HEIGHT_FOR_BORDERS
|
|
108
113
|
) {
|
|
109
114
|
// Add horizontal borders for visibility at high zoom
|
|
110
|
-
// Note: borders extend the effective clickable area
|
|
111
|
-
actualXPos = xPos - INSERTION_BORDER_WIDTH
|
|
112
|
-
actualWidth = INSERTION_BORDER_HEIGHT
|
|
113
115
|
fillRect(
|
|
114
116
|
ctx,
|
|
115
117
|
xPos - INSERTION_BORDER_WIDTH,
|
|
@@ -131,6 +133,8 @@ export function renderInsertions(
|
|
|
131
133
|
|
|
132
134
|
// Add insertion to spatial index with actual rendered dimensions
|
|
133
135
|
// Insertions always bypass distance filter
|
|
136
|
+
const isLargeInsertion =
|
|
137
|
+
insertionSequence.length > LARGE_INSERTION_THRESHOLD
|
|
134
138
|
if (shouldAddToSpatialIndex(actualXPos, context, true)) {
|
|
135
139
|
addToSpatialIndex(
|
|
136
140
|
context,
|
|
@@ -144,6 +148,7 @@ export function renderInsertions(
|
|
|
144
148
|
base: insertionSequence,
|
|
145
149
|
sampleId,
|
|
146
150
|
isInsertion: true,
|
|
151
|
+
isLargeInsertion,
|
|
147
152
|
},
|
|
148
153
|
)
|
|
149
154
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { fillRect } from '../util'
|
|
2
|
-
import { addToSpatialIndex, shouldAddToSpatialIndex } from './spatialIndex'
|
|
3
2
|
import { GAP_STROKE_OFFSET } from './types'
|
|
4
3
|
|
|
5
4
|
import type { RenderingContext } from './types'
|
|
@@ -10,9 +9,6 @@ export function renderMatches(
|
|
|
10
9
|
seq: string,
|
|
11
10
|
leftPx: number,
|
|
12
11
|
rowTop: number,
|
|
13
|
-
sampleId: number,
|
|
14
|
-
alignmentStart: number,
|
|
15
|
-
chr: string,
|
|
16
12
|
) {
|
|
17
13
|
if (context.showAllLetters) {
|
|
18
14
|
return
|
|
@@ -30,30 +26,9 @@ export function renderMatches(
|
|
|
30
26
|
if (seq[i] !== '-') {
|
|
31
27
|
// Only process non-gap positions in reference
|
|
32
28
|
const currentChar = alignment[i]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
seq[i] === currentChar &&
|
|
36
|
-
currentChar !== '-' &&
|
|
37
|
-
currentChar !== ' '
|
|
38
|
-
) {
|
|
29
|
+
if (seq[i] === currentChar && currentChar !== ' ') {
|
|
30
|
+
const xPos = leftPx + scale * genomicOffset
|
|
39
31
|
fillRect(ctx, xPos, rowTop, scale + GAP_STROKE_OFFSET, h, canvasWidth)
|
|
40
|
-
|
|
41
|
-
// Add to spatial index if distance filter allows
|
|
42
|
-
if (shouldAddToSpatialIndex(xPos, context)) {
|
|
43
|
-
addToSpatialIndex(
|
|
44
|
-
context,
|
|
45
|
-
xPos,
|
|
46
|
-
rowTop,
|
|
47
|
-
xPos + context.scale + GAP_STROKE_OFFSET,
|
|
48
|
-
rowTop + context.h,
|
|
49
|
-
{
|
|
50
|
-
pos: genomicOffset + alignmentStart,
|
|
51
|
-
chr,
|
|
52
|
-
base: currentChar || '',
|
|
53
|
-
sampleId,
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
32
|
}
|
|
58
33
|
genomicOffset++
|
|
59
34
|
}
|
|
@@ -20,9 +20,16 @@ export function shouldAddToSpatialIndex(
|
|
|
20
20
|
context: RenderingContext,
|
|
21
21
|
bypassDistanceFilter = false,
|
|
22
22
|
): boolean {
|
|
23
|
+
if (bypassDistanceFilter) {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Zoom-aware distance threshold: scale threshold based on zoom level
|
|
28
|
+
// At high zoom (small bpPerPx), use smaller threshold for more precision
|
|
29
|
+
// At low zoom (large bpPerPx), use larger threshold to reduce index size
|
|
23
30
|
return (
|
|
24
|
-
|
|
25
|
-
Math.
|
|
31
|
+
Math.abs(xPos - context.lastInsertedX) >
|
|
32
|
+
MIN_X_DISTANCE * Math.max(1, context.bpPerPx)
|
|
26
33
|
)
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getCharWidthHeight } from '../util'
|
|
2
1
|
import { CHAR_SIZE_WIDTH, VERTICAL_TEXT_OFFSET } from './types'
|
|
3
2
|
|
|
4
3
|
import type { RenderingContext } from './types'
|
|
@@ -34,8 +33,8 @@ export function renderText(
|
|
|
34
33
|
mismatchRendering,
|
|
35
34
|
contrastForBase,
|
|
36
35
|
showAsUpperCase,
|
|
36
|
+
charHeight,
|
|
37
37
|
} = context
|
|
38
|
-
const { charHeight } = getCharWidthHeight()
|
|
39
38
|
|
|
40
39
|
// Render text labels when zoomed in enough and row is tall enough
|
|
41
40
|
if (scale >= CHAR_SIZE_WIDTH) {
|
|
@@ -13,10 +13,7 @@ export const HIGH_BP_PER_PX_THRESHOLD = 10
|
|
|
13
13
|
export const INSERTION_BORDER_HEIGHT = 5
|
|
14
14
|
export const MIN_X_DISTANCE = 0.5
|
|
15
15
|
|
|
16
|
-
export
|
|
17
|
-
id: string
|
|
18
|
-
color?: string
|
|
19
|
-
}
|
|
16
|
+
export type { Sample } from '../../LinearMafDisplay/types'
|
|
20
17
|
|
|
21
18
|
export interface GenomicRegion {
|
|
22
19
|
start: number
|
|
@@ -30,6 +27,7 @@ export interface RenderedBase {
|
|
|
30
27
|
base: string
|
|
31
28
|
sampleId: number
|
|
32
29
|
isInsertion?: boolean
|
|
30
|
+
isLargeInsertion?: boolean
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
/**
|
|
@@ -38,6 +36,7 @@ export interface RenderedBase {
|
|
|
38
36
|
export interface RenderingContext {
|
|
39
37
|
ctx: CanvasRenderingContext2D
|
|
40
38
|
scale: number
|
|
39
|
+
bpPerPx: number
|
|
41
40
|
canvasWidth: number
|
|
42
41
|
rowHeight: number
|
|
43
42
|
h: number
|
|
@@ -49,6 +48,10 @@ export interface RenderingContext {
|
|
|
49
48
|
mismatchRendering: boolean
|
|
50
49
|
showAsUpperCase: boolean
|
|
51
50
|
|
|
51
|
+
// Cached char dimensions
|
|
52
|
+
charWidth: number
|
|
53
|
+
charHeight: number
|
|
54
|
+
|
|
52
55
|
// RBush spatial index for efficient spatial queries
|
|
53
56
|
spatialIndex: RenderedBase[]
|
|
54
57
|
spatialIndexCoords: number[]
|