react-msaview 5.8.0 → 5.10.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/bundle/index.js +84 -84
- package/bundle/index.js.map +3 -3
- package/dist/components/ConservationTrack.js +21 -16
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +17 -23
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/TextTrack.js +26 -21
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +39 -72
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/msa/DomainLegend.js +4 -5
- package/dist/components/msa/DomainLegend.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +19 -16
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +13 -6
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +31 -31
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +5 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +29 -24
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +19 -12
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
- package/dist/components/tree/renderTreeCanvas.js +17 -17
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/model.d.ts +16 -1
- package/dist/model.js +99 -114
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +12 -19
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.js +32 -43
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +3 -0
- package/dist/renderToSvg.js.map +1 -1
- package/dist/seqPosToGlobalCol.d.ts +6 -19
- package/dist/seqPosToGlobalCol.js +6 -33
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/useCanvasAutorun.d.ts +23 -1
- package/dist/useCanvasAutorun.js +20 -8
- package/dist/useCanvasAutorun.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ConservationTrack.tsx +7 -5
- package/src/components/MSAView.tsx +24 -30
- package/src/components/TextTrack.tsx +7 -12
- package/src/components/header/settingsMenuItems.ts +40 -72
- package/src/components/msa/DomainLegend.tsx +4 -3
- package/src/components/msa/MSACanvasBlock.tsx +16 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +10 -6
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
- package/src/components/msa/renderMSAMouseover.ts +37 -32
- package/src/components/overlayColors.ts +6 -0
- package/src/components/tree/TreeCanvas.tsx +6 -4
- package/src/components/tree/TreeCanvasBlock.tsx +11 -12
- package/src/components/tree/renderTreeCanvas.ts +20 -17
- package/src/constants.ts +1 -0
- package/src/model.ts +114 -130
- package/src/modelFilehandleLoaders.test.ts +137 -0
- package/src/neighborJoining.test.ts +12 -0
- package/src/neighborJoining.ts +12 -20
- package/src/parseAsn1.ts +32 -40
- package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
- package/src/renderToSvg.tsx +3 -0
- package/src/seqPosToGlobalCol.test.ts +39 -51
- package/src/seqPosToGlobalCol.ts +6 -41
- package/src/stripDefaultSnapshot.test.ts +11 -0
- package/src/useCanvasAutorun.test.tsx +98 -0
- package/src/useCanvasAutorun.ts +30 -11
- package/src/version.ts +1 -1
|
@@ -1,41 +1,34 @@
|
|
|
1
1
|
import type { MsaViewModel } from '../../model.ts'
|
|
2
2
|
import type { MenuItem } from '@jbrowse/core/ui/Menu'
|
|
3
3
|
|
|
4
|
+
function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
|
|
5
|
+
return {
|
|
6
|
+
label,
|
|
7
|
+
type: 'checkbox' as const,
|
|
8
|
+
checked,
|
|
9
|
+
onClick: () => {
|
|
10
|
+
set(!checked)
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
5
16
|
const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
|
|
6
17
|
return [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
label: 'Color letters instead of background of tiles',
|
|
25
|
-
type: 'checkbox',
|
|
26
|
-
checked: !bgColor,
|
|
27
|
-
onClick: () => {
|
|
28
|
-
model.setBgColor(!bgColor)
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
label: 'Enable hiding gappy columns?',
|
|
33
|
-
type: 'checkbox',
|
|
34
|
-
checked: hideGaps,
|
|
35
|
-
onClick: () => {
|
|
36
|
-
model.setHideGaps(!hideGaps)
|
|
37
|
-
},
|
|
38
|
-
},
|
|
18
|
+
toggle('Show column statistics on hover', showColumnStats, arg => {
|
|
19
|
+
model.setShowColumnStats(arg)
|
|
20
|
+
}),
|
|
21
|
+
toggle('Draw letters', drawMsaLetters, arg => {
|
|
22
|
+
model.setDrawMsaLetters(arg)
|
|
23
|
+
}),
|
|
24
|
+
// the checkbox reads as the inverse of the property it sets: bgColor draws
|
|
25
|
+
// the tile, and turning it off is what leaves the letter itself colored
|
|
26
|
+
toggle('Color letters instead of background of tiles', !bgColor, arg => {
|
|
27
|
+
model.setBgColor(!arg)
|
|
28
|
+
}),
|
|
29
|
+
toggle('Enable hiding gappy columns?', hideGaps, arg => {
|
|
30
|
+
model.setHideGaps(arg)
|
|
31
|
+
}),
|
|
39
32
|
]
|
|
40
33
|
}
|
|
41
34
|
|
|
@@ -48,45 +41,20 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
|
48
41
|
drawLabels,
|
|
49
42
|
} = model
|
|
50
43
|
return [
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
label: 'Draw clickable bubbles on tree branches',
|
|
69
|
-
type: 'checkbox',
|
|
70
|
-
checked: drawNodeBubbles,
|
|
71
|
-
onClick: () => {
|
|
72
|
-
model.setDrawNodeBubbles(!drawNodeBubbles)
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
label: 'Tree labels align right',
|
|
77
|
-
type: 'checkbox',
|
|
78
|
-
checked: labelsAlignRight,
|
|
79
|
-
onClick: () => {
|
|
80
|
-
model.setLabelsAlignRight(!labelsAlignRight)
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
label: 'Draw labels',
|
|
85
|
-
type: 'checkbox',
|
|
86
|
-
checked: drawLabels,
|
|
87
|
-
onClick: () => {
|
|
88
|
-
model.setDrawLabels(!drawLabels)
|
|
89
|
-
},
|
|
90
|
-
},
|
|
44
|
+
toggle('Show branch length', showBranchLen, arg => {
|
|
45
|
+
model.setShowBranchLen(arg)
|
|
46
|
+
}),
|
|
47
|
+
toggle('Show tree', drawTree, arg => {
|
|
48
|
+
model.setDrawTree(arg)
|
|
49
|
+
}),
|
|
50
|
+
toggle('Draw clickable bubbles on tree branches', drawNodeBubbles, arg => {
|
|
51
|
+
model.setDrawNodeBubbles(arg)
|
|
52
|
+
}),
|
|
53
|
+
toggle('Tree labels align right', labelsAlignRight, arg => {
|
|
54
|
+
model.setLabelsAlignRight(arg)
|
|
55
|
+
}),
|
|
56
|
+
toggle('Draw labels', drawLabels, arg => {
|
|
57
|
+
model.setDrawLabels(arg)
|
|
58
|
+
}),
|
|
91
59
|
]
|
|
92
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react'
|
|
2
2
|
|
|
3
3
|
import ExpandLess from '@mui/icons-material/ExpandLess'
|
|
4
4
|
import ExpandMore from '@mui/icons-material/ExpandMore'
|
|
@@ -8,11 +8,11 @@ import { observer } from 'mobx-react'
|
|
|
8
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
9
9
|
|
|
10
10
|
const DomainLegend = observer(function ({ model }: { model: MsaViewModel }) {
|
|
11
|
-
const [expanded, setExpanded] = useState(true)
|
|
12
11
|
const {
|
|
13
12
|
actuallyShowDomains,
|
|
14
13
|
visibleDomainTypes: visible,
|
|
15
14
|
fillPalette,
|
|
15
|
+
showDomainLegend: expanded,
|
|
16
16
|
} = model
|
|
17
17
|
const ExpandIcon = expanded ? ExpandLess : ExpandMore
|
|
18
18
|
|
|
@@ -41,8 +41,9 @@ const DomainLegend = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
41
41
|
>
|
|
42
42
|
<IconButton
|
|
43
43
|
size="small"
|
|
44
|
+
title={expanded ? 'Collapse domain key' : 'Expand domain key'}
|
|
44
45
|
onClick={() => {
|
|
45
|
-
|
|
46
|
+
model.setShowDomainLegend(!expanded)
|
|
46
47
|
}}
|
|
47
48
|
>
|
|
48
49
|
<ExpandIcon fontSize="small" />
|
|
@@ -31,24 +31,17 @@ const MSACanvasBlock = observer(function ({
|
|
|
31
31
|
},
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const canvasSize = blockSize * highResScaleFactor
|
|
35
|
+
const ref = useCanvasAutorun({
|
|
36
|
+
draw: ctx => {
|
|
37
37
|
ctx.resetTransform()
|
|
38
|
-
ctx.clearRect(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
renderBoxFeatureCanvasBlock({
|
|
46
|
-
ctx,
|
|
47
|
-
offsetX,
|
|
48
|
-
offsetY,
|
|
49
|
-
model,
|
|
50
|
-
})
|
|
51
|
-
}
|
|
38
|
+
ctx.clearRect(0, 0, canvasSize, canvasSize)
|
|
39
|
+
renderBoxFeatureCanvasBlock({
|
|
40
|
+
ctx,
|
|
41
|
+
offsetX,
|
|
42
|
+
offsetY,
|
|
43
|
+
model,
|
|
44
|
+
})
|
|
52
45
|
renderMSABlock({
|
|
53
46
|
ctx,
|
|
54
47
|
theme,
|
|
@@ -58,8 +51,10 @@ const MSACanvasBlock = observer(function ({
|
|
|
58
51
|
model,
|
|
59
52
|
})
|
|
60
53
|
},
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
width: canvasSize,
|
|
55
|
+
height: canvasSize,
|
|
56
|
+
deps: [model, offsetX, offsetY, theme, contrastScheme],
|
|
57
|
+
})
|
|
63
58
|
|
|
64
59
|
const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
|
|
65
60
|
|
|
@@ -80,8 +75,8 @@ const MSACanvasBlock = observer(function ({
|
|
|
80
75
|
onMouseLeave={() => {
|
|
81
76
|
onMouseLeave()
|
|
82
77
|
}}
|
|
83
|
-
width={
|
|
84
|
-
height={
|
|
78
|
+
width={canvasSize}
|
|
79
|
+
height={canvasSize}
|
|
85
80
|
style={{
|
|
86
81
|
position: 'absolute',
|
|
87
82
|
top: scrollY + offsetY,
|
|
@@ -16,21 +16,25 @@ const MSAMouseoverCanvas = observer(function ({
|
|
|
16
16
|
const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
|
|
17
17
|
model
|
|
18
18
|
const width = msaAreaWidth - verticalScrollbarWidth
|
|
19
|
-
const
|
|
20
|
-
|
|
19
|
+
const canvasWidth = width * highResScaleFactor
|
|
20
|
+
const canvasHeight = height * highResScaleFactor
|
|
21
|
+
const ref = useCanvasAutorun({
|
|
22
|
+
draw: ctx => {
|
|
21
23
|
if (isAlive(model)) {
|
|
22
24
|
renderMouseover({ ctx, model })
|
|
23
25
|
}
|
|
24
26
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
width: canvasWidth,
|
|
28
|
+
height: canvasHeight,
|
|
29
|
+
deps: [model],
|
|
30
|
+
})
|
|
27
31
|
|
|
28
32
|
return (
|
|
29
33
|
<canvas
|
|
30
34
|
ref={ref}
|
|
31
35
|
id="mouseover"
|
|
32
|
-
width={
|
|
33
|
-
height={
|
|
36
|
+
width={canvasWidth}
|
|
37
|
+
height={canvasHeight}
|
|
34
38
|
style={{
|
|
35
39
|
position: 'absolute',
|
|
36
40
|
top: 0,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import stateModelFactory from '../../model.ts'
|
|
4
|
+
import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
|
|
5
|
+
|
|
6
|
+
import type { RenderCtx } from '../renderCtx.ts'
|
|
7
|
+
|
|
8
|
+
const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
|
|
9
|
+
|
|
10
|
+
// only `human` carries the gene model; `mouse` is there to push it off the top
|
|
11
|
+
// of the block
|
|
12
|
+
const msa = `>mouse\n${row}\n>human\n${row}`
|
|
13
|
+
|
|
14
|
+
const gff = `##gff-version 3
|
|
15
|
+
human src exon 1 20 . . . ID=human.exon1;Name=exon-1
|
|
16
|
+
human src exon 21 40 . . . ID=human.exon2;Name=exon-2
|
|
17
|
+
human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
|
|
18
|
+
|
|
19
|
+
function drawnLabels() {
|
|
20
|
+
const model = stateModelFactory().create({
|
|
21
|
+
type: 'MsaView',
|
|
22
|
+
data: { msa },
|
|
23
|
+
})
|
|
24
|
+
model.setWidth(1000)
|
|
25
|
+
model.setRowHeight(20)
|
|
26
|
+
// segment numbers are drawn in place of residue letters, never on top of them
|
|
27
|
+
model.setDrawMsaLetters(false)
|
|
28
|
+
model.applyGFFText(gff)
|
|
29
|
+
expect(model.rowNames).toEqual(['mouse', 'human'])
|
|
30
|
+
|
|
31
|
+
const labels: string[] = []
|
|
32
|
+
const ctx = {
|
|
33
|
+
font: '12px sans-serif',
|
|
34
|
+
resetTransform() {},
|
|
35
|
+
scale() {},
|
|
36
|
+
translate() {},
|
|
37
|
+
fillRect() {},
|
|
38
|
+
strokeRect() {},
|
|
39
|
+
measureText: (text: string) => ({ width: text.length * 6 }),
|
|
40
|
+
fillText(text: string) {
|
|
41
|
+
labels.push(text)
|
|
42
|
+
},
|
|
43
|
+
} as unknown as RenderCtx
|
|
44
|
+
|
|
45
|
+
renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
|
|
46
|
+
return labels
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
|
|
50
|
+
expect(drawnLabels()).toEqual(['1', '2', '3'])
|
|
51
|
+
})
|
|
@@ -77,8 +77,13 @@ function drawTiles({
|
|
|
77
77
|
const xMin = offsetX - cull
|
|
78
78
|
const xMax = offsetX + blockWidth + cull
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// a segment (exon) number labels its band once per block, on the topmost
|
|
81
|
+
// visible row carrying that segment, so it reads as a column header for the
|
|
82
|
+
// whole band. Keyed by accession rather than drawn on row 0, because the rows
|
|
83
|
+
// carrying the gene model are often not the first ones in the alignment
|
|
84
|
+
const labelled = drawSegmentLabels ? new Set<string>() : undefined
|
|
85
|
+
|
|
86
|
+
for (const node of visibleLeaves) {
|
|
82
87
|
const y = node.x!
|
|
83
88
|
const bands = domainBands.get(node.data.name)
|
|
84
89
|
|
|
@@ -94,15 +99,15 @@ function drawTiles({
|
|
|
94
99
|
if (strand === undefined) {
|
|
95
100
|
ctx.fillRect(x, t, lw, h)
|
|
96
101
|
ctx.strokeRect(x, t, lw, h)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (label !== undefined && i === 0) {
|
|
102
|
+
const label =
|
|
103
|
+
labelled && !labelled.has(accession)
|
|
104
|
+
? segmentLabels.get(accession)
|
|
105
|
+
: undefined
|
|
106
|
+
if (label !== undefined) {
|
|
103
107
|
const fontSize = Math.min(h - 2, 11)
|
|
104
108
|
ctx.font = `${fontSize}px sans-serif`
|
|
105
109
|
if (ctx.measureText(label).width + 2 <= lw) {
|
|
110
|
+
labelled!.add(accession)
|
|
106
111
|
ctx.fillStyle = '#222'
|
|
107
112
|
ctx.textAlign = 'center'
|
|
108
113
|
ctx.textBaseline = 'middle'
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clickColor,
|
|
3
|
+
hoverColor,
|
|
4
|
+
multiRowHoverColor,
|
|
5
|
+
referenceColor,
|
|
6
|
+
} from '../overlayColors.ts'
|
|
2
7
|
|
|
3
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
4
9
|
|
|
5
|
-
const hoverColor = 'rgba(0,0,0,0.15)'
|
|
6
|
-
const highlightColor = 'rgba(128,128,0,0.2)'
|
|
7
10
|
// the persistent highlightColumns overlay: a stronger fill plus a solid border
|
|
8
11
|
// so a domain/motif band reads clearly over the colored alignment cells (the
|
|
9
12
|
// faint hover-style wash alone is invisible against clustalx coloring)
|
|
@@ -39,48 +42,50 @@ export function renderMouseover({
|
|
|
39
42
|
ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
|
|
40
43
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
41
44
|
|
|
42
|
-
//
|
|
45
|
+
// rows and columns sit at a fixed pitch, so an index is all it takes to place
|
|
46
|
+
// a full-width or full-height band (matching fillRow in TreeCanvas)
|
|
47
|
+
const rowBand = (index: number) => {
|
|
48
|
+
ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
|
|
49
|
+
}
|
|
50
|
+
const colBand = (index: number) => {
|
|
51
|
+
ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// the reference row (relativeTo) stays lit, as does every tip under a hovered
|
|
55
|
+
// tree node
|
|
43
56
|
if (referenceRowIndex !== undefined) {
|
|
44
57
|
ctx.fillStyle = referenceColor
|
|
45
|
-
|
|
58
|
+
rowBand(referenceRowIndex)
|
|
46
59
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
ctx.fillStyle = multiRowHoverColor
|
|
51
|
-
for (const rowIndex of hoveredRowIndices) {
|
|
52
|
-
ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
|
|
53
|
-
}
|
|
60
|
+
ctx.fillStyle = multiRowHoverColor
|
|
61
|
+
for (const rowIndex of hoveredRowIndices) {
|
|
62
|
+
rowBand(rowIndex)
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
// band so a domain/motif highlight
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ctx.strokeRect(x, 0, w, height)
|
|
67
|
-
}
|
|
65
|
+
// each contiguous run of highlighted columns draws as one filled, bordered
|
|
66
|
+
// band, so a domain/motif highlight stays visible over the alignment
|
|
67
|
+
ctx.lineWidth = 2
|
|
68
|
+
for (const { start, end } of highlightedColumnRuns) {
|
|
69
|
+
const x = start * colWidth + scrollX
|
|
70
|
+
const w = (end - start + 1) * colWidth
|
|
71
|
+
ctx.fillStyle = highlightColumnsFill
|
|
72
|
+
ctx.fillRect(x, 0, w, height)
|
|
73
|
+
ctx.strokeStyle = highlightColumnsBorder
|
|
74
|
+
ctx.strokeRect(x, 0, w, height)
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
ctx.fillStyle = hoverColor
|
|
70
78
|
if (mouseCol !== undefined) {
|
|
71
|
-
|
|
72
|
-
ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
|
|
79
|
+
colBand(mouseCol)
|
|
73
80
|
}
|
|
74
81
|
if (mouseRow !== undefined) {
|
|
75
|
-
|
|
76
|
-
ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
|
|
82
|
+
rowBand(mouseRow)
|
|
77
83
|
}
|
|
84
|
+
ctx.fillStyle = clickColor
|
|
78
85
|
if (mouseClickCol !== undefined) {
|
|
79
|
-
|
|
80
|
-
ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
|
|
86
|
+
colBand(mouseClickCol)
|
|
81
87
|
}
|
|
82
88
|
if (mouseClickRow !== undefined) {
|
|
83
|
-
|
|
84
|
-
ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
|
|
89
|
+
rowBand(mouseClickRow)
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
|
|
|
10
10
|
// while the alignment cells are already colored so a lighter wash reads better.
|
|
11
11
|
export const treeHoverColor = 'rgba(255,165,0,0.2)'
|
|
12
12
|
export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
|
|
13
|
+
|
|
14
|
+
// the hovered column/row, and the one pinned by a click. Drawn on the alignment
|
|
15
|
+
// overlay canvas and again as plain divs over the track area, which have to
|
|
16
|
+
// match or the crosshair appears to change color as it crosses the tracks.
|
|
17
|
+
export const hoverColor = 'rgba(0,0,0,0.15)'
|
|
18
|
+
export const clickColor = 'rgba(128,128,0,0.2)'
|
|
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
22
|
)
|
|
23
23
|
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
24
24
|
|
|
25
|
-
const mouseoverRef = useCanvasAutorun(
|
|
26
|
-
ctx => {
|
|
25
|
+
const mouseoverRef = useCanvasAutorun({
|
|
26
|
+
draw: ctx => {
|
|
27
27
|
if (isAlive(model)) {
|
|
28
28
|
const {
|
|
29
29
|
rowHeight,
|
|
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
width: treeAreaWidth,
|
|
65
|
+
height,
|
|
66
|
+
deps: [model],
|
|
67
|
+
})
|
|
66
68
|
|
|
67
69
|
return (
|
|
68
70
|
<div
|
|
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
|
|
|
60
60
|
const width = treeAreaWidth + padding
|
|
61
61
|
const height = blockSize
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
63
|
+
const canvasWidth = width * highResScaleFactor
|
|
64
|
+
const canvasHeight = height * highResScaleFactor
|
|
65
|
+
const ref = useCanvasAutorun({
|
|
66
|
+
draw: ctx => {
|
|
65
67
|
ctx.resetTransform()
|
|
66
|
-
ctx.clearRect(
|
|
67
|
-
0,
|
|
68
|
-
0,
|
|
69
|
-
width * highResScaleFactor,
|
|
70
|
-
height * highResScaleFactor,
|
|
71
|
-
)
|
|
68
|
+
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
|
72
69
|
renderTreeCanvas({
|
|
73
70
|
ctx,
|
|
74
71
|
model,
|
|
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
|
|
|
77
74
|
theme,
|
|
78
75
|
})
|
|
79
76
|
},
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
width: canvasWidth,
|
|
78
|
+
height: canvasHeight,
|
|
79
|
+
deps: [model, clickMap, offsetY, theme],
|
|
80
|
+
})
|
|
82
81
|
|
|
83
82
|
const style = {
|
|
84
83
|
width,
|
|
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
|
|
|
112
111
|
|
|
113
112
|
<canvas
|
|
114
113
|
ref={ref}
|
|
115
|
-
width={
|
|
116
|
-
height={
|
|
114
|
+
width={canvasWidth}
|
|
115
|
+
height={canvasHeight}
|
|
117
116
|
style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
|
|
118
117
|
onMouseMove={event => {
|
|
119
118
|
onMouseMove(event)
|
|
@@ -45,7 +45,7 @@ export function getNodeX(
|
|
|
45
45
|
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function renderTree({
|
|
49
49
|
offsetY,
|
|
50
50
|
ctx,
|
|
51
51
|
model,
|
|
@@ -89,7 +89,7 @@ export function renderTree({
|
|
|
89
89
|
})
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
function renderCollapsedTriangles({
|
|
93
93
|
ctx,
|
|
94
94
|
clickMap,
|
|
95
95
|
offsetY,
|
|
@@ -98,6 +98,7 @@ export function renderCollapsedTriangles({
|
|
|
98
98
|
tipX,
|
|
99
99
|
maxDepthToLeaf,
|
|
100
100
|
blockSizeYOverride,
|
|
101
|
+
collapsedSet,
|
|
101
102
|
}: {
|
|
102
103
|
ctx: RenderCtx
|
|
103
104
|
clickMap?: ClickMapIndex
|
|
@@ -107,11 +108,11 @@ export function renderCollapsedTriangles({
|
|
|
107
108
|
tipX: number
|
|
108
109
|
maxDepthToLeaf: number
|
|
109
110
|
blockSizeYOverride?: number
|
|
111
|
+
collapsedSet: Set<string>
|
|
110
112
|
}) {
|
|
111
113
|
const {
|
|
112
114
|
hierarchy,
|
|
113
115
|
showBranchLenEffective: showBranchLen,
|
|
114
|
-
collapsed,
|
|
115
116
|
blockSize,
|
|
116
117
|
rowHeight,
|
|
117
118
|
fontSize,
|
|
@@ -119,10 +120,9 @@ export function renderCollapsedTriangles({
|
|
|
119
120
|
} = model
|
|
120
121
|
// nothing collapsed is the common case, and the traversal below visits every
|
|
121
122
|
// node in the tree on every block of every redraw
|
|
122
|
-
if (
|
|
123
|
+
if (collapsedSet.size === 0) {
|
|
123
124
|
return
|
|
124
125
|
}
|
|
125
|
-
const collapsedSet = new Set(collapsed)
|
|
126
126
|
const by = blockSizeYOverride ?? blockSize
|
|
127
127
|
const halfHeight = Math.max(2, rowHeight * 0.42)
|
|
128
128
|
forEachDescendant(hierarchy, node => {
|
|
@@ -171,7 +171,7 @@ export function renderCollapsedTriangles({
|
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
function renderNodeBubbles({
|
|
175
175
|
ctx,
|
|
176
176
|
clickMap,
|
|
177
177
|
offsetY,
|
|
@@ -179,6 +179,7 @@ export function renderNodeBubbles({
|
|
|
179
179
|
tipX,
|
|
180
180
|
maxDepthToLeaf,
|
|
181
181
|
blockSizeYOverride,
|
|
182
|
+
collapsedSet,
|
|
182
183
|
}: {
|
|
183
184
|
ctx: RenderCtx
|
|
184
185
|
clickMap?: ClickMapIndex
|
|
@@ -187,16 +188,15 @@ export function renderNodeBubbles({
|
|
|
187
188
|
tipX: number
|
|
188
189
|
maxDepthToLeaf: number
|
|
189
190
|
blockSizeYOverride?: number
|
|
191
|
+
collapsedSet: Set<string>
|
|
190
192
|
}) {
|
|
191
193
|
const {
|
|
192
194
|
hierarchy,
|
|
193
195
|
showBranchLenEffective: showBranchLen,
|
|
194
|
-
collapsed,
|
|
195
196
|
blockSize,
|
|
196
197
|
marginLeft: ml,
|
|
197
198
|
} = model
|
|
198
199
|
const by = blockSizeYOverride ?? blockSize
|
|
199
|
-
const collapsedSet = new Set(collapsed)
|
|
200
200
|
forEachDescendant(hierarchy, node => {
|
|
201
201
|
const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
202
202
|
if (x === undefined) {
|
|
@@ -231,7 +231,7 @@ export function renderNodeBubbles({
|
|
|
231
231
|
})
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
function renderTreeLabels({
|
|
235
235
|
theme,
|
|
236
236
|
model,
|
|
237
237
|
offsetY,
|
|
@@ -240,6 +240,7 @@ export function renderTreeLabels({
|
|
|
240
240
|
tipX,
|
|
241
241
|
maxDepthToLeaf,
|
|
242
242
|
blockSizeYOverride,
|
|
243
|
+
collapsedSet,
|
|
243
244
|
}: {
|
|
244
245
|
model: MsaViewModel
|
|
245
246
|
offsetY: number
|
|
@@ -249,6 +250,7 @@ export function renderTreeLabels({
|
|
|
249
250
|
tipX: number
|
|
250
251
|
maxDepthToLeaf: number
|
|
251
252
|
blockSizeYOverride?: number
|
|
253
|
+
collapsedSet: Set<string>
|
|
252
254
|
}) {
|
|
253
255
|
const {
|
|
254
256
|
fontSize,
|
|
@@ -262,16 +264,11 @@ export function renderTreeLabels({
|
|
|
262
264
|
marginLeft,
|
|
263
265
|
noTree,
|
|
264
266
|
labelWidthMap,
|
|
265
|
-
collapsed,
|
|
266
267
|
} = model
|
|
268
|
+
const by = blockSizeYOverride ?? blockSize
|
|
267
269
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
268
270
|
// same slice the MSA renderer uses instead of walking every tip per block
|
|
269
|
-
const visibleLeaves = getVisibleLeaves({
|
|
270
|
-
model,
|
|
271
|
-
offsetY,
|
|
272
|
-
blockSizeY: blockSizeYOverride ?? model.blockSize,
|
|
273
|
-
})
|
|
274
|
-
const by = blockSizeYOverride ?? blockSize
|
|
271
|
+
const visibleLeaves = getVisibleLeaves({ model, offsetY, blockSizeY: by })
|
|
275
272
|
const emHeight = ctx.measureText('M').width
|
|
276
273
|
if (labelsAlignRight) {
|
|
277
274
|
ctx.textAlign = 'right'
|
|
@@ -289,7 +286,7 @@ export function renderTreeLabels({
|
|
|
289
286
|
const displayName = treeMetadata[name]?.genome || name
|
|
290
287
|
// a collapsed clade is drawn as a triangle + tip count; suppress its leaf
|
|
291
288
|
// label when the "name" is just the auto-generated internal-node id
|
|
292
|
-
const isAnonymousCollapsed =
|
|
289
|
+
const isAnonymousCollapsed = collapsedSet.has(id) && name === id
|
|
293
290
|
if (!isAnonymousCollapsed && inYBlock(y, offsetY, by)) {
|
|
294
291
|
// note: +rowHeight/4 matches with -rowHeight/4 in msa
|
|
295
292
|
const yp = y + fontSize / 4
|
|
@@ -389,6 +386,9 @@ export function renderTreeCanvas({
|
|
|
389
386
|
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
390
387
|
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
391
388
|
const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
|
|
389
|
+
// built once for all three passes: each of them asks about a different node on
|
|
390
|
+
// every block of every redraw, and `collapsed` is a list
|
|
391
|
+
const collapsedSet = new Set(model.collapsed)
|
|
392
392
|
|
|
393
393
|
if (!noTree && drawTree) {
|
|
394
394
|
renderTree({
|
|
@@ -410,6 +410,7 @@ export function renderTreeCanvas({
|
|
|
410
410
|
tipX,
|
|
411
411
|
maxDepthToLeaf,
|
|
412
412
|
blockSizeYOverride,
|
|
413
|
+
collapsedSet,
|
|
413
414
|
})
|
|
414
415
|
|
|
415
416
|
if (drawNodeBubbles) {
|
|
@@ -421,6 +422,7 @@ export function renderTreeCanvas({
|
|
|
421
422
|
tipX,
|
|
422
423
|
maxDepthToLeaf,
|
|
423
424
|
blockSizeYOverride,
|
|
425
|
+
collapsedSet,
|
|
424
426
|
})
|
|
425
427
|
}
|
|
426
428
|
}
|
|
@@ -435,6 +437,7 @@ export function renderTreeCanvas({
|
|
|
435
437
|
tipX,
|
|
436
438
|
maxDepthToLeaf,
|
|
437
439
|
blockSizeYOverride,
|
|
440
|
+
collapsedSet,
|
|
438
441
|
})
|
|
439
442
|
}
|
|
440
443
|
|