Skip to content

Commit 4f4e4f1

Browse files
committed
Optimizations
1 parent d5fda25 commit 4f4e4f1

12 files changed

Lines changed: 377 additions & 353 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"vitest": "^4.0.5"
6363
},
6464
"dependencies": {
65-
"@gmod/bgzf-filehandle": "^4.0.0",
65+
"@gmod/bgzf-filehandle": "^5.0.2",
6666
"abortable-promise-cache": "^1.5.0",
6767
"buffer": "^6.0.3",
6868
"d3-array": "^3.2.4",

src/LinearMafDisplay/components/LinearMafDisplayComponent.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from 'react'
1+
import React, { useCallback, useEffect, useRef, useState } from 'react'
22

33
import { Menu } from '@jbrowse/core/ui'
44
import { getContainingView, getEnv } from '@jbrowse/core/util'
@@ -102,11 +102,11 @@ const LinearMafDisplay = observer(function (props: {
102102
}
103103

104104
// Function to clear the selection box
105-
const clearSelectionBox = () => {
105+
const clearSelectionBox = useCallback(() => {
106106
setShowSelectionBox(false)
107107
setDragStartX(undefined)
108108
setDragEndX(undefined)
109-
}
109+
}, [])
110110

111111
// Add keydown event handler to clear selection box when Escape key is pressed
112112
useEffect(() => {

src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React from 'react'
1+
import React, { useEffect, useRef } from 'react'
22

33
import { getContainingView } from '@jbrowse/core/util'
4+
import { autorun } from 'mobx'
45
import { observer } from 'mobx-react'
6+
import { isAlive } from 'mobx-state-tree'
57

68
import type { LinearMafDisplayModel } from '../../stateModel'
79
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
@@ -15,25 +17,74 @@ const SvgWrapper = observer(function ({
1517
children: React.ReactNode
1618
exportSVG?: boolean
1719
}) {
20+
const mouseoverRef = useRef<HTMLCanvasElement>(null)
21+
22+
useEffect(() => {
23+
const ctx = mouseoverRef.current?.getContext('2d')
24+
return ctx
25+
? autorun(() => {
26+
if (isAlive(model)) {
27+
const {
28+
totalHeight,
29+
sidebarWidth,
30+
leafMap,
31+
rowHeight,
32+
highlightedRowNames,
33+
} = model
34+
35+
ctx.resetTransform()
36+
ctx.clearRect(0, 0, sidebarWidth, totalHeight)
37+
38+
if (highlightedRowNames) {
39+
ctx.fillStyle = 'rgba(255,165,0,0.2)'
40+
for (const name of highlightedRowNames) {
41+
const leaf = leafMap.get(name)
42+
if (leaf) {
43+
const y = leaf.x!
44+
ctx.fillRect(0, y - rowHeight / 2, sidebarWidth, rowHeight)
45+
}
46+
}
47+
}
48+
}
49+
})
50+
: undefined
51+
}, [model])
52+
1853
if (exportSVG) {
1954
return <>{children}</>
2055
} else {
21-
const { totalHeight } = model
56+
const { totalHeight, sidebarWidth } = model
2257
const { width } = getContainingView(model) as LinearGenomeViewModel
2358
return (
24-
<svg
25-
style={{
26-
position: 'absolute',
27-
userSelect: 'none',
28-
top: 0,
29-
left: 0,
30-
pointerEvents: 'none',
31-
height: totalHeight,
32-
width,
33-
}}
34-
>
35-
{children}
36-
</svg>
59+
<>
60+
<svg
61+
style={{
62+
position: 'absolute',
63+
userSelect: 'none',
64+
top: 0,
65+
left: 0,
66+
pointerEvents: 'none',
67+
height: totalHeight,
68+
width,
69+
}}
70+
>
71+
{children}
72+
</svg>
73+
<canvas
74+
ref={mouseoverRef}
75+
width={sidebarWidth}
76+
height={totalHeight}
77+
style={{
78+
position: 'absolute',
79+
top: 0,
80+
left: 0,
81+
width: sidebarWidth,
82+
height: totalHeight,
83+
zIndex: 1000,
84+
pointerEvents: 'none',
85+
}}
86+
/>
87+
</>
3788
)
3889
}
3990
})

src/LinearMafDisplay/components/Sidebar/Tree.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
1313

1414
hierarchy,
1515
showBranchLen,
16+
nodeDescendantNames,
1617
} = model
1718

1819
return (
@@ -26,14 +27,42 @@ const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
2627
const tx = showBranchLen ? target.len : target.y
2728
// @ts-expect-error
2829
const sx = showBranchLen ? source.len : source.y
30+
const key = `${sy}-${ty}-${tx}-${sx}`
2931

30-
// 1d line intersection to check if line crosses block at all, this is
31-
// an optimization that allows us to skip drawing most tree links
32-
// outside the block
3332
return (
34-
<React.Fragment key={[sy, ty, tx, sx].join('-')}>
35-
<line stroke="black" x1={sx} y1={sy} x2={sx} y2={ty} />
36-
<line stroke="black" x1={sx} y1={ty} x2={tx} y2={ty} />
33+
<React.Fragment key={key}>
34+
<line
35+
stroke="black"
36+
x1={sx}
37+
y1={sy}
38+
x2={sx}
39+
y2={ty}
40+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
41+
onMouseEnter={() => {
42+
model.setHighlightedRowNames(
43+
nodeDescendantNames.get(source),
44+
)
45+
}}
46+
onMouseLeave={() => {
47+
model.setHighlightedRowNames(undefined)
48+
}}
49+
/>
50+
<line
51+
stroke="black"
52+
x1={sx}
53+
y1={ty}
54+
x2={tx}
55+
y2={ty}
56+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
57+
onMouseEnter={() => {
58+
model.setHighlightedRowNames(
59+
nodeDescendantNames.get(target),
60+
)
61+
}}
62+
onMouseLeave={() => {
63+
model.setHighlightedRowNames(undefined)
64+
}}
65+
/>
3766
</React.Fragment>
3867
)
3968
})

src/LinearMafDisplay/stateModel.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export default function stateModelFactory(
101101
* #volatile
102102
*/
103103
volatileTree: undefined as any,
104+
/**
105+
* #volatile
106+
*/
107+
highlightedRowNames: undefined as string[] | undefined,
104108
}))
105109
.actions(self => ({
106110
/**
@@ -150,6 +154,12 @@ export default function stateModelFactory(
150154
setShowAsUpperCase(arg: boolean) {
151155
self.showAsUpperCase = arg
152156
},
157+
/**
158+
* #action
159+
*/
160+
setHighlightedRowNames(names?: string[]) {
161+
self.highlightedRowNames = names
162+
},
153163
}))
154164
.views(self => ({
155165
/**
@@ -238,6 +248,38 @@ export default function stateModelFactory(
238248
get leaves() {
239249
return self.root?.leaves()
240250
},
251+
/**
252+
* #getter
253+
*/
254+
get leafMap() {
255+
return new Map(this.leaves?.map(leaf => [leaf.data.name, leaf]))
256+
},
257+
/**
258+
* #getter
259+
* Precomputed map from hierarchy node to its descendant leaf names
260+
*/
261+
get nodeDescendantNames() {
262+
const map = new Map<unknown, string[]>()
263+
function computeDescendants(
264+
node: HierarchyNode<NodeWithIdsAndLength>,
265+
): string[] {
266+
if (!node.children || node.children.length === 0) {
267+
const names = [node.data.name]
268+
map.set(node, names)
269+
return names
270+
}
271+
const names: string[] = []
272+
for (const child of node.children) {
273+
names.push(...computeDescendants(child))
274+
}
275+
map.set(node, names)
276+
return names
277+
}
278+
if (this.hierarchy) {
279+
computeDescendants(this.hierarchy)
280+
}
281+
return map
282+
},
241283
/**
242284
* #getter
243285
*/
@@ -377,6 +419,12 @@ export default function stateModelFactory(
377419
0,
378420
)
379421
},
422+
/**
423+
* #getter
424+
*/
425+
get sidebarWidth() {
426+
return this.labelWidth + 5 + self.treeWidth
427+
},
380428
}))
381429
.actions(self => ({
382430
afterCreate() {

src/LinearMafDisplay/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { HierarchyNode } from 'd3-hierarchy'
66

77
export interface HoveredInfo {
88
sampleId: string
9+
sampleLabel: string
910
pos: number
1011
base: string
1112
chr: string
@@ -44,7 +45,7 @@ export function generateTooltipContent(
4445
contentLines.push(`Ref: ${p2.refName}:${toLocale(p2.coord)}`)
4546

4647
if (hoveredInfo) {
47-
const { base, sampleId, pos, chr, isInsertion } = hoveredInfo
48+
const { base, sampleLabel, pos, chr, isInsertion } = hoveredInfo
4849
const thresh = 20
4950
const len = base.length
5051
const lengthSuffix = len > 1 ? ` ${len}bp` : ''
@@ -53,7 +54,7 @@ export function generateTooltipContent(
5354
const insertionLabel = isInsertion ? ' Insertion' : ''
5455

5556
contentLines.push(
56-
`Alt ${sampleId}: ${chr}:${pos.toLocaleString('en-US')} (${baseDisplay}${lengthSuffix}${insertionLabel})`,
57+
`Alt ${sampleLabel}: ${chr}:${pos.toLocaleString('en-US')} (${baseDisplay}${lengthSuffix}${insertionLabel})`,
5758
)
5859
}
5960
}

src/LinearMafRenderer/components/LinearMafRendering.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const LinearMafRendering = observer(function (props: {
3737
const s = samples[r.sampleId]
3838
return {
3939
...r,
40-
sampleId: s?.label || s?.id || 'unknown',
40+
sampleId: s?.id ?? 'unknown',
41+
sampleLabel: s?.label || s?.id || 'unknown',
4142
}
4243
} else {
4344
return undefined
@@ -46,16 +47,20 @@ const LinearMafRendering = observer(function (props: {
4647
return (
4748
<div
4849
ref={ref}
49-
onMouseMove={e =>
50-
displayModel.setHoveredInfo?.(
51-
getFeatureUnderMouse(e.clientX, e.clientY),
50+
onMouseMove={e => {
51+
const feature = getFeatureUnderMouse(e.clientX, e.clientY)
52+
displayModel.setHoveredInfo?.(feature)
53+
displayModel.setHighlightedRowNames?.(
54+
feature?.sampleId ? [feature.sampleId] : undefined,
5255
)
53-
}
56+
}}
5457
onMouseLeave={() => {
5558
displayModel.setHoveredInfo?.(undefined)
59+
displayModel.setHighlightedRowNames?.(undefined)
5660
}}
5761
onMouseOut={() => {
5862
displayModel.setHoveredInfo?.(undefined)
63+
displayModel.setHighlightedRowNames?.(undefined)
5964
}}
6065
style={{
6166
overflow: 'visible',

src/LinearMafRenderer/makeImageData.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function makeImageData({
5959
const renderingContext: RenderingContext = {
6060
ctx,
6161
scale,
62+
bpPerPx,
6263
canvasWidth,
6364
rowHeight,
6465
h,

src/LinearMafRenderer/rendering/spatialIndex.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,19 @@ export function shouldAddToSpatialIndex(
2020
context: RenderingContext,
2121
bypassDistanceFilter = false,
2222
): boolean {
23-
return (
24-
bypassDistanceFilter ||
25-
Math.abs(xPos - context.lastInsertedX) > MIN_X_DISTANCE
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
30+
const dynamicThreshold = Math.max(
31+
MIN_X_DISTANCE,
32+
context.bpPerPx * MIN_X_DISTANCE,
2633
)
34+
35+
return Math.abs(xPos - context.lastInsertedX) > dynamicThreshold
2736
}
2837

2938
export function addToSpatialIndex(

src/LinearMafRenderer/rendering/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface RenderedBase {
3838
export interface RenderingContext {
3939
ctx: CanvasRenderingContext2D
4040
scale: number
41+
bpPerPx: number
4142
canvasWidth: number
4243
rowHeight: number
4344
h: number

0 commit comments

Comments
 (0)