react-msaview 8.2.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.
Files changed (192) hide show
  1. package/bundle/index.js +104 -117
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +4 -3
  4. package/dist/columnCounts.js +11 -6
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/CopyButton.d.ts +1 -1
  7. package/dist/components/CopyButton.js +1 -1
  8. package/dist/components/CopyButton.js.map +1 -1
  9. package/dist/components/DragHandle.js +14 -2
  10. package/dist/components/DragHandle.js.map +1 -1
  11. package/dist/components/Loading.js +10 -11
  12. package/dist/components/Loading.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +12 -1
  14. package/dist/components/MSAViewer.js +12 -1
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/SequenceTextArea.d.ts +11 -0
  17. package/dist/components/SequenceTextArea.js +40 -8
  18. package/dist/components/SequenceTextArea.js.map +1 -1
  19. package/dist/components/StatusMessage.d.ts +12 -0
  20. package/dist/components/StatusMessage.js +43 -0
  21. package/dist/components/StatusMessage.js.map +1 -0
  22. package/dist/components/Track.js +3 -3
  23. package/dist/components/Track.js.map +1 -1
  24. package/dist/components/VerticalScrollbar.js +6 -1
  25. package/dist/components/VerticalScrollbar.js.map +1 -1
  26. package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
  27. package/dist/components/dialogs/ExportSVGDialog.js +4 -2
  28. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  29. package/dist/components/dialogs/FeatureDialog.js +15 -10
  30. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  31. package/dist/components/getVisibleLeaves.d.ts +1 -1
  32. package/dist/components/header/FileMenu.js +9 -0
  33. package/dist/components/header/FileMenu.js.map +1 -1
  34. package/dist/components/header/GappynessSlider.js +15 -3
  35. package/dist/components/header/GappynessSlider.js.map +1 -1
  36. package/dist/components/header/Header.js +6 -7
  37. package/dist/components/header/Header.js.map +1 -1
  38. package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
  39. package/dist/components/header/RowSearch.js +15 -0
  40. package/dist/components/header/RowSearch.js.map +1 -0
  41. package/dist/components/header/revealRow.d.ts +3 -0
  42. package/dist/components/header/revealRow.js +15 -0
  43. package/dist/components/header/revealRow.js.map +1 -0
  44. package/dist/components/header/settingsMenuItems.js +57 -1
  45. package/dist/components/header/settingsMenuItems.js.map +1 -1
  46. package/dist/components/import/ImportForm.js +10 -4
  47. package/dist/components/import/ImportForm.js.map +1 -1
  48. package/dist/components/import/ImportFormExamples.js +19 -27
  49. package/dist/components/import/ImportFormExamples.js.map +1 -1
  50. package/dist/components/minimap/Minimap.js +6 -1
  51. package/dist/components/minimap/Minimap.js.map +1 -1
  52. package/dist/components/msa/AnnotationLegend.js +11 -5
  53. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  54. package/dist/components/msa/MSACanvas.js +6 -1
  55. package/dist/components/msa/MSACanvas.js.map +1 -1
  56. package/dist/components/msa/MSACanvasBlock.js +3 -7
  57. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  58. package/dist/components/msa/msaKeys.d.ts +5 -0
  59. package/dist/components/msa/msaKeys.js +35 -0
  60. package/dist/components/msa/msaKeys.js.map +1 -0
  61. package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
  62. package/dist/components/msa/useMsaBlockMouse.js +18 -5
  63. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  64. package/dist/components/rowpanels/RowPanelBlock.js +10 -14
  65. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  66. package/dist/components/tracks/drawTracks.d.ts +1 -0
  67. package/dist/components/tracks/drawTracks.js +14 -2
  68. package/dist/components/tracks/drawTracks.js.map +1 -1
  69. package/dist/components/tracks/useTrackHover.d.ts +1 -5
  70. package/dist/components/tracks/useTrackHover.js +5 -5
  71. package/dist/components/tracks/useTrackHover.js.map +1 -1
  72. package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
  73. package/dist/components/tree/ScaleBarSVG.js +11 -0
  74. package/dist/components/tree/ScaleBarSVG.js.map +1 -0
  75. package/dist/components/tree/TreeCanvas.js +8 -26
  76. package/dist/components/tree/TreeCanvas.js.map +1 -1
  77. package/dist/components/tree/TreeCanvasBlock.js +11 -20
  78. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  79. package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
  80. package/dist/components/tree/TreeMenu.js +48 -0
  81. package/dist/components/tree/TreeMenu.js.map +1 -0
  82. package/dist/components/tree/TreeRuler.js +3 -3
  83. package/dist/components/tree/TreeRuler.js.map +1 -1
  84. package/dist/components/tree/renderTreeCanvas.js +19 -9
  85. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  86. package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
  87. package/dist/components/tree/renderTreeMouseover.js +20 -0
  88. package/dist/components/tree/renderTreeMouseover.js.map +1 -0
  89. package/dist/components/tree/useTreeHover.d.ts +3 -8
  90. package/dist/components/tree/useTreeHover.js +8 -10
  91. package/dist/components/tree/useTreeHover.js.map +1 -1
  92. package/dist/components/useHoverAnchor.d.ts +19 -0
  93. package/dist/components/useHoverAnchor.js +18 -0
  94. package/dist/components/useHoverAnchor.js.map +1 -0
  95. package/dist/constants.d.ts +4 -1
  96. package/dist/constants.js +12 -3
  97. package/dist/constants.js.map +1 -1
  98. package/dist/expandSpec.js +3 -3
  99. package/dist/expandSpec.js.map +1 -1
  100. package/dist/exportFileName.d.ts +3 -3
  101. package/dist/exportFileName.js +4 -4
  102. package/dist/exportFileName.js.map +1 -1
  103. package/dist/hierarchy.d.ts +51 -0
  104. package/dist/hierarchy.js +125 -0
  105. package/dist/hierarchy.js.map +1 -1
  106. package/dist/index.d.ts +2 -2
  107. package/dist/model/treeModel.d.ts +12 -0
  108. package/dist/model/treeModel.js +14 -1
  109. package/dist/model/treeModel.js.map +1 -1
  110. package/dist/model.d.ts +168 -47
  111. package/dist/model.js +435 -192
  112. package/dist/model.js.map +1 -1
  113. package/dist/renderToSvg.js +2 -4
  114. package/dist/renderToSvg.js.map +1 -1
  115. package/dist/rerootTree.d.ts +33 -0
  116. package/dist/rerootTree.js +144 -0
  117. package/dist/rerootTree.js.map +1 -0
  118. package/dist/svgTestUtil.d.ts +106 -20
  119. package/dist/types.d.ts +25 -12
  120. package/dist/useMsaSvgFigure.d.ts +2 -1
  121. package/dist/useMsaSvgFigure.js +11 -4
  122. package/dist/useMsaSvgFigure.js.map +1 -1
  123. package/dist/version.d.ts +1 -1
  124. package/dist/version.js +1 -1
  125. package/dist/writeNewick.d.ts +8 -0
  126. package/dist/writeNewick.js +34 -0
  127. package/dist/writeNewick.js.map +1 -0
  128. package/package.json +3 -3
  129. package/src/columnCounts.ts +14 -6
  130. package/src/components/CopyButton.tsx +2 -2
  131. package/src/components/DragHandle.tsx +21 -1
  132. package/src/components/Loading.tsx +14 -30
  133. package/src/components/MSAViewer.tsx +26 -0
  134. package/src/components/SequenceTextArea.tsx +54 -8
  135. package/src/components/StatusMessage.tsx +70 -0
  136. package/src/components/Track.tsx +8 -4
  137. package/src/components/VerticalScrollbar.tsx +7 -0
  138. package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
  139. package/src/components/dialogs/FeatureDialog.tsx +59 -38
  140. package/src/components/header/FileMenu.tsx +9 -0
  141. package/src/components/header/GappynessSlider.tsx +17 -4
  142. package/src/components/header/Header.tsx +7 -8
  143. package/src/components/header/RowSearch.tsx +34 -0
  144. package/src/components/header/revealRow.ts +17 -0
  145. package/src/components/header/settingsMenuItems.ts +66 -0
  146. package/src/components/import/ImportForm.tsx +14 -7
  147. package/src/components/import/ImportFormExamples.tsx +20 -28
  148. package/src/components/minimap/Minimap.tsx +7 -0
  149. package/src/components/msa/AnnotationLegend.tsx +12 -6
  150. package/src/components/msa/MSACanvas.tsx +8 -0
  151. package/src/components/msa/MSACanvasBlock.tsx +5 -11
  152. package/src/components/msa/msaKeys.ts +39 -0
  153. package/src/components/msa/useMsaBlockMouse.ts +23 -7
  154. package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
  155. package/src/components/tracks/drawTracks.ts +15 -2
  156. package/src/components/tracks/useTrackHover.ts +5 -5
  157. package/src/components/tree/ScaleBarSVG.tsx +30 -0
  158. package/src/components/tree/TreeCanvas.tsx +9 -40
  159. package/src/components/tree/TreeCanvasBlock.tsx +19 -39
  160. package/src/components/tree/TreeMenu.tsx +129 -0
  161. package/src/components/tree/TreeRuler.tsx +7 -13
  162. package/src/components/tree/renderTreeCanvas.ts +20 -8
  163. package/src/components/tree/renderTreeMouseover.ts +41 -0
  164. package/src/components/tree/useTreeHover.ts +8 -17
  165. package/src/components/useHoverAnchor.ts +31 -0
  166. package/src/constants.ts +14 -3
  167. package/src/expandSpec.ts +3 -3
  168. package/src/exportFileName.ts +4 -3
  169. package/src/hierarchy.ts +151 -4
  170. package/src/index.ts +3 -1
  171. package/src/model/treeModel.ts +22 -0
  172. package/src/model.ts +534 -223
  173. package/src/renderToSvg.tsx +6 -9
  174. package/src/rerootTree.ts +172 -0
  175. package/src/types.ts +34 -12
  176. package/src/useMsaSvgFigure.ts +11 -4
  177. package/src/version.ts +1 -1
  178. package/src/writeNewick.ts +38 -0
  179. package/dist/components/PortalTooltip.d.ts +0 -11
  180. package/dist/components/PortalTooltip.js +0 -29
  181. package/dist/components/PortalTooltip.js.map +0 -1
  182. package/dist/components/header/HeaderStatusArea.js +0 -21
  183. package/dist/components/header/HeaderStatusArea.js.map +0 -1
  184. package/dist/components/tree/TreeBranchMenu.js +0 -29
  185. package/dist/components/tree/TreeBranchMenu.js.map +0 -1
  186. package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
  187. package/dist/components/tree/TreeNodeMenu.js +0 -41
  188. package/dist/components/tree/TreeNodeMenu.js.map +0 -1
  189. package/src/components/PortalTooltip.tsx +0 -47
  190. package/src/components/header/HeaderStatusArea.tsx +0 -32
  191. package/src/components/tree/TreeBranchMenu.tsx +0 -68
  192. 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, name } = node.data
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: count === undefined ? label : `${label} (${count} tips)`,
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, name } = data
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`, washed across the
576
- // tree area under the labels, with each set's label in the gutter at its first
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 currently under the pointer, and the model hover state that the
17
- * MSA panel mirrors. The index is a render artifact rather than display state, so
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 [hovered, setHovered] = useState<TreeHoverTarget>()
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 node menu rather than the branch menu
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
- setHovered(
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
- setHovered(undefined)
52
+ clearAnchor()
62
53
  model.setHoveredTreeNode(undefined)
63
54
  model.setMousePos(undefined, undefined)
64
55
  }
65
56
 
66
- return { clickMap, hovered, hitTest, onMouseMove, onMouseLeave }
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, keeping
102
- // shared URLs sendable. `unshareableData` warns using the same number.
103
- export const maxInlineSnapshotBytes = 50_000
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
package/src/expandSpec.ts CHANGED
@@ -59,9 +59,9 @@ function parseSpan(text: string, what: string) {
59
59
  `${what} "${text}" is not "start", "start-end" or either followed by a label`,
60
60
  )
61
61
  }
62
- const start = Number(match[1])
63
- const end = match[2] === undefined ? start : Number(match[2])
64
- return { start, end, label: match[3] }
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
65
  }
66
66
 
67
67
  function withRow<T extends RowChoice>(entry: T, query?: string) {
@@ -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 alignment's file name with the
5
- * extension swapped, or a plain default when the alignment was pasted in.
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 || 'image'}.${extension}`
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, set by clusterLayout. Leaves are
29
- // laid out in order, so a subtree occupies one contiguous run of rows and a
30
- // renderer can skip the whole thing when the run misses its block.
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. Set in the model's hierarchy getter.
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
@@ -36,7 +36,7 @@ export type { MsaSvgFigure } from './useMsaSvgFigure.ts'
36
36
  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
