react-msaview 8.1.0 → 8.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/bundle/index.js +104 -117
- package/bundle/index.js.map +4 -4
- package/dist/columnCounts.d.ts +4 -3
- package/dist/columnCounts.js +11 -6
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/CopyButton.d.ts +1 -1
- package/dist/components/CopyButton.js +1 -1
- package/dist/components/CopyButton.js.map +1 -1
- package/dist/components/DragHandle.js +14 -2
- package/dist/components/DragHandle.js.map +1 -1
- package/dist/components/Loading.js +10 -11
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +12 -1
- package/dist/components/MSAViewer.js +12 -1
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/SequenceTextArea.d.ts +11 -0
- package/dist/components/SequenceTextArea.js +40 -8
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/StatusMessage.d.ts +12 -0
- package/dist/components/StatusMessage.js +43 -0
- package/dist/components/StatusMessage.js.map +1 -0
- package/dist/components/Track.js +3 -3
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +6 -1
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
- package/dist/components/dialogs/ExportSVGDialog.js +4 -2
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +15 -10
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +1 -1
- package/dist/components/header/FileMenu.js +9 -0
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +15 -3
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/Header.js +6 -7
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
- package/dist/components/header/RowSearch.js +15 -0
- package/dist/components/header/RowSearch.js.map +1 -0
- package/dist/components/header/revealRow.d.ts +3 -0
- package/dist/components/header/revealRow.js +15 -0
- package/dist/components/header/revealRow.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +57 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +10 -4
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +19 -27
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -1
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/AnnotationLegend.js +11 -5
- package/dist/components/msa/AnnotationLegend.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +3 -7
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaKeys.d.ts +5 -0
- package/dist/components/msa/msaKeys.js +35 -0
- package/dist/components/msa/msaKeys.js.map +1 -0
- package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
- package/dist/components/msa/useMsaBlockMouse.js +18 -5
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +10 -14
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -0
- package/dist/components/tracks/drawTracks.js +14 -2
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tracks/useTrackHover.d.ts +1 -5
- package/dist/components/tracks/useTrackHover.js +5 -5
- package/dist/components/tracks/useTrackHover.js.map +1 -1
- package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
- package/dist/components/tree/ScaleBarSVG.js +11 -0
- package/dist/components/tree/ScaleBarSVG.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -26
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +11 -20
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
- package/dist/components/tree/TreeMenu.js +48 -0
- package/dist/components/tree/TreeMenu.js.map +1 -0
- package/dist/components/tree/TreeRuler.js +3 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +19 -9
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
- package/dist/components/tree/renderTreeMouseover.js +20 -0
- package/dist/components/tree/renderTreeMouseover.js.map +1 -0
- package/dist/components/tree/useTreeHover.d.ts +3 -8
- package/dist/components/tree/useTreeHover.js +8 -10
- package/dist/components/tree/useTreeHover.js.map +1 -1
- package/dist/components/useHoverAnchor.d.ts +19 -0
- package/dist/components/useHoverAnchor.js +18 -0
- package/dist/components/useHoverAnchor.js.map +1 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +12 -3
- package/dist/constants.js.map +1 -1
- package/dist/expandSpec.d.ts +46 -0
- package/dist/expandSpec.js +127 -0
- package/dist/expandSpec.js.map +1 -0
- package/dist/exportFileName.d.ts +3 -3
- package/dist/exportFileName.js +4 -4
- package/dist/exportFileName.js.map +1 -1
- package/dist/hierarchy.d.ts +51 -0
- package/dist/hierarchy.js +125 -0
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/treeModel.d.ts +12 -0
- package/dist/model/treeModel.js +14 -1
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +188 -47
- package/dist/model.js +482 -192
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +2 -4
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rerootTree.d.ts +33 -0
- package/dist/rerootTree.js +144 -0
- package/dist/rerootTree.js.map +1 -0
- package/dist/svgTestUtil.d.ts +114 -20
- package/dist/types.d.ts +25 -12
- package/dist/useMsaSvgFigure.d.ts +2 -1
- package/dist/useMsaSvgFigure.js +11 -4
- package/dist/useMsaSvgFigure.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/writeNewick.d.ts +8 -0
- package/dist/writeNewick.js +34 -0
- package/dist/writeNewick.js.map +1 -0
- package/package.json +3 -3
- package/src/columnCounts.ts +14 -6
- package/src/components/CopyButton.tsx +2 -2
- package/src/components/DragHandle.tsx +21 -1
- package/src/components/Loading.tsx +14 -30
- package/src/components/MSAViewer.tsx +26 -0
- package/src/components/SequenceTextArea.tsx +54 -8
- package/src/components/StatusMessage.tsx +70 -0
- package/src/components/Track.tsx +8 -4
- package/src/components/VerticalScrollbar.tsx +7 -0
- package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
- package/src/components/dialogs/FeatureDialog.tsx +59 -38
- package/src/components/header/FileMenu.tsx +9 -0
- package/src/components/header/GappynessSlider.tsx +17 -4
- package/src/components/header/Header.tsx +7 -8
- package/src/components/header/RowSearch.tsx +34 -0
- package/src/components/header/revealRow.ts +17 -0
- package/src/components/header/settingsMenuItems.ts +66 -0
- package/src/components/import/ImportForm.tsx +14 -7
- package/src/components/import/ImportFormExamples.tsx +20 -28
- package/src/components/minimap/Minimap.tsx +7 -0
- package/src/components/msa/AnnotationLegend.tsx +12 -6
- package/src/components/msa/MSACanvas.tsx +8 -0
- package/src/components/msa/MSACanvasBlock.tsx +5 -11
- package/src/components/msa/msaKeys.ts +39 -0
- package/src/components/msa/useMsaBlockMouse.ts +23 -7
- package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
- package/src/components/tracks/drawTracks.ts +15 -2
- package/src/components/tracks/useTrackHover.ts +5 -5
- package/src/components/tree/ScaleBarSVG.tsx +30 -0
- package/src/components/tree/TreeCanvas.tsx +9 -40
- package/src/components/tree/TreeCanvasBlock.tsx +19 -39
- package/src/components/tree/TreeMenu.tsx +129 -0
- package/src/components/tree/TreeRuler.tsx +7 -13
- package/src/components/tree/renderTreeCanvas.ts +20 -8
- package/src/components/tree/renderTreeMouseover.ts +41 -0
- package/src/components/tree/useTreeHover.ts +8 -17
- package/src/components/useHoverAnchor.ts +31 -0
- package/src/constants.ts +14 -3
- package/src/expandSpec.ts +196 -0
- package/src/exportFileName.ts +4 -3
- package/src/hierarchy.ts +151 -4
- package/src/index.ts +11 -1
- package/src/model/treeModel.ts +22 -0
- package/src/model.ts +588 -221
- package/src/renderToSvg.tsx +6 -9
- package/src/rerootTree.ts +172 -0
- package/src/types.ts +34 -12
- package/src/useMsaSvgFigure.ts +11 -4
- package/src/version.ts +1 -1
- package/src/writeNewick.ts +38 -0
- package/dist/components/PortalTooltip.d.ts +0 -11
- package/dist/components/PortalTooltip.js +0 -29
- package/dist/components/PortalTooltip.js.map +0 -1
- package/dist/components/header/HeaderStatusArea.js +0 -21
- package/dist/components/header/HeaderStatusArea.js.map +0 -1
- package/dist/components/tree/TreeBranchMenu.js +0 -29
- package/dist/components/tree/TreeBranchMenu.js.map +0 -1
- package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
- package/dist/components/tree/TreeNodeMenu.js +0 -41
- package/dist/components/tree/TreeNodeMenu.js.map +0 -1
- package/src/components/PortalTooltip.tsx +0 -47
- package/src/components/header/HeaderStatusArea.tsx +0 -32
- package/src/components/tree/TreeBranchMenu.tsx +0 -68
- package/src/components/tree/TreeNodeMenu.tsx +0 -95
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
highlightLabelHeight,
|
|
7
7
|
highlightRowFill,
|
|
8
8
|
} from '../msa/renderHighlights.ts'
|
|
9
|
+
import { referenceColor } from '../overlayColors.ts'
|
|
9
10
|
import { bracketBarWidth, bracketGap } from './cladeBrackets.ts'
|
|
10
11
|
|
|
11
12
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
@@ -64,6 +65,13 @@ function forEachNodeInBlock(
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// withId gives an unnamed node its path, node-0-1-1, as its name
|
|
69
|
+
function cladeName(node: HierarchyNode, unnamed: string) {
|
|
70
|
+
const { id, name } = node.data
|
|
71
|
+
const label = name === id ? unnamed : name
|
|
72
|
+
return node.value === undefined ? label : `${label} (${node.value} tips)`
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
// Calculate node x-coordinate for both phylogram (with branch lengths) and
|
|
68
76
|
// cladogram (topology only) modes. tipX is the pixel x the tips sit at.
|
|
69
77
|
// For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * tipX
|
|
@@ -182,7 +190,7 @@ function renderCollapsedTriangles({
|
|
|
182
190
|
const by = blockSizeYOverride ?? blockSize
|
|
183
191
|
const halfHeight = Math.max(2, rowHeight * 0.42)
|
|
184
192
|
forEachNodeInBlock(hierarchy, offsetY, by, blockPad(model), node => {
|
|
185
|
-
const { id
|
|
193
|
+
const { id } = node.data
|
|
186
194
|
if (collapsedSet.has(id)) {
|
|
187
195
|
const apexX = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
|
|
188
196
|
const y = node.x!
|
|
@@ -212,7 +220,6 @@ function renderCollapsedTriangles({
|
|
|
212
220
|
// the whole triangle is a click/hover target that opens the
|
|
213
221
|
// branch menu (Expand this node); the apex bubble's own click entry is
|
|
214
222
|
// skipped for collapsed nodes so this descriptive label wins
|
|
215
|
-
const label = name === id ? 'Collapsed clade' : name
|
|
216
223
|
clickMap?.insert({
|
|
217
224
|
minX: apexX + ml,
|
|
218
225
|
maxX: baseX + ml,
|
|
@@ -220,7 +227,7 @@ function renderCollapsedTriangles({
|
|
|
220
227
|
maxY: y + halfHeight,
|
|
221
228
|
branch: true,
|
|
222
229
|
id,
|
|
223
|
-
name:
|
|
230
|
+
name: cladeName(node, 'Collapsed clade'),
|
|
224
231
|
})
|
|
225
232
|
}
|
|
226
233
|
}
|
|
@@ -266,7 +273,7 @@ function renderNodeBubbles({
|
|
|
266
273
|
}
|
|
267
274
|
const { data } = node
|
|
268
275
|
const y = node.x!
|
|
269
|
-
const { id
|
|
276
|
+
const { id } = data
|
|
270
277
|
if (node.height >= 1 && inYBlock(y, offsetY, by, blockPad(model))) {
|
|
271
278
|
const isCollapsed = collapsedSet.has(id)
|
|
272
279
|
if (draw) {
|
|
@@ -290,7 +297,7 @@ function renderNodeBubbles({
|
|
|
290
297
|
maxY: y - radius + d,
|
|
291
298
|
branch: true,
|
|
292
299
|
id,
|
|
293
|
-
name,
|
|
300
|
+
name: cladeName(node, 'Clade'),
|
|
294
301
|
})
|
|
295
302
|
}
|
|
296
303
|
}
|
|
@@ -572,9 +579,9 @@ function renderCladeBrackets({
|
|
|
572
579
|
ctx.strokeStyle = theme.palette.text.primary
|
|
573
580
|
}
|
|
574
581
|
|
|
575
|
-
// the `rowTint` encoding and the row sets of `highlights`,
|
|
576
|
-
// tree area under the labels, with each set's label in the
|
|
577
|
-
// row
|
|
582
|
+
// the `rowTint` encoding, the reference row and the row sets of `highlights`,
|
|
583
|
+
// washed across the tree area under the labels, with each set's label in the
|
|
584
|
+
// gutter at its first row
|
|
578
585
|
function renderRowHighlights({
|
|
579
586
|
ctx,
|
|
580
587
|
model,
|
|
@@ -591,6 +598,7 @@ function renderRowHighlights({
|
|
|
591
598
|
const {
|
|
592
599
|
resolvedHighlights,
|
|
593
600
|
rowTints,
|
|
601
|
+
referenceRowIndex,
|
|
594
602
|
rowHeight,
|
|
595
603
|
treeAreaWidth,
|
|
596
604
|
marginLeft,
|
|
@@ -616,6 +624,10 @@ function renderRowHighlights({
|
|
|
616
624
|
}
|
|
617
625
|
}
|
|
618
626
|
}
|
|
627
|
+
if (referenceRowIndex !== undefined) {
|
|
628
|
+
ctx.fillStyle = referenceColor
|
|
629
|
+
fillRow(referenceRowIndex)
|
|
630
|
+
}
|
|
619
631
|
for (const { rowIndices, label, color } of resolvedHighlights) {
|
|
620
632
|
ctx.fillStyle = color ?? highlightRowFill
|
|
621
633
|
for (const index of rowIndices) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { treeHoverColor } from '../overlayColors.ts'
|
|
2
|
+
|
|
3
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
4
|
+
|
|
5
|
+
export function renderTreeMouseover({
|
|
6
|
+
ctx,
|
|
7
|
+
model,
|
|
8
|
+
}: {
|
|
9
|
+
ctx: CanvasRenderingContext2D
|
|
10
|
+
model: MsaViewModel
|
|
11
|
+
}) {
|
|
12
|
+
const {
|
|
13
|
+
rowHeight,
|
|
14
|
+
treeAreaWidth: width,
|
|
15
|
+
height,
|
|
16
|
+
scrollY,
|
|
17
|
+
mouseRow,
|
|
18
|
+
referenceRowIndex,
|
|
19
|
+
hoveredRowIndices,
|
|
20
|
+
highResScaleFactor,
|
|
21
|
+
} = model
|
|
22
|
+
ctx.resetTransform()
|
|
23
|
+
ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
|
|
24
|
+
ctx.scale(highResScaleFactor, highResScaleFactor)
|
|
25
|
+
|
|
26
|
+
const rowBand = (index: number) => {
|
|
27
|
+
ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ctx.fillStyle = treeHoverColor
|
|
31
|
+
for (const index of hoveredRowIndices) {
|
|
32
|
+
rowBand(index)
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
mouseRow !== undefined &&
|
|
36
|
+
mouseRow !== referenceRowIndex &&
|
|
37
|
+
!hoveredRowIndices.includes(mouseRow)
|
|
38
|
+
) {
|
|
39
|
+
rowBand(mouseRow)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
|
+
import { useHoverAnchor } from '../useHoverAnchor.ts'
|
|
3
4
|
import { ClickMapIndex } from './clickMap.ts'
|
|
4
5
|
|
|
5
6
|
import type { MsaViewModel } from '../../model.ts'
|
|
6
7
|
import type { ClickEntry } from './clickMap.ts'
|
|
7
8
|
import type React from 'react'
|
|
8
9
|
|
|
9
|
-
export interface TreeHoverTarget extends ClickEntry {
|
|
10
|
-
clientX: number
|
|
11
|
-
clientY: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
/**
|
|
15
11
|
* Owns hit-testing for one tree block: the spatial index the render pass fills
|
|
16
|
-
* in, the entry
|
|
17
|
-
* MSA panel mirrors.
|
|
18
|
-
* it lives in a ref; everything the component draws comes back as plain state.
|
|
12
|
+
* in, the entry under the pointer with its tooltip anchor, and the model hover
|
|
13
|
+
* state that the MSA panel mirrors.
|
|
19
14
|
*/
|
|
20
15
|
export function useTreeHover({
|
|
21
16
|
model,
|
|
@@ -25,10 +20,10 @@ export function useTreeHover({
|
|
|
25
20
|
offsetY: number
|
|
26
21
|
}) {
|
|
27
22
|
const [clickMap] = useState(() => new ClickMapIndex())
|
|
28
|
-
const
|
|
23
|
+
const { anchor, hoverAt, clearAnchor } = useHoverAnchor<ClickEntry>()
|
|
29
24
|
|
|
30
25
|
// leaf labels win over the branch/bubble targets they overlap, so a click on a
|
|
31
|
-
// name opens the
|
|
26
|
+
// name opens the leaf's menu
|
|
32
27
|
function hitTest(event: React.MouseEvent) {
|
|
33
28
|
const x = event.nativeEvent.offsetX
|
|
34
29
|
const y = event.nativeEvent.offsetY + offsetY
|
|
@@ -43,11 +38,7 @@ export function useTreeHover({
|
|
|
43
38
|
|
|
44
39
|
function onMouseMove(event: React.MouseEvent) {
|
|
45
40
|
const entry = hitTest(event)
|
|
46
|
-
|
|
47
|
-
entry
|
|
48
|
-
? { ...entry, clientX: event.clientX, clientY: event.clientY }
|
|
49
|
-
: undefined,
|
|
50
|
-
)
|
|
41
|
+
hoverAt(event, entry)
|
|
51
42
|
// hovering an internal node highlights every tip below it; hovering a leaf
|
|
52
43
|
// label additionally drives the single-row highlight
|
|
53
44
|
model.setHoveredTreeNode(entry?.id)
|
|
@@ -58,10 +49,10 @@ export function useTreeHover({
|
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
function onMouseLeave() {
|
|
61
|
-
|
|
52
|
+
clearAnchor()
|
|
62
53
|
model.setHoveredTreeNode(undefined)
|
|
63
54
|
model.setMousePos(undefined, undefined)
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
return { clickMap,
|
|
57
|
+
return { clickMap, anchor, hitTest, onMouseMove, onMouseLeave }
|
|
67
58
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface HoverAnchor<T> {
|
|
4
|
+
clientPoint: { x: number; y: number }
|
|
5
|
+
value: T
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The thing under the pointer and the point a tooltip about it hangs from,
|
|
10
|
+
* 15px below the cursor so the tooltip clears it.
|
|
11
|
+
*/
|
|
12
|
+
export function useHoverAnchor<T>() {
|
|
13
|
+
const [anchor, setAnchor] = useState<HoverAnchor<T>>()
|
|
14
|
+
|
|
15
|
+
function hoverAt(
|
|
16
|
+
event: { clientX: number; clientY: number },
|
|
17
|
+
value: T | undefined,
|
|
18
|
+
) {
|
|
19
|
+
setAnchor(
|
|
20
|
+
value === undefined
|
|
21
|
+
? undefined
|
|
22
|
+
: { value, clientPoint: { x: event.clientX, y: event.clientY + 15 } },
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clearAnchor() {
|
|
27
|
+
setAnchor(undefined)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { anchor, hoverAt, clearAnchor }
|
|
31
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TreeOrder } from './types.ts'
|
|
2
|
+
|
|
1
3
|
// Main model defaults
|
|
2
4
|
export const defaultRowHeight = 16
|
|
3
5
|
export const defaultColWidth = 12
|
|
@@ -75,6 +77,13 @@ export const defaultLabelsAlignRight = false
|
|
|
75
77
|
export const defaultTreeAreaWidth = 400
|
|
76
78
|
export const defaultTreeWidth = 300
|
|
77
79
|
export const defaultShowBranchLen = true
|
|
80
|
+
export const treeOrders = [
|
|
81
|
+
'branchLength',
|
|
82
|
+
'input',
|
|
83
|
+
'ladderize',
|
|
84
|
+
'ladderizeReverse',
|
|
85
|
+
] as const satisfies readonly TreeOrder[]
|
|
86
|
+
export const defaultTreeOrder: TreeOrder = 'branchLength'
|
|
78
87
|
export const defaultDrawTree = true
|
|
79
88
|
export const defaultDrawNodeBubbles = true
|
|
80
89
|
export const defaultDrawNodeLabels = false
|
|
@@ -98,9 +107,11 @@ export const cladeHighlightColor = '#fff3c4'
|
|
|
98
107
|
export const cladeHighlightAlpha = 0.6
|
|
99
108
|
|
|
100
109
|
// The largest inline document the snapshot carries. A larger pasted or
|
|
101
|
-
// locally-opened file stays in the live model and out of the snapshot,
|
|
102
|
-
//
|
|
103
|
-
|
|
110
|
+
// locally-opened file stays in the live model and out of the snapshot, and
|
|
111
|
+
// `unshareableData` warns using the same number. The demo app gzips the
|
|
112
|
+
// snapshot into `?data=`, where 15 kB of the hosted protein alignments encodes
|
|
113
|
+
// to 6,300-7,600 characters, under the 8,192 gmod.org accepts.
|
|
114
|
+
export const maxInlineSnapshotBytes = 15_000
|
|
104
115
|
|
|
105
116
|
// Height of the band the `rowPanels` headers draw in, above the strips in the
|
|
106
117
|
// top area beside the tree ruler and the minimap. A panel wider than the band
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ColumnTrackSpec, Highlight, Region } from './types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `row: null` opts one entry out of the spec's `query` row, back to alignment
|
|
5
|
+
* columns.
|
|
6
|
+
*/
|
|
7
|
+
type RowChoice = { row?: string | null }
|
|
8
|
+
|
|
9
|
+
export type HighlightShorthand =
|
|
10
|
+
| number
|
|
11
|
+
| string
|
|
12
|
+
| (Omit<Highlight, 'row'> & RowChoice)
|
|
13
|
+
|
|
14
|
+
export type RegionShorthand = string | (Omit<Region, 'row'> & RowChoice)
|
|
15
|
+
|
|
16
|
+
export type ColumnTrackShorthand = Omit<
|
|
17
|
+
ColumnTrackSpec,
|
|
18
|
+
'id' | 'kind' | 'row'
|
|
19
|
+
> &
|
|
20
|
+
RowChoice & {
|
|
21
|
+
id?: string
|
|
22
|
+
kind?: ColumnTrackSpec['kind']
|
|
23
|
+
/** the 1-based position `values` or `data` begins at */
|
|
24
|
+
start?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The short forms a hand-written or generated link can use. Every long form
|
|
29
|
+
* passes through unchanged, so a snapshot is also a valid spec.
|
|
30
|
+
*/
|
|
31
|
+
export interface MsaSpec {
|
|
32
|
+
/**
|
|
33
|
+
* The row the spec is about: sets `relativeTo`, and is the `row` of every
|
|
34
|
+
* highlight, region and column track that names none
|
|
35
|
+
*/
|
|
36
|
+
query?: string
|
|
37
|
+
/** a url, or the alignment itself when it spans more than one line */
|
|
38
|
+
msa?: string
|
|
39
|
+
/** a url, or a newick string when it starts with "(" */
|
|
40
|
+
tree?: string
|
|
41
|
+
highlights?: HighlightShorthand[]
|
|
42
|
+
region?: RegionShorthand
|
|
43
|
+
columnTracks?: ColumnTrackShorthand[]
|
|
44
|
+
[key: string]: unknown
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The label a single-residue shorthand highlight gets, filled in from the
|
|
49
|
+
* sequence when drawn: `175` on a row reading R there becomes "R175".
|
|
50
|
+
*/
|
|
51
|
+
export const residueLabel = '{residue}{position}'
|
|
52
|
+
|
|
53
|
+
const span = /^(\d+)(?:-(\d+))?(?:\s+(.+))?$/
|
|
54
|
+
|
|
55
|
+
function parseSpan(text: string, what: string) {
|
|
56
|
+
const match = span.exec(text.trim())
|
|
57
|
+
if (!match) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`${what} "${text}" is not "start", "start-end" or either followed by a label`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
const a = Number(match[1])
|
|
63
|
+
const b = match[2] === undefined ? a : Number(match[2])
|
|
64
|
+
return { start: Math.min(a, b), end: Math.max(a, b), label: match[3] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function withRow<T extends RowChoice>(entry: T, query?: string) {
|
|
68
|
+
const { row, ...rest } = entry
|
|
69
|
+
const resolved = row === undefined ? query : (row ?? undefined)
|
|
70
|
+
return resolved === undefined ? rest : { ...rest, row: resolved }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function expandHighlight(entry: HighlightShorthand, query?: string) {
|
|
74
|
+
if (typeof entry === 'object') {
|
|
75
|
+
return (
|
|
76
|
+
entry.rows
|
|
77
|
+
? entry
|
|
78
|
+
: withRow(entry, entry.start === undefined ? undefined : query)
|
|
79
|
+
) as Highlight
|
|
80
|
+
}
|
|
81
|
+
const { start, end, label } = parseSpan(String(entry), 'highlight')
|
|
82
|
+
const single = start === end && query !== undefined
|
|
83
|
+
return {
|
|
84
|
+
...(query === undefined ? {} : { row: query }),
|
|
85
|
+
start,
|
|
86
|
+
end,
|
|
87
|
+
...(label ? { label } : single ? { label: residueLabel } : {}),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function expandRegion(region: RegionShorthand, query?: string) {
|
|
92
|
+
if (typeof region === 'string') {
|
|
93
|
+
const { start, end } = parseSpan(region, 'region')
|
|
94
|
+
const parsed: Omit<Region, 'row'> & RowChoice = { start, end }
|
|
95
|
+
return withRow(parsed, query) as Region
|
|
96
|
+
}
|
|
97
|
+
return withRow(region, query) as Region
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function slug(name: string) {
|
|
101
|
+
return name
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replaceAll(/[^a-z0-9]+/g, '-')
|
|
104
|
+
.replaceAll(/^-|-$/g, '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function kindOf(track: ColumnTrackShorthand) {
|
|
108
|
+
if (track.kind) {
|
|
109
|
+
return track.kind
|
|
110
|
+
}
|
|
111
|
+
if (track.values) {
|
|
112
|
+
return 'bar'
|
|
113
|
+
}
|
|
114
|
+
if (track.data !== undefined) {
|
|
115
|
+
return 'text'
|
|
116
|
+
}
|
|
117
|
+
if (track.arcs) {
|
|
118
|
+
return 'arc'
|
|
119
|
+
}
|
|
120
|
+
throw new Error(
|
|
121
|
+
`column track "${track.name}" has no values, data or arcs to take its kind from`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function expandTrack(
|
|
126
|
+
track: ColumnTrackShorthand,
|
|
127
|
+
query: string | undefined,
|
|
128
|
+
ids: Set<string>,
|
|
129
|
+
): ColumnTrackSpec {
|
|
130
|
+
const { start, ...rest } = track
|
|
131
|
+
const pad = start && start > 1 ? start - 1 : 0
|
|
132
|
+
let id = track.id ?? (slug(track.name) || 'track')
|
|
133
|
+
for (let n = 2; !track.id && ids.has(id); n++) {
|
|
134
|
+
id = `${slug(track.name) || 'track'}-${n}`
|
|
135
|
+
}
|
|
136
|
+
ids.add(id)
|
|
137
|
+
return {
|
|
138
|
+
...(withRow(rest, query) as Omit<ColumnTrackSpec, 'id' | 'kind'>),
|
|
139
|
+
id,
|
|
140
|
+
kind: kindOf(track),
|
|
141
|
+
...(pad && track.values
|
|
142
|
+
? { values: [...Array<number>(pad).fill(0), ...track.values] }
|
|
143
|
+
: {}),
|
|
144
|
+
...(pad && track.data !== undefined
|
|
145
|
+
? { data: ' '.repeat(pad) + track.data }
|
|
146
|
+
: {}),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isInlineMsa(msa: string) {
|
|
151
|
+
return msa.includes('\n')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isInlineTree(tree: string) {
|
|
155
|
+
return tree.trimStart().startsWith('(')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function uri(location: string) {
|
|
159
|
+
return { uri: location, locationType: 'UriLocation' as const }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Expand the short forms of an MsaView spec into the snapshot the model takes.
|
|
164
|
+
* Idempotent, and a no-op on a spec that uses none of them.
|
|
165
|
+
*/
|
|
166
|
+
export function expandSpec(spec: MsaSpec): Record<string, unknown> {
|
|
167
|
+
const { query, msa, tree, highlights, region, columnTracks, ...rest } = spec
|
|
168
|
+
const data = {
|
|
169
|
+
...(rest.data as Record<string, unknown> | undefined),
|
|
170
|
+
...(msa !== undefined && isInlineMsa(msa) ? { msa } : {}),
|
|
171
|
+
...(tree !== undefined && isInlineTree(tree) ? { tree } : {}),
|
|
172
|
+
}
|
|
173
|
+
const ids = new Set(
|
|
174
|
+
(columnTracks ?? []).map(t => t.id).filter(id => id !== undefined),
|
|
175
|
+
)
|
|
176
|
+
return {
|
|
177
|
+
...rest,
|
|
178
|
+
...(msa !== undefined && !isInlineMsa(msa)
|
|
179
|
+
? { msaFilehandle: uri(msa) }
|
|
180
|
+
: {}),
|
|
181
|
+
...(tree !== undefined && !isInlineTree(tree)
|
|
182
|
+
? { treeFilehandle: uri(tree) }
|
|
183
|
+
: {}),
|
|
184
|
+
...(Object.keys(data).length > 0 ? { data } : {}),
|
|
185
|
+
...(query !== undefined && rest.relativeTo === undefined
|
|
186
|
+
? { relativeTo: query }
|
|
187
|
+
: {}),
|
|
188
|
+
...(highlights
|
|
189
|
+
? { highlights: highlights.map(h => expandHighlight(h, query)) }
|
|
190
|
+
: {}),
|
|
191
|
+
...(region === undefined ? {} : { region: expandRegion(region, query) }),
|
|
192
|
+
...(columnTracks
|
|
193
|
+
? { columnTracks: columnTracks.map(t => expandTrack(t, query, ids)) }
|
|
194
|
+
: {}),
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/exportFileName.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { FileLocation } from '@jbrowse/core/util/types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The name to save an export under: the loaded
|
|
5
|
-
*
|
|
4
|
+
* The name to save an export under: the loaded file's name with the extension
|
|
5
|
+
* swapped, or `fallback` when the data was pasted in.
|
|
6
6
|
*/
|
|
7
7
|
export function exportFileName(
|
|
8
8
|
location: FileLocation | undefined,
|
|
9
9
|
extension: string,
|
|
10
|
+
fallback = 'image',
|
|
10
11
|
) {
|
|
11
12
|
const loc = location as
|
|
12
13
|
| { uri?: string; localPath?: string; name?: string }
|
|
@@ -17,5 +18,5 @@ export function exportFileName(
|
|
|
17
18
|
?.split(/[/\\]/)
|
|
18
19
|
.pop()
|
|
19
20
|
?.replace(/\.[^.]*$/, '')
|
|
20
|
-
return `${base ||
|
|
21
|
+
return `${base || fallback}.${extension}`
|
|
21
22
|
}
|
package/src/hierarchy.ts
CHANGED
|
@@ -25,16 +25,16 @@ export interface HierarchyNode<T = NodeWithIds> extends CoreHierarchyNode<T> {
|
|
|
25
25
|
parent: HierarchyNode<T> | null
|
|
26
26
|
x?: number
|
|
27
27
|
y?: number
|
|
28
|
-
// the span of `x` over this node's subtree
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// the span of `x` over this node's subtree. Leaves are laid out in order, so
|
|
29
|
+
// a subtree occupies one contiguous run of rows and a renderer can skip the
|
|
30
|
+
// whole thing when the run misses its block.
|
|
31
31
|
xMin?: number
|
|
32
32
|
xMax?: number
|
|
33
33
|
len?: number
|
|
34
34
|
depthToLeaf?: number
|
|
35
35
|
_children?: HierarchyNode<T>[] | null
|
|
36
36
|
// pixel x-position of the farthest tip of a collapsed subtree, where the base
|
|
37
|
-
// of the collapsed-clade triangle goes
|
|
37
|
+
// of the collapsed-clade triangle goes
|
|
38
38
|
collapsedTipXFar?: number
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -111,6 +111,153 @@ export function clusterLayout<T>(
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
export interface TreeScale {
|
|
115
|
+
readonly rowHeight: number
|
|
116
|
+
readonly treeWidth: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface LayoutFrame {
|
|
120
|
+
scale: TreeScale
|
|
121
|
+
rootToTipLength: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A node of `layoutTree`: its source node's fields plus its place in unit
|
|
126
|
+
* space, rows down and branch length across. The pixel getters multiply by the
|
|
127
|
+
* scale on every read, so a zoom leaves the nodes valid, and an observer
|
|
128
|
+
* reading `x` observes whatever the scale reads.
|
|
129
|
+
*/
|
|
130
|
+
export class LaidOutNode<T> implements HierarchyNode<T> {
|
|
131
|
+
data: T
|
|
132
|
+
depth: number
|
|
133
|
+
height: number
|
|
134
|
+
value?: number
|
|
135
|
+
children: LaidOutNode<T>[] | null
|
|
136
|
+
depthToLeaf = 0
|
|
137
|
+
row = 0
|
|
138
|
+
rowMin = 0
|
|
139
|
+
rowMax = 0
|
|
140
|
+
depthFraction = 0
|
|
141
|
+
branchLength = 0
|
|
142
|
+
collapsedBranchLength?: number
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
source: HierarchyNode<T>,
|
|
146
|
+
readonly parent: LaidOutNode<T> | null,
|
|
147
|
+
private readonly frame: LayoutFrame,
|
|
148
|
+
) {
|
|
149
|
+
this.data = source.data
|
|
150
|
+
this.depth = source.depth
|
|
151
|
+
this.height = source.height
|
|
152
|
+
this.value = source.value
|
|
153
|
+
this.children = source.children ? [] : null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private get pxPerBranchLength() {
|
|
157
|
+
const { scale, rootToTipLength } = this.frame
|
|
158
|
+
return rootToTipLength ? scale.treeWidth / rootToTipLength : 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get x() {
|
|
162
|
+
return this.row * this.frame.scale.rowHeight
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get xMin() {
|
|
166
|
+
return this.rowMin * this.frame.scale.rowHeight
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get xMax() {
|
|
170
|
+
return this.rowMax * this.frame.scale.rowHeight
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get y() {
|
|
174
|
+
return this.depthFraction * this.frame.scale.treeWidth
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get len() {
|
|
178
|
+
return this.branchLength * this.pxPerBranchLength
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get collapsedTipXFar() {
|
|
182
|
+
return this.collapsedBranchLength === undefined
|
|
183
|
+
? undefined
|
|
184
|
+
: this.collapsedBranchLength * this.pxPerBranchLength
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The layout `clusterLayout` and `setBrLength` write, on new nodes: tips one
|
|
190
|
+
* row apart, a parent at the mean of its children, and branch length measured
|
|
191
|
+
* from the root, whose own branch is not drawn. A collapsed node records the
|
|
192
|
+
* farthest tip of its hidden subtree, where the base of its triangle goes.
|
|
193
|
+
*/
|
|
194
|
+
export function layoutTree<T extends { length?: number }>(
|
|
195
|
+
root: HierarchyNode<T>,
|
|
196
|
+
scale: TreeScale,
|
|
197
|
+
) {
|
|
198
|
+
const frame: LayoutFrame = { scale, rootToTipLength: 0 }
|
|
199
|
+
const nodes: LaidOutNode<T>[] = []
|
|
200
|
+
const leafNodes: LaidOutNode<T>[] = []
|
|
201
|
+
const rootHeight = root.height
|
|
202
|
+
const stack: { source: HierarchyNode<T>; parent: LaidOutNode<T> | null }[] = [
|
|
203
|
+
{ source: root, parent: null },
|
|
204
|
+
]
|
|
205
|
+
while (stack.length > 0) {
|
|
206
|
+
const { source, parent } = stack.pop()!
|
|
207
|
+
const node = new LaidOutNode(source, parent, frame)
|
|
208
|
+
nodes.push(node)
|
|
209
|
+
node.depthFraction =
|
|
210
|
+
rootHeight === 0 ? 1 : (source.depth - root.depth) / rootHeight
|
|
211
|
+
if (parent) {
|
|
212
|
+
parent.children!.push(node)
|
|
213
|
+
node.branchLength =
|
|
214
|
+
parent.branchLength + Math.max(source.data.length || 0, 0)
|
|
215
|
+
frame.rootToTipLength = Math.max(frame.rootToTipLength, node.branchLength)
|
|
216
|
+
}
|
|
217
|
+
if (source._children) {
|
|
218
|
+
node.collapsedBranchLength =
|
|
219
|
+
node.branchLength + collapsedSubtreeMaxLength(source)
|
|
220
|
+
}
|
|
221
|
+
if (source.children) {
|
|
222
|
+
for (let i = source.children.length - 1; i >= 0; i--) {
|
|
223
|
+
stack.push({ source: source.children[i]!, parent: node })
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
node.row = leafNodes.length + 0.5
|
|
227
|
+
node.depthToLeaf = source.depthToLeaf ?? 0
|
|
228
|
+
leafNodes.push(node)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
233
|
+
const node = nodes[i]!
|
|
234
|
+
if (node.children) {
|
|
235
|
+
let sum = 0
|
|
236
|
+
let min = Infinity
|
|
237
|
+
let max = -Infinity
|
|
238
|
+
let depthToLeaf = 0
|
|
239
|
+
for (const child of node.children) {
|
|
240
|
+
sum += child.row
|
|
241
|
+
min = Math.min(min, child.rowMin)
|
|
242
|
+
max = Math.max(max, child.rowMax)
|
|
243
|
+
depthToLeaf = Math.max(depthToLeaf, 1 + child.depthToLeaf)
|
|
244
|
+
}
|
|
245
|
+
node.row = sum / node.children.length
|
|
246
|
+
node.rowMin = min
|
|
247
|
+
node.rowMax = max
|
|
248
|
+
node.depthToLeaf = depthToLeaf
|
|
249
|
+
} else {
|
|
250
|
+
node.rowMin = node.row
|
|
251
|
+
node.rowMax = node.row
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
root: nodes[0]!,
|
|
256
|
+
leaves: leafNodes,
|
|
257
|
+
rootToTipLength: frame.rootToTipLength,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
114
261
|
export function collapse<T>(node: HierarchyNode<T>) {
|
|
115
262
|
if (node.children) {
|
|
116
263
|
// memoize the real subtree depth onto the node before detaching its
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,14 @@ export {
|
|
|
18
18
|
// the row count above which calculateNeighborJoiningTreeFromMSA throws, for
|
|
19
19
|
// hosts gating their own menu item
|
|
20
20
|
export { maxNeighborJoiningRows } from './constants.ts'
|
|
21
|
+
// the short forms a link or a script can write a view in; see docs/layers.md
|
|
22
|
+
export { expandSpec, residueLabel } from './expandSpec.ts'
|
|
23
|
+
export type {
|
|
24
|
+
ColumnTrackShorthand,
|
|
25
|
+
HighlightShorthand,
|
|
26
|
+
MsaSpec,
|
|
27
|
+
RegionShorthand,
|
|
28
|
+
} from './expandSpec.ts'
|
|
21
29
|
export { default as MSAView } from './components/Loading.tsx'
|
|
22
30
|
export { default as MSAViewer } from './components/MSAViewer.tsx'
|
|
23
31
|
export type { MSAViewerProps } from './components/MSAViewer.tsx'
|
|
@@ -28,7 +36,7 @@ export type { MsaSvgFigure } from './useMsaSvgFigure.ts'
|
|
|
28
36
|
export type { MountedViewer } from './mount.tsx'
|
|
29
37
|
export { type MsaViewModel, default as MSAModelF } from './model.ts'
|
|
30
38
|
export type { MSAFormat, MSAParserType } from 'msa-parsers'
|
|
31
|
-
export type { HierarchyNode } from './hierarchy.ts'
|
|
39
|
+
export type { HierarchyNode, LaidOutNode, TreeScale } from './hierarchy.ts'
|
|
32
40
|
// InterProScanResults is the EBI wire format that setDomains still accepts;
|
|
33
41
|
// Annotation is the source-agnostic shape everything else is written against
|
|
34
42
|
export type { InterProScanResults } from 'msa-parsers'
|
|
@@ -78,6 +86,8 @@ export type {
|
|
|
78
86
|
TextTrackModel,
|
|
79
87
|
TidyDomainAnnotation,
|
|
80
88
|
TrackKind,
|
|
89
|
+
TreeOrder,
|
|
90
|
+
TreeRoot,
|
|
81
91
|
UnshareableData,
|
|
82
92
|
Viewport,
|
|
83
93
|
} from './types.ts'
|