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,278 @@
1
+ import { generateNodeIds, parseEmfTree, parseNewick } from 'msa-parsers'
2
+
3
+ import { cladeHighlightAlpha, cladeHighlightColor } from './constants.ts'
4
+ import { flatToTree } from './flatToTree.ts'
5
+ import {
6
+ collapse,
7
+ find,
8
+ forEachDescendant,
9
+ hierarchy,
10
+ leafIndex,
11
+ leaves,
12
+ mrca,
13
+ sort,
14
+ sum as hierarchySum,
15
+ } from './hierarchy.ts'
16
+ import { parseAsn1 } from './parseAsn1.ts'
17
+ import { midpointRoot, preorder, rerootTree } from './rerootTree.ts'
18
+ import { withAlpha } from './util.ts'
19
+
20
+ import type { HierarchyNode } from './hierarchy.ts'
21
+ import type { TreeNode } from './rerootTree.ts'
22
+ import type {
23
+ Clade,
24
+ CladeMark,
25
+ NodeWithIds,
26
+ ResolvedClade,
27
+ TreeOrder,
28
+ TreeRoot,
29
+ } from './types.ts'
30
+
31
+ export function parseTreeText(text: string) {
32
+ if (text.startsWith('BioTreeContainer')) {
33
+ return flatToTree(parseAsn1(text))
34
+ }
35
+ return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
36
+ }
37
+
38
+ // the marks that set tree state from a node rather than drawing
39
+ const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
40
+
41
+ /**
42
+ * Each tip of the full tree in the order the tree draws it, with the on-screen
43
+ * row that shows it: its own, or the row of the collapsed node standing for it
44
+ */
45
+ function tipRows(root: HierarchyNode<NodeWithIds>) {
46
+ const rows = new Map<string, { order: number; row: number }>()
47
+ let row = 0
48
+ const pushReversed = (
49
+ stack: HierarchyNode<NodeWithIds>[],
50
+ nodes: HierarchyNode<NodeWithIds>[],
51
+ ) => {
52
+ for (let i = nodes.length - 1; i >= 0; i--) {
53
+ stack.push(nodes[i]!)
54
+ }
55
+ }
56
+ const stack = [root]
57
+ while (stack.length > 0) {
58
+ const node = stack.pop()!
59
+ if (node.children?.length) {
60
+ pushReversed(stack, node.children)
61
+ } else {
62
+ const hidden = [node]
63
+ while (hidden.length > 0) {
64
+ const tip = hidden.pop()!
65
+ const kids = tip.children ?? tip._children
66
+ if (kids?.length) {
67
+ pushReversed(hidden, kids)
68
+ } else {
69
+ rows.set(tip.data.name, { order: rows.size, row })
70
+ }
71
+ }
72
+ row++
73
+ }
74
+ }
75
+ return rows
76
+ }
77
+
78
+ /**
79
+ * The rows a clade covers and the node an `mrca` names, or undefined when the
80
+ * clade does not resolve: a tip name the tree does not have or has twice, or a
81
+ * leaf count `tips` disagrees with. A `range` counts its tips in the order
82
+ * `tips` gives them, so a collapsed clade inside it counts every leaf it
83
+ * stands for. `index` is the memoized leaf pass over the tree.
84
+ */
85
+ function cladeRows(
86
+ clade: Clade,
87
+ root: HierarchyNode<NodeWithIds>,
88
+ index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
89
+ tips: Map<string, { order: number; row: number }>,
90
+ ): { rows: [number, number]; nodeId?: string } | undefined {
91
+ if (clade.range) {
92
+ const [a, b] = clade.range.map(name =>
93
+ index.get(name) ? tips.get(name) : undefined,
94
+ )
95
+ if (a === undefined || b === undefined) {
96
+ return undefined
97
+ }
98
+ const [first, last] = a.order <= b.order ? [a, b] : [b, a]
99
+ return last.order - first.order + 1 === clade.tips
100
+ ? { rows: [first.row, last.row] }
101
+ : undefined
102
+ }
103
+ const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
104
+ if (!node) {
105
+ return undefined
106
+ }
107
+ const nodeTips = leaves(node)
108
+ if (nodeTips.length !== clade.tips) {
109
+ return undefined
110
+ }
111
+ let first = Infinity
112
+ let last = -Infinity
113
+ for (const tip of nodeTips) {
114
+ const row = tips.get(tip.data.name)?.row
115
+ if (row !== undefined) {
116
+ first = Math.min(first, row)
117
+ last = Math.max(last, row)
118
+ }
119
+ }
120
+ return first <= last
121
+ ? { rows: [first, last], nodeId: node.data.id }
122
+ : undefined
123
+ }
124
+
125
+ /**
126
+ * Every clade that resolves, with its fill color settled, against the rows of
127
+ * `displayed`, the tree as drawn. The tree panel resolves against the
128
+ * displayed rows and the overview against its own, so both take the tree they
129
+ * draw. A `range` record names no node, so the seeding marks, which need one,
130
+ * drop it.
131
+ */
132
+ export function resolveClades(
133
+ clades: Clade[],
134
+ root: HierarchyNode<NodeWithIds>,
135
+ displayed: HierarchyNode<NodeWithIds>,
136
+ ): ResolvedClade[] {
137
+ const index = leafIndex(root)
138
+ const tips = tipRows(displayed)
139
+ return clades.flatMap(clade => {
140
+ const resolved = cladeRows(clade, root, index, tips)
141
+ const seeding = seedingMarks.has(clade.mark)
142
+ return resolved && !(seeding && resolved.nodeId === undefined)
143
+ ? [
144
+ {
145
+ ...resolved,
146
+ mark: clade.mark,
147
+ color: withAlpha(
148
+ clade.color ?? cladeHighlightColor,
149
+ cladeHighlightAlpha,
150
+ ),
151
+ markColor: clade.color,
152
+ label: clade.label,
153
+ },
154
+ ]
155
+ : []
156
+ })
157
+ }
158
+
159
+ const childOrders: Record<
160
+ TreeOrder,
161
+ ((a: HierarchyNode, b: HierarchyNode) => number) | undefined
162
+ > = {
163
+ input: undefined,
164
+ branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
165
+ ladderize: (a, b) => a.value! - b.value!,
166
+ ladderizeReverse: (a, b) => b.value! - a.value!,
167
+ }
168
+
169
+ /**
170
+ * The tree with the display transforms applied: leaf counts summed, children
171
+ * put in `order` and each `rotated` node's reversed, `showOnly` taken as the
172
+ * new root, and each `collapsed` clade folded. The overview builds the same
173
+ * tree without the focus, so it shows the focused subtree inside the whole.
174
+ */
175
+ export function buildTreeRoot(
176
+ tree: NodeWithIds,
177
+ {
178
+ collapsed,
179
+ rotated,
180
+ order,
181
+ showOnly,
182
+ }: {
183
+ collapsed: readonly string[]
184
+ rotated: readonly string[]
185
+ order: TreeOrder
186
+ showOnly?: string
187
+ },
188
+ ) {
189
+ let hier = hierarchy(tree, d => d.children)
190
+ hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
191
+ const compare = childOrders[order]
192
+ if (compare) {
193
+ sort(hier, compare)
194
+ }
195
+ if (rotated.length > 0) {
196
+ const flip = new Set(rotated)
197
+ forEachDescendant(hier, n => {
198
+ if (flip.has(n.data.id)) {
199
+ n.children?.reverse()
200
+ }
201
+ })
202
+ }
203
+
204
+ if (showOnly) {
205
+ const res = find(hier, n => n.data.id === showOnly)
206
+ if (res) {
207
+ hier = res
208
+ }
209
+ }
210
+
211
+ for (const collapsedId of collapsed) {
212
+ const node = find(hier, n => n.data.id === collapsedId)
213
+ if (node) {
214
+ if (node.children) {
215
+ collapse(node)
216
+ } else if (node.parent?.children) {
217
+ node.parent.children = node.parent.children.filter(
218
+ c => c.data.id !== collapsedId,
219
+ )
220
+ }
221
+ }
222
+ }
223
+
224
+ return hier
225
+ }
226
+
227
+ /**
228
+ * The node whose branch an outgroup roots the tree on: the outgroup's most
229
+ * recent common ancestor, or the rest of the tips' where that ancestor is the
230
+ * root, since the outgroup then straddles the root and the branch above the
231
+ * other side is the same branch of the unrooted tree.
232
+ */
233
+ export function outgroupNode(root: HierarchyNode, outgroup: string[]) {
234
+ const index = leafIndex(root)
235
+ const node = mrca(root, outgroup, index)
236
+ if (node !== root) {
237
+ return node
238
+ }
239
+ const out = new Set(outgroup)
240
+ const rest = leaves(root)
241
+ .map(n => n.data.name)
242
+ .filter(name => !out.has(name))
243
+ const other = mrca(root, rest, index)
244
+ return other === root ? undefined : other
245
+ }
246
+
247
+ /**
248
+ * `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
249
+ * as written become names on unnamed nodes, and a rerooting would carry them
250
+ * along as labels, so they come off first.
251
+ */
252
+ export function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
253
+ const plain = new Map<NodeWithIds, TreeNode>()
254
+ for (const node of preorder(tree).reverse()) {
255
+ plain.set(node, {
256
+ name: node.name === node.id ? undefined : node.name,
257
+ length: node.length,
258
+ children: node.children.map(c => plain.get(c)!),
259
+ })
260
+ }
261
+ const input = plain.get(tree)!
262
+ if (treeRoot === 'midpoint') {
263
+ return generateNodeIds(midpointRoot(input))
264
+ }
265
+ const target = outgroupNode(
266
+ hierarchy(tree, d => d.children),
267
+ treeRoot.outgroup,
268
+ )
269
+ return target
270
+ ? generateNodeIds(rerootTree(input, plain.get(target.data)!))
271
+ : tree
272
+ }
273
+
274
+ // the inclusive tip indices a node covers, from its xMin/xMax in a layout one
275
+ // row per tip
276
+ export function tipRange(node: HierarchyNode): [number, number] {
277
+ return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
278
+ }
package/src/types.ts CHANGED
@@ -27,14 +27,11 @@ export type ColorScheme = string | Required<Pick<ScaleSpec, 'map'>>
27
27
  export interface BasicTrackModel {
28
28
  id: string
29
29
  name: string
30
- associatedRowName?: string
31
30
  height: number
32
31
  kind: TrackKind
33
32
  // the model height this track's divider writes, shared with every track
34
33
  // carrying the same key. Absent on a track no divider resizes.
35
34
  heightKey?: string
36
- // hidden until the user asks for it, unless they have already chosen
37
- defaultOff?: boolean
38
35
  }
