react-msaview 9.0.0 → 9.1.1

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 (352) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +111 -115
  3. package/bundle/index.js.map +4 -4
  4. package/dist/blosum62.d.ts +3 -0
  5. package/dist/blosum62.js +43 -0
  6. package/dist/blosum62.js.map +1 -0
  7. package/dist/colorSchemes.js +2 -3
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/columnCounts.d.ts +0 -1
  10. package/dist/columnCounts.js +1 -1
  11. package/dist/columnCounts.js.map +1 -1
  12. package/dist/components/Loading.d.ts +3 -2
  13. package/dist/components/Loading.js +7 -7
  14. package/dist/components/Loading.js.map +1 -1
  15. package/dist/components/MSAView.d.ts +3 -2
  16. package/dist/components/MSAView.js +7 -4
  17. package/dist/components/MSAView.js.map +1 -1
  18. package/dist/components/MSAViewer.d.ts +1941 -4
  19. package/dist/components/MSAViewer.js +144 -201
  20. package/dist/components/MSAViewer.js.map +1 -1
  21. package/dist/components/MetadataTable.d.ts +4 -1
  22. package/dist/components/MetadataTable.js +55 -16
  23. package/dist/components/MetadataTable.js.map +1 -1
  24. package/dist/components/ResizeHandles.js +3 -2
  25. package/dist/components/ResizeHandles.js.map +1 -1
  26. package/dist/components/Track.js +8 -1
  27. package/dist/components/Track.js.map +1 -1
  28. package/dist/components/controls.d.ts +11 -0
  29. package/dist/components/controls.js +49 -0
  30. package/dist/components/controls.js.map +1 -0
  31. package/dist/components/dialogs/AnnotationFileDialog.js +12 -18
  32. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  33. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  34. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  35. package/dist/components/dialogs/FeatureDialog.js +2 -2
  36. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  37. package/dist/components/{header/TreeSettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
  38. package/dist/components/dialogs/GapThresholdDialog.js +15 -0
  39. package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
  40. package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
  41. package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
  42. package/dist/components/dialogs/GappynessSlider.js.map +1 -0
  43. package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
  44. package/dist/components/dialogs/GoToBox.js +48 -0
  45. package/dist/components/dialogs/GoToBox.js.map +1 -0
  46. package/dist/components/dialogs/GoToDialog.d.ts +6 -0
  47. package/dist/components/dialogs/GoToDialog.js +10 -0
  48. package/dist/components/dialogs/GoToDialog.js.map +1 -0
  49. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  50. package/dist/components/dialogs/HelpDialog.js +30 -0
  51. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  52. package/dist/components/dialogs/MetadataDialog.js +11 -3
  53. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  54. package/dist/components/dialogs/goTo.d.ts +5 -0
  55. package/dist/components/{header → dialogs}/goTo.js +1 -1
  56. package/dist/components/dialogs/goTo.js.map +1 -0
  57. package/dist/components/dialogs/metadataLabels.d.ts +4 -0
  58. package/dist/components/dialogs/metadataLabels.js +52 -0
  59. package/dist/components/dialogs/metadataLabels.js.map +1 -0
  60. package/dist/components/dialogs/metadataRecord.d.ts +1 -0
  61. package/dist/components/dialogs/metadataRecord.js +38 -0
  62. package/dist/components/dialogs/metadataRecord.js.map +1 -0
  63. package/dist/components/header/Header.d.ts +6 -2
  64. package/dist/components/header/Header.js +30 -34
  65. package/dist/components/header/Header.js.map +1 -1
  66. package/dist/components/header/HiddenColumnsChip.d.ts +6 -0
  67. package/dist/components/header/HiddenColumnsChip.js +15 -0
  68. package/dist/components/header/HiddenColumnsChip.js.map +1 -0
  69. package/dist/components/header/{FileMenu.d.ts → MenuBar.d.ts} +3 -3
  70. package/dist/components/header/MenuBar.js +33 -0
  71. package/dist/components/header/MenuBar.js.map +1 -0
  72. package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
  73. package/dist/components/header/ScrollZoomCheckbox.js +19 -0
  74. package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
  75. package/dist/components/header/ZoomControls.js +12 -48
  76. package/dist/components/header/ZoomControls.js.map +1 -1
  77. package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
  78. package/dist/components/header/colorSchemeMenuItems.js +106 -0
  79. package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
  80. package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
  81. package/dist/components/header/getAnnotationsMenu.js +48 -54
  82. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  83. package/dist/components/header/menuBarItems.d.ts +11 -0
  84. package/dist/components/header/menuBarItems.js +277 -0
  85. package/dist/components/header/menuBarItems.js.map +1 -0
  86. package/dist/components/import/ImportForm.js +12 -6
  87. package/dist/components/import/ImportForm.js.map +1 -1
  88. package/dist/components/import/data/seq2.d.ts +0 -1
  89. package/dist/components/import/data/seq2.js +0 -17
  90. package/dist/components/import/data/seq2.js.map +1 -1
  91. package/dist/components/minimap/Minimap.js +7 -3
  92. package/dist/components/minimap/Minimap.js.map +1 -1
  93. package/dist/components/minimap/MinimapSVG.js +2 -1
  94. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  95. package/dist/components/minimap/minimapLayout.d.ts +12 -0
  96. package/dist/components/minimap/minimapLayout.js +20 -0
  97. package/dist/components/minimap/minimapLayout.js.map +1 -1
  98. package/dist/components/msa/MSACanvas.js +28 -10
  99. package/dist/components/msa/MSACanvas.js.map +1 -1
  100. package/dist/components/msa/MSACanvasBlock.js +17 -16
  101. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  102. package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
  103. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  104. package/dist/components/msa/WheelZoomHint.d.ts +7 -0
  105. package/dist/components/msa/WheelZoomHint.js +38 -0
  106. package/dist/components/msa/WheelZoomHint.js.map +1 -0
  107. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  108. package/dist/components/msa/drawFeatureSpans.js +99 -56
  109. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  110. package/dist/components/msa/msaKeys.js +3 -0
  111. package/dist/components/msa/msaKeys.js.map +1 -1
  112. package/dist/components/msa/msaRaster.d.ts +14 -2
  113. package/dist/components/msa/msaRaster.js +20 -10
  114. package/dist/components/msa/msaRaster.js.map +1 -1
  115. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  116. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +39 -8
  117. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  118. package/dist/components/msa/renderHighlights.d.ts +2 -5
  119. package/dist/components/msa/renderHighlights.js +5 -6
  120. package/dist/components/msa/renderHighlights.js.map +1 -1
  121. package/dist/components/msa/renderMSABlock.d.ts +6 -1
  122. package/dist/components/msa/renderMSABlock.js +44 -17
  123. package/dist/components/msa/renderMSABlock.js.map +1 -1
  124. package/dist/components/msa/renderMSAMouseover.js +2 -1
  125. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  126. package/dist/components/msa/tileColor.js +7 -0
  127. package/dist/components/msa/tileColor.js.map +1 -1
  128. package/dist/components/renderCtx.d.ts +1 -1
  129. package/dist/components/rowpanels/RowPanelBlock.js +2 -2
  130. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  131. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  132. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  133. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  134. package/dist/components/settings/MenuItemsForm.d.ts +10 -0
  135. package/dist/components/settings/MenuItemsForm.js +80 -0
  136. package/dist/components/settings/MenuItemsForm.js.map +1 -0
  137. package/dist/components/settings/SettingsPanel.d.ts +12 -0
  138. package/dist/components/settings/SettingsPanel.js +92 -0
  139. package/dist/components/settings/SettingsPanel.js.map +1 -0
  140. package/dist/components/{header/ZoomMenu.d.ts → settings/TrackList.d.ts} +2 -2
  141. package/dist/components/settings/TrackList.js +55 -0
  142. package/dist/components/settings/TrackList.js.map +1 -0
  143. package/dist/components/tracks/TrackBlocks.js +2 -2
  144. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  145. package/dist/components/tracks/drawTracks.d.ts +1 -53
  146. package/dist/components/tracks/drawTracks.js +5 -5
  147. package/dist/components/tracks/drawTracks.js.map +1 -1
  148. package/dist/components/tree/CladeLabels.d.ts +1 -1
  149. package/dist/components/tree/CladeLabels.js +22 -34
  150. package/dist/components/tree/CladeLabels.js.map +1 -1
  151. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  152. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  153. package/dist/components/tree/TreeMenu.js +4 -6
  154. package/dist/components/tree/TreeMenu.js.map +1 -1
  155. package/dist/components/tree/TreeOverview.js +2 -2
  156. package/dist/components/tree/TreeOverview.js.map +1 -1
  157. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  158. package/dist/components/tree/cladeBrackets.js +33 -3
  159. package/dist/components/tree/cladeBrackets.js.map +1 -1
  160. package/dist/components/tree/renderTreeCanvas.js +2 -8
  161. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  162. package/dist/components/tree/renderTreeOverview.js +4 -11
  163. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  164. package/dist/constants.d.ts +4 -1
  165. package/dist/constants.js +7 -1
  166. package/dist/constants.js.map +1 -1
  167. package/dist/darkReader.d.ts +7 -0
  168. package/dist/darkReader.js +42 -0
  169. package/dist/darkReader.js.map +1 -0
  170. package/dist/element.js +25 -53
  171. package/dist/element.js.map +1 -1
  172. package/dist/exportLayout.d.ts +39 -0
  173. package/dist/exportLayout.js +78 -0
  174. package/dist/exportLayout.js.map +1 -0
  175. package/dist/featurePanels.d.ts +37 -0
  176. package/dist/featurePanels.js +187 -0
  177. package/dist/featurePanels.js.map +1 -0
  178. package/dist/filehandleLoader.d.ts +41 -0
  179. package/dist/filehandleLoader.js +99 -0
  180. package/dist/filehandleLoader.js.map +1 -0
  181. package/dist/headlessRenderEnv.d.ts +12 -39
  182. package/dist/headlessRenderEnv.js +18 -12
  183. package/dist/headlessRenderEnv.js.map +1 -1
  184. package/dist/hierarchy.d.ts +5 -8
  185. package/dist/hierarchy.js +5 -79
  186. package/dist/hierarchy.js.map +1 -1
  187. package/dist/index.d.ts +6 -1
  188. package/dist/index.js +6 -1
  189. package/dist/index.js.map +1 -1
  190. package/dist/measureTextCanvas.d.ts +7 -0
  191. package/dist/measureTextCanvas.js +8 -6
  192. package/dist/measureTextCanvas.js.map +1 -1
  193. package/dist/model/treeModel.d.ts +6 -2
  194. package/dist/model/treeModel.js +8 -4
  195. package/dist/model/treeModel.js.map +1 -1
  196. package/dist/model.d.ts +180 -76
  197. package/dist/model.js +358 -618
  198. package/dist/model.js.map +1 -1
  199. package/dist/neighborJoining.js +1 -40
  200. package/dist/neighborJoining.js.map +1 -1
  201. package/dist/renderToSvg.d.ts +2 -7
  202. package/dist/renderToSvg.js +33 -133
  203. package/dist/renderToSvg.js.map +1 -1
  204. package/dist/rerootTree.d.ts +1 -0
  205. package/dist/rerootTree.js +7 -6
  206. package/dist/rerootTree.js.map +1 -1
  207. package/dist/schemeResidueColor.d.ts +12 -0
  208. package/dist/schemeResidueColor.js +33 -0
  209. package/dist/schemeResidueColor.js.map +1 -0
  210. package/dist/similarityColors.d.ts +11 -0
  211. package/dist/similarityColors.js +90 -0
  212. package/dist/similarityColors.js.map +1 -0
  213. package/dist/svgTestUtil.d.ts +74 -32
  214. package/dist/treeDisplay.d.ts +37 -0
  215. package/dist/treeDisplay.js +209 -0
  216. package/dist/treeDisplay.js.map +1 -0
  217. package/dist/types.d.ts +6 -3
  218. package/dist/useWheelScroll.d.ts +3 -1
  219. package/dist/useWheelScroll.js +36 -17
  220. package/dist/useWheelScroll.js.map +1 -1
  221. package/dist/util.d.ts +2 -0
  222. package/dist/util.js +2 -0
  223. package/dist/util.js.map +1 -1
  224. package/dist/version.d.ts +1 -1
  225. package/dist/version.js +1 -1
  226. package/dist/viewerSnapshot.d.ts +169 -0
  227. package/dist/viewerSnapshot.js +100 -0
  228. package/dist/viewerSnapshot.js.map +1 -0
  229. package/dist/wheelZoom.d.ts +8 -3
  230. package/dist/wheelZoom.js +44 -32
  231. package/dist/wheelZoom.js.map +1 -1
  232. package/dist/zoomSnapshot.d.ts +29 -0
  233. package/dist/zoomSnapshot.js +47 -0
  234. package/dist/zoomSnapshot.js.map +1 -0
  235. package/package.json +11 -11
  236. package/src/blosum62.ts +45 -0
  237. package/src/colorSchemes.ts +2 -4
  238. package/src/columnCounts.ts +1 -1
  239. package/src/components/Loading.tsx +25 -14
  240. package/src/components/MSAView.tsx +22 -5
  241. package/src/components/MSAViewer.tsx +275 -251
  242. package/src/components/MetadataTable.tsx +83 -26
  243. package/src/components/ResizeHandles.tsx +9 -2
  244. package/src/components/Track.tsx +15 -1
  245. package/src/components/controls.ts +62 -0
  246. package/src/components/dialogs/AnnotationFileDialog.tsx +16 -21
  247. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  248. package/src/components/dialogs/FeatureDialog.tsx +2 -5
  249. package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
  250. package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
  251. package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
  252. package/src/components/dialogs/GoToDialog.tsx +30 -0
  253. package/src/components/dialogs/HelpDialog.tsx +79 -0
  254. package/src/components/dialogs/MetadataDialog.tsx +32 -4
  255. package/src/components/{header → dialogs}/goTo.ts +1 -1
  256. package/src/components/dialogs/metadataLabels.ts +54 -0
  257. package/src/components/dialogs/metadataRecord.tsx +61 -0
  258. package/src/components/header/Header.tsx +75 -44
  259. package/src/components/header/HiddenColumnsChip.tsx +39 -0
  260. package/src/components/header/MenuBar.tsx +64 -0
  261. package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
  262. package/src/components/header/ZoomControls.tsx +23 -74
  263. package/src/components/header/colorSchemeMenuItems.ts +121 -0
  264. package/src/components/header/getAnnotationsMenu.ts +52 -56
  265. package/src/components/header/menuBarItems.ts +357 -0
  266. package/src/components/import/ImportForm.tsx +19 -7
  267. package/src/components/import/data/seq2.ts +0 -18
  268. package/src/components/minimap/Minimap.tsx +11 -3
  269. package/src/components/minimap/MinimapSVG.tsx +17 -1
  270. package/src/components/minimap/minimapLayout.ts +27 -0
  271. package/src/components/msa/MSACanvas.tsx +24 -4
  272. package/src/components/msa/MSACanvasBlock.tsx +20 -17
  273. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
  274. package/src/components/msa/WheelZoomHint.tsx +56 -0
  275. package/src/components/msa/drawFeatureSpans.ts +160 -79
  276. package/src/components/msa/msaKeys.ts +3 -0
  277. package/src/components/msa/msaRaster.ts +26 -10
  278. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +62 -24
  279. package/src/components/msa/renderHighlights.ts +5 -6
  280. package/src/components/msa/renderMSABlock.ts +51 -15
  281. package/src/components/msa/renderMSAMouseover.ts +5 -1
  282. package/src/components/msa/tileColor.ts +7 -0
  283. package/src/components/renderCtx.ts +0 -1
  284. package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
  285. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  286. package/src/components/settings/MenuItemsForm.tsx +225 -0
  287. package/src/components/settings/SettingsPanel.tsx +156 -0
  288. package/src/components/settings/TrackList.tsx +119 -0
  289. package/src/components/tracks/TrackBlocks.tsx +2 -2
  290. package/src/components/tracks/drawTracks.ts +5 -5
  291. package/src/components/tree/CladeLabels.tsx +30 -54
  292. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  293. package/src/components/tree/TreeMenu.tsx +4 -6
  294. package/src/components/tree/TreeOverview.tsx +2 -2
  295. package/src/components/tree/cladeBrackets.ts +50 -3
  296. package/src/components/tree/renderTreeCanvas.ts +2 -13
  297. package/src/components/tree/renderTreeOverview.ts +7 -14
  298. package/src/constants.ts +9 -1
  299. package/src/darkReader.ts +57 -0
  300. package/src/element.ts +37 -64
  301. package/src/exportLayout.ts +134 -0
  302. package/src/featurePanels.ts +269 -0
  303. package/src/filehandleLoader.ts +136 -0
  304. package/src/headlessRenderEnv.ts +24 -11
  305. package/src/hierarchy.ts +5 -88
  306. package/src/index.ts +9 -5
  307. package/src/measureTextCanvas.ts +8 -6
  308. package/src/model/treeModel.ts +9 -3
  309. package/src/model.ts +452 -807
  310. package/src/neighborJoining.ts +1 -42
  311. package/src/renderToSvg.tsx +56 -207
  312. package/src/rerootTree.ts +7 -6
  313. package/src/schemeResidueColor.ts +37 -0
  314. package/src/similarityColors.ts +112 -0
  315. package/src/treeDisplay.ts +278 -0
  316. package/src/types.ts +6 -4
  317. package/src/useWheelScroll.ts +43 -22
  318. package/src/util.ts +3 -0
  319. package/src/version.ts +1 -1
  320. package/src/viewerSnapshot.ts +147 -0
  321. package/src/wheelZoom.ts +55 -33
  322. package/src/zoomSnapshot.ts +80 -0
  323. package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
  324. package/dist/components/header/ColorSchemeMenu.js +0 -37
  325. package/dist/components/header/ColorSchemeMenu.js.map +0 -1
  326. package/dist/components/header/FileMenu.js +0 -66
  327. package/dist/components/header/FileMenu.js.map +0 -1
  328. package/dist/components/header/GappynessSlider.js.map +0 -1
  329. package/dist/components/header/GoToBox.js +0 -31
  330. package/dist/components/header/GoToBox.js.map +0 -1
  331. package/dist/components/header/MSASettingsMenu.d.ts +0 -6
  332. package/dist/components/header/MSASettingsMenu.js +0 -11
  333. package/dist/components/header/MSASettingsMenu.js.map +0 -1
  334. package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
  335. package/dist/components/header/ScrollZoomIcons.js +0 -35
  336. package/dist/components/header/ScrollZoomIcons.js.map +0 -1
  337. package/dist/components/header/TreeSettingsMenu.js +0 -42
  338. package/dist/components/header/TreeSettingsMenu.js.map +0 -1
  339. package/dist/components/header/ZoomMenu.js +0 -37
  340. package/dist/components/header/ZoomMenu.js.map +0 -1
  341. package/dist/components/header/goTo.d.ts +0 -20
  342. package/dist/components/header/goTo.js.map +0 -1
  343. package/dist/components/header/settingsMenuItems.d.ts +0 -4
  344. package/dist/components/header/settingsMenuItems.js +0 -127
  345. package/dist/components/header/settingsMenuItems.js.map +0 -1
  346. package/src/components/header/ColorSchemeMenu.tsx +0 -56
  347. package/src/components/header/FileMenu.tsx +0 -81
  348. package/src/components/header/MSASettingsMenu.tsx +0 -26
  349. package/src/components/header/ScrollZoomIcons.tsx +0 -52
  350. package/src/components/header/TreeSettingsMenu.tsx +0 -60
  351. package/src/components/header/ZoomMenu.tsx +0 -48
  352. package/src/components/header/settingsMenuItems.ts +0 -157
@@ -0,0 +1,269 @@
1
+ import { notEmpty } from '@jbrowse/core/util'
2
+
3
+ import { packDomainLanes } from './components/msa/packDomainLanes.ts'
4
+ import { defaultFeaturePanelWidth } from './constants.ts'
5
+ import { featureField } from './featureFields.ts'
6
+ import { resolveScale } from './scales.ts'
7
+ import { outlineColor } from './util.ts'
8
+
9
+ import type { ScaleSpec } from './scales.ts'
10
+ import type {
11
+ Annotation,
12
+ DomainBand,
13
+ RowFeaturesSpec,
14
+ RowPanelSpan,
15
+ RowPanelSpec,
16
+ } from './types.ts'
17
+
18
+ // the fill of a span under a scale that gives its value no color, such as a
19
+ // gene a `map` over gene names leaves out
20
+ const UNSCALED_FEATURE = '#d9d9d9'
21
+
22
+ // the fraction of a span a following one may cover before it takes a lane of
23
+ // its own in a `features` panel
24
+ const laneOverlap = 0.1
25
+
26
+ // a row panel's own width, or a default: the row height, which makes a strip
27
+ // cell square, and a gene neighborhood's width for a features panel. The row
28
+ // height is read only when used, so a panel with a width of its own does not
29
+ // recompute on a vertical zoom
30
+ export function rowPanelWidth(
31
+ panel: RowPanelSpec,
32
+ view: { rowHeight: number },
33
+ ) {
34
+ return (
35
+ panel.width ??
36
+ (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
37
+ )
38
+ }
39
+
40
+ // one row of the row table as the channels read it. A number or a boolean
41
+ // reads as its string, the way a GFF value does, and null or an object drops
42
+ export function stringFields(row: unknown) {
43
+ if (typeof row !== 'object' || row === null) {
44
+ return undefined
45
+ }
46
+ const fields: Record<string, string> = {}
47
+ for (const [field, value] of Object.entries(row)) {
48
+ if (typeof value === 'string') {
49
+ fields[field] = value
50
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
51
+ fields[field] = String(value)
52
+ }
53
+ }
54
+ return fields
55
+ }
56
+
57
+ // the color each row takes from a field of the row table under a scale
58
+ export function colorByRow(
59
+ rowData: Record<string, Record<string, string> | undefined>,
60
+ field: string,
61
+ colorOf: (value: string) => string | undefined,
62
+ ) {
63
+ const colors = new Map<string, string>()
64
+ for (const [name, row] of Object.entries(rowData)) {
65
+ const value = row?.[field]
66
+ const color = value === undefined ? undefined : colorOf(value)
67
+ if (color) {
68
+ colors.set(name, color)
69
+ }
70
+ }
71
+ return colors
72
+ }
73
+
74
+ // the fill and outline of every feature in a list: its own GFF `color=` first,
75
+ // then the scale an encoding resolves over one of its fields, then the
76
+ // accession palette
77
+ export function featureColorMap(
78
+ annotations: Annotation[],
79
+ encoding:
80
+ | { field: string; colorOf: (value: string) => string | undefined }
81
+ | undefined,
82
+ fillPalette: Record<string, string>,
83
+ ) {
84
+ const strokes = new Map<string, string>()
85
+ const strokeOf = (fill: string) => {
86
+ const hit = strokes.get(fill)
87
+ if (hit !== undefined) {
88
+ return hit
89
+ }
90
+ const stroke = outlineColor(fill)
91
+ strokes.set(fill, stroke)
92
+ return stroke
93
+ }
94
+ return new Map(
95
+ annotations.map(annotation => {
96
+ const value = encoding
97
+ ? featureField(annotation, encoding.field)
98
+ : undefined
99
+ const fill =
100
+ annotation.color ??
101
+ (value === undefined ? undefined : encoding!.colorOf(value)) ??
102
+ (encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
103
+ return [annotation, { fill, stroke: strokeOf(fill) }]
104
+ }),
105
+ )
106
+ }
107
+
108
+ // the text a label channel draws inside each span, for the features carrying
109
+ // the field it names
110
+ export function featureLabelMap(annotations: Annotation[], field: string) {
111
+ const labels = new Map<Annotation, string>()
112
+ for (const annotation of annotations) {
113
+ const value = featureField(annotation, field)
114
+ if (value !== undefined) {
115
+ labels.set(annotation, value)
116
+ }
117
+ }
118
+ return labels
119
+ }
120
+
121
+ /** what `packDomainLanes` needs to lay a row out, before it has its lane */
122
+ type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
123
+ startCol: number
124
+ endCol: number
125
+ }
126
+
127
+ /**
128
+ * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
129
+ * forward features stack above the line and the reverse below it, each strand
130
+ * packing on its own. The deepest row on each side sets the grid every row lays
131
+ * out on, so the line between the strands sits at one height down the panel and
132
+ * a reader can scan it. A feature with no strand piles with the forward ones.
133
+ */
134
+ function strandpileLanes(
135
+ rows: [string, Unlaned[]][],
136
+ ): Map<string, RowPanelSpan[]> {
137
+ const piled = rows.map(([name, bands]) => {
138
+ const reverse = packDomainLanes(
139
+ bands.filter(b => b.annotation.strand === -1),
140
+ )
141
+ const forward = packDomainLanes(
142
+ bands.filter(b => b.annotation.strand !== -1),
143
+ )
144
+ return { name, forward, reverse }
145
+ })
146
+ const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
147
+ const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
148
+ const laneCount = up + down
149
+ return new Map(
150
+ piled.map(({ name, forward, reverse }) => [
151
+ name,
152
+ [
153
+ // forward level 0 is the lane just above the line, stacking upward
154
+ ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
155
+ ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
156
+ ],
157
+ ]),
158
+ )
159
+ }
160
+
161
+ /** one row's spans laid out by the panel's `position` */
162
+ function panelLanes(
163
+ panel: RowFeaturesSpec,
164
+ rows: [string, Unlaned[]][],
165
+ ): Map<string, RowPanelSpan[]> {
166
+ return panel.position === 'strandpile'
167
+ ? strandpileLanes(rows)
168
+ : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
169
+ }
170
+
171
+ /**
172
+ * The spans a `features` panel draws, keyed by row name and measured in the
173
+ * panel's own pixels. `column` takes the bands the overlay draws, at the
174
+ * alignment's column width. `position` packs each row's features in its own
175
+ * residue positions, shifted by an `align` transform, and maps the extent they
176
+ * cover across every row onto the panel width, so a row with no alignment
177
+ * still has an x.
178
+ */
179
+ export function featurePanelSpans({
180
+ panel,
181
+ width,
182
+ colWidth,
183
+ domainBands,
184
+ annotationsByRow,
185
+ shifts,
186
+ }: {
187
+ panel: RowFeaturesSpec
188
+ width: number
189
+ colWidth: number
190
+ domainBands: Map<string, DomainBand[]>
191
+ annotationsByRow: Record<string, Annotation[]>
192
+ shifts: Map<string, number> | undefined
193
+ }): Map<string, RowPanelSpan[]> {
194
+ if (panel.x === 'column') {
195
+ return panelLanes(
196
+ panel,
197
+ [...domainBands].map(([name, bands]) => [
198
+ name,
199
+ bands.map(band => ({
200
+ ...band,
201
+ xStart: band.startCol * colWidth,
202
+ xEnd: band.endCol * colWidth,
203
+ })),
204
+ ]),
205
+ )
206
+ }
207
+ const shifted = Object.entries(annotationsByRow).map(
208
+ ([name, annotations]) => {
209
+ const shift = shifts?.get(name) ?? 0
210
+ return [
211
+ name,
212
+ annotations.map(annotation => ({
213
+ annotation,
214
+ start: annotation.start - 1 + shift,
215
+ end: annotation.end + shift,
216
+ })),
217
+ ] as const
218
+ },
219
+ )
220
+ let min = Infinity
221
+ let max = -Infinity
222
+ for (const [, features] of shifted) {
223
+ for (const { start, end } of features) {
224
+ min = Math.min(min, start)
225
+ max = Math.max(max, end)
226
+ }
227
+ }
228
+ // a gene arrow ends where its feature does, so the extent maps onto the whole
229
+ // panel less the pixel the rightmost stroke needs
230
+ const drawable = Math.max(1, width - 1)
231
+ const scale = max > min ? drawable / (max - min) : 0
232
+ // a lane opens where a span covers more than a tenth of the one before it,
233
+ // which keeps the genes of an operon on one lane: adjacent genes commonly
234
+ // share a few bases, and a stop codon overlapping the next start reads as a
235
+ // second lane over the whole row
236
+ return panelLanes(
237
+ panel,
238
+ shifted.map(([name, features]) => [
239
+ name,
240
+ features.map(({ annotation, start, end }) => ({
241
+ annotation,
242
+ xStart: (start - min) * scale,
243
+ xEnd: (end - min) * scale,
244
+ startCol: start,
245
+ endCol: end - (end - start) * laneOverlap,
246
+ })),
247
+ ]),
248
+ )
249
+ }
250
+
251
+ // a scale over the values a field takes across every feature loaded, so hiding
252
+ // a type in the filter dialog leaves the rest in their colors, and a legend of
253
+ // the values the features drawn carry
254
+ export function resolveFeatureScale(
255
+ field: string,
256
+ scale: ScaleSpec | undefined,
257
+ annotations: Annotation[],
258
+ drawn: Annotation[],
259
+ ) {
260
+ const valuesOf = (list: Annotation[]) =>
261
+ list.map(a => featureField(a, field)).filter(notEmpty)
262
+ const resolved = resolveScale(scale, valuesOf(annotations))
263
+ const shown = new Set(valuesOf(drawn))
264
+ return {
265
+ field,
266
+ ...resolved,
267
+ legend: resolved.legend.filter(entry => shown.has(entry.id)),
268
+ }
269
+ }
@@ -0,0 +1,136 @@
1
+ import { openLocation } from '@jbrowse/core/util/io'
2
+ import { addDisposer, isAlive } from '@jbrowse/mobx-state-tree'
3
+ import { autorun, transaction } from 'mobx'
4
+
5
+ import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
6
+
7
+ import type { FetchStatus } from './fetchUtils.ts'
8
+ import type { FileLocation } from '@jbrowse/core/util/types'
9
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree'
10
+
11
+ interface LoaderHost extends IStateTreeNode {
12
+ setStatus: (status?: FetchStatus) => void
13
+ setError: (error?: unknown) => void
14
+ addWarning: (warning: string) => void
15
+ }
16
+
17
+ interface FilehandleLoader {
18
+ /** the layer's name in a failure message */
19
+ what: string
20
+ getFilehandle: () => FileLocation | undefined
21
+ onLoad: (text: string) => void
22
+ setLoading?: (loading: boolean) => void
23
+ /** runs after a local file loads, since a blob has no URL to refetch, and on cancel */
24
+ clearFilehandle?: () => void
25
+ /** a failure adds a warning instead of an error */
26
+ optional?: boolean
27
+ }
28
+
29
+ /**
30
+ * Latest-wins abort rotation, after `createAbortRotation` in
31
+ * jbrowse-components. Each `begin()` aborts the previous fetch and returns a
32
+ * signal with an `isCurrent()` guard; gate every write after an await on it so a
33
+ * superseded or torn-down fetch cannot overwrite fresher data. `end()` in the
34
+ * `finally` closes the guard of a fetch that completed.
35
+ */
36
+ export function createAbortRotation(self: IStateTreeNode) {
37
+ let current: { controller: AbortController; ended: boolean } | undefined
38
+ return {
39
+ /** aborts the running fetch, and returns whether one was running */
40
+ cancel() {
41
+ const running = current !== undefined && !current.ended
42
+ current?.controller.abort()
43
+ current = undefined
44
+ return running
45
+ },
46
+ begin() {
47
+ current?.controller.abort()
48
+ const mine = { controller: new AbortController(), ended: false }
49
+ current = mine
50
+ return {
51
+ controller: mine.controller,
52
+ isCurrent: () => !mine.ended && mine === current && isAlive(self),
53
+ end() {
54
+ mine.ended = true
55
+ },
56
+ }
57
+ },
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Fetches the filehandle whenever it changes and hands the text to `onLoad`. A
63
+ * superseded or cleared request is aborted and clears its status line.
64
+ */
65
+ export function installFilehandleLoader(
66
+ self: LoaderHost,
67
+ {
68
+ what,
69
+ getFilehandle,
70
+ onLoad,
71
+ setLoading,
72
+ clearFilehandle,
73
+ optional = false,
74
+ }: FilehandleLoader,
75
+ ) {
76
+ const rotation = createAbortRotation(self)
77
+ const cancel = () => {
78
+ if (rotation.cancel() && isAlive(self)) {
79
+ self.setStatus(undefined)
80
+ }
81
+ }
82
+ addDisposer(self, cancel)
83
+ addDisposer(
84
+ self,
85
+ autorun(async () => {
86
+ const filehandle = getFilehandle()
87
+ cancel()
88
+ if (!filehandle) {
89
+ setLoading?.(false)
90
+ return
91
+ }
92
+ const { controller, isCurrent, end } = rotation.begin()
93
+ try {
94
+ setLoading?.(true)
95
+ if (!optional) {
96
+ self.setError(undefined)
97
+ }
98
+ const text = await fetchTextWithProgress(
99
+ openLocation(filehandle),
100
+ status => {
101
+ if (isCurrent()) {
102
+ self.setStatus(status)
103
+ }
104
+ },
105
+ { controller },
106
+ )
107
+ if (isCurrent()) {
108
+ transaction(() => {
109
+ onLoad(text)
110
+ if (filehandle.locationType === 'BlobLocation') {
111
+ clearFilehandle?.()
112
+ }
113
+ })
114
+ }
115
+ } catch (e) {
116
+ if (isCurrent()) {
117
+ if (isAbortError(e)) {
118
+ clearFilehandle?.()
119
+ } else {
120
+ console.error(e)
121
+ if (optional) {
122
+ self.addWarning(`The ${what} did not load: ${e}`)
123
+ } else {
124
+ self.setError(e)
125
+ }
126
+ }
127
+ }
128
+ } finally {
129
+ if (isCurrent()) {
130
+ setLoading?.(false)
131
+ }
132
+ end()
133
+ }
134
+ }),
135
+ )
136
+ }
@@ -12,7 +12,9 @@
12
12
  * leaves the rest undefined -- every emitted transform then carries `NaN` in its
13
13
  * x components, which no assertion on a drawn x-position can survive.
14
14
  */
15
- export class HeadlessDOMMatrix {
15
+ import { CHAR_WIDTH_RATIO } from './measureTextCanvas.ts'
16
+
17
+ class HeadlessDOMMatrix {
16
18
  a: number
17
19
  b: number
18
20
  c: number
@@ -50,7 +52,7 @@ export class HeadlessDOMMatrix {
50
52
  }
51
53
  }
52
54
 
53
- export class HeadlessDOMPoint {
55
+ class HeadlessDOMPoint {
54
56
  constructor(
55
57
  public x = 0,
56
58
  public y = 0,
@@ -69,10 +71,11 @@ export class HeadlessDOMPoint {
69
71
  * its own jsdom passes that window instead, because Node has no global
70
72
  * HTMLCanvasElement to patch.
71
73
  *
72
- * `measureText` reports 0.6em per character, close enough to a real sans-serif
73
- * average for layout and, more usefully, exactly predictable: a caller can
74
- * compute the width a renderer will see rather than hardcoding a number
75
- * measured from one machine's fonts.
74
+ * Without `measureContext`, `measureText` reports 0.6em per character, close
75
+ * enough to a real sans-serif average for layout and exactly predictable: a
76
+ * test can compute the width a renderer will see. A caller with a real canvas,
77
+ * such as react-msaview-cli with @napi-rs/canvas, passes one of its 2D
78
+ * contexts as `measureContext` to measure with the installed fonts.
76
79
  *
77
80
  * `contextExtras` adds to the context a test hands out -- the raster test
78
81
  * supplies the ImageData methods that make the export take its image path.
@@ -80,6 +83,7 @@ export class HeadlessDOMPoint {
80
83
  export function installHeadlessRenderEnv(
81
84
  win: unknown = globalThis,
82
85
  contextExtras: Partial<CanvasRenderingContext2D> = {},
86
+ measureContext?: MeasureContext,
83
87
  ) {
84
88
  const g = globalThis as Record<string, unknown>
85
89
  g.DOMMatrix = HeadlessDOMMatrix
@@ -96,14 +100,23 @@ export function installHeadlessRenderEnv(
96
100
  set font(v: string) {
97
101
  font = v
98
102
  },
99
- measureText: (t: string) => ({
100
- width: t.length * (Number.parseFloat(font) || 10) * CHAR_WIDTH_RATIO,
101
- }),
103
+ measureText: (t: string) => {
104
+ if (measureContext) {
105
+ measureContext.font = font
106
+ return { width: measureContext.measureText(t).width }
107
+ }
108
+ return {
109
+ width:
110
+ t.length * (Number.parseFloat(font) || 10) * CHAR_WIDTH_RATIO,
111
+ }
112
+ },
102
113
  },
103
114
  contextExtras,
104
115
  ) as unknown as CanvasRenderingContext2D
105
116
  } as unknown as typeof HTMLCanvasElement.prototype.getContext
106
117
  }
107
118
 
108
- /** the per-character width factor `measureText` above reports */
109
- export const CHAR_WIDTH_RATIO = 0.6
119
+ export interface MeasureContext {
120
+ font: string
121
+ measureText(text: string): { width: number }
122
+ }
package/src/hierarchy.ts CHANGED
@@ -61,56 +61,6 @@ export function hierarchy<T>(
61
61
  return coreHierarchy(data, childrenAccessor)
62
62
  }
63
63
 
64
- export function clusterLayout<T>(
65
- root: HierarchyNode<T>,
66
- sizeX: number,
67
- sizeY: number,
68
- ) {
69
- const leafNodes = leaves(root)
70
- const n = leafNodes.length
71
- const step = sizeX / n
72
-
73
- for (let i = 0; i < n; i++) {
74
- leafNodes[i]!.x = (i + 0.5) * step
75
- }
76
-
77
- // x of an internal node is the mean of its children's x, so process in
78
- // post-order (children before parents). The subtree span comes from the same
79
- // pass, for the block culls in the tree renderer.
80
- const nodes = descendants(root)
81
- for (let i = nodes.length - 1; i >= 0; i--) {
82
- const node = nodes[i]!
83
- if (node.children) {
84
- let sum = 0
85
- let min = Infinity
86
- let max = -Infinity
87
- for (const child of node.children) {
88
- sum += child.x!
89
- min = Math.min(min, child.xMin!)
90
- max = Math.max(max, child.xMax!)
91
- }
92
- node.x = sum / node.children.length
93
- node.xMin = min
94
- node.xMax = max
95
- } else {
96
- node.xMin = node.x
97
- node.xMax = node.x
98
- }
99
- }
100
-
101
- const rootHeight = root.height
102
- const stack = [{ node: root, depth: 0 }]
103
- while (stack.length > 0) {
104
- const { node, depth } = stack.pop()!
105
- node.y = rootHeight === 0 ? sizeY : (depth / rootHeight) * sizeY
106
- if (node.children) {
107
- for (const child of node.children) {
108
- stack.push({ node: child, depth: depth + 1 })
109
- }
110
- }
111
- }
112
- }
113
-
114
64
  export interface TreeScale {
115
65
  readonly rowHeight: number
116
66
  readonly treeWidth: number
@@ -186,10 +136,10 @@ export class LaidOutNode<T> implements HierarchyNode<T> {
186
136
  }
187
137
 
188
138
  /**
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.
139
+ * The tree laid out on new nodes: tips one row apart, a parent at the mean of
140
+ * its children, and branch length measured from the root, whose own branch is
141
+ * not drawn. A collapsed node records the farthest tip of its hidden subtree,
142
+ * where the base of its triangle goes.
193
143
  */
194
144
  export function layoutTree<T extends { length?: number }>(
195
145
  root: HierarchyNode<T>,
@@ -329,39 +279,6 @@ export function calcDepthToLeaf<T>(node: HierarchyNode<T>): number {
329
279
  return node.depthToLeaf!
330
280
  }
331
281
 
332
- // Max root-to-leaf sum of branch lengths within the subtree at d. Negative
333
- // lengths are floored at zero, the same as setBrLength does when it lays the
334
- // tree out, so the extent this reports is the one that gets drawn
335
- export function maxLength(d: HierarchyNode): number {
336
- const nodes = descendants(d)
337
- const pathLen = new Map<HierarchyNode, number>()
338
- for (let i = nodes.length - 1; i >= 0; i--) {
339
- const n = nodes[i]!
340
- let childMax = 0
341
- if (n.children) {
342
- for (const child of n.children) {
343
- childMax = Math.max(childMax, pathLen.get(child)!)
344
- }
345
- }
346
- pathLen.set(n, Math.max(n.data.length || 0, 0) + childMax)
347
- }
348
- return pathLen.get(d)!
349
- }
350
-
351
- export function setBrLength(d: HierarchyNode, y0: number, k: number) {
352
- const stack = [{ node: d, y0 }]
353
- while (stack.length > 0) {
354
- const { node, y0: acc } = stack.pop()!
355
- const next = acc + Math.max(node.data.length || 0, 0)
356
- node.len = next * k
357
- if (node.children) {
358
- for (const child of node.children) {
359
- stack.push({ node: child, y0: next })
360
- }
361
- }
362
- }
363
- }
364
-
365
282
  /**
366
283
  * Every leaf by name, with a name two leaves share mapping to undefined: a tip
367
284
  * set naming it cannot say which tip it means.
@@ -416,7 +333,7 @@ export function mrca<T extends { name: string }>(
416
333
 
417
334
  /**
418
335
  * The deepest node whose subtree covers a run of rows, given in the row-space
419
- * coordinates clusterLayout writes to xMin/xMax. A subtree's leaves are one
336
+ * coordinates layoutTree writes to xMin/xMax. A subtree's leaves are one
420
337
  * contiguous run, so at most one child per level covers the run and the descent
421
338
  * visits the tree's depth rather than its nodes.
422
339
  */
package/src/index.ts CHANGED
@@ -11,10 +11,8 @@ export { renderToSvg } from './renderToSvg.tsx'
11
11
  export type { ExportSvgOptions } from './renderToSvg.tsx'
12
12
  // renderToSvg outside a browser needs DOM bits jsdom omits; react-msaview-cli
13
13
  // and the README figure generator both drive it that way
14
- export {
15
- CHAR_WIDTH_RATIO,
16
- installHeadlessRenderEnv,
17
- } from './headlessRenderEnv.ts'
14
+ export { installHeadlessRenderEnv } from './headlessRenderEnv.ts'
15
+ export { CHAR_WIDTH_RATIO } from './measureTextCanvas.ts'
18
16
  // the row count above which calculateNeighborJoiningTreeFromMSA throws, for
19
17
  // hosts gating their own menu item
20
18
  export { maxNeighborJoiningRows } from './constants.ts'
@@ -27,8 +25,13 @@ export type {
27
25
  RegionShorthand,
28
26
  } from './expandSpec.ts'
29
27
  export { default as MSAView } from './components/Loading.tsx'
28
+ export type { ToolbarSlots } from './components/header/Header.tsx'
30
29
  export { default as MSAViewer } from './components/MSAViewer.tsx'
31
30
  export type { MSAViewerProps } from './components/MSAViewer.tsx'
31
+ // the snapshot MSAViewer builds from its props, for the CLI's --props
32
+ export { resolveTheme, viewerSnapshot } from './viewerSnapshot.ts'
33
+ // the active scheme's hex color for a row residue, to paint a structure with
34
+ export { schemeResidueColor } from './schemeResidueColor.ts'
32
35
  export { mount } from './mount.tsx'
33
36
  export { defineMsaElement } from './element.ts'
34
37
  export { useMsaSvgFigure } from './useMsaSvgFigure.ts'
@@ -37,7 +40,7 @@ export type { MountedViewer } from './mount.tsx'
37
40
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
38
41
  export type { MSAFormat, MSAParserType } from 'msa-parsers'
39
42
  export type { HierarchyNode, LaidOutNode, TreeScale } from './hierarchy.ts'
40
- // InterProScanResults is the EBI wire format that setDomains still accepts;
43
+ // InterProScanResults is the EBI wire format interProScanToAnnotations reads;
41
44
  // Annotation is the source-agnostic shape everything else is written against
42
45
  export type { InterProScanResults } from 'msa-parsers'
43
46
  // ColumnCounts, ColumnStats, DomainBand and TidyDomainAnnotation surface in the
@@ -47,6 +50,7 @@ export type { InterProScanResults } from 'msa-parsers'
47
50
  // reason.
48
51
  export type { ColumnCounts } from './columnCounts.ts'
49
52
  export type { ColumnStats } from './columnStats.ts'
53
+ export type { ZoomSnapshot } from './zoomSnapshot.ts'
50
54
  // the scale an encoding reads its field through, and what resolving one gives
51
55
  export type { ResolvedScale, ScaleSpec } from './scales.ts'
52
56
  export type {
@@ -16,16 +16,18 @@ function getCtx() {
16
16
  return ctxHandle
17
17
  }
18
18
 
19
- // Rough average glyph width as a fraction of the font size for the sans-serif
20
- // stack the tree labels use. Only reached where there is no canvas to measure
21
- // with (jsdom, SSR); estimating the label gutter beats throwing out of the
22
- // layout getters that read this on every render.
23
- const fallbackGlyphWidthRatio = 0.6
19
+ /**
20
+ * Rough average glyph width as a fraction of the font size for the sans-serif
21
+ * stack the labels use. measureTextCanvas falls back on it where there is no
22
+ * canvas to measure with (jsdom, SSR), and the headless render environment's
23
+ * `measureText` reports it.
24
+ */
25
+ export const CHAR_WIDTH_RATIO = 0.6
24
26
 
25
27
  export function measureTextCanvas(text: string, fontSize: number) {
26
28
  const ctx = getCtx()
27
29
  if (!ctx) {
28
- return text.length * fontSize * fallbackGlyphWidthRatio
30
+ return text.length * fontSize * CHAR_WIDTH_RATIO
29
31
  }
30
32
  setFontSize(ctx, fontSize)
31
33
  return ctx.measureText(text).width