react-msaview 8.2.0 → 8.4.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 (255) hide show
  1. package/bundle/index.js +77 -300
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blockFasta.d.ts +6 -0
  4. package/dist/blockFasta.js +12 -0
  5. package/dist/blockFasta.js.map +1 -0
  6. package/dist/colorSchemes.d.ts +7 -0
  7. package/dist/colorSchemes.js +15 -0
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/columnCounts.d.ts +4 -3
  10. package/dist/columnCounts.js +11 -6
  11. package/dist/columnCounts.js.map +1 -1
  12. package/dist/components/CopyButton.d.ts +1 -1
  13. package/dist/components/CopyButton.js +1 -1
  14. package/dist/components/CopyButton.js.map +1 -1
  15. package/dist/components/DragHandle.js +14 -2
  16. package/dist/components/DragHandle.js.map +1 -1
  17. package/dist/components/Loading.js +10 -11
  18. package/dist/components/Loading.js.map +1 -1
  19. package/dist/components/MSAView.js +15 -9
  20. package/dist/components/MSAView.js.map +1 -1
  21. package/dist/components/MSAViewer.d.ts +39 -7
  22. package/dist/components/MSAViewer.js +72 -22
  23. package/dist/components/MSAViewer.js.map +1 -1
  24. package/dist/components/MetadataTable.d.ts +5 -0
  25. package/dist/components/MetadataTable.js +29 -0
  26. package/dist/components/MetadataTable.js.map +1 -0
  27. package/dist/components/SequenceTextArea.d.ts +11 -0
  28. package/dist/components/SequenceTextArea.js +40 -8
  29. package/dist/components/SequenceTextArea.js.map +1 -1
  30. package/dist/components/StatusMessage.d.ts +12 -0
  31. package/dist/components/StatusMessage.js +43 -0
  32. package/dist/components/StatusMessage.js.map +1 -0
  33. package/dist/components/Track.js +13 -4
  34. package/dist/components/Track.js.map +1 -1
  35. package/dist/components/VerticalScrollbar.js +6 -1
  36. package/dist/components/VerticalScrollbar.js.map +1 -1
  37. package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
  38. package/dist/components/dialogs/ExportSVGDialog.js +4 -2
  39. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  40. package/dist/components/dialogs/FeatureDialog.js +15 -10
  41. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  42. package/dist/components/dialogs/MetadataDialog.js +5 -6
  43. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  44. package/dist/components/getVisibleLeaves.d.ts +1 -1
  45. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  46. package/dist/components/header/ColorSchemeMenu.js +23 -5
  47. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  48. package/dist/components/header/FileMenu.js +9 -0
  49. package/dist/components/header/FileMenu.js.map +1 -1
  50. package/dist/components/header/GappynessSlider.js +15 -3
  51. package/dist/components/header/GappynessSlider.js.map +1 -1
  52. package/dist/components/header/{HeaderStatusArea.d.ts → GoToBox.d.ts} +2 -2
  53. package/dist/components/header/GoToBox.js +21 -0
  54. package/dist/components/header/GoToBox.js.map +1 -0
  55. package/dist/components/header/Header.js +8 -7
  56. package/dist/components/header/Header.js.map +1 -1
  57. package/dist/components/header/SelectionMenu.d.ts +12 -0
  58. package/dist/components/header/SelectionMenu.js +72 -0
  59. package/dist/components/header/SelectionMenu.js.map +1 -0
  60. package/dist/components/header/goTo.d.ts +20 -0
  61. package/dist/components/header/goTo.js +69 -0
  62. package/dist/components/header/goTo.js.map +1 -0
  63. package/dist/components/header/settingsMenuItems.js +57 -1
  64. package/dist/components/header/settingsMenuItems.js.map +1 -1
  65. package/dist/components/import/ImportForm.js +10 -4
  66. package/dist/components/import/ImportForm.js.map +1 -1
  67. package/dist/components/import/ImportFormExamples.js +19 -27
  68. package/dist/components/import/ImportFormExamples.js.map +1 -1
  69. package/dist/components/minimap/Minimap.js +6 -1
  70. package/dist/components/minimap/Minimap.js.map +1 -1
  71. package/dist/components/msa/AnnotationLegend.js +11 -5
  72. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  73. package/dist/components/msa/MSACanvas.js +17 -1
  74. package/dist/components/msa/MSACanvas.js.map +1 -1
  75. package/dist/components/msa/MSACanvasBlock.js +6 -6
  76. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  77. package/dist/components/msa/msaKeys.d.ts +5 -0
  78. package/dist/components/msa/msaKeys.js +41 -0
  79. package/dist/components/msa/msaKeys.js.map +1 -0
  80. package/dist/components/msa/msaRaster.js +1 -0
  81. package/dist/components/msa/msaRaster.js.map +1 -1
  82. package/dist/components/msa/renderHighlights.d.ts +14 -3
  83. package/dist/components/msa/renderHighlights.js +29 -4
  84. package/dist/components/msa/renderHighlights.js.map +1 -1
  85. package/dist/components/msa/tileColor.js +9 -8
  86. package/dist/components/msa/tileColor.js.map +1 -1
  87. package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
  88. package/dist/components/msa/useMsaBlockMouse.js +34 -14
  89. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  90. package/dist/components/overlayColors.d.ts +2 -0
  91. package/dist/components/overlayColors.js +3 -0
  92. package/dist/components/overlayColors.js.map +1 -1
  93. package/dist/components/rowpanels/RowPanelBlock.js +10 -14
  94. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  95. package/dist/components/selectionDrag.d.ts +17 -0
  96. package/dist/components/selectionDrag.js +30 -0
  97. package/dist/components/selectionDrag.js.map +1 -0
  98. package/dist/components/tracks/drawTracks.d.ts +1 -0
  99. package/dist/components/tracks/drawTracks.js +14 -2
  100. package/dist/components/tracks/drawTracks.js.map +1 -1
  101. package/dist/components/tracks/useTrackHover.d.ts +4 -6
  102. package/dist/components/tracks/useTrackHover.js +30 -10
  103. package/dist/components/tracks/useTrackHover.js.map +1 -1
  104. package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
  105. package/dist/components/tree/ScaleBarSVG.js +11 -0
  106. package/dist/components/tree/ScaleBarSVG.js.map +1 -0
  107. package/dist/components/tree/TreeCanvas.js +8 -26
  108. package/dist/components/tree/TreeCanvas.js.map +1 -1
  109. package/dist/components/tree/TreeCanvasBlock.js +11 -20
  110. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  111. package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
  112. package/dist/components/tree/TreeMenu.js +48 -0
  113. package/dist/components/tree/TreeMenu.js.map +1 -0
  114. package/dist/components/tree/TreeRuler.js +3 -3
  115. package/dist/components/tree/TreeRuler.js.map +1 -1
  116. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
  117. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  118. package/dist/components/tree/renderTreeCanvas.js +19 -9
  119. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  120. package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
  121. package/dist/components/tree/renderTreeMouseover.js +20 -0
  122. package/dist/components/tree/renderTreeMouseover.js.map +1 -0
  123. package/dist/components/tree/useTreeHover.d.ts +3 -8
  124. package/dist/components/tree/useTreeHover.js +8 -10
  125. package/dist/components/tree/useTreeHover.js.map +1 -1
  126. package/dist/components/useHoverAnchor.d.ts +19 -0
  127. package/dist/components/useHoverAnchor.js +18 -0
  128. package/dist/components/useHoverAnchor.js.map +1 -0
  129. package/dist/constants.d.ts +3 -0
  130. package/dist/constants.js +12 -3
  131. package/dist/constants.js.map +1 -1
  132. package/dist/element.d.ts +7 -3
  133. package/dist/element.js +32 -4
  134. package/dist/element.js.map +1 -1
  135. package/dist/expandSpec.js +3 -3
  136. package/dist/expandSpec.js.map +1 -1
  137. package/dist/exportFileName.d.ts +3 -3
  138. package/dist/exportFileName.js +4 -4
  139. package/dist/exportFileName.js.map +1 -1
  140. package/dist/featuresLayer.d.ts +8 -0
  141. package/dist/featuresLayer.js +42 -0
  142. package/dist/featuresLayer.js.map +1 -0
  143. package/dist/hierarchy.d.ts +51 -0
  144. package/dist/hierarchy.js +125 -0
  145. package/dist/hierarchy.js.map +1 -1
  146. package/dist/index.d.ts +2 -2
  147. package/dist/model/DataModel.d.ts +1 -1
  148. package/dist/model/DataModel.js +3 -3
  149. package/dist/model/DataModel.js.map +1 -1
  150. package/dist/model/msaModel.d.ts +14 -1
  151. package/dist/model/msaModel.js +17 -1
  152. package/dist/model/msaModel.js.map +1 -1
  153. package/dist/model/treeModel.d.ts +12 -0
  154. package/dist/model/treeModel.js +14 -1
  155. package/dist/model/treeModel.js.map +1 -1
  156. package/dist/model.d.ts +285 -53
  157. package/dist/model.js +693 -231
  158. package/dist/model.js.map +1 -1
  159. package/dist/renderToSvg.js +2 -4
  160. package/dist/renderToSvg.js.map +1 -1
  161. package/dist/rerootTree.d.ts +33 -0
  162. package/dist/rerootTree.js +144 -0
  163. package/dist/rerootTree.js.map +1 -0
  164. package/dist/svgTestUtil.d.ts +172 -26
  165. package/dist/types.d.ts +75 -12
  166. package/dist/useMsaSvgFigure.d.ts +2 -1
  167. package/dist/useMsaSvgFigure.js +11 -4
  168. package/dist/useMsaSvgFigure.js.map +1 -1
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.js +1 -1
  171. package/dist/writeNewick.d.ts +8 -0
  172. package/dist/writeNewick.js +34 -0
  173. package/dist/writeNewick.js.map +1 -0
  174. package/package.json +3 -3
  175. package/src/blockFasta.ts +18 -0
  176. package/src/colorSchemes.ts +20 -0
  177. package/src/columnCounts.ts +14 -6
  178. package/src/components/CopyButton.tsx +2 -2
  179. package/src/components/DragHandle.tsx +21 -1
  180. package/src/components/Loading.tsx +14 -30
  181. package/src/components/MSAView.tsx +16 -9
  182. package/src/components/MSAViewer.tsx +121 -26
  183. package/src/components/MetadataTable.tsx +55 -0
  184. package/src/components/SequenceTextArea.tsx +54 -8
  185. package/src/components/StatusMessage.tsx +70 -0
  186. package/src/components/Track.tsx +21 -4
  187. package/src/components/VerticalScrollbar.tsx +7 -0
  188. package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
  189. package/src/components/dialogs/FeatureDialog.tsx +59 -38
  190. package/src/components/dialogs/MetadataDialog.tsx +9 -8
  191. package/src/components/header/ColorSchemeMenu.tsx +31 -12
  192. package/src/components/header/FileMenu.tsx +9 -0
  193. package/src/components/header/GappynessSlider.tsx +17 -4
  194. package/src/components/header/GoToBox.tsx +50 -0
  195. package/src/components/header/Header.tsx +11 -8
  196. package/src/components/header/SelectionMenu.tsx +117 -0
  197. package/src/components/header/goTo.ts +75 -0
  198. package/src/components/header/settingsMenuItems.ts +66 -0
  199. package/src/components/import/ImportForm.tsx +14 -7
  200. package/src/components/import/ImportFormExamples.tsx +20 -28
  201. package/src/components/minimap/Minimap.tsx +7 -0
  202. package/src/components/msa/AnnotationLegend.tsx +12 -6
  203. package/src/components/msa/MSACanvas.tsx +19 -1
  204. package/src/components/msa/MSACanvasBlock.tsx +9 -11
  205. package/src/components/msa/msaKeys.ts +45 -0
  206. package/src/components/msa/msaRaster.ts +1 -0
  207. package/src/components/msa/renderHighlights.ts +46 -4
  208. package/src/components/msa/tileColor.ts +9 -8
  209. package/src/components/msa/useMsaBlockMouse.ts +44 -31
  210. package/src/components/overlayColors.ts +4 -0
  211. package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
  212. package/src/components/selectionDrag.ts +42 -0
  213. package/src/components/tracks/drawTracks.ts +15 -2
  214. package/src/components/tracks/useTrackHover.ts +32 -10
  215. package/src/components/tree/ScaleBarSVG.tsx +30 -0
  216. package/src/components/tree/TreeCanvas.tsx +9 -40
  217. package/src/components/tree/TreeCanvasBlock.tsx +19 -39
  218. package/src/components/tree/TreeMenu.tsx +129 -0
  219. package/src/components/tree/TreeRuler.tsx +7 -13
  220. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
  221. package/src/components/tree/renderTreeCanvas.ts +20 -8
  222. package/src/components/tree/renderTreeMouseover.ts +41 -0
  223. package/src/components/tree/useTreeHover.ts +8 -17
  224. package/src/components/useHoverAnchor.ts +31 -0
  225. package/src/constants.ts +14 -3
  226. package/src/element.ts +45 -5
  227. package/src/expandSpec.ts +3 -3
  228. package/src/exportFileName.ts +4 -3
  229. package/src/featuresLayer.ts +48 -0
  230. package/src/hierarchy.ts +151 -4
  231. package/src/index.ts +8 -1
  232. package/src/model/DataModel.ts +3 -3
  233. package/src/model/msaModel.ts +19 -1
  234. package/src/model/treeModel.ts +22 -0
  235. package/src/model.ts +841 -272
  236. package/src/renderToSvg.tsx +6 -9
  237. package/src/rerootTree.ts +172 -0
  238. package/src/types.ts +92 -12
  239. package/src/useMsaSvgFigure.ts +11 -4
  240. package/src/version.ts +1 -1
  241. package/src/writeNewick.ts +38 -0
  242. package/dist/components/PortalTooltip.d.ts +0 -11
  243. package/dist/components/PortalTooltip.js +0 -29
  244. package/dist/components/PortalTooltip.js.map +0 -1
  245. package/dist/components/header/HeaderStatusArea.js +0 -21
  246. package/dist/components/header/HeaderStatusArea.js.map +0 -1
  247. package/dist/components/tree/TreeBranchMenu.js +0 -29
  248. package/dist/components/tree/TreeBranchMenu.js.map +0 -1
  249. package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
  250. package/dist/components/tree/TreeNodeMenu.js +0 -41
  251. package/dist/components/tree/TreeNodeMenu.js.map +0 -1
  252. package/src/components/PortalTooltip.tsx +0 -47
  253. package/src/components/header/HeaderStatusArea.tsx +0 -32
  254. package/src/components/tree/TreeBranchMenu.tsx +0 -68
  255. package/src/components/tree/TreeNodeMenu.tsx +0 -95
