react-msaview 7.0.0 → 8.0.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 (310) hide show
  1. package/README.md +11 -10
  2. package/bundle/index.js +122 -111
  3. package/bundle/index.js.map +4 -4
  4. package/dist/colorSchemes.js +1 -2
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/columnStats.d.ts +28 -0
  7. package/dist/columnStats.js +34 -0
  8. package/dist/columnStats.js.map +1 -0
  9. package/dist/components/DragHandle.d.ts +3 -2
  10. package/dist/components/DragHandle.js +11 -5
  11. package/dist/components/DragHandle.js.map +1 -1
  12. package/dist/components/Loading.js.map +1 -1
  13. package/dist/components/MSAView.js +17 -6
  14. package/dist/components/MSAView.js.map +1 -1
  15. package/dist/components/MSAViewer.d.ts +64 -7
  16. package/dist/components/MSAViewer.js +128 -23
  17. package/dist/components/MSAViewer.js.map +1 -1
  18. package/dist/components/PortalTooltip.d.ts +11 -0
  19. package/dist/components/PortalTooltip.js +29 -0
  20. package/dist/components/PortalTooltip.js.map +1 -0
  21. package/dist/components/ResizeHandles.js +11 -29
  22. package/dist/components/ResizeHandles.js.map +1 -1
  23. package/dist/components/SequenceTextArea.js +3 -3
  24. package/dist/components/SequenceTextArea.js.map +1 -1
  25. package/dist/components/Track.js +41 -22
  26. package/dist/components/Track.js.map +1 -1
  27. package/dist/components/dialogs/ExportSVGDialog.js +5 -3
  28. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  29. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  30. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  31. package/dist/components/header/GappynessSlider.js +6 -11
  32. package/dist/components/header/GappynessSlider.js.map +1 -1
  33. package/dist/components/header/Header.js +3 -0
  34. package/dist/components/header/Header.js.map +1 -1
  35. package/dist/components/header/LoadWarnings.js +3 -4
  36. package/dist/components/header/LoadWarnings.js.map +1 -1
  37. package/dist/components/header/ScrollZoomIcons.d.ts +6 -0
  38. package/dist/components/header/ScrollZoomIcons.js +35 -0
  39. package/dist/components/header/ScrollZoomIcons.js.map +1 -0
  40. package/dist/components/header/TreeSettingsMenu.js +1 -3
  41. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  42. package/dist/components/header/UnshareableDataWarning.js +2 -4
  43. package/dist/components/header/UnshareableDataWarning.js.map +1 -1
  44. package/dist/components/header/ZoomControls.js +49 -6
  45. package/dist/components/header/ZoomControls.js.map +1 -1
  46. package/dist/components/header/getAnnotationsMenu.js +3 -6
  47. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  48. package/dist/components/header/settingsMenuItems.js +10 -7
  49. package/dist/components/header/settingsMenuItems.js.map +1 -1
  50. package/dist/components/import/util.js +1 -2
  51. package/dist/components/import/util.js.map +1 -1
  52. package/dist/components/minimap/Minimap.js +1 -2
  53. package/dist/components/minimap/Minimap.js.map +1 -1
  54. package/dist/components/msa/AnnotationLegend.js +27 -19
  55. package/dist/components/msa/AnnotationLegend.js.map +1 -1
  56. package/dist/components/msa/MSACanvas.js +1 -1
  57. package/dist/components/msa/MSACanvas.js.map +1 -1
  58. package/dist/components/msa/MSACanvasBlock.js +13 -15
  59. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  60. package/dist/components/msa/MSAMouseoverCanvas.js +2 -1
  61. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  62. package/dist/components/msa/domainBandCursor.d.ts +7 -11
  63. package/dist/components/msa/domainBandCursor.js +7 -11
  64. package/dist/components/msa/domainBandCursor.js.map +1 -1
  65. package/dist/components/msa/drawFeatureSpans.d.ts +53 -0
  66. package/dist/components/msa/drawFeatureSpans.js +78 -0
  67. package/dist/components/msa/drawFeatureSpans.js.map +1 -0
  68. package/dist/components/msa/legendRows.d.ts +8 -0
  69. package/dist/components/msa/legendRows.js +16 -0
  70. package/dist/components/msa/legendRows.js.map +1 -0
  71. package/dist/components/msa/msaRaster.d.ts +24 -31
  72. package/dist/components/msa/msaRaster.js +30 -39
  73. package/dist/components/msa/msaRaster.js.map +1 -1
  74. package/dist/components/msa/packDomainLanes.d.ts +20 -0
  75. package/dist/components/msa/packDomainLanes.js +33 -0
  76. package/dist/components/msa/packDomainLanes.js.map +1 -0
  77. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +3 -1
  78. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +51 -87
  79. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  80. package/dist/components/msa/renderHighlights.d.ts +15 -13
  81. package/dist/components/msa/renderHighlights.js +43 -20
  82. package/dist/components/msa/renderHighlights.js.map +1 -1
  83. package/dist/components/msa/renderMSABlock.js +5 -4
  84. package/dist/components/msa/renderMSABlock.js.map +1 -1
  85. package/dist/components/msa/tileColor.js +4 -7
  86. package/dist/components/msa/tileColor.js.map +1 -1
  87. package/dist/components/msa/useMsaBlockMouse.d.ts +5 -3
  88. package/dist/components/msa/useMsaBlockMouse.js +6 -6
  89. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  90. package/dist/components/rowpanels/RowPanelBlock.d.ts +9 -0
  91. package/dist/components/rowpanels/RowPanelBlock.js +86 -0
  92. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -0
  93. package/dist/components/rowpanels/RowPanelHeaders.d.ts +12 -0
  94. package/dist/components/rowpanels/RowPanelHeaders.js +38 -0
  95. package/dist/components/rowpanels/RowPanelHeaders.js.map +1 -0
  96. package/dist/components/{msa/ColumnStats.d.ts → rowpanels/RowPanels.d.ts} +2 -2
  97. package/dist/components/rowpanels/RowPanels.js +32 -0
  98. package/dist/components/rowpanels/RowPanels.js.map +1 -0
  99. package/dist/components/rowpanels/headerLayout.d.ts +1 -0
  100. package/dist/components/rowpanels/headerLayout.js +5 -0
  101. package/dist/components/rowpanels/headerLayout.js.map +1 -0
  102. package/dist/components/rowpanels/renderFeaturePanel.d.ts +21 -0
  103. package/dist/components/rowpanels/renderFeaturePanel.js +37 -0
  104. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -0
  105. package/dist/components/rowpanels/renderRowPanel.d.ts +23 -0
  106. package/dist/components/rowpanels/renderRowPanel.js +22 -0
  107. package/dist/components/rowpanels/renderRowPanel.js.map +1 -0
  108. package/dist/components/rowpanels/renderStrip.d.ts +20 -0
  109. package/dist/components/rowpanels/renderStrip.js +26 -0
  110. package/dist/components/rowpanels/renderStrip.js.map +1 -0
  111. package/dist/components/tracks/TrackBlocks.js +3 -5
  112. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  113. package/dist/components/tracks/TrackTooltipContent.d.ts +15 -0
  114. package/dist/components/tracks/TrackTooltipContent.js +109 -0
  115. package/dist/components/tracks/TrackTooltipContent.js.map +1 -0
  116. package/dist/components/tracks/drawTracks.d.ts +4 -4
  117. package/dist/components/tracks/drawTracks.js +17 -34
  118. package/dist/components/tracks/drawTracks.js.map +1 -1
  119. package/dist/components/tracks/useTrackHover.d.ts +22 -0
  120. package/dist/components/tracks/useTrackHover.js +30 -0
  121. package/dist/components/tracks/useTrackHover.js.map +1 -0
  122. package/dist/components/tree/CladeLabels.d.ts +12 -0
  123. package/dist/components/tree/CladeLabels.js +48 -0
  124. package/dist/components/tree/CladeLabels.js.map +1 -0
  125. package/dist/components/tree/TreeCanvas.js +4 -1
  126. package/dist/components/tree/TreeCanvas.js.map +1 -1
  127. package/dist/components/tree/TreeCanvasBlock.js +4 -21
  128. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  129. package/dist/components/tree/TreeOverview.d.ts +14 -0
  130. package/dist/components/tree/TreeOverview.js +75 -0
  131. package/dist/components/tree/TreeOverview.js.map +1 -0
  132. package/dist/components/tree/TreeRuler.js +9 -13
  133. package/dist/components/tree/TreeRuler.js.map +1 -1
  134. package/dist/components/tree/cladeBrackets.d.ts +25 -0
  135. package/dist/components/tree/cladeBrackets.js +51 -0
  136. package/dist/components/tree/cladeBrackets.js.map +1 -0
  137. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +2 -2
  138. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  139. package/dist/components/tree/renderTreeCanvas.js +158 -24
  140. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  141. package/dist/components/tree/renderTreeOverview.d.ts +52 -0
  142. package/dist/components/tree/renderTreeOverview.js +138 -0
  143. package/dist/components/tree/renderTreeOverview.js.map +1 -0
  144. package/dist/components/tree/useTreeHover.js +2 -4
  145. package/dist/components/tree/useTreeHover.js.map +1 -1
  146. package/dist/constants.d.ts +15 -0
  147. package/dist/constants.js +48 -22
  148. package/dist/constants.js.map +1 -1
  149. package/dist/createPaletteMap.d.ts +7 -6
  150. package/dist/createPaletteMap.js +10 -9
  151. package/dist/createPaletteMap.js.map +1 -1
  152. package/dist/element.d.ts +13 -0
  153. package/dist/element.js +269 -0
  154. package/dist/element.js.map +1 -0
  155. package/dist/featureFields.d.ts +11 -0
  156. package/dist/featureFields.js +17 -0
  157. package/dist/featureFields.js.map +1 -0
  158. package/dist/flatToTree.js +1 -2
  159. package/dist/flatToTree.js.map +1 -1
  160. package/dist/ggplotPalettes.d.ts +10 -0
  161. package/dist/ggplotPalettes.js +58 -0
  162. package/dist/ggplotPalettes.js.map +1 -1
  163. package/dist/hierarchy.d.ts +27 -0
  164. package/dist/hierarchy.js +66 -9
  165. package/dist/hierarchy.js.map +1 -1
  166. package/dist/index.d.ts +9 -1
  167. package/dist/index.js +5 -3
  168. package/dist/index.js.map +1 -1
  169. package/dist/model/DataModel.js +2 -5
  170. package/dist/model/DataModel.js.map +1 -1
  171. package/dist/model/msaModel.d.ts +3 -2
  172. package/dist/model/msaModel.js +3 -2
  173. package/dist/model/msaModel.js.map +1 -1
  174. package/dist/model/treeModel.d.ts +30 -0
  175. package/dist/model/treeModel.js +37 -1
  176. package/dist/model/treeModel.js.map +1 -1
  177. package/dist/model.d.ts +531 -275
  178. package/dist/model.js +1295 -547
  179. package/dist/model.js.map +1 -1
  180. package/dist/mount.d.ts +11 -0
  181. package/dist/mount.js +22 -0
  182. package/dist/mount.js.map +1 -0
  183. package/dist/neighborJoining.js +8 -32
  184. package/dist/neighborJoining.js.map +1 -1
  185. package/dist/renderToStaticMarkup.d.ts +10 -19
  186. package/dist/renderToStaticMarkup.js +13 -24
  187. package/dist/renderToStaticMarkup.js.map +1 -1
  188. package/dist/renderToSvg.js +180 -56
  189. package/dist/renderToSvg.js.map +1 -1
  190. package/dist/rowCoordinateCalculations.d.ts +4 -6
  191. package/dist/rowCoordinateCalculations.js +4 -6
  192. package/dist/rowCoordinateCalculations.js.map +1 -1
  193. package/dist/scales.d.ts +22 -0
  194. package/dist/scales.js +30 -0
  195. package/dist/scales.js.map +1 -0
  196. package/dist/seqPosToGlobalCol.d.ts +2 -3
  197. package/dist/seqPosToGlobalCol.js +2 -3
  198. package/dist/seqPosToGlobalCol.js.map +1 -1
  199. package/dist/sequenceLogo.d.ts +6 -11
  200. package/dist/sequenceLogo.js +6 -11
  201. package/dist/sequenceLogo.js.map +1 -1
  202. package/dist/stripDefault.d.ts +7 -14
  203. package/dist/stripDefault.js +7 -14
  204. package/dist/stripDefault.js.map +1 -1
  205. package/dist/svgTestUtil.d.ts +172 -52
  206. package/dist/types.d.ts +219 -21
  207. package/dist/umd.d.ts +2 -0
  208. package/dist/umd.js +4 -2
  209. package/dist/umd.js.map +1 -1
  210. package/dist/useCanvasAutorun.d.ts +5 -10
  211. package/dist/useCanvasAutorun.js +5 -10
  212. package/dist/useCanvasAutorun.js.map +1 -1
  213. package/dist/useMsaSvgFigure.d.ts +20 -0
  214. package/dist/useMsaSvgFigure.js +46 -0
  215. package/dist/useMsaSvgFigure.js.map +1 -0
  216. package/dist/util.d.ts +5 -6
  217. package/dist/util.js +13 -6
  218. package/dist/util.js.map +1 -1
  219. package/dist/version.d.ts +1 -1
  220. package/dist/version.js +1 -1
  221. package/package.json +19 -12
  222. package/src/colorSchemes.ts +1 -2
  223. package/src/columnStats.ts +58 -0
  224. package/src/components/DragHandle.tsx +18 -7
  225. package/src/components/Loading.tsx +2 -6
  226. package/src/components/MSAView.tsx +19 -5
  227. package/src/components/MSAViewer.tsx +240 -28
  228. package/src/components/PortalTooltip.tsx +47 -0
  229. package/src/components/ResizeHandles.tsx +17 -32
  230. package/src/components/SequenceTextArea.tsx +3 -3
  231. package/src/components/Track.tsx +55 -18
  232. package/src/components/dialogs/ExportSVGDialog.tsx +7 -2
  233. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  234. package/src/components/header/GappynessSlider.tsx +6 -11
  235. package/src/components/header/Header.tsx +6 -0
  236. package/src/components/header/LoadWarnings.tsx +3 -4
  237. package/src/components/header/ScrollZoomIcons.tsx +52 -0
  238. package/src/components/header/TreeSettingsMenu.tsx +1 -3
  239. package/src/components/header/UnshareableDataWarning.tsx +2 -4
  240. package/src/components/header/ZoomControls.tsx +74 -14
  241. package/src/components/header/getAnnotationsMenu.ts +3 -6
  242. package/src/components/header/settingsMenuItems.ts +11 -6
  243. package/src/components/import/ImportFormExamples.tsx +2 -2
  244. package/src/components/import/util.ts +1 -2
  245. package/src/components/minimap/Minimap.tsx +1 -2
  246. package/src/components/msa/AnnotationLegend.tsx +48 -38
  247. package/src/components/msa/MSACanvas.tsx +3 -5
  248. package/src/components/msa/MSACanvasBlock.tsx +13 -16
  249. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -1
  250. package/src/components/msa/domainBandCursor.ts +7 -11
  251. package/src/components/msa/drawFeatureSpans.ts +150 -0
  252. package/src/components/msa/legendRows.ts +24 -0
  253. package/src/components/msa/msaRaster.ts +30 -39
  254. package/src/components/msa/packDomainLanes.ts +36 -0
  255. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +69 -123
  256. package/src/components/msa/renderHighlights.ts +53 -20
  257. package/src/components/msa/renderMSABlock.ts +5 -3
  258. package/src/components/msa/tileColor.ts +4 -7
  259. package/src/components/msa/useMsaBlockMouse.ts +6 -6
  260. package/src/components/rowpanels/RowPanelBlock.tsx +139 -0
  261. package/src/components/rowpanels/RowPanelHeaders.tsx +57 -0
  262. package/src/components/rowpanels/RowPanels.tsx +77 -0
  263. package/src/components/rowpanels/headerLayout.ts +5 -0
  264. package/src/components/rowpanels/renderFeaturePanel.ts +66 -0
  265. package/src/components/rowpanels/renderRowPanel.ts +41 -0
  266. package/src/components/rowpanels/renderStrip.ts +46 -0
  267. package/src/components/tracks/TrackBlocks.tsx +3 -5
  268. package/src/components/tracks/TrackTooltipContent.tsx +156 -0
  269. package/src/components/tracks/drawTracks.ts +17 -34
  270. package/src/components/tracks/useTrackHover.ts +42 -0
  271. package/src/components/tree/CladeLabels.tsx +77 -0
  272. package/src/components/tree/TreeCanvas.tsx +2 -0
  273. package/src/components/tree/TreeCanvasBlock.tsx +8 -32
  274. package/src/components/tree/TreeOverview.tsx +106 -0
  275. package/src/components/tree/TreeRuler.tsx +11 -22
  276. package/src/components/tree/cladeBrackets.ts +73 -0
  277. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +2 -2
  278. package/src/components/tree/renderTreeCanvas.ts +240 -20
  279. package/src/components/tree/renderTreeOverview.ts +212 -0
  280. package/src/components/tree/useTreeHover.ts +2 -4
  281. package/src/constants.ts +56 -22
  282. package/src/createPaletteMap.ts +10 -9
  283. package/src/element.ts +321 -0
  284. package/src/featureFields.ts +19 -0
  285. package/src/flatToTree.ts +1 -2
  286. package/src/ggplotPalettes.ts +60 -0
  287. package/src/hierarchy.ts +77 -9
  288. package/src/index.ts +31 -7
  289. package/src/model/DataModel.ts +2 -5
  290. package/src/model/msaModel.ts +3 -2
  291. package/src/model/treeModel.ts +42 -0
  292. package/src/model.ts +1523 -560
  293. package/src/mount.tsx +32 -0
  294. package/src/neighborJoining.ts +8 -32
  295. package/src/renderToStaticMarkup.ts +13 -24
  296. package/src/renderToSvg.tsx +335 -68
  297. package/src/rowCoordinateCalculations.ts +4 -6
  298. package/src/scales.ts +47 -0
  299. package/src/seqPosToGlobalCol.ts +2 -3
  300. package/src/sequenceLogo.ts +6 -11
  301. package/src/stripDefault.ts +7 -14
  302. package/src/types.ts +258 -45
  303. package/src/umd.ts +4 -2
  304. package/src/useCanvasAutorun.ts +5 -10
  305. package/src/useMsaSvgFigure.ts +72 -0
  306. package/src/util.ts +15 -6
  307. package/src/version.ts +1 -1
  308. package/dist/components/msa/ColumnStats.js +0 -42
  309. package/dist/components/msa/ColumnStats.js.map +0 -1
  310. package/src/components/msa/ColumnStats.tsx +0 -54
