Skip to content

Commit 0f3afc0

Browse files
committed
Insertion sequence dialog
1 parent 98f2c85 commit 0f3afc0

8 files changed

Lines changed: 282 additions & 55 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useState } from 'react'
2+
3+
import { Dialog } from '@jbrowse/core/ui'
4+
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
5+
import { observer } from 'mobx-react'
6+
import { makeStyles } from 'tss-react/mui'
7+
8+
const useStyles = makeStyles()({
9+
dialogContent: {
10+
width: '60em',
11+
},
12+
textAreaInput: {
13+
fontFamily: 'monospace',
14+
whiteSpace: 'pre',
15+
overflowX: 'auto',
16+
},
17+
})
18+
19+
const InsertionSequenceDialog = observer(function ({
20+
onClose,
21+
model,
22+
insertionData,
23+
}: {
24+
onClose: () => void
25+
model: {
26+
showAsUpperCase: boolean
27+
}
28+
insertionData: {
29+
sequence: string
30+
sampleLabel: string
31+
chr: string
32+
pos: number
33+
}
34+
}) {
35+
const { classes } = useStyles()
36+
const [copied, setCopied] = useState(false)
37+
const { sequence, sampleLabel, chr, pos } = insertionData
38+
const { showAsUpperCase } = model
39+
const displaySequence = showAsUpperCase
40+
? sequence.toUpperCase()
41+
: sequence.toLowerCase()
42+
43+
return (
44+
<Dialog
45+
open
46+
onClose={onClose}
47+
title={`Insertion Sequence (${sequence.length}bp)`}
48+
maxWidth="lg"
49+
>
50+
<DialogContent>
51+
<div style={{ marginBottom: 16 }}>
52+
<strong>Sample:</strong> {sampleLabel}
53+
<br />
54+
<strong>Position:</strong> {chr}:{pos.toLocaleString('en-US')}
55+
<br />
56+
<strong>Length:</strong> {sequence.length}bp
57+
</div>
58+
<TextField
59+
variant="outlined"
60+
multiline
61+
minRows={3}
62+
maxRows={10}
63+
className={classes.dialogContent}
64+
fullWidth
65+
value={displaySequence}
66+
slotProps={{
67+
input: {
68+
readOnly: true,
69+
classes: {
70+
input: classes.textAreaInput,
71+
},
72+
},
73+
}}
74+
/>
75+
</DialogContent>
76+
<DialogActions>
77+
<Button
78+
variant="contained"
79+
color="primary"
80+
onClick={() => {
81+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
82+
;(async () => {
83+
try {
84+
await navigator.clipboard.writeText(displaySequence)
85+
setCopied(true)
86+
setTimeout(() => {
87+
setCopied(false)
88+
}, 1000)
89+
} catch (e) {
90+
console.error(e)
91+
}
92+
})()
93+
}}
94+
>
95+
{copied ? 'Copied!' : 'Copy to Clipboard'}
96+
</Button>
97+
<Button color="secondary" variant="contained" onClick={onClose}>
98+
Close
99+
</Button>
100+
</DialogActions>
101+
</Dialog>
102+
)
103+
})
104+
105+
export default InsertionSequenceDialog

