react-msaview 9.0.0 → 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/MSAViewer.d.ts +1891 -4
- package/dist/components/MSAViewer.js +144 -201
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/ResizeHandles.js +3 -2
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/Track.js +2 -1
- 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 +44 -0
- 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/cladeBrackets.d.ts +17 -10
- package/dist/components/tree/cladeBrackets.js +33 -3
- package/dist/components/tree/cladeBrackets.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -8
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeOverview.js +4 -11
- package/dist/components/tree/renderTreeOverview.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 +8 -6
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/treeModel.d.ts +0 -5
- package/dist/model/treeModel.js +0 -7
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +38 -44
- package/dist/model.js +153 -112
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.d.ts +2 -7
- package/dist/renderToSvg.js +32 -120
- package/dist/renderToSvg.js.map +1 -1
- package/dist/svgTestUtil.d.ts +12 -20
- 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/MSAViewer.tsx +275 -251
- package/src/components/ResizeHandles.tsx +9 -2
- package/src/components/Track.tsx +2 -1
- 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 +59 -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/cladeBrackets.ts +50 -3
- package/src/components/tree/renderTreeCanvas.ts +2 -13
- package/src/components/tree/renderTreeOverview.ts +7 -14
- 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 +8 -6
- package/src/model/treeModel.ts +0 -7
- package/src/model.ts +170 -122
- package/src/renderToSvg.tsx +55 -194
- 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
|
-
effectiveTreeAreaWidth,
|
|
28
|
-
fontSize,
|
|
29
|
-
} = model
|
|
30
17
|
const theme = useTheme()
|
|
31
|
-
|
|
32
|
-
return null
|
|
33
|
-
}
|
|
34
|
-
const gutterLeft = effectiveTreeAreaWidth - 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
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { labelFitFontSize } from '../../constants.ts'
|
|
2
2
|
import { measureTextCanvas } from '../../measureTextCanvas.ts'
|
|
3
3
|
|
|
4
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
4
5
|
import type { Clade, ResolvedClade } from '../../types.ts'
|
|
5
6
|
|
|
6
7
|
// The bar of the bracket mark and the space around it, in px.
|
|
@@ -19,7 +20,7 @@ export function cladeInGutter(clade: Pick<Clade, 'mark' | 'label'>) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** the pixel height of the rows a clade covers */
|
|
22
|
-
|
|
23
|
+
function cladeHeight(clade: ResolvedClade, rowHeight: number) {
|
|
23
24
|
return (clade.rows[1] - clade.rows[0] + 1) * rowHeight
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -27,7 +28,7 @@ export function cladeHeight(clade: ResolvedClade, rowHeight: number) {
|
|
|
27
28
|
* Whether a label reads across the rows. It does where they are taller than the
|
|
28
29
|
* font, and runs up the bar where they are not.
|
|
29
30
|
*/
|
|
30
|
-
|
|
31
|
+
function cladeLabelHorizontal(
|
|
31
32
|
clade: ResolvedClade,
|
|
32
33
|
rowHeight: number,
|
|
33
34
|
fontSize: number,
|
|
@@ -36,7 +37,7 @@ export function cladeLabelHorizontal(
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** the px between the left edge of the gutter and the label's own column */
|
|
39
|
-
|
|
40
|
+
function cladeLabelOffset(clade: Pick<Clade, 'mark'>) {
|
|
40
41
|
return clade.mark === 'bracket'
|
|
41
42
|
? bracketGap + bracketBarWidth + bracketGap
|
|
42
43
|
: bracketGap
|
|
@@ -64,3 +65,49 @@ export function cladeGutterWidth(clades: Pick<Clade, 'mark' | 'label'>[]) {
|
|
|
64
65
|
// the measurement here sized the gutter for
|
|
65
66
|
return Math.min(maxCladeGutterWidth, Math.ceil(widest))
|
|
66
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
|
+
}
|
|
@@ -36,7 +36,7 @@ function inYBlock(y: number, offsetY: number, by: number, pad: number) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Every node whose subtree reaches into the block, skipping the subtrees that
|
|
38
38
|
* do not. A subtree's leaves are one contiguous run of rows, recorded by
|
|
39
|
-
*
|
|
39
|
+
* layoutTree as xMin/xMax, so a block of a 200k-tip tree visits a few dozen
|
|
40
40
|
* nodes.
|
|
41
41
|
*
|
|
42
42
|
* A link out of a visited node is drawn even when its child is pruned, so a
|
|
@@ -706,18 +706,7 @@ export function renderTreeCanvas({
|
|
|
706
706
|
|
|
707
707
|
// memoized on the model and shared across the tree/bubble/label passes (and
|
|
708
708
|
// across all tree blocks) rather than re-traversing the hierarchy in each
|
|
709
|
-
const {
|
|
710
|
-
maxBranchLength,
|
|
711
|
-
maxDepthToLeaf,
|
|
712
|
-
showBranchLenEffective,
|
|
713
|
-
effectiveTreeWidth,
|
|
714
|
-
} = model
|
|
715
|
-
// pixel x of the tips. A phylogram scales branch lengths so the longest
|
|
716
|
-
// root-to-tip path lands at maxBranchLength; a cladogram ignores lengths and
|
|
717
|
-
// spreads topological depth across the whole tree area instead. Reusing
|
|
718
|
-
// maxBranchLength for the cladogram would pin every node to x=0 for a tree
|
|
719
|
-
// with no branch lengths -- which is exactly what forces cladogram mode.
|
|
720
|
-
const tipX = showBranchLenEffective ? maxBranchLength : effectiveTreeWidth
|
|
709
|
+
const { maxDepthToLeaf, effectiveTreeWidth: tipX } = model
|
|
721
710
|
// built once for all three passes: each of them asks about a different node on
|
|
722
711
|
// every block of every redraw, and `collapsed` is a list
|
|
723
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)
|
package/src/element.ts
CHANGED
|
@@ -49,10 +49,6 @@ const ownAttributes = [
|
|
|
49
49
|
// the attributes a <nightingale-manager> writes on the elements it holds
|
|
50
50
|
const nightingaleAttributes = ['display-start', 'display-end', 'highlight']
|
|
51
51
|
|
|
52
|
-
function location(uri: string | null) {
|
|
53
|
-
return uri ? { uri, locationType: 'UriLocation' as const } : undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
function numberAttribute(value: string | null | undefined) {
|
|
57
53
|
const n = value ? Number(value) : Number.NaN
|
|
58
54
|
return Number.isFinite(n) ? n : undefined
|
|
@@ -188,9 +184,9 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
|
|
|
188
184
|
const theme = this.getAttribute('theme')
|
|
189
185
|
return {
|
|
190
186
|
...this.#data,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
msaUrl: this.getAttribute('msa-url') ?? undefined,
|
|
188
|
+
treeUrl: this.getAttribute('tree-url') ?? undefined,
|
|
189
|
+
gffUrl: this.getAttribute('gff-url') ?? undefined,
|
|
194
190
|
colorScheme:
|
|
195
191
|
this.#data.colorScheme ??
|
|
196
192
|
this.getAttribute('color-scheme') ??
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { legendRows } from './components/msa/legendRows.ts'
|
|
2
|
+
import { treeScaleBarHeight } from './constants.ts'
|
|
3
|
+
|
|
4
|
+
import type { LegendRow } from './components/msa/legendRows.ts'
|
|
5
|
+
import type { MsaViewModel } from './model.ts'
|
|
6
|
+
import type { Theme } from '@mui/material'
|
|
7
|
+
|
|
8
|
+
export interface ExportSvgOptions {
|
|
9
|
+
theme: Theme
|
|
10
|
+
includeMinimap?: boolean
|
|
11
|
+
includeTracks?: boolean
|
|
12
|
+
exportType: 'entire' | 'viewport'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// legend geometry, shared by the width calculation and the renderer so the
|
|
16
|
+
// reserved column on the right exactly fits the drawn legends
|
|
17
|
+
export const LEGEND_PAD = 8
|
|
18
|
+
export const LEGEND_ROW_H = 16
|
|
19
|
+
export const LEGEND_SWATCH = 12
|
|
20
|
+
export const LEGEND_FONT = 12
|
|
21
|
+
export const LEGEND_CHAR_W = 7
|
|
22
|
+
|
|
23
|
+
export const LEGEND_MAX_W = 360
|
|
24
|
+
export const LEGEND_TEXT_X = LEGEND_PAD + LEGEND_SWATCH + 6
|
|
25
|
+
|
|
26
|
+
export function getLegendWidth(rows: LegendRow[]) {
|
|
27
|
+
const maxLen = rows.reduce((a, r) => Math.max(a, r.label.length), 0)
|
|
28
|
+
const w = LEGEND_TEXT_X + LEGEND_PAD + maxLen * LEGEND_CHAR_W
|
|
29
|
+
return Math.min(LEGEND_MAX_W, Math.max(120, w))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function legendHeight(rows: LegendRow[]) {
|
|
33
|
+
return LEGEND_PAD * 2 + rows.length * LEGEND_ROW_H
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// resolved sizes/offsets (in svg user units) for the chosen export, shared by
|
|
37
|
+
// every layer; 'entire' renders the whole alignment unscrolled, 'viewport'
|
|
38
|
+
// mirrors the live scroll position
|
|
39
|
+
export interface Layout {
|
|
40
|
+
width: number
|
|
41
|
+
msaAreaWidth: number
|
|
42
|
+
height: number
|
|
43
|
+
contentHeight: number
|
|
44
|
+
trackHeight: number
|
|
45
|
+
// the band the minimap, the row panel headers and the tree overview stacked
|
|
46
|
+
// on the scale bar share across the top
|
|
47
|
+
topHeight: number
|
|
48
|
+
offsetX: number
|
|
49
|
+
offsetY: number
|
|
50
|
+
includeMinimap: boolean
|
|
51
|
+
legendWidth: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getLayout(model: MsaViewModel, opts: ExportSvgOptions): Layout {
|
|
55
|
+
const {
|
|
56
|
+
scrollX,
|
|
57
|
+
scrollY,
|
|
58
|
+
totalWidth,
|
|
59
|
+
totalHeight,
|
|
60
|
+
effectiveTreeAreaWidth,
|
|
61
|
+
rowPanelsWidth,
|
|
62
|
+
rowPanelsHeaderHeight,
|
|
63
|
+
totalTrackAreaHeight,
|
|
64
|
+
minimapHeight,
|
|
65
|
+
msaAreaHeight,
|
|
66
|
+
msaCanvasWidth,
|
|
67
|
+
showHorizontalScrollbar,
|
|
68
|
+
treeOverviewHeight,
|
|
69
|
+
treeScaleBar,
|
|
70
|
+
} = model
|
|
71
|
+
const trackHeight = opts.includeTracks ? totalTrackAreaHeight : 0
|
|
72
|
+
// the minimap reflects the live viewport scroll position, so it's only
|
|
73
|
+
// meaningful for a viewport export, never for the entire alignment -- and
|
|
74
|
+
// only when the live view shows one at all, since a minimap over an alignment
|
|
75
|
+
// that already fits marks a viewport wider than the bar
|
|
76
|
+
const includeMinimap =
|
|
77
|
+
opts.exportType === 'viewport' &&
|
|
78
|
+
!!opts.includeMinimap &&
|
|
79
|
+
showHorizontalScrollbar
|
|
80
|
+
const rows = legendRows(model.legends)
|
|
81
|
+
const legendWidth = rows.length > 0 ? getLegendWidth(rows) : 0
|
|
82
|
+
const topHeight = Math.max(
|
|
83
|
+
includeMinimap ? minimapHeight : 0,
|
|
84
|
+
rowPanelsHeaderHeight,
|
|
85
|
+
treeOverviewHeight + (treeScaleBar ? treeScaleBarHeight : 0),
|
|
86
|
+
)
|
|
87
|
+
const gutterWidth = effectiveTreeAreaWidth + rowPanelsWidth
|
|
88
|
+
|
|
89
|
+
// width stays content-only; the legend occupies an extra column added at the
|
|
90
|
+
// svg root in MsaSvg. The viewport export takes the alignment canvas's own
|
|
91
|
+
// size rather than the widget's -- the widget box also covers the header, the
|
|
92
|
+
// resize handle and the scrollbars, and rendering that box exports rows and
|
|
93
|
+
// columns the scrollbars hide on screen
|
|
94
|
+
return opts.exportType === 'entire'
|
|
95
|
+
? {
|
|
96
|
+
width: totalWidth + gutterWidth,
|
|
97
|
+
msaAreaWidth: totalWidth,
|
|
98
|
+
height: totalHeight + trackHeight + topHeight,
|
|
99
|
+
contentHeight: totalHeight,
|
|
100
|
+
trackHeight,
|
|
101
|
+
topHeight,
|
|
102
|
+
offsetX: 0,
|
|
103
|
+
offsetY: 0,
|
|
104
|
+
includeMinimap,
|
|
105
|
+
legendWidth,
|
|
106
|
+
}
|
|
107
|
+
: {
|
|
108
|
+
width: msaCanvasWidth + gutterWidth,
|
|
109
|
+
msaAreaWidth: msaCanvasWidth,
|
|
110
|
+
height: msaAreaHeight + topHeight + trackHeight,
|
|
111
|
+
contentHeight: msaAreaHeight,
|
|
112
|
+
trackHeight,
|
|
113
|
+
topHeight,
|
|
114
|
+
offsetX: -scrollX,
|
|
115
|
+
offsetY: -scrollY,
|
|
116
|
+
includeMinimap,
|
|
117
|
+
legendWidth,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* the exported page's size: the layout plus the legend column, and as tall as
|
|
123
|
+
* the legend where the key outgrows a short alignment
|
|
124
|
+
*/
|
|
125
|
+
export function exportPageSize(model: MsaViewModel, layout: Layout) {
|
|
126
|
+
const { width, height, legendWidth, topHeight } = layout
|
|
127
|
+
return {
|
|
128
|
+
width: width + legendWidth,
|
|
129
|
+
height:
|
|
130
|
+
legendWidth > 0
|
|
131
|
+
? Math.max(height, topHeight + legendHeight(legendRows(model.legends)))
|
|
132
|
+
: height,
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/headlessRenderEnv.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* leaves the rest undefined -- every emitted transform then carries `NaN` in its
|
|
13
13
|
* x components, which no assertion on a drawn x-position can survive.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
import { CHAR_WIDTH_RATIO } from './measureTextCanvas.ts'
|
|
16
|
+
|
|
17
|
+
class HeadlessDOMMatrix {
|
|
16
18
|
a: number
|
|
17
19
|
b: number
|
|
18
20
|
c: number
|
|
@@ -50,7 +52,7 @@ export class HeadlessDOMMatrix {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
class HeadlessDOMPoint {
|
|
54
56
|
constructor(
|
|
55
57
|
public x = 0,
|
|
56
58
|
public y = 0,
|
|
@@ -69,10 +71,11 @@ export class HeadlessDOMPoint {
|
|
|
69
71
|
* its own jsdom passes that window instead, because Node has no global
|
|
70
72
|
* HTMLCanvasElement to patch.
|
|
71
73
|
*
|
|
72
|
-
* `measureText` reports 0.6em per character, close
|
|
73
|
-
* average for layout and
|
|
74
|
-
* compute the width a renderer will see
|
|
75
|
-
*
|
|
74
|
+
* Without `measureContext`, `measureText` reports 0.6em per character, close
|
|
75
|
+
* enough to a real sans-serif average for layout and exactly predictable: a
|
|
76
|
+
* test can compute the width a renderer will see. A caller with a real canvas,
|
|
77
|
+
* such as react-msaview-cli with @napi-rs/canvas, passes one of its 2D
|
|
78
|
+
* contexts as `measureContext` to measure with the installed fonts.
|
|
76
79
|
*
|
|
77
80
|
* `contextExtras` adds to the context a test hands out -- the raster test
|
|
78
81
|
* supplies the ImageData methods that make the export take its image path.
|
|
@@ -80,6 +83,7 @@ export class HeadlessDOMPoint {
|
|
|
80
83
|
export function installHeadlessRenderEnv(
|
|
81
84
|
win: unknown = globalThis,
|
|
82
85
|
contextExtras: Partial<CanvasRenderingContext2D> = {},
|
|
86
|
+
measureContext?: MeasureContext,
|
|
83
87
|
) {
|
|
84
88
|
const g = globalThis as Record<string, unknown>
|
|
85
89
|
g.DOMMatrix = HeadlessDOMMatrix
|
|
@@ -96,14 +100,23 @@ export function installHeadlessRenderEnv(
|
|
|
96
100
|
set font(v: string) {
|
|
97
101
|
font = v
|
|
98
102
|
},
|
|
99
|
-
measureText: (t: string) =>
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
measureText: (t: string) => {
|
|
104
|
+
if (measureContext) {
|
|
105
|
+
measureContext.font = font
|
|
106
|
+
return { width: measureContext.measureText(t).width }
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
width:
|
|
110
|
+
t.length * (Number.parseFloat(font) || 10) * CHAR_WIDTH_RATIO,
|
|
111
|
+
}
|
|
112
|
+
},
|
|
102
113
|
},
|
|
103
114
|
contextExtras,
|
|
104
115
|
) as unknown as CanvasRenderingContext2D
|
|
105
116
|
} as unknown as typeof HTMLCanvasElement.prototype.getContext
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
export interface MeasureContext {
|
|
120
|
+
font: string
|
|
121
|
+
measureText(text: string): { width: number }
|
|
122
|
+
}
|
package/src/hierarchy.ts
CHANGED
|
@@ -61,56 +61,6 @@ export function hierarchy<T>(
|
|
|
61
61
|
return coreHierarchy(data, childrenAccessor)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function clusterLayout<T>(
|
|
65
|
-
root: HierarchyNode<T>,
|
|
66
|
-
sizeX: number,
|
|
67
|
-
sizeY: number,
|
|
68
|
-
) {
|
|
69
|
-
const leafNodes = leaves(root)
|
|
70
|
-
const n = leafNodes.length
|
|
71
|
-
const step = sizeX / n
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < n; i++) {
|
|
74
|
-
leafNodes[i]!.x = (i + 0.5) * step
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// x of an internal node is the mean of its children's x, so process in
|
|
78
|
-
// post-order (children before parents). The subtree span comes from the same
|
|
79
|
-
// pass, for the block culls in the tree renderer.
|
|
80
|
-
const nodes = descendants(root)
|
|
81
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
82
|
-
const node = nodes[i]!
|
|
83
|
-
if (node.children) {
|
|
84
|
-
let sum = 0
|
|
85
|
-
let min = Infinity
|
|
86
|
-
let max = -Infinity
|
|
87
|
-
for (const child of node.children) {
|
|
88
|
-
sum += child.x!
|
|
89
|
-
min = Math.min(min, child.xMin!)
|
|
90
|
-
max = Math.max(max, child.xMax!)
|
|
91
|
-
}
|
|
92
|
-
node.x = sum / node.children.length
|
|
93
|
-
node.xMin = min
|
|
94
|
-
node.xMax = max
|
|
95
|
-
} else {
|
|
96
|
-
node.xMin = node.x
|
|
97
|
-
node.xMax = node.x
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const rootHeight = root.height
|
|
102
|
-
const stack = [{ node: root, depth: 0 }]
|
|
103
|
-
while (stack.length > 0) {
|
|
104
|
-
const { node, depth } = stack.pop()!
|
|
105
|
-
node.y = rootHeight === 0 ? sizeY : (depth / rootHeight) * sizeY
|
|
106
|
-
if (node.children) {
|
|
107
|
-
for (const child of node.children) {
|
|
108
|
-
stack.push({ node: child, depth: depth + 1 })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
64
|
export interface TreeScale {
|
|
115
65
|
readonly rowHeight: number
|
|
116
66
|
readonly treeWidth: number
|
|
@@ -186,10 +136,10 @@ export class LaidOutNode<T> implements HierarchyNode<T> {
|
|
|
186
136
|
}
|
|
187
137
|
|
|
188
138
|
/**
|
|
189
|
-
* The
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
139
|
+
* The tree laid out on new nodes: tips one row apart, a parent at the mean of
|
|
140
|
+
* its children, and branch length measured from the root, whose own branch is
|
|
141
|
+
* not drawn. A collapsed node records the farthest tip of its hidden subtree,
|
|
142
|
+
* where the base of its triangle goes.
|
|
193
143
|
*/
|
|
194
144
|
export function layoutTree<T extends { length?: number }>(
|
|
195
145
|
root: HierarchyNode<T>,
|
|
@@ -329,39 +279,6 @@ export function calcDepthToLeaf<T>(node: HierarchyNode<T>): number {
|
|
|
329
279
|
return node.depthToLeaf!
|
|
330
280
|
}
|
|
331
281
|
|
|
332
|
-
// Max root-to-leaf sum of branch lengths within the subtree at d. Negative
|
|
333
|
-
// lengths are floored at zero, the same as setBrLength does when it lays the
|
|
334
|
-
// tree out, so the extent this reports is the one that gets drawn
|
|
335
|
-
export function maxLength(d: HierarchyNode): number {
|
|
336
|
-
const nodes = descendants(d)
|
|
337
|
-
const pathLen = new Map<HierarchyNode, number>()
|
|
338
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
339
|
-
const n = nodes[i]!
|
|
340
|
-
let childMax = 0
|
|
341
|
-
if (n.children) {
|
|
342
|
-
for (const child of n.children) {
|
|
343
|
-
childMax = Math.max(childMax, pathLen.get(child)!)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
pathLen.set(n, Math.max(n.data.length || 0, 0) + childMax)
|
|
347
|
-
}
|
|
348
|
-
return pathLen.get(d)!
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function setBrLength(d: HierarchyNode, y0: number, k: number) {
|
|
352
|
-
const stack = [{ node: d, y0 }]
|
|
353
|
-
while (stack.length > 0) {
|
|
354
|
-
const { node, y0: acc } = stack.pop()!
|
|
355
|
-
const next = acc + Math.max(node.data.length || 0, 0)
|
|
356
|
-
node.len = next * k
|
|
357
|
-
if (node.children) {
|
|
358
|
-
for (const child of node.children) {
|
|
359
|
-
stack.push({ node: child, y0: next })
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
282
|
/**
|
|
366
283
|
* Every leaf by name, with a name two leaves share mapping to undefined: a tip
|
|
367
284
|
* set naming it cannot say which tip it means.
|
|
@@ -416,7 +333,7 @@ export function mrca<T extends { name: string }>(
|
|
|
416
333
|
|
|
417
334
|
/**
|
|
418
335
|
* The deepest node whose subtree covers a run of rows, given in the row-space
|
|
419
|
-
* coordinates
|
|
336
|
+
* coordinates layoutTree writes to xMin/xMax. A subtree's leaves are one
|
|
420
337
|
* contiguous run, so at most one child per level covers the run and the descent
|
|
421
338
|
* visits the tree's depth rather than its nodes.
|
|
422
339
|
*/
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,8 @@ export { renderToSvg } from './renderToSvg.tsx'
|
|
|
11
11
|
export type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
12
12
|
// renderToSvg outside a browser needs DOM bits jsdom omits; react-msaview-cli
|
|
13
13
|
// and the README figure generator both drive it that way
|
|
14
|
-
export {
|
|
15
|
-
|
|
16
|
-
installHeadlessRenderEnv,
|
|
17
|
-
} from './headlessRenderEnv.ts'
|
|
14
|
+
export { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
|
|
15
|
+
export { CHAR_WIDTH_RATIO } from './measureTextCanvas.ts'
|
|
18
16
|
// the row count above which calculateNeighborJoiningTreeFromMSA throws, for
|
|
19
17
|
// hosts gating their own menu item
|
|
20
18
|
export { maxNeighborJoiningRows } from './constants.ts'
|
|
@@ -29,6 +27,8 @@ export type {
|
|
|
29
27
|
export { default as MSAView } from './components/Loading.tsx'
|
|
30
28
|
export { default as MSAViewer } from './components/MSAViewer.tsx'
|
|
31
29
|
export type { MSAViewerProps } from './components/MSAViewer.tsx'
|
|
30
|
+
// the snapshot MSAViewer builds from its props, for the CLI's --props
|
|
31
|
+
export { resolveTheme, viewerSnapshot } from './viewerSnapshot.ts'
|
|
32
32
|
export { mount } from './mount.tsx'
|
|
33
33
|
export { defineMsaElement } from './element.ts'
|
|
34
34
|
export { useMsaSvgFigure } from './useMsaSvgFigure.ts'
|
|
@@ -37,7 +37,7 @@ export type { MountedViewer } from './mount.tsx'
|
|
|
37
37
|
export { type MsaViewModel, default as MSAModelF } from './model.ts'
|
|
38
38
|
export type { MSAFormat, MSAParserType } from 'msa-parsers'
|
|
39
39
|
export type { HierarchyNode, LaidOutNode, TreeScale } from './hierarchy.ts'
|
|
40
|
-
// InterProScanResults is the EBI wire format
|
|
40
|
+
// InterProScanResults is the EBI wire format interProScanToAnnotations reads;
|
|
41
41
|
// Annotation is the source-agnostic shape everything else is written against
|
|
42
42
|
export type { InterProScanResults } from 'msa-parsers'
|
|
43
43
|
// ColumnCounts, ColumnStats, DomainBand and TidyDomainAnnotation surface in the
|
package/src/measureTextCanvas.ts
CHANGED
|
@@ -16,16 +16,18 @@ function getCtx() {
|
|
|
16
16
|
return ctxHandle
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Rough average glyph width as a fraction of the font size for the sans-serif
|
|
21
|
+
* stack the labels use. measureTextCanvas falls back on it where there is no
|
|
22
|
+
* canvas to measure with (jsdom, SSR), and the headless render environment's
|
|
23
|
+
* `measureText` reports it.
|
|
24
|
+
*/
|
|
25
|
+
export const CHAR_WIDTH_RATIO = 0.6
|
|
24
26
|
|
|
25
27
|
export function measureTextCanvas(text: string, fontSize: number) {
|
|
26
28
|
const ctx = getCtx()
|
|
27
29
|
if (!ctx) {
|
|
28
|
-
return text.length * fontSize *
|
|
30
|
+
return text.length * fontSize * CHAR_WIDTH_RATIO
|
|
29
31
|
}
|
|
30
32
|
setFontSize(ctx, fontSize)
|
|
31
33
|
return ctx.measureText(text).width
|