@@ -1,4 +1,4 @@
1
- import { calcDepthToLeaf } from '../../hierarchy.ts'
1
+ import { calcDepthToLeaf, nodeCoveringRows } from '../../hierarchy.ts'
2
2
  import { setFontSize } from '../../setFontSize.ts'
3
3
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
4
4
  import {
@@ -6,6 +6,7 @@ import {
6
6
  highlightLabelHeight,
7
7
  highlightRowFill,
8
8
  } from '../msa/renderHighlights.ts'
9
+ import { bracketBarWidth, bracketGap } from './cladeBrackets.ts'
9
10
 
10
11
  import type { HierarchyNode } from '../../hierarchy.ts'
11
12
  import type { MsaViewModel } from '../../model.ts'
@@ -32,12 +33,12 @@ function inYBlock(y: number, offsetY: number, by: number, pad: number) {
32
33
 
33
34
  /**
34
35
  * Every node whose subtree reaches into the block, skipping the subtrees that
35
- * do not. A subtree's leaves are one contiguous run of rows -- clusterLayout
36
- * records the run as xMin/xMax -- so a block of a 200k-tip tree visits a few
37
- * dozen nodes instead of every one of them, three times over.
36
+ * do not. A subtree's leaves are one contiguous run of rows, recorded by
37
+ * clusterLayout as xMin/xMax, so a block of a 200k-tip tree visits a few dozen
38
+ * nodes.
38
39
  *
39
- * A link out of a visited node is still drawn even when the child it reaches is
40
- * pruned, which is what keeps a branch that crosses the block from vanishing.
40
+ * A link out of a visited node is drawn even when its child is pruned, so a
41
+ * branch crossing the block stays visible.
41
42
  */
42
43
  function forEachNodeInBlock(
43
44
  root: HierarchyNode,
@@ -101,9 +102,15 @@ function renderTree({
101
102
  maxDepthToLeaf: number
102
103
  blockSizeYOverride?: number
103
104
  }) {
104
- const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
105
+ const {
106
+ hierarchy,
107
+ showBranchLenEffective: showBranchLen,
108
+ blockSize,
109
+ branchColors,
110
+ } = model
105
111
  const by = blockSizeYOverride ?? blockSize
106
- ctx.strokeStyle = theme.palette.text.primary
112
+ const defaultStroke = theme.palette.text.primary
113
+ ctx.strokeStyle = defaultStroke
107
114
  forEachNodeInBlock(hierarchy, offsetY, by, blockPad(model), source => {
108
115
  const sy = source.x!
109
116
  const sx = getNodeX(source, showBranchLen, tipX, maxDepthToLeaf)
@@ -122,6 +129,10 @@ function renderTree({
122
129
  // optimization that allows us to skip drawing most tree links outside the
123
130
  // block
124
131
  if (offsetY + by >= y1 && y2 >= offsetY) {
132
+ // the edge belongs to the child, whose id the branch channel colors
133
+ if (branchColors) {
134
+ ctx.strokeStyle = branchColors.get(target.data.id) ?? defaultStroke
135
+ }
125
136
  ctx.beginPath()
126
137
  ctx.moveTo(sx, sy)
127
138
  ctx.lineTo(sx, ty)
@@ -130,6 +141,7 @@ function renderTree({
130
141
  }
131
142
  }
132
143
  })
144
+ ctx.strokeStyle = defaultStroke
133
145
  }
134
146
 
135
147
  function renderCollapsedTriangles({
@@ -160,6 +172,7 @@ function renderCollapsedTriangles({
160
172
  rowHeight,
161
173
  fontSize,
162
174
  marginLeft: ml,
175
+ branchColors,
163
176
  } = model
164
177
  // nothing collapsed is the common case, and the traversal below visits every
165
178
  // node in the tree on every block of every redraw
@@ -183,7 +196,7 @@ function renderCollapsedTriangles({
183
196
  ctx.lineTo(baseX, y - halfHeight)
184
197
  ctx.lineTo(baseX, y + halfHeight)
185
198
  ctx.closePath()
186
- ctx.fillStyle = theme.palette.action.disabled
199
+ ctx.fillStyle = branchColors?.get(id) ?? theme.palette.action.disabled
187
200
  ctx.fill()
188
201
  ctx.strokeStyle = theme.palette.text.primary
189
202
  ctx.stroke()
@@ -218,6 +231,7 @@ function renderCollapsedTriangles({
218
231
  // always runs and `draw` only decides whether it also strokes the circles
219
232
  function renderNodeBubbles({
220
233
  ctx,
234
+ theme,
221
235
  clickMap,
222
236
  offsetY,
223
237
  model,
@@ -236,6 +250,7 @@ function renderNodeBubbles({
236
250
  blockSizeYOverride?: number
237
251
  collapsedSet: Set<string>
238
252
  draw: boolean
253
+ theme: Theme
239
254
  }) {
240
255
  const {
241
256
  hierarchy,
@@ -255,8 +270,10 @@ function renderNodeBubbles({
255
270
  if (node.height >= 1 && inYBlock(y, offsetY, by, blockPad(model))) {
256
271
  const isCollapsed = collapsedSet.has(id)
257
272
  if (draw) {
258
- ctx.strokeStyle = 'black'
259
- ctx.fillStyle = isCollapsed ? 'black' : 'white'
273
+ ctx.strokeStyle = theme.palette.text.primary
274
+ ctx.fillStyle = isCollapsed
275
+ ? theme.palette.text.primary
276
+ : theme.palette.background.default
260
277
  ctx.beginPath()
261
278
  ctx.arc(x, y, radius, 0, 2 * Math.PI)
262
279
  ctx.fill()
@@ -280,6 +297,53 @@ function renderNodeBubbles({
280
297
  })
281
298
  }
282
299
 
300
+ // The label a newick file puts on an internal node, which is where a bootstrap
301
+ // or posterior support value lands: ((A,B)95,(C,D)80);. withId falls back to the
302
+ // path-derived id for an unnamed node, so a node whose name is its id has no
303
+ // label of its own and an ungated pass prints node-0-1-1 across the tree.
304
+ function renderNodeLabels({
305
+ ctx,
306
+ theme,
307
+ offsetY,
308
+ model,
309
+ tipX,
310
+ maxDepthToLeaf,
311
+ blockSizeYOverride,
312
+ }: {
313
+ ctx: RenderCtx
314
+ theme: Theme
315
+ offsetY: number
316
+ model: MsaViewModel
317
+ tipX: number
318
+ maxDepthToLeaf: number
319
+ blockSizeYOverride?: number
320
+ }) {
321
+ const {
322
+ hierarchy,
323
+ showBranchLenEffective: showBranchLen,
324
+ blockSize,
325
+ fontSize,
326
+ } = model
327
+ const by = blockSizeYOverride ?? blockSize
328
+ const pad = blockPad(model)
329
+ setFontSize(ctx, Math.max(6, fontSize - 3))
330
+ ctx.fillStyle = theme.palette.text.primary
331
+ ctx.textAlign = 'right'
332
+ forEachNodeInBlock(hierarchy, offsetY, by, pad, node => {
333
+ const { id, name } = node.data
334
+ if (node.height < 1 || name === id) {
335
+ return
336
+ }
337
+ const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
338
+ const y = node.x!
339
+ if (x !== undefined && inYBlock(y, offsetY, by, pad)) {
340
+ ctx.fillText(name, x - radius - 1, y - radius - 1)
341
+ }
342
+ })
343
+ ctx.textAlign = 'start'
344
+ setFontSize(ctx, fontSize)
345
+ }
346
+
283
347
  function renderTreeLabels({
284
348
  theme,
285
349
  model,
@@ -304,7 +368,7 @@ function renderTreeLabels({
304
368
  const {
305
369
  fontSize,
306
370
  showBranchLenEffective: showBranchLen,
307
- treeMetadata,
371
+ tipLabelColors,
308
372
  blockSize,
309
373
  labelsAlignRight,
310
374
  drawTree,
@@ -333,7 +397,7 @@ function renderTreeLabels({
333
397
  } = node
334
398
  const y = node.x!
335
399
 
336
- const displayName = treeMetadata[name]?.genome || name
400
+ const displayName = model.rowDataOf(name)?.genome || name
337
401
  // a collapsed clade is drawn as a triangle + tip count; suppress its leaf
338
402
  // label when the "name" is just the auto-generated internal-node id
339
403
  const isAnonymousCollapsed = collapsedSet.has(id) && name === id
@@ -351,7 +415,7 @@ function renderTreeLabels({
351
415
  ? ctx.measureText(displayName).width
352
416
  : measured * labelWidthScale
353
417
 
354
- ctx.fillStyle = theme.palette.text.primary
418
+ ctx.fillStyle = tipLabelColors?.get(name) ?? theme.palette.text.primary
355
419
  if (labelsAlignRight) {
356
420
  const smallPadding = 2
357
421
  const offset = treeAreaWidthMinusMargin - smallPadding
@@ -393,8 +457,123 @@ function renderTreeLabels({
393
457
  ctx.setLineDash([])
394
458
  }
395
459
 
396
- // the row sets of `highlights`, tinted across the tree area under the labels
397
- // with the label in the gutter at the first row of each set
460
+ // the first row of a set, as a loop: spreading into Math.min passes one
461
+ // argument per row, and the ~125k argument limit throws RangeError on a set
462
+ // covering the 230k-tip COVID tree
463
+ function firstRow(rowIndices: number[]) {
464
+ let min = Infinity
465
+ for (const index of rowIndices) {
466
+ if (index < min) {
467
+ min = index
468
+ }
469
+ }
470
+ return min
471
+ }
472
+
473
+ // the highlight mark of the `clades` layer: a translucent rectangle from the
474
+ // clade's common ancestor to the right edge of the tree area, under the
475
+ // branches. A clade spanning hundreds of rows crosses several blocks, so each
476
+ // block draws the whole rectangle and the canvas takes the part that is its.
477
+ function renderCladeHighlights({
478
+ ctx,
479
+ model,
480
+ offsetY,
481
+ tipX,
482
+ maxDepthToLeaf,
483
+ blockSizeYOverride,
484
+ }: {
485
+ ctx: RenderCtx
486
+ model: MsaViewModel
487
+ offsetY: number
488
+ tipX: number
489
+ maxDepthToLeaf: number
490
+ blockSizeYOverride?: number
491
+ }) {
492
+ const {
493
+ resolvedClades,
494
+ hierarchy,
495
+ showBranchLenEffective: showBranchLen,
496
+ rowHeight,
497
+ treeAreaWidth,
498
+ marginLeft,
499
+ blockSize,
500
+ } = model
501
+ const by = blockSizeYOverride ?? blockSize
502
+ const right = treeAreaWidth - marginLeft
503
+ for (const { rows, color, mark } of resolvedClades) {
504
+ if (mark !== 'highlight') {
505
+ continue
506
+ }
507
+ const top = rows[0] * rowHeight
508
+ const bottom = (rows[1] + 1) * rowHeight
509
+ if (bottom < offsetY || top > offsetY + by) {
510
+ continue
511
+ }
512
+ const node = nodeCoveringRows(
513
+ hierarchy,
514
+ top + rowHeight / 2,
515
+ bottom - rowHeight / 2,
516
+ )
517
+ const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
518
+ ctx.fillStyle = color
519
+ ctx.fillRect(x, top, right - x, bottom - top)
520
+ }
521
+ }
522
+
523
+ // the bar of the bracket mark, in the gutter `cladeGutterWidth` reserves at the
524
+ // right of the tree area. The label beside it is DOM text on screen and a
525
+ // <text> in the export, since the canvas layer has no rotation.
526
+ function renderCladeBrackets({
527
+ ctx,
528
+ model,
529
+ theme,
530
+ offsetY,
531
+ blockSizeYOverride,
532
+ }: {
533
+ ctx: RenderCtx
534
+ model: MsaViewModel
535
+ theme: Theme
536
+ offsetY: number
537
+ blockSizeYOverride?: number
538
+ }) {
539
+ const {
540
+ resolvedClades,
541
+ cladeGutterWidth,
542
+ rowHeight,
543
+ treeAreaWidth,
544
+ marginLeft,
545
+ blockSize,
546
+ } = model
547
+ if (cladeGutterWidth === 0) {
548
+ return
549
+ }
550
+ const by = blockSizeYOverride ?? blockSize
551
+ const x =
552
+ treeAreaWidth -
553
+ marginLeft -
554
+ cladeGutterWidth +
555
+ bracketGap +
556
+ bracketBarWidth / 2
557
+ ctx.lineWidth = bracketBarWidth
558
+ for (const { rows, mark, markColor } of resolvedClades) {
559
+ const top = rows[0] * rowHeight + 1
560
+ const bottom = (rows[1] + 1) * rowHeight - 1
561
+ if (mark !== 'bracket' || bottom < offsetY || top > offsetY + by) {
562
+ continue
563
+ }
564
+ ctx.strokeStyle = markColor ?? theme.palette.text.primary
565
+ ctx.beginPath()
566
+ ctx.moveTo(x, top)
567
+ ctx.lineTo(x, bottom)
568
+ ctx.stroke()
569
+ }
570
+ ctx.lineWidth = 1
571
+ ctx.strokeStyle = theme.palette.text.primary
572
+ }
573
+
574
+ // the `rowTint` encoding and the row sets of `highlights`, washed across the
575
+ // tree area under the labels, with each set's label in the gutter at its first
576
+ // row
398
577
  function renderRowHighlights({
399
578
  ctx,
400
579
  model,
@@ -410,6 +589,7 @@ function renderRowHighlights({
410
589
  }) {
411
590
  const {
412
591
  resolvedHighlights,
592
+ rowTints,
413
593
  rowHeight,
414
594
  treeAreaWidth,
415
595
  marginLeft,
@@ -418,15 +598,31 @@ function renderRowHighlights({
418
598
  const by = blockSizeYOverride ?? blockSize
419
599
  const pad = blockPad(model)
420
600
  const placed: LabelBox[] = []
601
+ const fillRow = (index: number) => {
602
+ ctx.fillRect(-marginLeft, index * rowHeight, treeAreaWidth, rowHeight)
603
+ }
604
+ if (rowTints) {
605
+ const top = Math.max(0, Math.floor((offsetY - pad) / rowHeight))
606
+ const bottom = Math.min(
607
+ rowTints.length - 1,
608
+ Math.ceil((offsetY + by + pad) / rowHeight),
609
+ )
610
+ for (let index = top; index <= bottom; index++) {
611
+ const color = rowTints[index]
612
+ if (color) {
613
+ ctx.fillStyle = color
614
+ fillRow(index)
615
+ }
616
+ }
617
+ }
421
618
  for (const { rowIndices, label, color } of resolvedHighlights) {
422
619
  ctx.fillStyle = color ?? highlightRowFill
423
620
  for (const index of rowIndices) {
424
- const y = index * rowHeight
425
- if (inYBlock(y + rowHeight / 2, offsetY, by, pad)) {
426
- ctx.fillRect(-marginLeft, y, treeAreaWidth, rowHeight)
621
+ if (inYBlock(index * rowHeight + rowHeight / 2, offsetY, by, pad)) {
622
+ fillRow(index)
427
623
  }
428
624
  }
429
- const first = Math.min(...rowIndices)
625
+ const first = firstRow(rowIndices)
430
626
  if (
431
627
  label &&
432
628
  rowIndices.length &&
@@ -470,6 +666,7 @@ export function renderTreeCanvas({
470
666
  noTree,
471
667
  drawTree,
472
668
  drawNodeBubbles,
669
+ drawNodeLabels,
473
670
  highResScaleFactor,
474
671
  fontSize,
475
672
  showTreeText,
@@ -500,6 +697,14 @@ export function renderTreeCanvas({
500
697
  // every block of every redraw, and `collapsed` is a list
501
698
  const collapsedSet = new Set(model.collapsed)
502
699
 
700
+ renderCladeHighlights({
701
+ ctx,
702
+ model,
703
+ offsetY,
704
+ tipX,
705
+ maxDepthToLeaf,
706
+ blockSizeYOverride,
707
+ })
503
708
  renderRowHighlights({ ctx, model, theme, offsetY, blockSizeYOverride })
504
709
  setFontSize(ctx, fontSize)
505
710
 
@@ -528,6 +733,7 @@ export function renderTreeCanvas({
528
733
 
529
734
  renderNodeBubbles({
530
735
  ctx,
736
+ theme,
531
737
  offsetY,
532
738
  clickMap,
533
739
  model,
@@ -537,6 +743,18 @@ export function renderTreeCanvas({
537
743
  collapsedSet,
538
744
  draw: drawNodeBubbles,
539
745
  })
746
+
747
+ if (drawNodeLabels) {
748
+ renderNodeLabels({
749
+ ctx,
750
+ theme,
751
+ offsetY,
752
+ model,
753
+ tipX,
754
+ maxDepthToLeaf,
755
+ blockSizeYOverride,
756
+ })
757
+ }
540
758
  }
541
759
 
542
760
  if (showTreeText) {
@@ -553,6 +771,8 @@ export function renderTreeCanvas({
553
771
  })
554
772
  }
555
773
 
774
+ renderCladeBrackets({ ctx, model, theme, offsetY, blockSizeYOverride })
775
+
556
776
  // Finish the index so it's ready for queries
557
777
  clickMap?.finish()
558
778
  }
@@ -0,0 +1,212 @@
1
+ import { descendants } from '../../hierarchy.ts'
2
+ import { getNodeX } from './renderTreeCanvas.ts'
3
+
4
+ import type { HierarchyNode } from '../../hierarchy.ts'
5
+ import type { MsaViewModel } from '../../model.ts'
6
+ import type { RasterCanvas } from '../msa/msaRaster.ts'
7
+ import type { RenderCtx } from '../renderCtx.ts'
8
+ import type { Theme } from '@mui/material'
9
+
10
+ // the tree draws this far inside the band, so a box around the first or the
11
+ // last row keeps both of its edges
12
+ const pad = 1
13
+
14
+ interface OverviewArgs {
15
+ model: MsaViewModel
16
+ ctx: RenderCtx
17
+ theme: Theme
18
+ width: number
19
+ height: number
20
+ }
21
+
22
+ function rowScale(numTips: number, height: number) {
23
+ const inner = height - pad * 2
24
+ return (row: number) => pad + (row / numTips) * inner
25
+ }
26
+
27
+ /**
28
+ * The whole tree at the size of the overview band, with the clade highlights
29
+ * under it.
30
+ *
31
+ * A branch that moves less than a pixel on both axes is dropped. The 230k-tip
32
+ * COVID tree has 460k of them over a 120px band, and the ones landing on the
33
+ * pixel their parent already covers cost a path segment each and change no
34
+ * pixel; the test in renderTreeOverview.test.ts measures a 200k-tip tree.
35
+ */
36
+ export function drawTreeOverview({
37
+ model,
38
+ ctx,
39
+ theme,
40
+ width,
41
+ height,
42
+ }: OverviewArgs) {
43
+ const layout = model.treeOverviewLayout
44
+ if (!layout) {
45
+ return
46
+ }
47
+ const { root, numTips, maxDepthToLeaf, showBranchLen } = layout
48
+ const innerWidth = width - pad * 2
49
+ const rowY = rowScale(numTips, height)
50
+ const nodeX = (node: HierarchyNode) =>
51
+ pad + (getNodeX(node, showBranchLen, 1, maxDepthToLeaf) ?? 0) * innerWidth
52
+
53
+ for (const { rows, color } of model.treeOverviewClades) {
54
+ ctx.fillStyle = color
55
+ ctx.fillRect(0, rowY(rows[0]), width, rowY(rows[1] + 1) - rowY(rows[0]))
56
+ }
57
+
58
+ ctx.strokeStyle = theme.palette.text.primary
59
+ ctx.beginPath()
60
+ for (const source of descendants(root)) {
61
+ if (!source.children) {
62
+ continue
63
+ }
64
+ const sx = nodeX(source)
65
+ const sy = rowY(source.x!)
66
+ for (const target of source.children) {
67
+ const tx = nodeX(target)
68
+ const ty = rowY(target.x!)
69
+ if (Math.abs(tx - sx) < 1 && Math.abs(ty - sy) < 1) {
70
+ continue
71
+ }
72
+ ctx.moveTo(sx, sy)
73
+ ctx.lineTo(sx, ty)
74
+ ctx.lineTo(tx, ty)
75
+ }
76
+ }
77
+ ctx.stroke()
78
+ }
79
+
80
+ /**
81
+ * A box across the overview around a run of tip rows: the focused subtree's
82
+ * extent, or the one a hovered pixel would focus.
83
+ */
84
+ export function drawTreeOverviewBox({
85
+ model,
86
+ ctx,
87
+ color,
88
+ rows,
89
+ width,
90
+ height,
91
+ }: Omit<OverviewArgs, 'theme'> & {
92
+ color: string
93
+ rows: [number, number]
94
+ }) {
95
+ const layout = model.treeOverviewLayout
96
+ if (!layout) {
97
+ return
98
+ }
99
+ const rowY = rowScale(layout.numTips, height)
100
+ const top = rowY(rows[0])
101
+ ctx.strokeStyle = color
102
+ ctx.strokeRect(0.5, top, width - 1, Math.max(2, rowY(rows[1] + 1) - top))
103
+ }
104
+
105
+ /**
106
+ * The overview as the figure carries it: the tree, the clade highlights and
107
+ * the focus box. The SVG export draws it straight onto a svgcanvas Context,
108
+ * and the live canvas blits `treeOverviewImage` in place of the first two.
109
+ */
110
+ export function renderTreeOverview(args: OverviewArgs) {
111
+ drawTreeOverview(args)
112
+ const rows = args.model.treeOverviewFocusRows
113
+ if (rows) {
114
+ drawTreeOverviewBox({
115
+ ...args,
116
+ rows,
117
+ color: args.theme.palette.text.primary,
118
+ })
119
+ }
120
+ }
121
+
122
+ interface OverviewCache {
123
+ keys: unknown[]
124
+ canvas: RasterCanvas | undefined
125
+ }
126
+
127
+ const caches = new WeakMap<object, OverviewCache>()
128
+
129
+ /**
130
+ * Everything the drawn tree depends on. `treeOverviewLayout` and
131
+ * `treeOverviewClades` are computed views, which recompute on every read
132
+ * outside a reaction, so the key holds the values behind them instead.
133
+ */
134
+ function overviewKeys({
135
+ model,
136
+ theme,
137
+ width,
138
+ height,
139
+ scale,
140
+ }: {
141
+ model: MsaViewModel
142
+ theme: Theme
143
+ width: number
144
+ height: number
145
+ scale: number
146
+ }) {
147
+ return [
148
+ model.data.tree,
149
+ model.data.msa,
150
+ model.currentAlignment,
151
+ model.collapsed.join(','),
152
+ JSON.stringify(model.clades),
153
+ model.showBranchLen,
154
+ width,
155
+ height,
156
+ scale,
157
+ theme.palette.text.primary,
158
+ ]
159
+ }
160
+
161
+ function makeCanvas(width: number, height: number) {
162
+ const canvas =
163
+ typeof OffscreenCanvas !== 'undefined'
164
+ ? new OffscreenCanvas(width, height)
165
+ : typeof document !== 'undefined'
166
+ ? Object.assign(document.createElement('canvas'), { width, height })
167
+ : undefined
168
+ const ctx = canvas?.getContext('2d')
169
+ return ctx && typeof (ctx as CanvasRenderingContext2D).stroke === 'function'
170
+ ? { canvas: canvas as RasterCanvas, ctx: ctx as CanvasRenderingContext2D }
171
+ : undefined
172
+ }
173
+
174
+ /**
175
+ * The tree and its clade highlights on an offscreen canvas, drawn once per
176
+ * change of anything they depend on and blitted every frame after that. A
177
+ * 230k-branch tree takes hundreds of milliseconds to walk and cannot be redrawn
178
+ * as the focus box follows the pointer.
179
+ *
180
+ * Returns undefined where a canvas has no 2d context (jsdom), and the caller
181
+ * then draws the tree itself.
182
+ */
183
+ export function treeOverviewImage({
184
+ model,
185
+ theme,
186
+ width,
187
+ height,
188
+ scale,
189
+ }: {
190
+ model: MsaViewModel
191
+ theme: Theme
192
+ width: number
193
+ height: number
194
+ scale: number
195
+ }) {
196
+ const keys = overviewKeys({ model, theme, width, height, scale })
197
+ const prev = caches.get(model)
198
+ if (prev && keys.every((key, i) => key === prev.keys[i])) {
199
+ return prev.canvas
200
+ }
201
+ const made = makeCanvas(
202
+ Math.max(1, Math.round(width * scale)),
203
+ Math.max(1, Math.round(height * scale)),
204
+ )
205
+ if (made) {
206
+ made.ctx.scale(scale, scale)
207
+ drawTreeOverview({ model, ctx: made.ctx, theme, width, height })
208
+ }
209
+ const next = { keys, canvas: made?.canvas }
210
+ caches.set(model, next)
211
+ return next.canvas
212
+ }
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react'
1
+ import { useState } from 'react'
2
2
 
3
3
  import { ClickMapIndex } from './clickMap.ts'
4
4
 
@@ -24,9 +24,7 @@ export function useTreeHover({
24
24
  model: MsaViewModel
25
25
  offsetY: number
26
26
  }) {
27
- const clickMapRef = useRef<ClickMapIndex>(null)
28
- clickMapRef.current ??= new ClickMapIndex()
29
- const clickMap = clickMapRef.current
27
+ const [clickMap] = useState(() => new ClickMapIndex())
30
28
  const [hovered, setHovered] = useState<TreeHoverTarget>()
31
29
 
32
30
  // leaf labels win over the branch/bubble targets they overlap, so a click on a