react-msaview 8.4.1 → 9.1.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/README.md +19 -10
- package/bundle/index.js +74 -74
- package/bundle/index.js.map +4 -4
- package/dist/colorSchemes.js +1 -3
- package/dist/colorSchemes.js.map +1 -1
- package/dist/columnCounts.d.ts +0 -1
- package/dist/columnCounts.js +1 -1
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/Loading.js +5 -5
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +2 -2
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +1892 -7
- package/dist/components/MSAViewer.js +144 -202
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.js +4 -3
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/Track.js +3 -2
- package/dist/components/Track.js.map +1 -1
- package/dist/components/controls.d.ts +11 -0
- package/dist/components/controls.js +48 -0
- package/dist/components/controls.js.map +1 -0
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +5 -7
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/HelpDialog.d.ts +6 -0
- package/dist/components/dialogs/HelpDialog.js +30 -0
- package/dist/components/dialogs/HelpDialog.js.map +1 -0
- package/dist/components/header/FileMenu.js +33 -7
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/Header.js +4 -6
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -32
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +28 -44
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/settingsMenuItems.d.ts +1 -0
- package/dist/components/header/settingsMenuItems.js +48 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/data/seq2.d.ts +0 -1
- package/dist/components/import/data/seq2.js +0 -17
- package/dist/components/import/data/seq2.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +2 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +2 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
- package/dist/components/msa/drawFeatureSpans.js +99 -56
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +14 -1
- package/dist/components/msa/msaRaster.js +19 -9
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +2 -2
- package/dist/components/msa/renderHighlights.js +2 -3
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +2 -1
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
- package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
- package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -53
- package/dist/components/tracks/drawTracks.js +5 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/CladeLabels.d.ts +1 -1
- package/dist/components/tree/CladeLabels.js +22 -34
- package/dist/components/tree/CladeLabels.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +4 -4
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -4
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeOverview.js +10 -5
- package/dist/components/tree/TreeOverview.js.map +1 -1
- package/dist/components/tree/TreePanel.js +6 -2
- package/dist/components/tree/TreePanel.js.map +1 -1
- package/dist/components/tree/TreeRuler.js +2 -2
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/cladeBrackets.d.ts +22 -17
- package/dist/components/tree/cladeBrackets.js +40 -9
- package/dist/components/tree/cladeBrackets.js.map +1 -1
- package/dist/components/tree/clickMap.d.ts +2 -0
- package/dist/components/tree/clickMap.js.map +1 -1
- package/dist/components/tree/ellipsize.d.ts +8 -0
- package/dist/components/tree/ellipsize.js +20 -0
- package/dist/components/tree/ellipsize.js.map +1 -0
- package/dist/components/tree/renderTreeCanvas.js +30 -32
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeMouseover.js +1 -1
- package/dist/components/tree/renderTreeMouseover.js.map +1 -1
- package/dist/components/tree/renderTreeOverview.js +4 -11
- package/dist/components/tree/renderTreeOverview.js.map +1 -1
- package/dist/constants.d.ts +6 -3
- package/dist/constants.js +16 -6
- package/dist/constants.js.map +1 -1
- package/dist/element.js +3 -6
- package/dist/element.js.map +1 -1
- package/dist/exportLayout.d.ts +39 -0
- package/dist/exportLayout.js +78 -0
- package/dist/exportLayout.js.map +1 -0
- package/dist/headlessRenderEnv.d.ts +12 -39
- package/dist/headlessRenderEnv.js +18 -12
- package/dist/headlessRenderEnv.js.map +1 -1
- package/dist/hierarchy.d.ts +5 -8
- package/dist/hierarchy.js +5 -79
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/measureTextCanvas.d.ts +7 -0
- package/dist/measureTextCanvas.js +10 -8
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/treeModel.d.ts +14 -25
- package/dist/model/treeModel.js +16 -30
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +108 -79
- package/dist/model.js +258 -209
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.d.ts +2 -7
- package/dist/renderToSvg.js +49 -135
- package/dist/renderToSvg.js.map +1 -1
- package/dist/svgTestUtil.d.ts +48 -48
- package/dist/tipLabels.d.ts +8 -0
- package/dist/tipLabels.js +46 -0
- package/dist/tipLabels.js.map +1 -0
- package/dist/types.d.ts +0 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewerSnapshot.d.ts +169 -0
- package/dist/viewerSnapshot.js +100 -0
- package/dist/viewerSnapshot.js.map +1 -0
- package/package.json +3 -3
- package/src/colorSchemes.ts +1 -4
- package/src/columnCounts.ts +1 -1
- package/src/components/Loading.tsx +19 -12
- package/src/components/MSAView.tsx +2 -2
- package/src/components/MSAViewer.tsx +276 -256
- package/src/components/ResizeHandles.tsx +10 -3
- package/src/components/Track.tsx +3 -2
- package/src/components/controls.ts +61 -0
- package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
- package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
- package/src/components/dialogs/HelpDialog.tsx +79 -0
- package/src/components/header/FileMenu.tsx +33 -7
- package/src/components/header/Header.tsx +4 -6
- package/src/components/header/TreeSettingsMenu.tsx +2 -32
- package/src/components/header/ZoomControls.tsx +30 -66
- package/src/components/header/settingsMenuItems.ts +67 -0
- package/src/components/import/data/seq2.ts +0 -18
- package/src/components/msa/MSACanvas.tsx +2 -1
- package/src/components/msa/MSACanvasBlock.tsx +2 -6
- package/src/components/msa/drawFeatureSpans.ts +160 -79
- package/src/components/msa/msaRaster.ts +27 -9
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
- package/src/components/msa/renderHighlights.ts +2 -3
- package/src/components/msa/renderMSAMouseover.ts +5 -1
- package/src/components/renderCtx.ts +0 -1
- package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
- package/src/components/tracks/drawTracks.ts +5 -5
- package/src/components/tree/CladeLabels.tsx +30 -54
- package/src/components/tree/TreeCanvas.tsx +10 -4
- package/src/components/tree/TreeCanvasBlock.tsx +4 -4
- package/src/components/tree/TreeOverview.tsx +10 -5
- package/src/components/tree/TreePanel.tsx +8 -2
- package/src/components/tree/TreeRuler.tsx +7 -3
- package/src/components/tree/cladeBrackets.ts +59 -19
- package/src/components/tree/clickMap.ts +2 -0
- package/src/components/tree/ellipsize.ts +24 -0
- package/src/components/tree/renderTreeCanvas.ts +37 -36
- package/src/components/tree/renderTreeMouseover.ts +1 -1
- package/src/components/tree/renderTreeOverview.ts +7 -14
- package/src/constants.ts +17 -6
- package/src/element.ts +3 -7
- package/src/exportLayout.ts +134 -0
- package/src/headlessRenderEnv.ts +24 -11
- package/src/hierarchy.ts +5 -88
- package/src/index.ts +5 -5
- package/src/measureTextCanvas.ts +10 -8
- package/src/model/treeModel.ts +17 -34
- package/src/model.ts +296 -243
- package/src/renderToSvg.tsx +72 -209
- package/src/tipLabels.ts +51 -0
- package/src/types.ts +0 -1
- package/src/version.ts +1 -1
- package/src/viewerSnapshot.ts +147 -0
- package/dist/components/header/ZoomMenu.d.ts +0 -6
- package/dist/components/header/ZoomMenu.js +0 -37
- package/dist/components/header/ZoomMenu.js.map +0 -1
- package/src/components/header/ZoomMenu.tsx +0 -48
|
@@ -3,13 +3,7 @@ import React from 'react'
|
|
|
3
3
|
import { useTheme } from '@mui/material'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
bracketGap,
|
|
8
|
-
cladeHeight,
|
|
9
|
-
cladeInGutter,
|
|
10
|
-
cladeLabelHorizontal,
|
|
11
|
-
cladeLabelOffset,
|
|
12
|
-
} from './cladeBrackets.ts'
|
|
6
|
+
import { cladeLabelBoxes } from './cladeBrackets.ts'
|
|
13
7
|
|
|
14
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
15
9
|
|
|
@@ -20,56 +14,38 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
20
14
|
* the same labels with a `rotate(-90)`.
|
|
21
15
|
*/
|
|
22
16
|
const CladeLabels = observer(function ({ model }: { model: MsaViewModel }) {
|
|
23
|
-
const {
|
|
24
|
-
resolvedClades,
|
|
25
|
-
cladeGutterWidth,
|
|
26
|
-
rowHeight,
|
|
27
|
-
treeAreaWidth,
|
|
28
|
-
fontSize,
|
|
29
|
-
} = model
|
|
30
17
|
const theme = useTheme()
|
|
31
|
-
|
|
32
|
-
return null
|
|
33
|
-
}
|
|
34
|
-
const gutterLeft = treeAreaWidth - cladeGutterWidth
|
|
18
|
+
const { fontSize } = model
|
|
35
19
|
return (
|
|
36
20
|
<>
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
? {}
|
|
66
|
-
: { writingMode: 'vertical-rl', transform: 'rotate(180deg)' }),
|
|
67
|
-
}}
|
|
68
|
-
>
|
|
69
|
-
{label}
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
})}
|
|
21
|
+
{cladeLabelBoxes(model).map(box => (
|
|
22
|
+
<div
|
|
23
|
+
key={box.key}
|
|
24
|
+
title={box.label}
|
|
25
|
+
style={{
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
left: box.left,
|
|
28
|
+
top: box.top,
|
|
29
|
+
width: box.width,
|
|
30
|
+
height: box.height,
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
color: box.color ?? theme.palette.text.primary,
|
|
34
|
+
fontSize,
|
|
35
|
+
overflow: 'hidden',
|
|
36
|
+
textOverflow: 'ellipsis',
|
|
37
|
+
whiteSpace: 'nowrap',
|
|
38
|
+
pointerEvents: 'none',
|
|
39
|
+
// vertical-rl reads top to bottom, and the half turn makes it
|
|
40
|
+
// read upwards, which is what rotate(-90) exports
|
|
41
|
+
...(box.horizontal
|
|
42
|
+
? {}
|
|
43
|
+
: { writingMode: 'vertical-rl', transform: 'rotate(180deg)' }),
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{box.label}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
73
49
|
</>
|
|
74
50
|
)
|
|
75
51
|
})
|
|
@@ -13,7 +13,13 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
13
13
|
|
|
14
14
|
const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
15
15
|
const ref = useRef<HTMLDivElement>(null)
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
height,
|
|
18
|
+
blocksY,
|
|
19
|
+
effectiveTreeAreaWidth,
|
|
20
|
+
scrollY,
|
|
21
|
+
highResScaleFactor,
|
|
22
|
+
} = model
|
|
17
23
|
const onScrollY = useCallback(
|
|
18
24
|
(d: number) => {
|
|
19
25
|
model.doScrollY(d)
|
|
@@ -22,7 +28,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
22
28
|
)
|
|
23
29
|
const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
|
|
24
30
|
|
|
25
|
-
const canvasWidth =
|
|
31
|
+
const canvasWidth = effectiveTreeAreaWidth * highResScaleFactor
|
|
26
32
|
const canvasHeight = height * highResScaleFactor
|
|
27
33
|
const mouseoverRef = useCanvasAutorun({
|
|
28
34
|
draw: ctx => {
|
|
@@ -43,7 +49,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
43
49
|
style={{
|
|
44
50
|
height,
|
|
45
51
|
position: 'relative',
|
|
46
|
-
width:
|
|
52
|
+
width: effectiveTreeAreaWidth,
|
|
47
53
|
}}
|
|
48
54
|
>
|
|
49
55
|
{/* one transform for the whole block set, matching MSACanvas: the tree
|
|
@@ -70,7 +76,7 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
70
76
|
position: 'absolute',
|
|
71
77
|
top: 0,
|
|
72
78
|
left: 0,
|
|
73
|
-
width:
|
|
79
|
+
width: effectiveTreeAreaWidth,
|
|
74
80
|
height,
|
|
75
81
|
zIndex: 1000,
|
|
76
82
|
pointerEvents: 'none',
|
|
@@ -37,9 +37,9 @@ const TreeCanvasBlock = observer(function ({
|
|
|
37
37
|
)
|
|
38
38
|
const hovered = anchor?.value
|
|
39
39
|
|
|
40
|
-
const {
|
|
41
|
-
// TreePanel clips to
|
|
42
|
-
const width =
|
|
40
|
+
const { effectiveTreeAreaWidth, blockSize, highResScaleFactor } = model
|
|
41
|
+
// TreePanel clips to the tree area, so a wider canvas is wasted backing store
|
|
42
|
+
const width = effectiveTreeAreaWidth
|
|
43
43
|
const height = blockSize
|
|
44
44
|
|
|
45
45
|
const canvasWidth = width * highResScaleFactor
|
|
@@ -121,7 +121,7 @@ const TreeCanvasBlock = observer(function ({
|
|
|
121
121
|
|
|
122
122
|
{anchor ? (
|
|
123
123
|
<BaseTooltip clientPoint={anchor.clientPoint}>
|
|
124
|
-
{anchor.value.name}
|
|
124
|
+
{anchor.value.label ?? anchor.value.name}
|
|
125
125
|
</BaseTooltip>
|
|
126
126
|
) : null}
|
|
127
127
|
</>
|
|
@@ -21,10 +21,10 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
21
21
|
* moving across it costs are a blit and two boxes.
|
|
22
22
|
*/
|
|
23
23
|
const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
24
|
-
const {
|
|
24
|
+
const { effectiveTreeAreaWidth, overviewHeight, highResScaleFactor } = model
|
|
25
25
|
const theme = useTheme()
|
|
26
26
|
const [hover, setHover] = useState<{ id: string; rows: [number, number] }>()
|
|
27
|
-
const width = Math.round(
|
|
27
|
+
const width = Math.round(effectiveTreeAreaWidth * highResScaleFactor)
|
|
28
28
|
const height = Math.round(overviewHeight * highResScaleFactor)
|
|
29
29
|
|
|
30
30
|
const ref = useCanvasAutorun({
|
|
@@ -34,12 +34,17 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
34
34
|
const image = treeOverviewImage({
|
|
35
35
|
model,
|
|
36
36
|
theme,
|
|
37
|
-
width:
|
|
37
|
+
width: effectiveTreeAreaWidth,
|
|
38
38
|
height: overviewHeight,
|
|
39
39
|
scale: highResScaleFactor,
|
|
40
40
|
})
|
|
41
41
|
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
42
|
-
const box = {
|
|
42
|
+
const box = {
|
|
43
|
+
model,
|
|
44
|
+
ctx,
|
|
45
|
+
width: effectiveTreeAreaWidth,
|
|
46
|
+
height: overviewHeight,
|
|
47
|
+
}
|
|
43
48
|
if (image) {
|
|
44
49
|
ctx.drawImage(
|
|
45
50
|
image,
|
|
@@ -86,7 +91,7 @@ const TreeOverview = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
86
91
|
display: 'block',
|
|
87
92
|
flexShrink: 0,
|
|
88
93
|
cursor: 'pointer',
|
|
89
|
-
width:
|
|
94
|
+
width: effectiveTreeAreaWidth,
|
|
90
95
|
height: overviewHeight,
|
|
91
96
|
}}
|
|
92
97
|
onMouseMove={event => {
|
|
@@ -7,9 +7,15 @@ import TreeCanvas from './TreeCanvas.tsx'
|
|
|
7
7
|
import type { MsaViewModel } from '../../model.ts'
|
|
8
8
|
|
|
9
9
|
const TreePanel = observer(function ({ model }: { model: MsaViewModel }) {
|
|
10
|
-
const {
|
|
10
|
+
const { effectiveTreeAreaWidth } = model
|
|
11
11
|
return (
|
|
12
|
-
<div
|
|
12
|
+
<div
|
|
13
|
+
style={{
|
|
14
|
+
overflow: 'hidden',
|
|
15
|
+
flexShrink: 0,
|
|
16
|
+
width: effectiveTreeAreaWidth,
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
13
19
|
<TreeCanvas model={model} />
|
|
14
20
|
</div>
|
|
15
21
|
)
|
|
@@ -13,13 +13,17 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
13
13
|
// scale bar: without one the branch lengths are drawn to a scale nothing
|
|
14
14
|
// states.
|
|
15
15
|
const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
|
|
16
|
-
const {
|
|
16
|
+
const { effectiveTreeAreaWidth, marginLeft, treeScaleBar: bar } = model
|
|
17
17
|
const theme = useTheme()
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<div style={{ flexShrink: 0, width:
|
|
20
|
+
<div style={{ flexShrink: 0, width: effectiveTreeAreaWidth }}>
|
|
21
21
|
{bar ? (
|
|
22
|
-
<svg
|
|
22
|
+
<svg
|
|
23
|
+
width={effectiveTreeAreaWidth}
|
|
24
|
+
height={h}
|
|
25
|
+
style={{ display: 'block' }}
|
|
26
|
+
>
|
|
23
27
|
<ScaleBarSVG
|
|
24
28
|
bar={bar}
|
|
25
29
|
marginLeft={marginLeft}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { labelFitFontSize } from '../../constants.ts'
|
|
1
2
|
import { measureTextCanvas } from '../../measureTextCanvas.ts'
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
5
|
+
import type { Clade, ResolvedClade } from '../../types.ts'
|
|
4
6
|
|
|
5
7
|
// The bar of the bracket mark and the space around it, in px.
|
|
6
8
|
export const bracketBarWidth = 3
|
|
@@ -11,14 +13,14 @@ export const bracketGap = 4
|
|
|
11
13
|
export const maxCladeGutterWidth = 140
|
|
12
14
|
|
|
13
15
|
/** whether a clade draws in the gutter: a bracket, or a highlight with a label */
|
|
14
|
-
export function cladeInGutter(clade:
|
|
16
|
+
export function cladeInGutter(clade: Pick<Clade, 'mark' | 'label'>) {
|
|
15
17
|
return (
|
|
16
18
|
clade.mark === 'bracket' || (clade.mark === 'highlight' && !!clade.label)
|
|
17
19
|
)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** the pixel height of the rows a clade covers */
|
|
21
|
-
|
|
23
|
+
function cladeHeight(clade: ResolvedClade, rowHeight: number) {
|
|
22
24
|
return (clade.rows[1] - clade.rows[0] + 1) * rowHeight
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ export function cladeHeight(clade: ResolvedClade, rowHeight: number) {
|
|
|
26
28
|
* Whether a label reads across the rows. It does where they are taller than the
|
|
27
29
|
* font, and runs up the bar where they are not.
|
|
28
30
|
*/
|
|
29
|
-
|
|
31
|
+
function cladeLabelHorizontal(
|
|
30
32
|
clade: ResolvedClade,
|
|
31
33
|
rowHeight: number,
|
|
32
34
|
fontSize: number,
|
|
@@ -35,7 +37,7 @@ export function cladeLabelHorizontal(
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** the px between the left edge of the gutter and the label's own column */
|
|
38
|
-
|
|
40
|
+
function cladeLabelOffset(clade: Pick<Clade, 'mark'>) {
|
|
39
41
|
return clade.mark === 'bracket'
|
|
40
42
|
? bracketGap + bracketBarWidth + bracketGap
|
|
41
43
|
: bracketGap
|
|
@@ -43,27 +45,19 @@ export function cladeLabelOffset(clade: ResolvedClade) {
|
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* The gutter the bracket mark needs at the right of the tree area: the bar plus
|
|
46
|
-
* the widest label
|
|
47
|
-
* most maxCladeGutterWidth.
|
|
48
|
+
* the widest label read across at labelFitFontSize, zero where no clade draws
|
|
49
|
+
* there, and at most maxCladeGutterWidth. The records size it, so a zoom, a
|
|
50
|
+
* collapse or a focus leaves it as it is, and a renderer clips a label that
|
|
51
|
+
* outgrows it.
|
|
48
52
|
*/
|
|
49
|
-
export function cladeGutterWidth({
|
|
50
|
-
clades,
|
|
51
|
-
rowHeight,
|
|
52
|
-
fontSize,
|
|
53
|
-
}: {
|
|
54
|
-
clades: ResolvedClade[]
|
|
55
|
-
rowHeight: number
|
|
56
|
-
fontSize: number
|
|
57
|
-
}) {
|
|
53
|
+
export function cladeGutterWidth(clades: Pick<Clade, 'mark' | 'label'>[]) {
|
|
58
54
|
let widest = 0
|
|
59
55
|
for (const clade of clades) {
|
|
60
56
|
if (!cladeInGutter(clade)) {
|
|
61
57
|
continue
|
|
62
58
|
}
|
|
63
59
|
const text = clade.label
|
|
64
|
-
?
|
|
65
|
-
? measureTextCanvas(clade.label, fontSize)
|
|
66
|
-
: fontSize
|
|
60
|
+
? measureTextCanvas(clade.label, labelFitFontSize)
|
|
67
61
|
: 0
|
|
68
62
|
widest = Math.max(widest, cladeLabelOffset(clade) + text + bracketGap)
|
|
69
63
|
}
|
|
@@ -71,3 +65,49 @@ export function cladeGutterWidth({
|
|
|
71
65
|
// the measurement here sized the gutter for
|
|
72
66
|
return Math.min(maxCladeGutterWidth, Math.ceil(widest))
|
|
73
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The box each gutter label takes, in unscrolled tree-area px: from the
|
|
71
|
+
* label's column to the right edge of the gutter, across the clade's rows. The
|
|
72
|
+
* live view sets DOM text in it, and the SVG export a `<text>`.
|
|
73
|
+
*/
|
|
74
|
+
export function cladeLabelBoxes(
|
|
75
|
+
model: Pick<
|
|
76
|
+
MsaViewModel,
|
|
77
|
+
| 'resolvedClades'
|
|
78
|
+
| 'cladeGutterWidth'
|
|
79
|
+
| 'rowHeight'
|
|
80
|
+
| 'effectiveTreeAreaWidth'
|
|
81
|
+
| 'fontSize'
|
|
82
|
+
>,
|
|
83
|
+
) {
|
|
84
|
+
const {
|
|
85
|
+
resolvedClades,
|
|
86
|
+
cladeGutterWidth,
|
|
87
|
+
rowHeight,
|
|
88
|
+
effectiveTreeAreaWidth,
|
|
89
|
+
fontSize,
|
|
90
|
+
} = model
|
|
91
|
+
const gutterLeft = effectiveTreeAreaWidth - cladeGutterWidth
|
|
92
|
+
return cladeGutterWidth === 0
|
|
93
|
+
? []
|
|
94
|
+
: resolvedClades.flatMap((clade, index) => {
|
|
95
|
+
const { label, markColor, rows } = clade
|
|
96
|
+
if (!label || !cladeInGutter(clade)) {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
const left = gutterLeft + cladeLabelOffset(clade)
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
key: `${rows[0]}-${rows[1]}-${index}`,
|
|
103
|
+
label,
|
|
104
|
+
color: markColor,
|
|
105
|
+
left,
|
|
106
|
+
top: rows[0] * rowHeight,
|
|
107
|
+
width: effectiveTreeAreaWidth - left - bracketGap,
|
|
108
|
+
height: cladeHeight(clade, rowHeight),
|
|
109
|
+
horizontal: cladeLabelHorizontal(clade, rowHeight, fontSize),
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const ellipsis = '…'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* the longest prefix of `text`, plus an ellipsis, that measures at most
|
|
5
|
+
* `room` px
|
|
6
|
+
*/
|
|
7
|
+
export function ellipsize(
|
|
8
|
+
measure: (text: string) => number,
|
|
9
|
+
text: string,
|
|
10
|
+
room: number,
|
|
11
|
+
) {
|
|
12
|
+
const width = measure(text)
|
|
13
|
+
if (width <= room) {
|
|
14
|
+
return { text, width }
|
|
15
|
+
}
|
|
16
|
+
for (let n = Math.floor((text.length * room) / width); n > 0; n--) {
|
|
17
|
+
const truncated = text.slice(0, n).trimEnd() + ellipsis
|
|
18
|
+
const truncatedWidth = measure(truncated)
|
|
19
|
+
if (truncatedWidth <= room) {
|
|
20
|
+
return { text: truncated, width: truncatedWidth }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { text: '', width: 0 }
|
|
24
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '../msa/renderHighlights.ts'
|
|
9
9
|
import { referenceColor } from '../overlayColors.ts'
|
|
10
10
|
import { bracketBarWidth, bracketGap } from './cladeBrackets.ts'
|
|
11
|
+
import { ellipsize } from './ellipsize.ts'
|
|
11
12
|
|
|
12
13
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
13
14
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -35,7 +36,7 @@ function inYBlock(y: number, offsetY: number, by: number, pad: number) {
|
|
|
35
36
|
/**
|
|
36
37
|
* Every node whose subtree reaches into the block, skipping the subtrees that
|
|
37
38
|
* do not. A subtree's leaves are one contiguous run of rows, recorded by
|
|
38
|
-
*
|
|
39
|
+
* layoutTree as xMin/xMax, so a block of a 200k-tip tree visits a few dozen
|
|
39
40
|
* nodes.
|
|
40
41
|
*
|
|
41
42
|
* A link out of a visited node is drawn even when its child is pruned, so a
|
|
@@ -382,9 +383,8 @@ function renderTreeLabels({
|
|
|
382
383
|
treeAreaWidthMinusMargin,
|
|
383
384
|
marginLeft,
|
|
384
385
|
noTree,
|
|
385
|
-
labelWidthMap,
|
|
386
|
-
labelWidthScale,
|
|
387
386
|
} = model
|
|
387
|
+
const treeDrawn = drawTree && !noTree
|
|
388
388
|
const by = blockSizeYOverride ?? blockSize
|
|
389
389
|
// labels only exist for leaves, which are laid out top to bottom, so take the
|
|
390
390
|
// same slice the MSA renderer uses instead of walking every tip per block
|
|
@@ -412,22 +412,22 @@ function renderTreeLabels({
|
|
|
412
412
|
// fontSize/4 below the row center, the baseline the alignment letters
|
|
413
413
|
// share
|
|
414
414
|
const yp = y + fontSize / 4
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const width
|
|
422
|
-
measured === undefined
|
|
423
|
-
? ctx.measureText(displayName).width
|
|
424
|
-
: measured * labelWidthScale
|
|
415
|
+
const xp = treeDrawn
|
|
416
|
+
? (getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0)
|
|
417
|
+
: 0
|
|
418
|
+
const fit = (room: number) =>
|
|
419
|
+
ellipsize(text => ctx.measureText(text).width, displayName, room)
|
|
420
|
+
// a label cut to nothing still gets a click box the font size wide
|
|
421
|
+
const boxWidth = (width: number) => Math.max(width, fontSize)
|
|
425
422
|
|
|
426
423
|
ctx.fillStyle = tipLabelColors?.get(name) ?? theme.palette.text.primary
|
|
427
424
|
if (labelsAlignRight) {
|
|
428
425
|
const smallPadding = 2
|
|
429
426
|
const offset = treeAreaWidthMinusMargin - smallPadding
|
|
430
|
-
|
|
427
|
+
const { text, width } = fit(
|
|
428
|
+
offset - (treeDrawn ? xp + radius + 2 + smallPadding : 0),
|
|
429
|
+
)
|
|
430
|
+
if (treeDrawn) {
|
|
431
431
|
// beginPath is load-bearing: without it every leader line re-strokes
|
|
432
432
|
// all the ones before it, which is quadratic in the label count
|
|
433
433
|
ctx.beginPath()
|
|
@@ -435,28 +435,31 @@ function renderTreeLabels({
|
|
|
435
435
|
ctx.lineTo(offset - smallPadding - width, y)
|
|
436
436
|
ctx.stroke()
|
|
437
437
|
}
|
|
438
|
-
ctx.fillText(
|
|
438
|
+
ctx.fillText(text, offset, yp)
|
|
439
439
|
// everything here is drawn under translate(marginLeft), so the click box
|
|
440
440
|
// has to be the text's own right edge in that same space -- taking
|
|
441
|
-
//
|
|
441
|
+
// the tree area width instead put the target smallPadding off the label
|
|
442
442
|
const labelRight = offset + marginLeft
|
|
443
443
|
clickMap?.insert({
|
|
444
|
-
minX: labelRight - width,
|
|
444
|
+
minX: labelRight - boxWidth(width),
|
|
445
445
|
maxX: labelRight,
|
|
446
446
|
minY: yp - emHeight,
|
|
447
447
|
maxY: yp,
|
|
448
448
|
name,
|
|
449
|
+
label: displayName,
|
|
449
450
|
id,
|
|
450
451
|
})
|
|
451
452
|
} else {
|
|
452
|
-
const labelX =
|
|
453
|
-
|
|
453
|
+
const labelX = treeDrawn ? xp + d : 2
|
|
454
|
+
const { text, width } = fit(treeAreaWidthMinusMargin - labelX)
|
|
455
|
+
ctx.fillText(text, labelX, yp)
|
|
454
456
|
clickMap?.insert({
|
|
455
457
|
minX: labelX + marginLeft,
|
|
456
|
-
maxX: labelX + width + marginLeft,
|
|
458
|
+
maxX: labelX + boxWidth(width) + marginLeft,
|
|
457
459
|
minY: yp - emHeight,
|
|
458
460
|
maxY: yp,
|
|
459
461
|
name,
|
|
462
|
+
label: displayName,
|
|
460
463
|
id,
|
|
461
464
|
})
|
|
462
465
|
}
|
|
@@ -502,12 +505,12 @@ function renderCladeHighlights({
|
|
|
502
505
|
hierarchy,
|
|
503
506
|
showBranchLenEffective: showBranchLen,
|
|
504
507
|
rowHeight,
|
|
505
|
-
|
|
508
|
+
effectiveTreeAreaWidth,
|
|
506
509
|
marginLeft,
|
|
507
510
|
blockSize,
|
|
508
511
|
} = model
|
|
509
512
|
const by = blockSizeYOverride ?? blockSize
|
|
510
|
-
const right =
|
|
513
|
+
const right = effectiveTreeAreaWidth - marginLeft
|
|
511
514
|
for (const { rows, color, mark } of resolvedClades) {
|
|
512
515
|
if (mark !== 'highlight') {
|
|
513
516
|
continue
|
|
@@ -548,7 +551,7 @@ function renderCladeBrackets({
|
|
|
548
551
|
resolvedClades,
|
|
549
552
|
cladeGutterWidth,
|
|
550
553
|
rowHeight,
|
|
551
|
-
|
|
554
|
+
effectiveTreeAreaWidth,
|
|
552
555
|
marginLeft,
|
|
553
556
|
blockSize,
|
|
554
557
|
} = model
|
|
@@ -557,7 +560,7 @@ function renderCladeBrackets({
|
|
|
557
560
|
}
|
|
558
561
|
const by = blockSizeYOverride ?? blockSize
|
|
559
562
|
const x =
|
|
560
|
-
|
|
563
|
+
effectiveTreeAreaWidth -
|
|
561
564
|
marginLeft -
|
|
562
565
|
cladeGutterWidth +
|
|
563
566
|
bracketGap +
|
|
@@ -600,7 +603,7 @@ function renderRowHighlights({
|
|
|
600
603
|
rowTints,
|
|
601
604
|
referenceRowIndex,
|
|
602
605
|
rowHeight,
|
|
603
|
-
|
|
606
|
+
effectiveTreeAreaWidth,
|
|
604
607
|
marginLeft,
|
|
605
608
|
blockSize,
|
|
606
609
|
} = model
|
|
@@ -608,7 +611,12 @@ function renderRowHighlights({
|
|
|
608
611
|
const pad = blockPad(model)
|
|
609
612
|
const placed: LabelBox[] = []
|
|
610
613
|
const fillRow = (index: number) => {
|
|
611
|
-
ctx.fillRect(
|
|
614
|
+
ctx.fillRect(
|
|
615
|
+
-marginLeft,
|
|
616
|
+
index * rowHeight,
|
|
617
|
+
effectiveTreeAreaWidth,
|
|
618
|
+
rowHeight,
|
|
619
|
+
)
|
|
612
620
|
}
|
|
613
621
|
if (rowTints) {
|
|
614
622
|
const top = Math.max(0, Math.floor((offsetY - pad) / rowHeight))
|
|
@@ -647,8 +655,8 @@ function renderRowHighlights({
|
|
|
647
655
|
label,
|
|
648
656
|
x: -marginLeft + 2,
|
|
649
657
|
y: first * rowHeight,
|
|
650
|
-
spanWidth:
|
|
651
|
-
bounds: { min: -marginLeft, max:
|
|
658
|
+
spanWidth: effectiveTreeAreaWidth,
|
|
659
|
+
bounds: { min: -marginLeft, max: effectiveTreeAreaWidth - marginLeft },
|
|
652
660
|
placed,
|
|
653
661
|
})
|
|
654
662
|
}
|
|
@@ -698,14 +706,7 @@ export function renderTreeCanvas({
|
|
|
698
706
|
|
|
699
707
|
// memoized on the model and shared across the tree/bubble/label passes (and
|
|
700
708
|
// across all tree blocks) rather than re-traversing the hierarchy in each
|
|
701
|
-
const {
|
|
702
|
-
model
|
|
703
|
-
// pixel x of the tips. A phylogram scales branch lengths so the longest
|
|
704
|
-
// root-to-tip path lands at maxBranchLength; a cladogram ignores lengths and
|
|
705
|
-
// spreads topological depth across the whole tree area instead. Reusing
|
|
706
|
-
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
707
|
-
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
708
|
-
const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
|
|
709
|
+
const { maxDepthToLeaf, effectiveTreeWidth: tipX } = model
|
|
709
710
|
// built once for all three passes: each of them asks about a different node on
|
|
710
711
|
// every block of every redraw, and `collapsed` is a list
|
|
711
712
|
const collapsedSet = new Set(model.collapsed)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { descendants } from '../../hierarchy.ts'
|
|
2
|
+
import { makeOffscreenCanvas } from '../msa/msaRaster.ts'
|
|
2
3
|
import { getNodeX } from './renderTreeCanvas.ts'
|
|
3
4
|
|
|
4
5
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -62,10 +63,10 @@ export function drawTreeOverview({
|
|
|
62
63
|
continue
|
|
63
64
|
}
|
|
64
65
|
const sx = nodeX(source)
|
|
65
|
-
const sy = rowY(source.x
|
|
66
|
+
const sy = rowY(source.x)
|
|
66
67
|
for (const target of source.children) {
|
|
67
68
|
const tx = nodeX(target)
|
|
68
|
-
const ty = rowY(target.x
|
|
69
|
+
const ty = rowY(target.x)
|
|
69
70
|
if (Math.abs(tx - sx) < 1 && Math.abs(ty - sy) < 1) {
|
|
70
71
|
continue
|
|
71
72
|
}
|
|
@@ -158,17 +159,8 @@ function overviewKeys({
|
|
|
158
159
|
]
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
typeof OffscreenCanvas !== 'undefined'
|
|
164
|
-
? new OffscreenCanvas(width, height)
|
|
165
|
-
: typeof document !== 'undefined'
|
|
166
|
-
? Object.assign(document.createElement('canvas'), { width, height })
|
|
167
|
-
: undefined
|
|
168
|
-
const ctx = canvas?.getContext('2d')
|
|
169
|
-
return ctx && typeof (ctx as CanvasRenderingContext2D).stroke === 'function'
|
|
170
|
-
? { canvas: canvas as RasterCanvas, ctx: ctx as CanvasRenderingContext2D }
|
|
171
|
-
: undefined
|
|
162
|
+
function canStroke(ctx: unknown): ctx is CanvasRenderingContext2D {
|
|
163
|
+
return typeof (ctx as CanvasRenderingContext2D | null)?.stroke === 'function'
|
|
172
164
|
}
|
|
173
165
|
|
|
174
166
|
/**
|
|
@@ -198,9 +190,10 @@ export function treeOverviewImage({
|
|
|
198
190
|
if (prev && keys.every((key, i) => key === prev.keys[i])) {
|
|
199
191
|
return prev.canvas
|
|
200
192
|
}
|
|
201
|
-
const made =
|
|
193
|
+
const made = makeOffscreenCanvas(
|
|
202
194
|
Math.max(1, Math.round(width * scale)),
|
|
203
195
|
Math.max(1, Math.round(height * scale)),
|
|
196
|
+
canStroke,
|
|
204
197
|
)
|
|
205
198
|
if (made) {
|
|
206
199
|
made.ctx.scale(scale, scale)
|