@@ -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
@@ -97,9 +106,11 @@ export const rowTintAlpha = 0.25
97
106
  export const cladeHighlightColor = '#fff3c4'
98
107
  export const cladeHighlightAlpha = 0.6
99
108
 
100
- // 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.
109
+ // The largest inline document a snapshot carries by default, which keeps a
110
+ // jbrowse session small. A larger pasted or locally-opened file stays in the
111
+ // live model and out of the snapshot, and `unshareableData` warns using the
112
+ // same number. `MSAModelF({ maxInlineSnapshotBytes })` sets another; the demo
113
+ // app's links hold far more.
103
114
  export const maxInlineSnapshotBytes = 50_000
104
115
 
105
116
  // Height of the band the `rowPanels` headers draw in, above the strips in the
package/src/element.ts CHANGED
@@ -7,15 +7,26 @@ import type { MsaViewModel } from './model.ts'
7
7
  import type { MountedViewer } from './mount.tsx'
8
8
  import type {
9
9
  Cell,
10
+ ColorScheme,
10
11
  ColumnTrackSpec,
12
+ Feature,
11
13
  Highlight,
14
+ MsaSelection,
12
15
  ResidueMapping,
13
16
  Viewport,
14
17
  } from './types.ts'
15
18
 
16
19
  type ElementData = Pick<
