react-msaview 5.2.0 → 5.3.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 +41 -8
- package/bundle/index.js +17 -17
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/components/ConservationTrack.js +2 -13
- package/dist/components/ConservationTrack.js.map +1 -1
- package/dist/components/MSAView.js +8 -1
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/ResizeHandles.d.ts +3 -0
- package/dist/components/ResizeHandles.js +60 -105
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/TextTrack.js +2 -1
- package/dist/components/TextTrack.js.map +1 -1
- package/dist/components/Track.js +1 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +2 -2
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/UserProvidedDomainsDialog.js +1 -2
- package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.js +1 -1
- package/dist/components/header/ColorSchemeMenu.js.map +1 -1
- package/dist/components/header/FileMenu.js +1 -1
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/HeaderInfoArea.js +28 -8
- package/dist/components/header/HeaderInfoArea.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.js +1 -1
- package/dist/components/header/MSASettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.js +1 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/ZoomControls.js +7 -2
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/ZoomMenu.js +9 -0
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -12
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +7 -13
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +9 -0
- package/dist/components/minimap/minimapLayout.js +15 -0
- package/dist/components/minimap/minimapLayout.js.map +1 -0
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +5 -1
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +5 -7
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +3 -2
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tracks/renderTracksSvg.js +2 -1
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -2
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/flatToTree.js +6 -0
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +1 -1
- package/dist/hierarchy.js +136 -102
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.js +22 -9
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/measureTextCanvas.js +2 -1
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model.d.ts +212 -150
- package/dist/model.js +239 -205
- package/dist/model.js.map +1 -1
- package/dist/parseAsn1.js +8 -2
- package/dist/parseAsn1.js.map +1 -1
- package/dist/renderToSvg.js +27 -24
- package/dist/renderToSvg.js.map +1 -1
- package/dist/setFontSize.d.ts +3 -0
- package/dist/setFontSize.js +14 -0
- package/dist/setFontSize.js.map +1 -0
- package/dist/useDevicePixelRatio.d.ts +1 -0
- package/dist/useDevicePixelRatio.js +27 -0
- package/dist/useDevicePixelRatio.js.map +1 -0
- package/dist/useWheelScroll.d.ts +3 -1
- package/dist/useWheelScroll.js +64 -22
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +4 -0
- package/dist/util.js +36 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webpack.js +0 -1
- package/dist/webpack.js.map +1 -1
- package/dist/wheelZoom.d.ts +4 -0
- package/dist/wheelZoom.js +43 -0
- package/dist/wheelZoom.js.map +1 -0
- package/package.json +5 -5
- package/src/components/ConservationTrack.tsx +2 -18
- package/src/components/MSAView.tsx +10 -1
- package/src/components/ResizeHandles.tsx +101 -113
- package/src/components/TextTrack.tsx +2 -1
- package/src/components/Track.tsx +1 -1
- package/src/components/VerticalScrollbar.tsx +2 -2
- package/src/components/dialogs/UserProvidedDomainsDialog.tsx +1 -2
- package/src/components/header/ColorSchemeMenu.tsx +1 -0
- package/src/components/header/FileMenu.tsx +1 -0
- package/src/components/header/HeaderInfoArea.tsx +32 -7
- package/src/components/header/MSASettingsMenu.tsx +1 -0
- package/src/components/header/TreeSettingsMenu.tsx +1 -0
- package/src/components/header/ZoomControls.tsx +14 -1
- package/src/components/header/ZoomMenu.tsx +10 -0
- package/src/components/minimap/Minimap.tsx +6 -15
- package/src/components/minimap/MinimapSVG.tsx +9 -22
- package/src/components/minimap/minimapLayout.ts +17 -0
- package/src/components/msa/MSACanvas.tsx +9 -0
- package/src/components/msa/MSACanvasBlock.tsx +7 -1
- package/src/components/msa/MSAMouseoverCanvas.tsx +4 -3
- package/src/components/msa/renderMSABlock.ts +6 -9
- package/src/components/msa/renderMSAMouseover.ts +3 -1
- package/src/components/tracks/renderTracksSvg.ts +2 -1
- package/src/components/tree/renderTreeCanvas.ts +2 -2
- package/src/constants.ts +8 -0
- package/src/flatToTree.test.ts +30 -0
- package/src/flatToTree.ts +10 -1
- package/src/hierarchy.test.ts +53 -1
- package/src/hierarchy.ts +139 -113
- package/src/index.ts +1 -0
- package/src/launchInterProScan.ts +33 -11
- package/src/measureTextCanvas.ts +3 -1
- package/src/model.ts +312 -242
- package/src/parseAsn1.test.ts +6 -0
- package/src/parseAsn1.ts +11 -3
- package/src/renderToSvg.tsx +79 -67
- package/src/setFontSize.test.ts +33 -0
- package/src/setFontSize.ts +20 -0
- package/src/useDevicePixelRatio.ts +29 -0
- package/src/useWheelScroll.ts +79 -21
- package/src/util.ts +35 -0
- package/src/version.ts +1 -1
- package/src/webpack.ts +0 -1
- package/src/wheelZoom.test.ts +46 -0
- package/src/wheelZoom.ts +47 -0
|
@@ -37,6 +37,16 @@ const ZoomMenu = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
37
37
|
model.resetZoom()
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
label: 'Scroll zoom',
|
|
42
|
+
type: 'checkbox',
|
|
43
|
+
checked: model.scrollZoom,
|
|
44
|
+
onClick: () => {
|
|
45
|
+
model.setScrollZoom(!model.scrollZoom)
|
|
46
|
+
},
|
|
47
|
+
helpText:
|
|
48
|
+
'When enabled, scrolling the mouse wheel zooms in and out without holding ctrl. Hold shift to pan instead. Ctrl/⌘+wheel always zooms.',
|
|
49
|
+
},
|
|
40
50
|
]}
|
|
41
51
|
>
|
|
42
52
|
<MoreVert />
|
|
@@ -2,20 +2,16 @@ import React, { useCallback, useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
+
import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
|
|
5
6
|
import { useDragScroll } from '../../useDragScroll.ts'
|
|
6
7
|
|
|
7
8
|
import type { MsaViewModel } from '../../model.ts'
|
|
8
9
|
|
|
9
10
|
const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
10
11
|
const [hovered, setHovered] = useState(false)
|
|
11
|
-
const {
|
|
12
|
-
const unit
|
|
13
|
-
const left = -scrollX
|
|
14
|
-
const right = left + msaAreaWidth
|
|
15
|
-
const s = left * unit
|
|
16
|
-
const e = right * unit
|
|
12
|
+
const { minimapHeight } = model
|
|
13
|
+
const { unit, s, w, polygonHeight, polygonPoints } = getMinimapLayout(model)
|
|
17
14
|
const fill = 'rgba(66, 119, 127, 0.3)'
|
|
18
|
-
const w = Math.max(e - s, 20)
|
|
19
15
|
|
|
20
16
|
const { startDrag } = useDragScroll(
|
|
21
17
|
'x',
|
|
@@ -27,8 +23,6 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
27
23
|
),
|
|
28
24
|
)
|
|
29
25
|
|
|
30
|
-
const barHeight = 12
|
|
31
|
-
const polygonHeight = minimapHeight - barHeight
|
|
32
26
|
return (
|
|
33
27
|
<div
|
|
34
28
|
style={{
|
|
@@ -39,7 +33,7 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
39
33
|
>
|
|
40
34
|
<div
|
|
41
35
|
style={{
|
|
42
|
-
height:
|
|
36
|
+
height: MINIMAP_BAR_HEIGHT,
|
|
43
37
|
boxSizing: 'border-box',
|
|
44
38
|
border: '1px solid #555',
|
|
45
39
|
}}
|
|
@@ -51,7 +45,7 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
51
45
|
left: Math.max(0, s),
|
|
52
46
|
background: hovered ? 'rgba(66,119,127,0.6)' : fill,
|
|
53
47
|
cursor: 'pointer',
|
|
54
|
-
height:
|
|
48
|
+
height: MINIMAP_BAR_HEIGHT,
|
|
55
49
|
width: w,
|
|
56
50
|
zIndex: 100,
|
|
57
51
|
}}
|
|
@@ -67,10 +61,7 @@ const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
67
61
|
/>
|
|
68
62
|
|
|
69
63
|
<svg height={polygonHeight} style={{ width: '100%' }}>
|
|
70
|
-
<polygon
|
|
71
|
-
fill={fill}
|
|
72
|
-
points={`${s + w},0 ${s},0 0,${polygonHeight} ${msaAreaWidth},${polygonHeight}`}
|
|
73
|
-
/>
|
|
64
|
+
<polygon fill={fill} points={polygonPoints} />
|
|
74
65
|
</svg>
|
|
75
66
|
</div>
|
|
76
67
|
)
|
|
@@ -2,26 +2,13 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
+
import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
|
|
6
|
+
|
|
5
7
|
import type { MsaViewModel } from '../../model.ts'
|
|
6
8
|
|
|
7
9
|
const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
msaAreaWidth: W,
|
|
11
|
-
minimapHeight: H,
|
|
12
|
-
colWidth,
|
|
13
|
-
numColumns,
|
|
14
|
-
} = model
|
|
15
|
-
|
|
16
|
-
const BAR_HEIGHT = 12
|
|
17
|
-
const H2 = H - BAR_HEIGHT
|
|
18
|
-
|
|
19
|
-
const unit = W / numColumns / colWidth
|
|
20
|
-
const left = -scrollX
|
|
21
|
-
const right = left + W
|
|
22
|
-
const s = left * unit
|
|
23
|
-
const e = right * unit
|
|
24
|
-
const w = Math.max(e - s, 20)
|
|
10
|
+
const { msaAreaWidth } = model
|
|
11
|
+
const { s, w, polygonPoints } = getMinimapLayout(model)
|
|
25
12
|
const fillColor = 'rgb(66, 119, 127)'
|
|
26
13
|
const fillOpacity = 0.3
|
|
27
14
|
|
|
@@ -30,8 +17,8 @@ const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
|
|
|
30
17
|
<rect
|
|
31
18
|
x={0}
|
|
32
19
|
y={0}
|
|
33
|
-
width={
|
|
34
|
-
height={
|
|
20
|
+
width={msaAreaWidth}
|
|
21
|
+
height={MINIMAP_BAR_HEIGHT}
|
|
35
22
|
stroke="#555"
|
|
36
23
|
fill="none"
|
|
37
24
|
/>
|
|
@@ -39,16 +26,16 @@ const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
|
|
|
39
26
|
x={Math.max(0, s)}
|
|
40
27
|
y={0}
|
|
41
28
|
width={w}
|
|
42
|
-
height={
|
|
29
|
+
height={MINIMAP_BAR_HEIGHT}
|
|
43
30
|
fill={fillColor}
|
|
44
31
|
fillOpacity={fillOpacity}
|
|
45
32
|
stroke="#555"
|
|
46
33
|
/>
|
|
47
|
-
<g transform={`translate(0 ${
|
|
34
|
+
<g transform={`translate(0 ${MINIMAP_BAR_HEIGHT})`}>
|
|
48
35
|
<polygon
|
|
49
36
|
fill={fillColor}
|
|
50
37
|
fillOpacity={fillOpacity}
|
|
51
|
-
points={
|
|
38
|
+
points={polygonPoints}
|
|
52
39
|
/>
|
|
53
40
|
</g>
|
|
54
41
|
</>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
2
|
+
|
|
3
|
+
export const MINIMAP_BAR_HEIGHT = 12
|
|
4
|
+
|
|
5
|
+
// viewport-rectangle and trapezoid geometry shared by the interactive Minimap
|
|
6
|
+
// and the static MinimapSVG (used for SVG export)
|
|
7
|
+
export function getMinimapLayout(model: MsaViewModel) {
|
|
8
|
+
const { scrollX, msaAreaWidth, minimapHeight, colWidth, numColumns } = model
|
|
9
|
+
const unit = msaAreaWidth / numColumns / colWidth
|
|
10
|
+
const left = -scrollX
|
|
11
|
+
const s = left * unit
|
|
12
|
+
const e = (left + msaAreaWidth) * unit
|
|
13
|
+
const w = Math.max(e - s, 20)
|
|
14
|
+
const polygonHeight = minimapHeight - MINIMAP_BAR_HEIGHT
|
|
15
|
+
const polygonPoints = `${s + w},0 ${s},0 0,${polygonHeight} ${msaAreaWidth},${polygonHeight}`
|
|
16
|
+
return { unit, s, w, polygonHeight, polygonPoints }
|
|
17
|
+
}
|
|
@@ -16,6 +16,7 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
16
16
|
height,
|
|
17
17
|
msaAreaWidth,
|
|
18
18
|
blocks2d,
|
|
19
|
+
scrollZoom,
|
|
19
20
|
} = model
|
|
20
21
|
const ref = useRef<HTMLDivElement>(null)
|
|
21
22
|
const onScrollX = useCallback(
|
|
@@ -30,10 +31,18 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
30
31
|
},
|
|
31
32
|
[model],
|
|
32
33
|
)
|
|
34
|
+
const onZoom = useCallback(
|
|
35
|
+
(scaleFactor: number, offsetX: number, offsetY: number) => {
|
|
36
|
+
model.zoomToPos(scaleFactor, offsetX, offsetY)
|
|
37
|
+
},
|
|
38
|
+
[model],
|
|
39
|
+
)
|
|
33
40
|
const { onMouseDown, onMouseUp } = useWheelScroll({
|
|
34
41
|
ref,
|
|
35
42
|
onScrollX,
|
|
36
43
|
onScrollY,
|
|
44
|
+
onZoom,
|
|
45
|
+
scrollZoom,
|
|
37
46
|
})
|
|
38
47
|
|
|
39
48
|
return (
|
|
@@ -97,7 +97,6 @@ const MSACanvasBlock = observer(function ({
|
|
|
97
97
|
ref={ref}
|
|
98
98
|
onMouseMove={event => {
|
|
99
99
|
if (ref.current) {
|
|
100
|
-
setMousePosition({ x: event.clientX, y: event.clientY })
|
|
101
100
|
const { col, row } = eventToColRow(
|
|
102
101
|
event,
|
|
103
102
|
ref.current,
|
|
@@ -117,6 +116,13 @@ const MSACanvasBlock = observer(function ({
|
|
|
117
116
|
} else {
|
|
118
117
|
model.setMousePos(undefined, undefined)
|
|
119
118
|
}
|
|
119
|
+
// only track client coords when an insertion is hovered (the only
|
|
120
|
+
// consumer is the tooltip below); avoids a re-render on every move
|
|
121
|
+
setMousePosition(
|
|
122
|
+
model.hoveredInsertion
|
|
123
|
+
? { x: event.clientX, y: event.clientY }
|
|
124
|
+
: undefined,
|
|
125
|
+
)
|
|
120
126
|
}
|
|
121
127
|
}}
|
|
122
128
|
onClick={event => {
|
|
@@ -14,7 +14,8 @@ const MSAMouseoverCanvas = observer(function ({
|
|
|
14
14
|
model: MsaViewModel
|
|
15
15
|
}) {
|
|
16
16
|
const ref = useRef<HTMLCanvasElement>(null)
|
|
17
|
-
const { height, msaAreaWidth, verticalScrollbarWidth } =
|
|
17
|
+
const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
|
|
18
|
+
model
|
|
18
19
|
const width = msaAreaWidth - verticalScrollbarWidth
|
|
19
20
|
useEffect(() => {
|
|
20
21
|
const ctx = ref.current?.getContext('2d')
|
|
@@ -31,8 +32,8 @@ const MSAMouseoverCanvas = observer(function ({
|
|
|
31
32
|
<canvas
|
|
32
33
|
ref={ref}
|
|
33
34
|
id="mouseover"
|
|
34
|
-
width={width}
|
|
35
|
-
height={height}
|
|
35
|
+
width={width * highResScaleFactor}
|
|
36
|
+
height={height * highResScaleFactor}
|
|
36
37
|
style={{
|
|
37
38
|
position: 'absolute',
|
|
38
39
|
top: 0,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getVisibleLeaves } from './getVisibleLeaves.ts'
|
|
2
2
|
import { visibleColRange } from './visibleColRange.ts'
|
|
3
|
+
import { setFontSize } from '../../setFontSize.ts'
|
|
3
4
|
import { adjustColorForContrast } from '../../util.ts'
|
|
4
5
|
|
|
5
6
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -45,13 +46,7 @@ export function renderMSABlock({
|
|
|
45
46
|
ctx.scale(k, k)
|
|
46
47
|
ctx.translate(-offsetX, rowHeight / 2 - offsetY)
|
|
47
48
|
ctx.textAlign = 'center'
|
|
48
|
-
|
|
49
|
-
// (an invalid "bold bold 16px" string would be silently ignored by canvas,
|
|
50
|
-
// freezing the font size against zoom changes)
|
|
51
|
-
ctx.font = ctx.font.replace(
|
|
52
|
-
/(?:bold )?\d+px/,
|
|
53
|
-
`${bgColor ? '' : 'bold '}${fontSize}px`,
|
|
54
|
-
)
|
|
49
|
+
setFontSize(ctx, fontSize, !bgColor)
|
|
55
50
|
|
|
56
51
|
const { xStart, xEnd } = visibleColRange({
|
|
57
52
|
offsetX,
|
|
@@ -127,6 +122,8 @@ function drawTilesAndText({
|
|
|
127
122
|
rowHeight,
|
|
128
123
|
relativeTo,
|
|
129
124
|
showMsaLetters,
|
|
125
|
+
colClustalX,
|
|
126
|
+
colConsensus,
|
|
130
127
|
} = model
|
|
131
128
|
|
|
132
129
|
const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
|
|
@@ -158,9 +155,9 @@ function drawTilesAndText({
|
|
|
158
155
|
|
|
159
156
|
let color: string | undefined
|
|
160
157
|
if (isClustalX) {
|
|
161
|
-
color =
|
|
158
|
+
color = colClustalX[xStart + j]![letter]
|
|
162
159
|
} else if (isPercentIdentity) {
|
|
163
|
-
const consensus =
|
|
160
|
+
const consensus = colConsensus[xStart + j]!
|
|
164
161
|
color = letter === consensus.letter ? consensus.color : undefined
|
|
165
162
|
} else {
|
|
166
163
|
color = colorScheme[letter]
|
|
@@ -27,10 +27,12 @@ export function renderMouseover({
|
|
|
27
27
|
relativeTo,
|
|
28
28
|
hoveredTreeNode,
|
|
29
29
|
highlightedColumns,
|
|
30
|
+
highResScaleFactor,
|
|
30
31
|
} = model
|
|
31
32
|
const width = msaAreaWidth - verticalScrollbarWidth
|
|
32
33
|
ctx.resetTransform()
|
|
33
|
-
ctx.clearRect(0, 0, width, height)
|
|
34
|
+
ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
|
|
35
|
+
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
34
36
|
|
|
35
37
|
// Highlight reference row (relativeTo) persistently
|
|
36
38
|
if (relativeTo) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setFontSize } from '../../setFontSize.ts'
|
|
1
2
|
import { visibleColRange } from '../msa/visibleColRange.ts'
|
|
2
3
|
|
|
3
4
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -152,7 +153,7 @@ export function renderTextTrack({
|
|
|
152
153
|
ctx.scale(k, k)
|
|
153
154
|
ctx.translate(-offsetX, offsetY)
|
|
154
155
|
ctx.textAlign = 'center'
|
|
155
|
-
ctx
|
|
156
|
+
setFontSize(ctx, fontSize)
|
|
156
157
|
|
|
157
158
|
drawTextTrackContent({
|
|
158
159
|
ctx,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
forEachDescendant,
|
|
4
4
|
forEachLink,
|
|
5
5
|
} from '../../hierarchy.ts'
|
|
6
|
+
import { setFontSize } from '../../setFontSize.ts'
|
|
6
7
|
|
|
7
8
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
8
9
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -288,8 +289,7 @@ export function renderTreeCanvas({
|
|
|
288
289
|
ctx.scale(k, k)
|
|
289
290
|
ctx.translate(marginLeft, -offsetY)
|
|
290
291
|
|
|
291
|
-
|
|
292
|
-
ctx.font = font.replace(/\d+px/, `${fontSize}px`)
|
|
292
|
+
setFontSize(ctx, fontSize)
|
|
293
293
|
|
|
294
294
|
// memoized on the model and shared across the tree/bubble/label passes (and
|
|
295
295
|
// across all tree blocks) rather than re-traversing the hierarchy in each
|
package/src/constants.ts
CHANGED
|
@@ -10,6 +10,14 @@ export const defaultHideGaps = true
|
|
|
10
10
|
export const defaultAllowedGappyness = 100
|
|
11
11
|
export const defaultSubFeatureRows = false
|
|
12
12
|
export const defaultDrawMsaLetters = true
|
|
13
|
+
export const defaultScrollZoom = false
|
|
14
|
+
|
|
15
|
+
// Zoom limits, px per cell. maxCellSize caps both smooth zoom and the stepwise
|
|
16
|
+
// zoom-in buttons (which otherwise grow unbounded). minColWidth/minRowHeight are
|
|
17
|
+
// the smooth-zoom floors.
|
|
18
|
+
export const minColWidth = 0.2
|
|
19
|
+
export const minRowHeight = 1
|
|
20
|
+
export const maxCellSize = 80
|
|
13
21
|
|
|
14
22
|
// MSA model defaults
|
|
15
23
|
export const defaultBgColor = true
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { flatToTree } from './flatToTree.ts'
|
|
4
|
+
|
|
5
|
+
describe('flatToTree', () => {
|
|
6
|
+
test('builds a tree from a flat parent list', () => {
|
|
7
|
+
const root = flatToTree([
|
|
8
|
+
{ id: 0 },
|
|
9
|
+
{ id: 1, parent: 0 },
|
|
10
|
+
{ id: 2, parent: 0 },
|
|
11
|
+
{ id: 3, parent: 1 },
|
|
12
|
+
])
|
|
13
|
+
expect(root.id).toBe('0')
|
|
14
|
+
expect(root.children.map(c => c.id)).toEqual(['1', '2'])
|
|
15
|
+
expect(root.children[0]!.children.map(c => c.id)).toEqual(['3'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('throws on empty input rather than returning undefined', () => {
|
|
19
|
+
expect(() => flatToTree([])).toThrow(/no root/)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('throws when every node has a parent (cycle) rather than returning undefined', () => {
|
|
23
|
+
expect(() =>
|
|
24
|
+
flatToTree([
|
|
25
|
+
{ id: 0, parent: 1 },
|
|
26
|
+
{ id: 1, parent: 0 },
|
|
27
|
+
]),
|
|
28
|
+
).toThrow(/no root/)
|
|
29
|
+
})
|
|
30
|
+
})
|
package/src/flatToTree.ts
CHANGED
|
@@ -35,5 +35,14 @@ export function flatToTree(items: FlatItem[]): TreeNode {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
if (root === undefined) {
|
|
39
|
+
// no node lacks a (resolvable) parent: empty input, or every node's parent
|
|
40
|
+
// points back into the set (a cycle). returning undefined here would crash
|
|
41
|
+
// cryptically downstream, so fail with a diagnosable message instead
|
|
42
|
+
throw new Error(
|
|
43
|
+
'could not build tree from flat node list: no root found (input was empty or contains a parent cycle)',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return root
|
|
39
48
|
}
|
package/src/hierarchy.test.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
calcDepthToLeaf,
|
|
5
|
+
collapse,
|
|
6
|
+
find,
|
|
7
|
+
findMaxBranchLen,
|
|
8
|
+
hierarchy,
|
|
9
|
+
leaves,
|
|
10
|
+
maxLength,
|
|
11
|
+
setBrLength,
|
|
12
|
+
sort,
|
|
13
|
+
sum,
|
|
14
|
+
} from './hierarchy.ts'
|
|
4
15
|
|
|
5
16
|
import type { NodeWithIds } from './types.ts'
|
|
6
17
|
|
|
18
|
+
// Builds a fully unbalanced "caterpillar" tree of the given depth: each internal
|
|
19
|
+
// node has one leaf child and one internal child. Recursion over this would blow
|
|
20
|
+
// the call stack; the iterative traversals must handle it.
|
|
21
|
+
function makeDeepTree(depth: number): NodeWithIds {
|
|
22
|
+
let node: NodeWithIds = { id: 'leaf0', name: 'leaf0', children: [] }
|
|
23
|
+
for (let i = 1; i <= depth; i++) {
|
|
24
|
+
node = {
|
|
25
|
+
id: `node${i}`,
|
|
26
|
+
name: `node${i}`,
|
|
27
|
+
length: 1,
|
|
28
|
+
children: [{ id: `leaf${i}`, name: `leaf${i}`, children: [] }, node],
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return node
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
function makeTree(): NodeWithIds {
|
|
8
35
|
return {
|
|
9
36
|
id: 'root',
|
|
@@ -118,3 +145,28 @@ describe('sum', () => {
|
|
|
118
145
|
expect(find(h, n => n.data.id === 'A')?.value).toBe(2)
|
|
119
146
|
})
|
|
120
147
|
})
|
|
148
|
+
|
|
149
|
+
describe('deep (caterpillar) trees do not overflow the stack', () => {
|
|
150
|
+
const depth = 100_000
|
|
151
|
+
|
|
152
|
+
test('build, height, leaf count, and ordering', () => {
|
|
153
|
+
const h = hierarchy(makeDeepTree(depth), d => d.children)
|
|
154
|
+
expect(h.height).toBe(depth)
|
|
155
|
+
const l = leaves(h)
|
|
156
|
+
expect(l.length).toBe(depth + 1)
|
|
157
|
+
// each level contributes its own leafN before descending; deepest leaf last
|
|
158
|
+
expect(l[0]!.data.name).toBe(`leaf${depth}`)
|
|
159
|
+
expect(l.at(-1)!.data.name).toBe('leaf0')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('accumulation helpers (sum, find, maxLength, setBrLength, depth)', () => {
|
|
163
|
+
const h = hierarchy(makeDeepTree(depth), d => d.children)
|
|
164
|
+
sum(h, d => (d.children.length > 0 ? 0 : 1))
|
|
165
|
+
expect(h.value).toBe(depth + 1)
|
|
166
|
+
expect(find(h, n => n.data.id === 'leaf0')?.data.name).toBe('leaf0')
|
|
167
|
+
expect(maxLength(h)).toBe(depth)
|
|
168
|
+
expect(calcDepthToLeaf(h)).toBe(depth)
|
|
169
|
+
setBrLength(h, 0, 1)
|
|
170
|
+
expect(findMaxBranchLen(h)).toBe(depth)
|
|
171
|
+
})
|
|
172
|
+
})
|