src/LinearMafDisplay/components/Sidebar/ColorLegend.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const ColorLegend = observer(function ({
1717
canDisplayLabel,
1818
totalHeight,
1919
treeWidth,
20+
sidebarWidth,
2021
samples = [],
2122
rowHeight,
2223
svgFontSize,
@@ -25,12 +26,7 @@ const ColorLegend = observer(function ({
2526

2627
return (
2728
<>
28-
<RectBg
29-
y={0}
30-
x={0}
31-
width={labelWidth + 5 + treeWidth}
32-
height={totalHeight}
33-
/>
29+
<RectBg y={0} x={0} width={sidebarWidth} height={totalHeight} />
3430
<Tree model={model} />
3531
<g transform={`translate(${treeWidth + 5},0)`}>
3632
{samples.map((sample, idx) => (

src/LinearMafDisplay/components/Sidebar/SvgWrapper.tsx

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useRef } from 'react'
22

3+
import { ResizeHandle } from '@jbrowse/core/ui'
34
import { getContainingView } from '@jbrowse/core/util'
45
import { autorun } from 'mobx'
56
import { observer } from 'mobx-react'
@@ -8,6 +9,16 @@ import { isAlive } from 'mobx-state-tree'
89
import type { LinearMafDisplayModel } from '../../stateModel'
910
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
1011

12+
const resizeHandleStyle = {
13+
position: 'absolute',
14+
top: 0,
15+
height: '100%',
16+
width: 4,
17+
zIndex: 1001,
18+
background: 'rgba(0,0,0,0.1)',
19+
cursor: 'col-resize',
20+
} as const
21+
1122
const SvgWrapper = observer(function ({
1223
children,
1324
model,
@@ -26,24 +37,38 @@ const SvgWrapper = observer(function ({
2637
if (isAlive(model)) {
2738
const {
2839
totalHeight,
29-
sidebarWidth,
3040
leafMap,
3141
rowHeight,
3242
highlightedRowNames,
43+
hoveredTreeNode,
3344
} = model
45+
const { width: viewWidth } = getContainingView(
46+
model,
47+
) as LinearGenomeViewModel
3448

3549
ctx.resetTransform()
36-
ctx.clearRect(0, 0, sidebarWidth, totalHeight)
50+
ctx.clearRect(0, 0, viewWidth, totalHeight)
3751

3852
if (highlightedRowNames) {
3953
ctx.fillStyle = 'rgba(255,165,0,0.2)'
4054
const halfRowHeight = rowHeight / 2
4155
for (const name of highlightedRowNames) {
4256
const leaf = leafMap.get(name)
4357
if (leaf) {
44-
ctx.fillRect(0, leaf.x! - halfRowHeight, sidebarWidth, rowHeight)
58+
ctx.fillRect(0, leaf.x! - halfRowHeight, viewWidth, rowHeight)
4559
}
4660
}
61+
62+
// Draw orange dot at hovered tree node
63+
if (hoveredTreeNode) {
64+
ctx.fillStyle = 'rgba(255,165,0,0.8)'
65+
ctx.beginPath()
66+
ctx.arc(hoveredTreeNode.y, hoveredTreeNode.x, 4, 0, 2 * Math.PI)
67+
ctx.fill()
68+
ctx.strokeStyle = 'rgba(255,140,0,1)'
69+
ctx.lineWidth = 1
70+
ctx.stroke()
71+
}
4772
}
4873
}
4974
})
@@ -53,7 +78,7 @@ const SvgWrapper = observer(function ({
5378
if (exportSVG) {
5479
return <>{children}</>
5580
} else {
56-
const { totalHeight, sidebarWidth } = model
81+
const { totalHeight, treeWidth, hierarchy } = model
5782
const { width } = getContainingView(model) as LinearGenomeViewModel
5883
return (
5984
<>
@@ -72,18 +97,39 @@ const SvgWrapper = observer(function ({
7297
</svg>
7398
<canvas
7499
ref={mouseoverRef}
75-
width={sidebarWidth}
100+
width={width}
76101
height={totalHeight}
77102
style={{
78103
position: 'absolute',
79104
top: 0,
80105
left: 0,
81-
width: sidebarWidth,
106+
width,
82107
height: totalHeight,
83108
zIndex: 1000,
84109
pointerEvents: 'none',
85110
}}
86111
/>
112+
{hierarchy ? (
113+
<div
114+
onMouseDown={e => {
115+
e.stopPropagation()
116+
}}
117+
>
118+
<ResizeHandle
119+
onDrag={distance => {
120+
model.setTreeAreaWidth(
121+
Math.max(20, model.treeAreaWidth + distance),
122+
)
123+
return undefined
124+
}}
125+
style={{
126+
...resizeHandleStyle,
127+
left: treeWidth,
128+
}}
129+
vertical
130+
/>
131+
</div>
132+
) : null}
87133
</>
88134
)
89135
}

src/LinearMafDisplay/components/Sidebar/Tree.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
import React, { useCallback } from 'react'
1+
import React, { useCallback, useMemo } from 'react'
22

33
import { observer } from 'mobx-react'
44

55
import type { LinearMafDisplayModel } from '../../stateModel'
6-
import type { HierarchyNode } from 'd3-hierarchy'
76
import type { NodeWithIdsAndLength } from '../../types'
7+
import type { HierarchyNode } from 'd3-hierarchy'
8+
9+
const hitboxStyle = {
10+
pointerEvents: 'all',
11+
cursor: 'pointer',
12+
strokeWidth: 8,
13+
stroke: 'transparent',
14+
} as const
815

916
const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
1017
const {
11-
// this is needed for redrawing after zoom change, similar to react-msaview
12-
// renderTreeCanvas
18+
// rowHeight is needed for redrawing after zoom change
1319
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1420
rowHeight: _rowHeight,
15-
21+
treeAreaWidth,
1622
hierarchy,
1723
showBranchLen,
1824
nodeDescendantNames,
@@ -22,12 +28,20 @@ const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
2228
model.setHighlightedRowNames(undefined)
2329
}, [model])
2430

25-
const makeMouseEnterHandler = useCallback(
26-
(node: HierarchyNode<NodeWithIdsAndLength>) => () => {
27-
model.setHighlightedRowNames(nodeDescendantNames.get(node))
28-
},
29-
[model, nodeDescendantNames],
30-
)
31+
const nodeHandlers = useMemo(() => {
32+
const handlers = new Map<HierarchyNode<NodeWithIdsAndLength>, () => void>()
33+
if (hierarchy) {
34+
for (const node of hierarchy.descendants()) {
35+
handlers.set(node, () => {
36+
model.setHighlightedRowNames(nodeDescendantNames.get(node), {
37+
x: node.x!,
38+
y: node.y!,
39+
})
40+
})
41+
}
42+
}
43+
return handlers
44+
}, [model, hierarchy, nodeDescendantNames, treeAreaWidth])
3145

3246
return (
3347
<>
@@ -40,28 +54,29 @@ const Tree = observer(function ({ model }: { model: LinearMafDisplayModel }) {
4054
const tx = showBranchLen ? target.len : target.y
4155
// @ts-expect-error
4256
const sx = showBranchLen ? source.len : source.y
43-
const key = `${sy}-${ty}-${tx}-${sx}`
4457

4558
return (
46-
<React.Fragment key={key}>
59+
<React.Fragment key={`${treeAreaWidth}-${sy}-${ty}-${tx}-${sx}`}>
60+
{/* Visible lines */}
61+
<line stroke="black" x1={sx} y1={sy} x2={sx} y2={ty} />
62+
<line stroke="black" x1={sx} y1={ty} x2={tx} y2={ty} />
63+
{/* Invisible hitbox lines */}
4764
<line
48-
stroke="black"
4965
x1={sx}
5066
y1={sy}
5167
x2={sx}
5268
y2={ty}
53-
style={{ pointerEvents: 'all', cursor: 'pointer' }}
54-
onMouseEnter={makeMouseEnterHandler(source)}
69+
style={hitboxStyle}
70+
onMouseEnter={nodeHandlers.get(source)}
5571
onMouseLeave={clearHighlight}
5672
/>
5773
<line
58-
stroke="black"
5974
x1={sx}
6075
y1={ty}
6176
x2={tx}
6277
y2={ty}
63-
style={{ pointerEvents: 'all', cursor: 'pointer' }}
64-
onMouseEnter={makeMouseEnterHandler(target)}
78+
style={hitboxStyle}
79+
onMouseEnter={nodeHandlers.get(target)}
6580
onMouseLeave={clearHighlight}
6681
/>
6782
</React.Fragment>

0 commit comments

Comments
 (0)