17
20
  MSAViewerProps,
18
- 'msa' | 'tree' | 'gff' | 'highlights' | 'columnTracks' | 'residueMappings'
21
+ | 'msa'
22
+ | 'tree'
23
+ | 'gff'
24
+ | 'features'
25
+ | 'colorScheme'
26
+ | 'highlights'
27
+ | 'columnTracks'
28
+ | 'residueMappings'
29
+ | 'selection'
19
30
  >
20
31
 
21
32
  interface NightingaleManager extends HTMLElement {
@@ -78,9 +89,13 @@ function residueRange(model: MsaViewModel, row: string, viewport: Viewport) {
78
89
  /**
79
90
  * Registers a custom element that renders MSAViewer, for pages with no React.
80
91
  * Attributes: msa-url, tree-url, gff-url, color-scheme, height, hide-header,
81
- * theme, tree-area-width and reference-row. Properties: msa, tree and gff text, highlights,
82
- * columnTracks and residueMappings. Events: cell-hover, cell-click and
83
- * viewport-change, each with the MSAViewer callback's value as detail.
92
+ * theme, tree-area-width and reference-row. Properties: msa, tree and gff text,
93
+ * features, colorScheme, highlights, columnTracks, residueMappings and
94
+ * selection. The
95
+ * colorScheme property takes a `{map}` the color-scheme attribute cannot hold,
96
+ * and wins over the attribute while set. Events: cell-hover, cell-click,
97
+ * viewport-change and selection-change, each with the MSAViewer callback's
98
+ * value as detail.
84
99
  *
85
100
  * Inside a <nightingale-manager>, the element registers with it and speaks its
86
101
  * protocol: it follows the display-start, display-end and highlight attributes
@@ -122,6 +137,18 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
122
137
  set gff(value: string | undefined) {
123
138
  this.#setData({ gff: value })
124
139
  }
140
+ get features() {
141
+ return this.#data.features
142
+ }
143
+ set features(value: Feature[] | undefined) {
144
+ this.#setData({ features: value })
145
+ }
146
+ get colorScheme() {
147
+ return this.#data.colorScheme
148
+ }
149
+ set colorScheme(value: ColorScheme | undefined) {
150
+ this.#setData({ colorScheme: value })
151
+ }
125
152
  get highlights() {
126
153
  return this.#data.highlights
127
154
  }
@@ -140,6 +167,13 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
140
167
  set residueMappings(value: ResidueMapping[] | undefined) {
141
168
  this.#setData({ residueMappings: value })
142
169
  }
170
+ // the viewer's selection, which the reader's drags change
171
+ get selection() {
172
+ return this.#model ? this.#model.selection : this.#data.selection
173
+ }
174
+ set selection(value: MsaSelection | undefined) {
175
+ this.#setData({ selection: value })
176
+ }
143
177
 
144
178
  get #referenceRow() {
145
179
  return this.getAttribute('reference-row') ?? undefined
@@ -157,7 +191,10 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
157
191
  msaFilehandle: location(this.getAttribute('msa-url')),
158
192
  treeFilehandle: location(this.getAttribute('tree-url')),
159
193
  gffFilehandle: location(this.getAttribute('gff-url')),
160
- colorScheme: this.getAttribute('color-scheme') ?? undefined,
194
+ colorScheme:
195
+ this.#data.colorScheme ??
196
+ this.getAttribute('color-scheme') ??
197
+ undefined,
161
198
  height: numberAttribute(this.getAttribute('height')),
162
199
  treeAreaWidth: numberAttribute(this.getAttribute('tree-area-width')),
163
200
  hideHeader: this.hasAttribute('hide-header'),
@@ -173,6 +210,9 @@ export function defineMsaElement(tagName = 'jbrowse-msa') {
173
210
  this.#emit('viewport-change', viewport)
174
211
  this.#reportRange(viewport)
175
212
  },
213
+ onSelectionChange: selection => {
214
+ this.#emit('selection-change', selection)
215
+ },
176
216
  onModel: model => {
177
217
  this.#model = model
178
218
  this.#followRange()
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
  }
@@ -0,0 +1,48 @@
1
+ import type { Annotation, Feature } from './types.ts'
2
+
3
+ const placement = new Set(['row', 'start', 'end'])
4
+
5
+ // The colors and labels are maps keyed by annotation, and a getter read
6
+ // outside a reaction recomputes on every read. Caching on the frozen feature
7
+ // gives it one annotation however often the layer converts.
8
+ const converted = new WeakMap<Feature, Annotation>()
9
+
10
+ /**
11
+ * The `features` layer as the annotations a GFF would parse to. Every field
12
+ * but the placement is also an attribute, as a string like a GFF value, so an
13
+ * encoding reads `type` or a host's own field the way it reads a GFF column 9
14
+ * key.
15
+ */
16
+ export function featuresToAnnotations(features: Feature[]): Annotation[] {
17
+ return features.map(feature => {
18
+ const cached = converted.get(feature)
19
+ if (cached) {
20
+ return cached
21
+ }
22
+ const annotation = featureAnnotation(feature)
23
+ converted.set(feature, annotation)
24
+ return annotation
25
+ })
26
+ }
27
+
28
+ function featureAnnotation(feature: Feature): Annotation {
29
+ const { row, start, end, name, description, type, strand, color } = feature
30
+ const accession = name ?? `${row}:${start}-${end}`
31
+ const attributes = Object.fromEntries(
32
+ Object.entries(feature)
33
+ .filter(([key, value]) => !placement.has(key) && value !== undefined)
34
+ .map(([key, value]) => [key, String(value)]),
35
+ )
36
+ return {
37
+ id: row,
38
+ accession,
39
+ name: name ?? accession,
40
+ description: description ?? name ?? accession,
41
+ featureType: type,
42
+ start,
43
+ end,
44
+ strand: strand === '+' ? 1 : strand === '-' ? -1 : undefined,
45
+ color,
46
+ attributes,
47
+ }
48
+ }
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'
@@ -60,13 +60,16 @@ export type {
60
60
  Cell,
61
61
  Clade,
62
62
  CladeMark,
63
+ ColorScheme,
63
64
  ColumnTrackSpec,
64
65
  DomainBand,
65
66
  Encoding,
66
67
  EncodingChannel,
68
+ Feature,
67
69
  Highlight,
68
70
  Legend,
69
71
  LegendEntry,
72
+ MsaSelection,
70
73
  Node,
71
74
  MappedStructure,
72
75
  NodeWithIds,
@@ -80,12 +83,16 @@ export type {
80
83
  ResolvedEncoding,
81
84
  ResolvedHighlight,
82
85
  ResolvedRowPanel,
86
+ ResolvedSelection,
87
+ RowDataInput,
83
88
  RowPanelSpec,
84
89
  RowResidue,
85
90
  StructureResidue,
86
91
  TextTrackModel,
87
92
  TidyDomainAnnotation,
88
93
  TrackKind,
94
+ TreeOrder,
95
+ TreeRoot,
89
96
  UnshareableData,
90
97
  Viewport,
91
98
  } from './types.ts'
@@ -9,7 +9,7 @@ import { maxInlineSnapshotBytes } from '../constants.ts'
9
9
  * data will not be persisted in saved session snapshots, it will be fetched
10
10
  * from msaFilehandle at startup
11
11
  */
12
- export function DataModelF() {
12
+ export function DataModelF(maxInlineBytes = maxInlineSnapshotBytes) {
13
13
  return types
14
14
  .model({
15
15
  /**
@@ -56,12 +56,12 @@ export function DataModelF() {
56
56
  },
57
57
  }))
58
58
  .postProcessSnapshot(snap =>
59
- // drops any field over maxInlineSnapshotBytes; the parent model's
59
+ // drops any field over maxInlineBytes; the parent model's
60
60
  // `unshareableData` reports what was dropped
61
61
  Object.fromEntries(
62
62
  Object.entries(snap).map(([key, text]) => [
63
63
  key,
64
- text && text.length > maxInlineSnapshotBytes ? undefined : text,
64
+ text && text.length > maxInlineBytes ? undefined : text,
65
65
  ]),
66
66
  ),
67
67
  )
@@ -29,6 +29,13 @@ export function MSAModelF() {
29
29
  */
30
30
  colorSchemeName: stripDefault(types.string, defaultColorSchemeName),
31
31
 
32
+ /**
33
+ * #property
34
+ * a color per residue letter, which replaces the `colorSchemeName` table
35
+ * while set. A letter the map leaves out takes no color
36
+ */
37
+ customColorScheme: types.frozen<Record<string, string> | undefined>(),
38
+
32
39
  /**
33
40
  * #property
34
41
  * show a tooltip with the hovered column's value for a track while
@@ -49,10 +56,21 @@ export function MSAModelF() {
49
56
  .actions(self => ({
50
57
  /**
51
58
  * #action
52
- * set color scheme name
59
+ * pick a scheme from the built-in table, which clears
60
+ * `customColorScheme`
53
61
  */
54
62
  setColorSchemeName(name: string) {
55
63
  self.colorSchemeName = name
64
+ self.customColorScheme = undefined
65
+ },
66
+
67
+ /**
68
+ * #action
69
+ * color residues from a map of letter to color, or pass undefined to
70
+ * return to `colorSchemeName`
71
+ */
72
+ setCustomColorScheme(map?: Record<string, string>) {
73
+ self.customColorScheme = map
56
74
  },
57
75
 
58
76
  /**
@@ -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
  */