39
36
 
40
37
  export interface TextTrackModel extends BasicTrackModel {
@@ -64,7 +61,6 @@ export interface ArcTrackModel extends BasicTrackModel {
64
61
  }
65
62
 
66
63
  export interface BasicTrack {
67
- ReactComponent: React.FC<any>
68
64
  model: TextTrackModel & BarTrackModel & ArcTrackModel
69
65
  }
70
66
 
@@ -210,6 +206,12 @@ export interface MappedStructure {
210
206
  id: string
211
207
  kind?: 'experimental' | 'predicted'
212
208
  asymId?: string
209
+ /**
210
+ * which id `asymId` is, the file's `label_asym_id` or the author's
211
+ * `auth_asym_id`. Unset reads as either, label first, which is right for the
212
+ * common entry whose two ids agree and wrong where they collide.
213
+ */
214
+ asymIdKind?: 'label' | 'auth'
213
215
  url?: string
214
216
  }
215
217
 
@@ -1,30 +1,34 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
 
3
- import {
4
- SCROLL_ZOOM_DIVISOR,
5
- getZoomNormalizer,
6
- normalizeWheelDelta,
7
- zoomFactorFromAccum,
8
- } from './wheelZoom.ts'
3
+ import { WheelZoomSmoother } from './wheelZoom.ts'
4
+
5
+ const zoomSettleMs = 150
9
6
 
10
7
  export function useWheelScroll({
11
8
  ref,
12
9
  onScrollX,
13
10
  onScrollY,
14
11
  onZoom,
12
+ onZoomSettle,
13
+ onWheelScroll,
15
14
  scrollZoom,
16
15
  }: {
17
16
  ref: React.RefObject<HTMLDivElement | null>
18
17
  onScrollX?: (delta: number) => void
19
18
  onScrollY?: (delta: number) => void
20
19
  onZoom?: (scaleFactor: number, offsetX: number, offsetY: number) => void
20
+ onZoomSettle?: () => void
21
+ onWheelScroll?: (event: WheelEvent) => void
21
22
  scrollZoom?: boolean
22
23
  }) {
23
24
  const scheduled = useRef(false)
24
- const zoomScheduled = useRef(false)
25
+ const zoomFrame = useRef<number | undefined>(undefined)
26
+ const zoomSettle = useRef<ReturnType<typeof setTimeout> | undefined>(
27
+ undefined,
28
+ )
25
29
  const deltaX = useRef(0)
26
30
  const deltaY = useRef(0)
27
- const zoomAccum = useRef(0)
31
+ const zoomSmoother = useRef(new WheelZoomSmoother())
28
32
  const zoomX = useRef(0)
29
33
  const zoomY = useRef(0)
30
34
  const lastZoomTime = useRef<number | null>(null)
@@ -47,28 +51,31 @@ export function useWheelScroll({
47
51
  !event.shiftKey &&
48
52
  Math.abs(event.deltaY) >= Math.abs(event.deltaX)
49
53
  if (onZoom && (isCtrlZoom || isScrollZoom)) {
54
+ const zoom = onZoom
50
55
  const rect = curr!.getBoundingClientRect()
51
- // normalize away deltaMode (line/page), then divide by an adaptive
52
- // factor so fine pinches and coarse notches feel consistent
53
- const dY = normalizeWheelDelta(event.deltaY, event.deltaMode)
54
- const divisor = isCtrlZoom ? getZoomNormalizer(dY) : SCROLL_ZOOM_DIVISOR
55
- zoomAccum.current += dY / divisor
56
+ zoomSmoother.current.add(event.deltaY, event.deltaMode, isCtrlZoom)
56
57
  zoomX.current = event.clientX - rect.left
57
58
  zoomY.current = event.clientY - rect.top
58
- if (!zoomScheduled.current) {
59
- zoomScheduled.current = true
60
- requestAnimationFrame(now => {
59
+ if (zoomFrame.current === undefined) {
60
+ lastZoomTime.current = null
61
+ const frame = (now: number) => {
61
62
  const elapsed =
62
63
  lastZoomTime.current === null ? 16.67 : now - lastZoomTime.current
63
64
  lastZoomTime.current = now
64
- onZoom(
65
- zoomFactorFromAccum(zoomAccum.current, Math.min(100, elapsed)),
65
+ zoom(
66
+ zoomSmoother.current.step(Math.min(100, elapsed)),
66
67
  zoomX.current,
67
68
  zoomY.current,
68
69
  )
69
- zoomAccum.current = 0
70
- zoomScheduled.current = false
71
- })
70
+ clearTimeout(zoomSettle.current)
71
+ zoomSettle.current = setTimeout(() => {
72
+ onZoomSettle?.()
73
+ }, zoomSettleMs)
74
+ zoomFrame.current = zoomSmoother.current.idle
75
+ ? undefined
76
+ : requestAnimationFrame(frame)
77
+ }
78
+ zoomFrame.current = requestAnimationFrame(frame)
72
79
  }
73
80
  event.preventDefault()
74
81
  event.stopPropagation()
@@ -79,6 +86,7 @@ export function useWheelScroll({
79
86
  const consumeX = !!onScrollX && event.deltaX !== 0
80
87
  const consumeY = !!onScrollY && event.deltaY !== 0
81
88
  if (consumeX || consumeY) {
89
+ onWheelScroll?.(event)
82
90
  if (onScrollX) {
83
91
  deltaX.current += event.deltaX
84
92
  }
@@ -107,8 +115,21 @@ export function useWheelScroll({
107
115
  curr.addEventListener('wheel', onWheel, { passive: false })
108
116
  return () => {
109
117
  curr.removeEventListener('wheel', onWheel)
118
+ if (zoomFrame.current !== undefined) {
119
+ cancelAnimationFrame(zoomFrame.current)
120
+ zoomFrame.current = undefined
121
+ }
122
+ clearTimeout(zoomSettle.current)
110
123
  }
111
- }, [ref, onScrollX, onScrollY, onZoom, scrollZoom])
124
+ }, [
125
+ ref,
126
+ onScrollX,
127
+ onScrollY,
128
+ onZoom,
129
+ onZoomSettle,
130
+ onWheelScroll,
131
+ scrollZoom,
132
+ ])
112
133
 
113
134
  useEffect(() => {
114
135
  if (!mouseDragging) {
package/src/util.ts CHANGED
@@ -142,3 +142,6 @@ export function computeRowInsertions(blanks: number[], seq: string) {
142
142
  export function len(a: { end: number; start: number }) {
143
143
  return a.end - a.start
144
144
  }
145
+
146
+ /** reads computeds so a reaction observes them, which keeps them cached */
147
+ export function readComputed(..._values: unknown[]) {}
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '9.0.0'
1
+ export const version = '9.1.1'
@@ -0,0 +1,147 @@
1
+ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
2
+
3
+ import type { MSAViewerProps } from './components/MSAViewer.tsx'
4
+ import type { ColorScheme, Highlight } from './types.ts'
5
+
6
+ export function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
7
+ return typeof scheme === 'string'
8
+ ? { colorSchemeName: scheme }
9
+ : scheme
10
+ ? { customColorScheme: scheme.map }
11
+ : {}
12
+ }
13
+
14
+ /**
15
+ * The `highlightColumns` prop as `highlights`: one `{start, end}` per run of
16
+ * adjacent 1-based alignment columns
17
+ */
18
+ export function columnHighlights(columns: number[] = []) {
19
+ const sorted = [...new Set(columns)].sort((a, b) => a - b)
20
+ const runs: { start: number; end: number }[] = []
21
+ for (const column of sorted) {
22
+ const last = runs.at(-1)
23
+ if (last?.end === column - 1) {
24
+ last.end = column
25
+ } else {
26
+ runs.push({ start: column, end: column })
27
+ }
28
+ }
29
+ return runs
30
+ }
31
+
32
+ export function viewerHighlights({
33
+ highlights = [],
34
+ highlightColumns,
35
+ }: Pick<MSAViewerProps, 'highlights' | 'highlightColumns'>): Highlight[] {
36
+ return [...highlights, ...columnHighlights(highlightColumns)]
37
+ }
38
+
39
+ function urlLocation(uri: string | undefined) {
40
+ return uri ? { uri, locationType: 'UriLocation' as const } : undefined
41
+ }
42
+
43
+ /** each filehandle prop, or the URL prop standing in for it */
44
+ export function fileLocations({
45
+ msaFilehandle,
46
+ treeFilehandle,
47
+ gffFilehandle,
48
+ msaUrl,
49
+ treeUrl,
50
+ gffUrl,
51
+ }: MSAViewerProps) {
52
+ return {
53
+ msaFilehandle: msaFilehandle ?? urlLocation(msaUrl),
54
+ treeFilehandle: treeFilehandle ?? urlLocation(treeUrl),
55
+ gffFilehandle: gffFilehandle ?? urlLocation(gffUrl),
56
+ }
57
+ }
58
+
59
+ export function resolveTheme(theme: MSAViewerProps['theme']) {
60
+ return createJBrowseTheme(
61
+ theme === 'dark'
62
+ ? { palette: { mode: 'dark' } }
63
+ : typeof theme === 'object'
64
+ ? theme
65
+ : undefined,
66
+ )
67
+ }
68
+
69
+ /**
70
+ * The model snapshot `MSAViewer` creates from its props. The CLI's `--props`
71
+ * reads the same JSON the R and Python widgets send, so a figure exported
72
+ * headlessly draws what the widget drew.
73
+ */
74
+ export function viewerSnapshot(props: MSAViewerProps) {
75
+ const {
76
+ msa,
77
+ tree,
78
+ gff,
79
+ rowData,
80
+ colorScheme,
81
+ height,
82
+ colWidth,
83
+ rowHeight,
84
+ allowedGappyness,
85
+ highlightColumns,
86
+ highlights,
87
+ features,
88
+ selection,
89
+ clades,
90
+ relativeTo,
91
+ columnTracks,
92
+ residueMappings,
93
+ encodings,
94
+ rowPanels,
95
+ drawTree,
96
+ treeAreaWidth,
97
+ showBranchLen,
98
+ treeOrder,
99
+ treeRoot,
100
+ region,
101
+ residueEncoding,
102
+ } = props
103
+ const { msaFilehandle, treeFilehandle, gffFilehandle } = fileLocations(props)
104
+ return {
105
+ type: 'MsaView' as const,
106
+ ...(msa || tree || gff || rowData
107
+ ? {
108
+ data: {
109
+ msa: msa ?? '',
110
+ tree: tree ?? '',
111
+ gff,
112
+ // the row table is stored as this JSON string, which is what a
113
+ // shared URL and the size limit already cover
114
+ ...(rowData ? { treeMetadata: JSON.stringify(rowData) } : {}),
115
+ },
116
+ }
117
+ : {}),
118
+ ...(msaFilehandle ? { msaFilehandle } : {}),
119
+ ...(treeFilehandle ? { treeFilehandle } : {}),
120
+ ...(gffFilehandle ? { gffFilehandle } : {}),
121
+ ...colorSchemeSnapshot(colorScheme),
122
+ ...(height ? { height } : {}),
123
+ ...(colWidth ? { colWidth } : {}),
124
+ ...(rowHeight ? { rowHeight } : {}),
125
+ ...(allowedGappyness !== undefined ? { allowedGappyness } : {}),
126
+ ...(highlights || highlightColumns
127
+ ? { highlights: viewerHighlights({ highlights, highlightColumns }) }
128
+ : {}),
129
+ ...(features ? { features } : {}),
130
+ ...(selection ? { selection } : {}),
131
+ ...(clades ? { clades } : {}),
132
+ ...(relativeTo ? { relativeTo } : {}),
133
+ ...(columnTracks ? { columnTracks } : {}),
134
+ ...(residueMappings ? { residueMappings } : {}),
135
+ ...(encodings ? { encodings } : {}),
136
+ ...(rowPanels ? { rowPanels } : {}),
137
+ ...(drawTree !== undefined ? { drawTree } : {}),
138
+ ...(treeAreaWidth ? { treeAreaWidth } : {}),
139
+ ...(showBranchLen !== undefined ? { showBranchLen } : {}),
140
+ ...(treeOrder ? { treeOrder } : {}),
141
+ ...(treeRoot ? { treeRoot } : {}),
142
+ ...(region ? { region } : {}),
143
+ ...(residueEncoding !== undefined
144
+ ? { bgColor: residueEncoding === 'fill' }
145
+ : {}),
146
+ }
147
+ }
package/src/wheelZoom.ts CHANGED
@@ -1,47 +1,69 @@
1
- // Wheel / trackpad-pinch delta handling, adapted from @jbrowse/core's
2
- // util/wheelZoom (not yet in our pinned @jbrowse/core). Turns raw wheel deltas
3
- // into a per-frame zoom factor that feels consistent across fine trackpad
4
- // pinches and coarse mouse notches.
1
+ // Chrome and Firefox encode a trackpad pinch as ctrl+wheel with
2
+ // deltaY = -100 * ln(scale), so dividing by 100 tracks the fingers 1:1
3
+ const PINCH_PX_PER_LOG = 100
5
4
 
6
- // Fixed divisor for plain-wheel scroll-zoom mode, where deltas are big notches.
7
- export const SCROLL_ZOOM_DIVISOR = 500
5
+ // a plain scroll covers more pixels than a pinch for the same intent
6
+ const SCROLL_PX_PER_LOG = 400
8
7
 
9
- // Cap the zoom change per millisecond so a burst of momentum-scroll events
10
- // coalesced into one frame can't jump the zoom level (0.2 per ~60fps frame).
11
- const MAX_ZOOM_RATE_PER_MS = 0.2 / 16.67
8
+ // a pixel delta at least this large counts as a wheel notch
9
+ const NOTCH_MIN_PX = 40
10
+
11
+ export const NOTCH_ZOOM = 1.25
12
+
13
+ // the time constant a notch eases in over
14
+ const NOTCH_TAU_MS = 60
15
+
16
+ // a burst of continuous events coalesced into one frame applies at most 2x
17
+ const MAX_FRAME_LOG = Math.log(2)
18
+
19
+ // the queue holds at most four notches, which caps a fast spin
20
+ const MAX_PENDING_LOG = 4 * Math.log(NOTCH_ZOOM)
12
21
 
13
- // Convert a wheel delta to pixels regardless of the browser's deltaMode; some
14
- // browsers/mice report lines or pages instead of pixels.
15
22
  export function normalizeWheelDelta(delta: number, mode: number) {
16
23
  if (mode === 1) {
17
- return delta * 16 // DOM_DELTA_LINE
24
+ return delta * 16
18
25
  }
19
26
  if (mode === 2) {
20
- return delta * 100 // DOM_DELTA_PAGE
27
+ return delta * 100
21
28
  }
22
- return delta // DOM_DELTA_PIXEL
29
+ return delta
23
30
  }
24
31
 
25
- // Adaptive divisor: tiny pinch deltas zoom in larger steps, big mouse notches
26
- // in finer steps, so a trackpad pinch and a mouse wheel feel similar.
27
- export function getZoomNormalizer(deltaY: number) {
28
- const abs = Math.abs(deltaY)
29
- if (abs < 6) {
30
- return 25
31
- }
32
- if (abs > 150) {
33
- return 500
32
+ const clampAbs = (x: number, max: number) => Math.max(-max, Math.min(max, x))
33
+
34
+ // Positive log zooms in. Feed every wheel event to add() and call step() once
35
+ // per animation frame with the time since the last frame; step() returns the
36
+ // multiplier for colWidth/rowHeight.
37
+ export class WheelZoomSmoother {
38
+ private continuous = 0
39
+ private pending = 0
40
+
41
+ add(deltaY: number, deltaMode: number, isPinch: boolean) {
42
+ const px = normalizeWheelDelta(deltaY, deltaMode)
43
+ if (deltaMode !== 0 || Math.abs(px) >= NOTCH_MIN_PX) {
44
+ const notches =
45
+ deltaMode !== 0 ? 1 : Math.min(Math.max(Math.abs(px) / 100, 1), 3)
46
+ this.pending = clampAbs(
47
+ this.pending - Math.sign(px) * notches * Math.log(NOTCH_ZOOM),
48
+ MAX_PENDING_LOG,
49
+ )
50
+ } else {
51
+ this.continuous -= px / (isPinch ? PINCH_PX_PER_LOG : SCROLL_PX_PER_LOG)
52
+ }
34
53
  }
35
- if (abs > 30) {
36
- return 150
54
+
55
+ get idle() {
56
+ return this.continuous === 0 && this.pending === 0
37
57
  }
38
- return 75
39
- }
40
58
 
41
- // Turn accumulated, normalized zoom intent into a colWidth/rowHeight multiplier.
42
- // Positive accum (wheel toward user) zooms out (factor < 1); negative zooms in.
43
- export function zoomFactorFromAccum(zoomAccum: number, elapsedMs: number) {
44
- const max = MAX_ZOOM_RATE_PER_MS * elapsedMs
45
- const d = Math.max(-max, Math.min(max, zoomAccum))
46
- return d > 0 ? 1 / (1 + d) : 1 - d
59
+ step(elapsedMs: number) {
60
+ let eased = this.pending * (1 - Math.exp(-elapsedMs / NOTCH_TAU_MS))
61
+ if (Math.abs(this.pending - eased) < 0.005) {
62
+ eased = this.pending
63
+ }
64
+ this.pending -= eased
65
+ const log = clampAbs(this.continuous + eased, MAX_FRAME_LOG)
66
+ this.continuous = 0
67
+ return Math.exp(log)
68
+ }
47
69
  }