- export type { HierarchyNode } from './hierarchy.ts'
39
+ export type { HierarchyNode, LaidOutNode, TreeScale } from './hierarchy.ts'
40
40
  // InterProScanResults is the EBI wire format that setDomains still accepts;
41
41
  // Annotation is the source-agnostic shape everything else is written against
42
42
  export type { InterProScanResults } from 'msa-parsers'
@@ -86,6 +86,8 @@ export type {
86
86
  TextTrackModel,
87
87
  TidyDomainAnnotation,
88
88
  TrackKind,
89
+ TreeOrder,
90
+ TreeRoot,
89
91
  UnshareableData,
90
92
  Viewport,
91
93
  } from './types.ts'
@@ -8,12 +8,16 @@ import {
8
8
  defaultLabelsAlignRight,
9
9
  defaultOverviewHeight,
10
10
  defaultShowBranchLen,
11
+ defaultTreeOrder,
11
12
  defaultShowTreeOverview,
12
13
  defaultTreeAreaWidth,
13
14
  defaultTreeWidth,
15
+ treeOrders,
14
16
  } from '../constants.ts'
15
17
  import { stripDefault } from '../stripDefault.ts'
16
18
 
19
+ import type { TreeOrder } from '../types.ts'
20
+
17
21
  /**
18
22
  * #stateModel Tree
19
23
  */
@@ -51,6 +55,17 @@ export function TreeModelF() {
51
55
  */
52
56
  showBranchLen: stripDefault(types.boolean, defaultShowBranchLen),
53
57
 
58
+ /**
59
+ * #property
60
+ * the order each node's children draw in: `branchLength` (shortest
61
+ * first), `input` (as the file gives them), `ladderize` (fewest tips
62
+ * first) or `ladderizeReverse` (most tips first)
63
+ */
64
+ treeOrder: stripDefault(
65
+ types.enumeration<TreeOrder>('TreeOrder', [...treeOrders]),
66
+ defaultTreeOrder,
67
+ ),
68
+
54
69
  /**
55
70
  * #property
56
71
  * draw tree, boolean
@@ -133,6 +148,13 @@ export function TreeModelF() {
133
148
  self.showBranchLen = arg
134
149
  },
135
150
 
151
+ /**
152
+ * #action
153
+ */
154
+ setTreeOrder(order: TreeOrder) {
155
+ self.treeOrder = order
156
+ },
157
+
136
158
  /**
137
159
  * #action
138
160
  */