react-msaview 5.0.7 → 5.0.16
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 +135 -35
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/components/Checkbox2.js +3 -6
- package/dist/components/Checkbox2.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +14 -0
- package/dist/components/MSAViewer.js +34 -0
- package/dist/components/MSAViewer.js.map +1 -0
- package/dist/components/SequenceTextArea.js +4 -4
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +5 -24
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/DomainDialog.js +2 -5
- package/dist/components/dialogs/DomainDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +7 -7
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/SettingsDialog.js +3 -19
- package/dist/components/dialogs/SettingsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
- package/dist/components/header/ColorSchemeMenu.js +19 -0
- package/dist/components/header/ColorSchemeMenu.js.map +1 -0
- package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
- package/dist/components/header/FileMenu.js +71 -0
- package/dist/components/header/FileMenu.js.map +1 -0
- package/dist/components/header/Header.js +8 -6
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +3 -145
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.d.ts +6 -0
- package/dist/components/header/MSASettingsMenu.js +36 -0
- package/dist/components/header/MSASettingsMenu.js.map +1 -0
- package/dist/components/header/SettingsMenu.js +1 -21
- package/dist/components/header/SettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
- package/dist/components/header/TreeSettingsMenu.js +74 -0
- package/dist/components/header/TreeSettingsMenu.js.map +1 -0
- package/dist/components/header/ZoomMenu.js +0 -8
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/header/getDomainsMenu.d.ts +31 -0
- package/dist/components/header/getDomainsMenu.js +75 -0
- package/dist/components/header/getDomainsMenu.js.map +1 -0
- package/dist/components/import/ImportFormExamples.js +22 -20
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +13 -84
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +1 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +2 -4
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -7
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +14 -91
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +5 -16
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +55 -22
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +0 -1
- package/dist/fetchUtils.js +0 -4
- package/dist/fetchUtils.js.map +1 -1
- package/dist/flatToTree.d.ts +0 -5
- package/dist/flatToTree.js +13 -30
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.d.ts +0 -5
- package/dist/launchInterProScan.js +5 -3
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model/DataModel.d.ts +9 -0
- package/dist/model/DataModel.js +12 -1
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +3 -0
- package/dist/model/msaModel.js +0 -1
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +3 -6
- package/dist/model/treeModel.js +3 -15
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +24 -77
- package/dist/model.js +118 -239
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +38 -629
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.d.ts +0 -12
- package/dist/parseAsn1.js +125 -332
- package/dist/parseAsn1.js.map +1 -1
- package/dist/useWheelScroll.d.ts +8 -0
- package/dist/useWheelScroll.js +93 -0
- package/dist/useWheelScroll.js.map +1 -0
- package/dist/util.d.ts +1 -6
- package/dist/util.js +5 -34
- package/dist/util.js.map +1 -1
- package/dist/vendor/copyToClipboard.d.ts +1 -10
- package/dist/vendor/copyToClipboard.js +14 -109
- package/dist/vendor/copyToClipboard.js.map +1 -1
- package/dist/vendor/fileSaver.d.ts +1 -11
- package/dist/vendor/fileSaver.js +7 -76
- package/dist/vendor/fileSaver.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 +10 -13
- package/src/collapseLogic.test.ts +115 -0
- package/src/components/Checkbox2.tsx +9 -18
- package/src/components/MSAViewer.tsx +67 -0
- package/src/components/SequenceTextArea.tsx +4 -4
- package/src/components/Track.tsx +10 -26
- package/src/components/dialogs/DomainDialog.tsx +4 -5
- package/src/components/dialogs/InterProScanDialog.tsx +7 -7
- package/src/components/dialogs/SettingsDialog.tsx +0 -37
- package/src/components/header/ColorSchemeMenu.tsx +35 -0
- package/src/components/header/FileMenu.tsx +84 -0
- package/src/components/header/Header.tsx +8 -6
- package/src/components/header/HeaderMenu.tsx +4 -155
- package/src/components/header/MSASettingsMenu.tsx +48 -0
- package/src/components/header/SettingsMenu.tsx +0 -23
- package/src/components/header/TreeSettingsMenu.tsx +96 -0
- package/src/components/header/ZoomMenu.tsx +0 -8
- package/src/components/header/getDomainsMenu.ts +83 -0
- package/src/components/import/ImportFormExamples.tsx +38 -35
- package/src/components/msa/MSACanvas.tsx +21 -91
- package/src/components/msa/MSACanvasBlock.tsx +1 -3
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
- package/src/components/msa/renderMSABlock.ts +2 -5
- package/src/components/msa/renderMSAMouseover.ts +0 -6
- package/src/components/tree/TreeCanvas.tsx +35 -100
- package/src/components/tree/TreeNodeMenu.tsx +5 -14
- package/src/components/tree/renderTreeCanvas.test.ts +205 -0
- package/src/components/tree/renderTreeCanvas.ts +64 -27
- package/src/constants.ts +0 -2
- package/src/fetchUtils.ts +0 -5
- package/src/flatToTree.ts +20 -38
- package/src/hierarchy.test.ts +120 -0
- package/src/hierarchy.ts +221 -0
- package/src/index.ts +2 -0
- package/src/launchInterProScan.ts +4 -3
- package/src/model/DataModel.ts +12 -1
- package/src/model/msaModel.ts +0 -2
- package/src/model/treeModel.ts +2 -18
- package/src/model.ts +180 -278
- package/src/neighborJoining.ts +38 -628
- package/src/parseAsn1.test.ts +4 -1
- package/src/parseAsn1.ts +135 -405
- package/src/useWheelScroll.ts +109 -0
- package/src/util.ts +5 -50
- package/src/vendor/copyToClipboard.ts +14 -122
- package/src/vendor/fileSaver.ts +8 -105
- package/src/version.ts +1 -1
- package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
- package/dist/components/dialogs/AddTrackDialog.js +0 -30
- package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
- package/dist/components/dialogs/TabPanel.d.ts +0 -6
- package/dist/components/dialogs/TabPanel.js +0 -6
- package/dist/components/dialogs/TabPanel.js.map +0 -1
- package/dist/components/header/ZoomStar.js +0 -40
- package/dist/components/header/ZoomStar.js.map +0 -1
- package/dist/layout.d.ts +0 -26
- package/dist/layout.js +0 -74
- package/dist/layout.js.map +0 -1
- package/dist/reparseTree.d.ts +0 -2
- package/dist/reparseTree.js +0 -15
- package/dist/reparseTree.js.map +0 -1
- package/src/components/dialogs/AddTrackDialog.tsx +0 -85
- package/src/components/dialogs/TabPanel.tsx +0 -19
- package/src/components/header/ZoomStar.tsx +0 -74
- package/src/createPaletteMap.test.ts +0 -57
- package/src/layout.ts +0 -118
- package/src/reparseTree.ts +0 -18
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useCallback, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
5
|
import Loading from './Loading.tsx'
|
|
6
6
|
import MSACanvasBlock from './MSACanvasBlock.tsx'
|
|
7
|
+
import { useWheelScroll } from '../../useWheelScroll.ts'
|
|
7
8
|
|
|
8
9
|
import type { MsaViewModel } from '../../model.ts'
|
|
9
10
|
|
|
@@ -17,100 +18,29 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
17
18
|
blocks2d,
|
|
18
19
|
} = model
|
|
19
20
|
const ref = useRef<HTMLDivElement>(null)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!scheduled.current) {
|
|
38
|
-
scheduled.current = true
|
|
39
|
-
requestAnimationFrame(() => {
|
|
40
|
-
model.doScrollX(-deltaX.current)
|
|
41
|
-
model.doScrollY(-deltaY.current)
|
|
42
|
-
deltaX.current = 0
|
|
43
|
-
deltaY.current = 0
|
|
44
|
-
scheduled.current = false
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
event.preventDefault()
|
|
48
|
-
event.stopPropagation()
|
|
49
|
-
}
|
|
50
|
-
curr.addEventListener('wheel', onWheel, { passive: false })
|
|
51
|
-
return () => {
|
|
52
|
-
curr.removeEventListener('wheel', onWheel)
|
|
53
|
-
}
|
|
54
|
-
}, [model])
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (mouseDragging) {
|
|
58
|
-
function globalMouseMove(event: MouseEvent) {
|
|
59
|
-
event.preventDefault()
|
|
60
|
-
const currX = event.clientX
|
|
61
|
-
const currY = event.clientY
|
|
62
|
-
const distanceX = currX - prevX.current
|
|
63
|
-
const distanceY = currY - prevY.current
|
|
64
|
-
if (distanceX || distanceY) {
|
|
65
|
-
if (!scheduled.current) {
|
|
66
|
-
scheduled.current = true
|
|
67
|
-
window.requestAnimationFrame(() => {
|
|
68
|
-
model.doScrollX(distanceX)
|
|
69
|
-
model.doScrollY(distanceY)
|
|
70
|
-
scheduled.current = false
|
|
71
|
-
prevX.current = event.clientX
|
|
72
|
-
prevY.current = event.clientY
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function globalMouseUp() {
|
|
79
|
-
prevX.current = 0
|
|
80
|
-
setMouseDragging(false)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
window.addEventListener('mousemove', globalMouseMove, true)
|
|
84
|
-
window.addEventListener('mouseup', globalMouseUp, true)
|
|
85
|
-
return () => {
|
|
86
|
-
window.removeEventListener('mousemove', globalMouseMove, true)
|
|
87
|
-
window.removeEventListener('mouseup', globalMouseUp, true)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return undefined
|
|
91
|
-
}, [model, mouseDragging])
|
|
21
|
+
const onScrollX = useCallback(
|
|
22
|
+
(d: number) => {
|
|
23
|
+
model.doScrollX(d)
|
|
24
|
+
},
|
|
25
|
+
[model],
|
|
26
|
+
)
|
|
27
|
+
const onScrollY = useCallback(
|
|
28
|
+
(d: number) => {
|
|
29
|
+
model.doScrollY(d)
|
|
30
|
+
},
|
|
31
|
+
[model],
|
|
32
|
+
)
|
|
33
|
+
const { onMouseDown, onMouseUp } = useWheelScroll({
|
|
34
|
+
ref,
|
|
35
|
+
onScrollX,
|
|
36
|
+
onScrollY,
|
|
37
|
+
})
|
|
92
38
|
|
|
93
39
|
return (
|
|
94
40
|
<div
|
|
95
41
|
ref={ref}
|
|
96
|
-
onMouseDown={
|
|
97
|
-
|
|
98
|
-
const target = event.target as HTMLElement
|
|
99
|
-
if (target.draggable || target.dataset.resizer) {
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// otherwise do click and drag scroll
|
|
104
|
-
if (event.button === 0) {
|
|
105
|
-
prevX.current = event.clientX
|
|
106
|
-
prevY.current = event.clientY
|
|
107
|
-
setMouseDragging(true)
|
|
108
|
-
}
|
|
109
|
-
}}
|
|
110
|
-
onMouseUp={event => {
|
|
111
|
-
event.preventDefault()
|
|
112
|
-
setMouseDragging(false)
|
|
113
|
-
}}
|
|
42
|
+
onMouseDown={onMouseDown}
|
|
43
|
+
onMouseUp={onMouseUp}
|
|
114
44
|
onMouseLeave={event => {
|
|
115
45
|
event.preventDefault()
|
|
116
46
|
}}
|
|
@@ -52,8 +52,7 @@ const MSACanvasBlock = observer(function ({
|
|
|
52
52
|
blockSize * highResScaleFactor,
|
|
53
53
|
blockSize * highResScaleFactor,
|
|
54
54
|
)
|
|
55
|
-
|
|
56
|
-
if (actuallyShowDomains) {
|
|
55
|
+
if (model.actuallyShowDomains) {
|
|
57
56
|
renderBoxFeatureCanvasBlock({
|
|
58
57
|
ctx,
|
|
59
58
|
offsetX,
|
|
@@ -102,7 +101,6 @@ const MSACanvasBlock = observer(function ({
|
|
|
102
101
|
if (x >= 0 && x < model.numColumns && y >= 0 && y < model.numRows) {
|
|
103
102
|
model.setMousePos(x, y)
|
|
104
103
|
} else {
|
|
105
|
-
// Clear mouse position when outside bounds
|
|
106
104
|
model.setMousePos(undefined, undefined)
|
|
107
105
|
}
|
|
108
106
|
}}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
1
2
|
import type { MsaViewModel } from '../../model.ts'
|
|
2
3
|
import type { NodeWithIdsAndLength } from '../../types.ts'
|
|
3
|
-
import type { HierarchyNode } from 'd3-hierarchy'
|
|
4
4
|
|
|
5
5
|
export function renderBoxFeatureCanvasBlock({
|
|
6
6
|
model,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
1
2
|
import type { MsaViewModel } from '../../model.ts'
|
|
2
3
|
import type { NodeWithIdsAndLength } from '../../types.ts'
|
|
3
4
|
import type { Theme } from '@mui/material'
|
|
4
|
-
import type { HierarchyNode } from 'd3-hierarchy'
|
|
5
5
|
|
|
6
6
|
export function renderMSABlock({
|
|
7
7
|
model,
|
|
@@ -180,7 +180,6 @@ function drawText({
|
|
|
180
180
|
colorScheme,
|
|
181
181
|
columns,
|
|
182
182
|
colWidth,
|
|
183
|
-
contrastLettering,
|
|
184
183
|
rowHeight,
|
|
185
184
|
relativeTo,
|
|
186
185
|
} = model
|
|
@@ -214,9 +213,7 @@ function drawText({
|
|
|
214
213
|
const displayLetter = isMatchingReference ? '.' : letter
|
|
215
214
|
|
|
216
215
|
const color = colorScheme[letter.toUpperCase()]
|
|
217
|
-
const contrast =
|
|
218
|
-
? contrastScheme[letter.toUpperCase()] || 'black'
|
|
219
|
-
: 'black'
|
|
216
|
+
const contrast = contrastScheme[letter.toUpperCase()] || 'black'
|
|
220
217
|
|
|
221
218
|
// note: -rowHeight/4 matches +rowHeight/4 in tree
|
|
222
219
|
ctx.fillStyle = actuallyShowDomains
|
|
@@ -22,8 +22,6 @@ export function renderMouseover({
|
|
|
22
22
|
scrollX,
|
|
23
23
|
scrollY,
|
|
24
24
|
mouseRow,
|
|
25
|
-
// @ts-expect-error
|
|
26
|
-
mouseCol2,
|
|
27
25
|
mouseClickRow,
|
|
28
26
|
mouseClickCol,
|
|
29
27
|
relativeTo,
|
|
@@ -79,8 +77,4 @@ export function renderMouseover({
|
|
|
79
77
|
ctx.fillStyle = highlightColor
|
|
80
78
|
ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
|
|
81
79
|
}
|
|
82
|
-
if (mouseCol2 !== undefined) {
|
|
83
|
-
ctx.fillStyle = highlightColor
|
|
84
|
-
ctx.fillRect(mouseCol2 * colWidth + scrollX, 0, colWidth, height)
|
|
85
|
-
}
|
|
86
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef
|
|
1
|
+
import React, { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
4
4
|
import { autorun } from 'mobx'
|
|
@@ -6,79 +6,25 @@ import { observer } from 'mobx-react'
|
|
|
6
6
|
|
|
7
7
|
import TreeCanvasBlock from './TreeCanvasBlock.tsx'
|
|
8
8
|
import { padding } from './renderTreeCanvas.ts'
|
|
9
|
+
import { useWheelScroll } from '../../useWheelScroll.ts'
|
|
9
10
|
|
|
10
11
|
import type { MsaViewModel } from '../../model.ts'
|
|
11
12
|
|
|
13
|
+
const referenceColor = 'rgba(0,128,255,0.3)'
|
|
14
|
+
const treeHoverColor = 'rgba(255,165,0,0.2)'
|
|
15
|
+
|
|
12
16
|
const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
13
17
|
const ref = useRef<HTMLDivElement>(null)
|
|
14
18
|
const mouseoverRef = useRef<HTMLCanvasElement>(null)
|
|
15
|
-
const scheduled = useRef(false)
|
|
16
|
-
const deltaY = useRef(0)
|
|
17
|
-
const prevY = useRef(0)
|
|
18
19
|
const { treeWidth, height, blocksY, treeAreaWidth } = model
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function onWheel(event: WheelEvent) {
|
|
27
|
-
deltaY.current += event.deltaY
|
|
28
|
-
|
|
29
|
-
if (!scheduled.current) {
|
|
30
|
-
scheduled.current = true
|
|
31
|
-
requestAnimationFrame(() => {
|
|
32
|
-
model.doScrollY(-deltaY.current)
|
|
33
|
-
deltaY.current = 0
|
|
34
|
-
scheduled.current = false
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
event.preventDefault()
|
|
38
|
-
event.stopPropagation()
|
|
39
|
-
}
|
|
40
|
-
curr.addEventListener('wheel', onWheel, { passive: false })
|
|
41
|
-
return () => {
|
|
42
|
-
curr.removeEventListener('wheel', onWheel)
|
|
43
|
-
}
|
|
44
|
-
}, [model])
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (mouseDragging) {
|
|
48
|
-
function globalMouseMove(event: MouseEvent) {
|
|
49
|
-
event.preventDefault()
|
|
50
|
-
const currY = event.clientY
|
|
51
|
-
const distanceY = currY - prevY.current
|
|
52
|
-
if (distanceY) {
|
|
53
|
-
if (!scheduled.current) {
|
|
54
|
-
scheduled.current = true
|
|
55
|
-
window.requestAnimationFrame(() => {
|
|
56
|
-
model.doScrollY(distanceY)
|
|
57
|
-
scheduled.current = false
|
|
58
|
-
prevY.current = event.clientY
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function globalMouseUp() {
|
|
65
|
-
prevY.current = 0
|
|
66
|
-
setMouseDragging(false)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
window.addEventListener('mousemove', globalMouseMove, true)
|
|
70
|
-
window.addEventListener('mouseup', globalMouseUp, true)
|
|
71
|
-
return () => {
|
|
72
|
-
window.removeEventListener('mousemove', globalMouseMove, true)
|
|
73
|
-
window.removeEventListener('mouseup', globalMouseUp, true)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return undefined
|
|
77
|
-
}, [model, mouseDragging])
|
|
20
|
+
const onScrollY = useCallback(
|
|
21
|
+
(d: number) => {
|
|
22
|
+
model.doScrollY(d)
|
|
23
|
+
},
|
|
24
|
+
[model],
|
|
25
|
+
)
|
|
26
|
+
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
78
27
|
|
|
79
|
-
// Global tree mouseover effect. Only [model] is needed in the dependency
|
|
80
|
-
// array because autorun internally tracks all accessed observables
|
|
81
|
-
// (treeAreaWidth, height, scrollY, etc.) and re-runs when they change
|
|
82
28
|
useEffect(() => {
|
|
83
29
|
const ctx = mouseoverRef.current?.getContext('2d')
|
|
84
30
|
return ctx
|
|
@@ -97,33 +43,38 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
97
43
|
ctx.resetTransform()
|
|
98
44
|
ctx.clearRect(0, 0, w, h)
|
|
99
45
|
|
|
100
|
-
// Highlight reference row (relativeTo) persistently
|
|
101
46
|
if (relativeTo) {
|
|
102
47
|
const referenceLeaf = leaves.find(
|
|
103
48
|
leaf => leaf.data.name === relativeTo,
|
|
104
49
|
)
|
|
105
50
|
if (referenceLeaf) {
|
|
106
|
-
|
|
107
|
-
ctx.
|
|
108
|
-
|
|
51
|
+
ctx.fillStyle = referenceColor
|
|
52
|
+
ctx.fillRect(
|
|
53
|
+
0,
|
|
54
|
+
referenceLeaf.x! + sy - rowHeight / 2,
|
|
55
|
+
w,
|
|
56
|
+
rowHeight,
|
|
57
|
+
)
|
|
109
58
|
}
|
|
110
59
|
}
|
|
111
60
|
|
|
112
|
-
// Highlight multiple rows when hovering over tree nodes
|
|
113
61
|
if (hoveredTreeNode) {
|
|
114
|
-
ctx.fillStyle =
|
|
62
|
+
ctx.fillStyle = treeHoverColor
|
|
115
63
|
for (const descendantName of hoveredTreeNode.descendantNames) {
|
|
116
64
|
const matchingLeaf = leaves.find(
|
|
117
65
|
leaf => leaf.data.name === descendantName,
|
|
118
66
|
)
|
|
119
67
|
if (matchingLeaf) {
|
|
120
|
-
|
|
121
|
-
|
|
68
|
+
ctx.fillRect(
|
|
69
|
+
0,
|
|
70
|
+
matchingLeaf.x! + sy - rowHeight / 2,
|
|
71
|
+
w,
|
|
72
|
+
rowHeight,
|
|
73
|
+
)
|
|
122
74
|
}
|
|
123
75
|
}
|
|
124
76
|
}
|
|
125
77
|
|
|
126
|
-
// Highlight single tree row corresponding to MSA mouseover (if not part of multi-row hover)
|
|
127
78
|
if (
|
|
128
79
|
mouseOverRowName &&
|
|
129
80
|
mouseOverRowName !== relativeTo &&
|
|
@@ -133,9 +84,13 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
133
84
|
leaf => leaf.data.name === mouseOverRowName,
|
|
134
85
|
)
|
|
135
86
|
if (matchingLeaf) {
|
|
136
|
-
|
|
137
|
-
ctx.
|
|
138
|
-
|
|
87
|
+
ctx.fillStyle = treeHoverColor
|
|
88
|
+
ctx.fillRect(
|
|
89
|
+
0,
|
|
90
|
+
matchingLeaf.x! + sy - rowHeight / 2,
|
|
91
|
+
w,
|
|
92
|
+
rowHeight,
|
|
93
|
+
)
|
|
139
94
|
}
|
|
140
95
|
}
|
|
141
96
|
}
|
|
@@ -143,31 +98,11 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
143
98
|
: undefined
|
|
144
99
|
}, [model])
|
|
145
100
|
|
|
146
|
-
function mouseDown(event: React.MouseEvent) {
|
|
147
|
-
// check if clicking a draggable element or a resize handle
|
|
148
|
-
const target = event.target as HTMLElement
|
|
149
|
-
if (target.draggable || target.dataset.resizer) {
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// otherwise do click and drag scroll
|
|
154
|
-
if (event.button === 0) {
|
|
155
|
-
prevY.current = event.clientY
|
|
156
|
-
setMouseDragging(true)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
101
|
return (
|
|
161
102
|
<div
|
|
162
103
|
ref={ref}
|
|
163
|
-
onMouseDown={
|
|
164
|
-
onMouseUp={
|
|
165
|
-
// this local mouseup is used in addition to the global because
|
|
166
|
-
// sometimes the global add/remove are not called in time, resulting in
|
|
167
|
-
// issue #533
|
|
168
|
-
event.preventDefault()
|
|
169
|
-
setMouseDragging(false)
|
|
170
|
-
}}
|
|
104
|
+
onMouseDown={onMouseDown}
|
|
105
|
+
onMouseUp={onMouseUp}
|
|
171
106
|
onMouseLeave={event => {
|
|
172
107
|
event.preventDefault()
|
|
173
108
|
}}
|
|
@@ -24,8 +24,9 @@ const TreeMenu = observer(function ({
|
|
|
24
24
|
model: MsaViewModel
|
|
25
25
|
onClose: () => void
|
|
26
26
|
}) {
|
|
27
|
-
const { collapsed
|
|
28
|
-
const { name } = node
|
|
27
|
+
const { collapsed } = model
|
|
28
|
+
const { name, id } = node
|
|
29
|
+
const isCollapsed = collapsed.includes(id)
|
|
29
30
|
return (
|
|
30
31
|
<Menu
|
|
31
32
|
anchorReference="anchorPosition"
|
|
@@ -62,21 +63,11 @@ const TreeMenu = observer(function ({
|
|
|
62
63
|
<MenuItem
|
|
63
64
|
dense
|
|
64
65
|
onClick={() => {
|
|
65
|
-
|
|
66
|
-
model.toggleCollapsed(node.id)
|
|
67
|
-
} else {
|
|
68
|
-
if (node.id.endsWith('-leafnode')) {
|
|
69
|
-
model.toggleCollapsedLeaf(node.id)
|
|
70
|
-
} else {
|
|
71
|
-
model.toggleCollapsedLeaf(`${node.id}-leafnode`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
66
|
+
model.toggleCollapsed(id)
|
|
74
67
|
onClose()
|
|
75
68
|
}}
|
|
76
69
|
>
|
|
77
|
-
{
|
|
78
|
-
? 'Show node'
|
|
79
|
-
: 'Hide node'}
|
|
70
|
+
{isCollapsed ? 'Show node' : 'Hide node'}
|
|
80
71
|
</MenuItem>
|
|
81
72
|
<MenuItem
|
|
82
73
|
dense
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
function calcDepthToLeaf(node: any): number {
|
|
4
|
+
if (node.depthToLeaf !== undefined) {
|
|
5
|
+
return node.depthToLeaf
|
|
6
|
+
}
|
|
7
|
+
if (!node.children || node.children.length === 0) {
|
|
8
|
+
node.depthToLeaf = 0
|
|
9
|
+
} else {
|
|
10
|
+
let maxDepth = 0
|
|
11
|
+
for (const child of node.children) {
|
|
12
|
+
maxDepth = Math.max(maxDepth, 1 + calcDepthToLeaf(child))
|
|
13
|
+
}
|
|
14
|
+
node.depthToLeaf = maxDepth
|
|
15
|
+
}
|
|
16
|
+
return node.depthToLeaf
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findMaxBranchLen(node: any): number {
|
|
20
|
+
let maxLen = node.len || 0
|
|
21
|
+
if (node.children) {
|
|
22
|
+
for (const child of node.children) {
|
|
23
|
+
maxLen = Math.max(maxLen, findMaxBranchLen(child))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return maxLen
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getNodeX(
|
|
30
|
+
node: any,
|
|
31
|
+
showBranchLen: boolean,
|
|
32
|
+
maxBranchLen: number,
|
|
33
|
+
maxDepthToLeaf: number,
|
|
34
|
+
): number | undefined {
|
|
35
|
+
if (showBranchLen) {
|
|
36
|
+
return node.len
|
|
37
|
+
}
|
|
38
|
+
const depthToLeaf = calcDepthToLeaf(node)
|
|
39
|
+
return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('Tree rendering positioning', () => {
|
|
43
|
+
describe('calcDepthToLeaf', () => {
|
|
44
|
+
it('should return 0 for leaf nodes', () => {
|
|
45
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
46
|
+
expect(calcDepthToLeaf(leaf)).toBe(0)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should return 1 for nodes with only leaf children', () => {
|
|
50
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
51
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
52
|
+
const parent: any = {
|
|
53
|
+
id: 'parent',
|
|
54
|
+
data: { id: 'parent' },
|
|
55
|
+
children: [leaf1, leaf2],
|
|
56
|
+
}
|
|
57
|
+
expect(calcDepthToLeaf(parent)).toBe(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should return correct depth for nested tree', () => {
|
|
61
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
62
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
63
|
+
const intermediate: any = {
|
|
64
|
+
id: 'int',
|
|
65
|
+
data: { id: 'int' },
|
|
66
|
+
children: [leaf1, leaf2],
|
|
67
|
+
}
|
|
68
|
+
const root: any = {
|
|
69
|
+
id: 'root',
|
|
70
|
+
data: { id: 'root' },
|
|
71
|
+
children: [intermediate],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(calcDepthToLeaf(leaf1)).toBe(0)
|
|
75
|
+
expect(calcDepthToLeaf(intermediate)).toBe(1)
|
|
76
|
+
expect(calcDepthToLeaf(root)).toBe(2)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should cache result', () => {
|
|
80
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
81
|
+
const depth1 = calcDepthToLeaf(leaf)
|
|
82
|
+
const depth2 = calcDepthToLeaf(leaf)
|
|
83
|
+
expect(depth1).toBe(depth2)
|
|
84
|
+
expect(leaf.depthToLeaf).toBeDefined()
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('findMaxBranchLen', () => {
|
|
89
|
+
it('should return node len for leaf', () => {
|
|
90
|
+
const leaf: any = {
|
|
91
|
+
id: 'leaf',
|
|
92
|
+
data: { id: 'leaf' },
|
|
93
|
+
len: 1.5,
|
|
94
|
+
children: null,
|
|
95
|
+
}
|
|
96
|
+
expect(findMaxBranchLen(leaf)).toBe(1.5)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should return max len from descendants', () => {
|
|
100
|
+
const leaf1: any = {
|
|
101
|
+
id: 'leaf1',
|
|
102
|
+
data: { id: 'leaf1' },
|
|
103
|
+
len: 0.5,
|
|
104
|
+
children: null,
|
|
105
|
+
}
|
|
106
|
+
const leaf2: any = {
|
|
107
|
+
id: 'leaf2',
|
|
108
|
+
data: { id: 'leaf2' },
|
|
109
|
+
len: 1.5,
|
|
110
|
+
children: null,
|
|
111
|
+
}
|
|
112
|
+
const parent: any = {
|
|
113
|
+
id: 'parent',
|
|
114
|
+
data: { id: 'parent' },
|
|
115
|
+
len: 0.3,
|
|
116
|
+
children: [leaf1, leaf2],
|
|
117
|
+
}
|
|
118
|
+
expect(findMaxBranchLen(parent)).toBe(1.5)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should handle undefined len', () => {
|
|
122
|
+
const leaf: any = { id: 'leaf', data: { id: 'leaf' }, children: null }
|
|
123
|
+
expect(findMaxBranchLen(leaf)).toBe(0)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('getNodeX cladogram positioning', () => {
|
|
128
|
+
it('should position all leaves at rightmost for cladogram', () => {
|
|
129
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
130
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
131
|
+
const root: any = {
|
|
132
|
+
id: 'root',
|
|
133
|
+
data: { id: 'root' },
|
|
134
|
+
children: [leaf1, leaf2],
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
calcDepthToLeaf(root)
|
|
138
|
+
const maxBranchLen = 100
|
|
139
|
+
const maxDepthToLeaf = 1
|
|
140
|
+
|
|
141
|
+
const x1 = getNodeX(leaf1, false, maxBranchLen, maxDepthToLeaf)
|
|
142
|
+
const x2 = getNodeX(leaf2, false, maxBranchLen, maxDepthToLeaf)
|
|
143
|
+
|
|
144
|
+
expect(x1).toBe(maxBranchLen)
|
|
145
|
+
expect(x2).toBe(maxBranchLen)
|
|
146
|
+
expect(x1).toBe(x2)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should position root at leftmost for cladogram', () => {
|
|
150
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
151
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
152
|
+
const root: any = {
|
|
153
|
+
id: 'root',
|
|
154
|
+
data: { id: 'root' },
|
|
155
|
+
children: [leaf1, leaf2],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
calcDepthToLeaf(root)
|
|
159
|
+
const maxBranchLen = 100
|
|
160
|
+
const maxDepthToLeaf = 1
|
|
161
|
+
|
|
162
|
+
const xRoot = getNodeX(root, false, maxBranchLen, maxDepthToLeaf)
|
|
163
|
+
expect(xRoot).toBe(0)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should position internal nodes between root and leaves', () => {
|
|
167
|
+
const leaf1: any = { id: 'leaf1', data: { id: 'leaf1' }, children: null }
|
|
168
|
+
const leaf2: any = { id: 'leaf2', data: { id: 'leaf2' }, children: null }
|
|
169
|
+
const intermediate: any = {
|
|
170
|
+
id: 'int',
|
|
171
|
+
data: { id: 'int' },
|
|
172
|
+
children: [leaf1, leaf2],
|
|
173
|
+
}
|
|
174
|
+
const root: any = {
|
|
175
|
+
id: 'root',
|
|
176
|
+
data: { id: 'root' },
|
|
177
|
+
children: [intermediate],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
calcDepthToLeaf(root)
|
|
181
|
+
const maxBranchLen = 100
|
|
182
|
+
const maxDepthToLeaf = 2
|
|
183
|
+
|
|
184
|
+
const xRoot = getNodeX(root, false, maxBranchLen, maxDepthToLeaf)!
|
|
185
|
+
const xInt = getNodeX(intermediate, false, maxBranchLen, maxDepthToLeaf)!
|
|
186
|
+
const xLeaf = getNodeX(leaf1, false, maxBranchLen, maxDepthToLeaf)!
|
|
187
|
+
|
|
188
|
+
expect(xRoot).toBe(0)
|
|
189
|
+
expect(xInt).toBeGreaterThan(xRoot)
|
|
190
|
+
expect(xLeaf).toBeGreaterThan(xInt)
|
|
191
|
+
expect(xLeaf).toBe(maxBranchLen)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should use branch length when showBranchLen is true', () => {
|
|
195
|
+
const leaf: any = {
|
|
196
|
+
id: 'leaf',
|
|
197
|
+
data: { id: 'leaf' },
|
|
198
|
+
len: 2.5,
|
|
199
|
+
children: null,
|
|
200
|
+
}
|
|
201
|
+
const x = getNodeX(leaf, true, 100, 1)
|
|
202
|
+
expect(x).toBe(2.5)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
})
|