@genome-spy/core 0.64.0 → 0.66.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 (285) hide show
  1. package/dist/bundle/{index-CCJIjehY.js → AbortablePromiseCache-CcuMrnn7.js} +22 -91
  2. package/dist/bundle/browser-BRemItdO.js +138 -0
  3. package/dist/bundle/index-BatuyGAI.js +271 -0
  4. package/dist/bundle/index-ByuE8dvu.js +332 -0
  5. package/dist/bundle/index-Cq3QFUxX.js +1781 -0
  6. package/dist/bundle/{index-C08YCM2T.js → index-D-w7Mmt9.js} +246 -126
  7. package/dist/bundle/index-D28m8tSW.js +1607 -0
  8. package/dist/bundle/index-D74H8TTz.js +508 -0
  9. package/dist/bundle/index-DbJ0oeYM.js +631 -0
  10. package/dist/bundle/index.es.js +15034 -13842
  11. package/dist/bundle/index.js +223 -237
  12. package/dist/bundle/inflate-GtwLkvSP.js +1048 -0
  13. package/dist/bundle/unzip-NywezaRR.js +1492 -0
  14. package/dist/schema.json +22 -4
  15. package/dist/src/config/scaleDefaults.d.ts +8 -0
  16. package/dist/src/config/scaleDefaults.d.ts.map +1 -0
  17. package/dist/src/config/scaleDefaults.js +45 -0
  18. package/dist/src/data/collector.d.ts +7 -2
  19. package/dist/src/data/collector.d.ts.map +1 -1
  20. package/dist/src/data/collector.js +13 -2
  21. package/dist/src/data/dataFlow.d.ts +20 -42
  22. package/dist/src/data/dataFlow.d.ts.map +1 -1
  23. package/dist/src/data/dataFlow.js +57 -80
  24. package/dist/src/data/flowHandle.d.ts +15 -0
  25. package/dist/src/data/flowHandle.d.ts.map +1 -0
  26. package/dist/src/data/flowHandle.js +13 -0
  27. package/dist/src/data/flowInit.d.ts +85 -0
  28. package/dist/src/data/flowInit.d.ts.map +1 -0
  29. package/dist/src/data/flowInit.js +238 -0
  30. package/dist/src/data/flowInit.test.d.ts +2 -0
  31. package/dist/src/data/flowInit.test.d.ts.map +1 -0
  32. package/dist/src/data/flowOptimizer.d.ts +6 -4
  33. package/dist/src/data/flowOptimizer.d.ts.map +1 -1
  34. package/dist/src/data/flowOptimizer.js +29 -14
  35. package/dist/src/data/sources/lazy/axisTickSource.js +1 -1
  36. package/dist/src/data/sources/lazy/bamSource.js +1 -1
  37. package/dist/src/data/sources/lazy/bigBedSource.js +1 -1
  38. package/dist/src/data/sources/lazy/bigWigSource.js +1 -1
  39. package/dist/src/data/sources/lazy/gff3Source.d.ts +2 -6
  40. package/dist/src/data/sources/lazy/gff3Source.d.ts.map +1 -1
  41. package/dist/src/data/sources/lazy/gff3Source.js +4 -8
  42. package/dist/src/data/sources/lazy/indexedFastaSource.d.ts.map +1 -1
  43. package/dist/src/data/sources/lazy/indexedFastaSource.js +17 -17
  44. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
  45. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  46. package/dist/src/data/sources/lazy/singleAxisLazySource.js +10 -3
  47. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  48. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +5 -1
  49. package/dist/src/data/sources/lazy/tabixSource.js +1 -1
  50. package/dist/src/data/transforms/filterScoredLabels.d.ts +1 -1
  51. package/dist/src/data/transforms/filterScoredLabels.d.ts.map +1 -1
  52. package/dist/src/data/transforms/filterScoredLabels.js +1 -1
  53. package/dist/src/data/transforms/linearizeGenomicCoordinate.d.ts.map +1 -1
  54. package/dist/src/data/transforms/linearizeGenomicCoordinate.js +2 -1
  55. package/dist/src/encoder/encoder.d.ts +1 -1
  56. package/dist/src/encoder/encoder.d.ts.map +1 -1
  57. package/dist/src/encoder/encoder.js +1 -1
  58. package/dist/src/genome/scaleLocus.d.ts +39 -0
  59. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  60. package/dist/src/genome/scaleLocus.js +76 -0
  61. package/dist/src/genomeSpy/canvasExport.d.ts +19 -0
  62. package/dist/src/genomeSpy/canvasExport.d.ts.map +1 -0
  63. package/dist/src/genomeSpy/canvasExport.js +66 -0
  64. package/dist/src/genomeSpy/containerUi.d.ts +17 -0
  65. package/dist/src/genomeSpy/containerUi.d.ts.map +1 -0
  66. package/dist/src/genomeSpy/containerUi.js +78 -0
  67. package/dist/src/genomeSpy/eventListenerRegistry.d.ts +19 -0
  68. package/dist/src/genomeSpy/eventListenerRegistry.d.ts.map +1 -0
  69. package/dist/src/genomeSpy/eventListenerRegistry.js +38 -0
  70. package/dist/src/genomeSpy/inputBindingManager.d.ts +14 -0
  71. package/dist/src/genomeSpy/inputBindingManager.d.ts.map +1 -0
  72. package/dist/src/genomeSpy/inputBindingManager.js +63 -0
  73. package/dist/src/genomeSpy/interactionController.d.ts +40 -0
  74. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -0
  75. package/dist/src/genomeSpy/interactionController.js +371 -0
  76. package/dist/src/genomeSpy/keyboardListenerManager.d.ts +10 -0
  77. package/dist/src/genomeSpy/keyboardListenerManager.d.ts.map +1 -0
  78. package/dist/src/genomeSpy/keyboardListenerManager.js +31 -0
  79. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +15 -0
  80. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -0
  81. package/dist/src/genomeSpy/loadingIndicatorManager.js +92 -0
  82. package/dist/src/genomeSpy/renderCoordinator.d.ts +22 -0
  83. package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -0
  84. package/dist/src/genomeSpy/renderCoordinator.js +118 -0
  85. package/dist/src/genomeSpy/viewContextFactory.d.ts +18 -0
  86. package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -0
  87. package/dist/src/genomeSpy/viewContextFactory.js +79 -0
  88. package/dist/src/genomeSpy/viewDataInit.d.ts +12 -0
  89. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -0
  90. package/dist/src/genomeSpy/viewDataInit.js +41 -0
  91. package/dist/src/genomeSpy/viewHierarchyConfig.d.ts +14 -0
  92. package/dist/src/genomeSpy/viewHierarchyConfig.d.ts.map +1 -0
  93. package/dist/src/genomeSpy/viewHierarchyConfig.js +24 -0
  94. package/dist/src/genomeSpy/viewHighlight.d.ts +5 -0
  95. package/dist/src/genomeSpy/viewHighlight.d.ts.map +1 -0
  96. package/dist/src/genomeSpy/viewHighlight.js +30 -0
  97. package/dist/src/genomeSpy.d.ts +17 -72
  98. package/dist/src/genomeSpy.d.ts.map +1 -1
  99. package/dist/src/genomeSpy.js +180 -789
  100. package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
  101. package/dist/src/gl/webGLHelper.d.ts +2 -2
  102. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  103. package/dist/src/gl/webGLHelper.js +4 -4
  104. package/dist/src/index.d.ts.map +1 -1
  105. package/dist/src/index.js +2 -12
  106. package/dist/src/marks/mark.d.ts +1 -0
  107. package/dist/src/marks/mark.d.ts.map +1 -1
  108. package/dist/src/marks/mark.js +26 -3
  109. package/dist/src/{view → scales}/axisResolution.d.ts +12 -14
  110. package/dist/src/scales/axisResolution.d.ts.map +1 -0
  111. package/dist/src/{view → scales}/axisResolution.js +38 -12
  112. package/dist/src/scales/axisResolution.test.d.ts.map +1 -0
  113. package/dist/src/scales/scaleDomainAggregator.d.ts +57 -0
  114. package/dist/src/scales/scaleDomainAggregator.d.ts.map +1 -0
  115. package/dist/src/scales/scaleDomainAggregator.js +162 -0
  116. package/dist/src/scales/scaleDomainAggregator.test.d.ts +2 -0
  117. package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +1 -0
  118. package/dist/src/scales/scaleInstanceManager.d.ts +40 -0
  119. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -0
  120. package/dist/src/scales/scaleInstanceManager.js +313 -0
  121. package/dist/src/scales/scaleInstanceManager.test.d.ts +2 -0
  122. package/dist/src/scales/scaleInstanceManager.test.d.ts.map +1 -0
  123. package/dist/src/scales/scaleInteractionController.d.ts +73 -0
  124. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -0
  125. package/dist/src/scales/scaleInteractionController.js +336 -0
  126. package/dist/src/scales/scaleInteractionController.test.d.ts +2 -0
  127. package/dist/src/scales/scaleInteractionController.test.d.ts.map +1 -0
  128. package/dist/src/scales/scalePropsResolver.d.ts +23 -0
  129. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -0
  130. package/dist/src/scales/scalePropsResolver.js +74 -0
  131. package/dist/src/{view → scales}/scaleResolution.d.ts +53 -31
  132. package/dist/src/scales/scaleResolution.d.ts.map +1 -0
  133. package/dist/src/scales/scaleResolution.js +658 -0
  134. package/dist/src/scales/scaleResolution.test.d.ts.map +1 -0
  135. package/dist/src/scales/scaleResolutionConstants.d.ts +6 -0
  136. package/dist/src/scales/scaleResolutionConstants.d.ts.map +1 -0
  137. package/dist/src/scales/scaleResolutionConstants.js +5 -0
  138. package/dist/src/scales/scaleRules.d.ts +16 -0
  139. package/dist/src/scales/scaleRules.d.ts.map +1 -0
  140. package/dist/src/scales/scaleRules.js +103 -0
  141. package/dist/src/scales/scaleRules.test.d.ts +2 -0
  142. package/dist/src/scales/scaleRules.test.d.ts.map +1 -0
  143. package/dist/src/spec/channel.d.ts +13 -18
  144. package/dist/src/spec/sampleView.d.ts +3 -2
  145. package/dist/src/spec/scale.d.ts +6 -0
  146. package/dist/src/types/embedApi.d.ts +5 -0
  147. package/dist/src/types/scaleResolutionApi.d.ts +1 -1
  148. package/dist/src/types/viewContext.d.ts +1 -1
  149. package/dist/src/view/concatView.d.ts +18 -0
  150. package/dist/src/view/concatView.d.ts.map +1 -1
  151. package/dist/src/view/concatView.js +73 -0
  152. package/dist/src/view/concatView.test.d.ts +2 -0
  153. package/dist/src/view/concatView.test.d.ts.map +1 -0
  154. package/dist/src/view/containerMutationHelper.d.ts +74 -0
  155. package/dist/src/view/containerMutationHelper.d.ts.map +1 -0
  156. package/dist/src/view/containerMutationHelper.js +114 -0
  157. package/dist/src/view/containerView.d.ts +0 -7
  158. package/dist/src/view/containerView.d.ts.map +1 -1
  159. package/dist/src/view/containerView.js +0 -10
  160. package/dist/src/view/facetView.d.ts.map +1 -1
  161. package/dist/src/view/facetView.js +0 -14
  162. package/dist/src/view/flowBuilder.d.ts +2 -2
  163. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  164. package/dist/src/view/flowBuilder.js +21 -4
  165. package/dist/src/view/gridView/gridChild.d.ts +11 -0
  166. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  167. package/dist/src/view/gridView/gridChild.js +32 -6
  168. package/dist/src/view/gridView/gridView.d.ts +39 -1
  169. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  170. package/dist/src/view/gridView/gridView.js +113 -42
  171. package/dist/src/view/gridView/gridView.test.d.ts +2 -0
  172. package/dist/src/view/gridView/gridView.test.d.ts.map +1 -0
  173. package/dist/src/view/gridView/scrollbar.d.ts +39 -8
  174. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  175. package/dist/src/view/gridView/scrollbar.js +184 -69
  176. package/dist/src/view/gridView/selectionRect.d.ts +8 -4
  177. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  178. package/dist/src/view/gridView/selectionRect.js +28 -3
  179. package/dist/src/view/gridView/selectionRect.test.d.ts +2 -0
  180. package/dist/src/view/gridView/selectionRect.test.d.ts.map +1 -0
  181. package/dist/src/view/layerView.d.ts +14 -0
  182. package/dist/src/view/layerView.d.ts.map +1 -1
  183. package/dist/src/view/layerView.js +66 -0
  184. package/dist/src/view/layerView.test.d.ts +2 -0
  185. package/dist/src/view/layerView.test.d.ts.map +1 -0
  186. package/dist/src/view/paramMediator.d.ts +2 -1
  187. package/dist/src/view/paramMediator.d.ts.map +1 -1
  188. package/dist/src/view/paramMediator.js +13 -1
  189. package/dist/src/view/testUtils.d.ts.map +1 -1
  190. package/dist/src/view/testUtils.js +18 -5
  191. package/dist/src/view/unitView.d.ts.map +1 -1
  192. package/dist/src/view/unitView.js +52 -12
  193. package/dist/src/view/view.d.ts +23 -7
  194. package/dist/src/view/view.d.ts.map +1 -1
  195. package/dist/src/view/view.js +61 -5
  196. package/dist/src/view/viewDispose.test.d.ts +2 -0
  197. package/dist/src/view/viewDispose.test.d.ts.map +1 -0
  198. package/dist/src/view/viewUtils.d.ts +4 -4
  199. package/dist/src/view/viewUtils.d.ts.map +1 -1
  200. package/dist/src/view/viewUtils.js +19 -15
  201. package/dist/src/view/viewUtils.test.d.ts +2 -0
  202. package/dist/src/view/viewUtils.test.d.ts.map +1 -0
  203. package/package.json +10 -10
  204. package/dist/bundle/__vite-browser-external-C--ziKoh.js +0 -8
  205. package/dist/bundle/_commonjsHelpers-DjF3Plf2.js +0 -26
  206. package/dist/bundle/index-5ajWdKly.js +0 -1319
  207. package/dist/bundle/index-B03-Om4z.js +0 -274
  208. package/dist/bundle/index-BftNdA0O.js +0 -27
  209. package/dist/bundle/index-Bg7C4Xat.js +0 -2750
  210. package/dist/bundle/index-C3QR8Lv6.js +0 -2131
  211. package/dist/bundle/index-DTcHjAHp.js +0 -505
  212. package/dist/bundle/index-DnIkxb0L.js +0 -1025
  213. package/dist/bundle/index-Ww3TAo6_.js +0 -71
  214. package/dist/bundle/index-g8iXgW0W.js +0 -651
  215. package/dist/bundle/long-B-FASCSo.js +0 -2387
  216. package/dist/bundle/remoteFile-BuaqFGWk.js +0 -94
  217. package/dist/src/data/collector.test.js +0 -138
  218. package/dist/src/data/dataFlow.test.js +0 -5
  219. package/dist/src/data/flow.test.js +0 -81
  220. package/dist/src/data/flowNode.test.js +0 -50
  221. package/dist/src/data/flowOptimizer.test.js +0 -204
  222. package/dist/src/data/formats/fasta.test.js +0 -27
  223. package/dist/src/data/sources/inlineSource.test.js +0 -63
  224. package/dist/src/data/sources/sequenceSource.test.js +0 -81
  225. package/dist/src/data/transforms/aggregate.test.js +0 -134
  226. package/dist/src/data/transforms/clone.test.js +0 -11
  227. package/dist/src/data/transforms/coverage.test.js +0 -238
  228. package/dist/src/data/transforms/filter.test.js +0 -20
  229. package/dist/src/data/transforms/flatten.test.js +0 -96
  230. package/dist/src/data/transforms/flattenDelimited.test.js +0 -90
  231. package/dist/src/data/transforms/flattenSequence.test.js +0 -34
  232. package/dist/src/data/transforms/formula.test.js +0 -25
  233. package/dist/src/data/transforms/identifier.test.js +0 -92
  234. package/dist/src/data/transforms/pileup.test.js +0 -70
  235. package/dist/src/data/transforms/project.test.js +0 -32
  236. package/dist/src/data/transforms/regexExtract.test.js +0 -70
  237. package/dist/src/data/transforms/regexFold.test.js +0 -201
  238. package/dist/src/data/transforms/sample.test.js +0 -38
  239. package/dist/src/data/transforms/stack.test.js +0 -91
  240. package/dist/src/encoder/accessor.test.js +0 -162
  241. package/dist/src/encoder/encoder.test.js +0 -105
  242. package/dist/src/genome/genome.test.js +0 -268
  243. package/dist/src/genome/genomes.test.js +0 -8
  244. package/dist/src/genome/scaleIndex.test.js +0 -78
  245. package/dist/src/genome/scaleLocus.test.js +0 -4
  246. package/dist/src/scale/scale.test.js +0 -326
  247. package/dist/src/scale/ticks.test.js +0 -46
  248. package/dist/src/selection/selection.test.js +0 -14
  249. package/dist/src/utils/addBaseUrl.test.js +0 -30
  250. package/dist/src/utils/binnedIndex.test.js +0 -201
  251. package/dist/src/utils/cloner.test.js +0 -35
  252. package/dist/src/utils/coalesce.test.js +0 -16
  253. package/dist/src/utils/concatIterables.test.js +0 -8
  254. package/dist/src/utils/domainArray.test.js +0 -130
  255. package/dist/src/utils/indexer.test.js +0 -49
  256. package/dist/src/utils/interactionEvent.test.js +0 -35
  257. package/dist/src/utils/iterateNestedMaps.test.js +0 -33
  258. package/dist/src/utils/kWayMerge.test.js +0 -30
  259. package/dist/src/utils/mergeObjects.test.js +0 -42
  260. package/dist/src/utils/numberExtractor.test.js +0 -6
  261. package/dist/src/utils/propertyCacher.test.js +0 -89
  262. package/dist/src/utils/propertyCoalescer.test.js +0 -25
  263. package/dist/src/utils/radixSort.test.js +0 -51
  264. package/dist/src/utils/reservationMap.test.js +0 -20
  265. package/dist/src/utils/ringBuffer.test.js +0 -39
  266. package/dist/src/utils/topK.test.js +0 -54
  267. package/dist/src/utils/trees.test.js +0 -135
  268. package/dist/src/utils/url.test.js +0 -28
  269. package/dist/src/utils/variableTools.test.js +0 -13
  270. package/dist/src/view/axisResolution.d.ts.map +0 -1
  271. package/dist/src/view/axisResolution.test.d.ts.map +0 -1
  272. package/dist/src/view/axisResolution.test.js +0 -206
  273. package/dist/src/view/flowBuilder.test.js +0 -125
  274. package/dist/src/view/layout/flexLayout.test.js +0 -323
  275. package/dist/src/view/layout/grid.test.js +0 -71
  276. package/dist/src/view/layout/rectangle.test.js +0 -192
  277. package/dist/src/view/paramMediator.test.js +0 -260
  278. package/dist/src/view/scaleResolution.d.ts.map +0 -1
  279. package/dist/src/view/scaleResolution.js +0 -1049
  280. package/dist/src/view/scaleResolution.test.d.ts.map +0 -1
  281. package/dist/src/view/scaleResolution.test.js +0 -645
  282. package/dist/src/view/view.test.js +0 -245
  283. package/dist/src/view/viewFactory.test.js +0 -25
  284. /package/dist/src/{view → scales}/axisResolution.test.d.ts +0 -0
  285. /package/dist/src/{view → scales}/scaleResolution.test.d.ts +0 -0
@@ -1,53 +1,63 @@
1
1
  import { formats as vegaFormats } from "vega-loader";
2
- import { html, nothing, render } from "lit";
3
- import { styleMap } from "lit/directives/style-map.js";
4
- import SPINNER from "./img/90-ring-with-bg.svg";
5
-
6
- import css from "./styles/genome-spy.css.js";
7
- import Tooltip from "./utils/ui/tooltip.js";
8
2
 
9
3
  import {
10
- checkForDuplicateScaleNames,
11
- setImplicitScaleNames,
12
- calculateCanvasSize,
13
- } from "./view/viewUtils.js";
4
+ createContainerUi,
5
+ createMessageBox,
6
+ } from "./genomeSpy/containerUi.js";
7
+ import LoadingIndicatorManager from "./genomeSpy/loadingIndicatorManager.js";
8
+ import { createViewHighlighter } from "./genomeSpy/viewHighlight.js";
9
+ import KeyboardListenerManager from "./genomeSpy/keyboardListenerManager.js";
10
+ import EventListenerRegistry from "./genomeSpy/eventListenerRegistry.js";
11
+ import InputBindingManager from "./genomeSpy/inputBindingManager.js";
12
+
13
+ import { calculateCanvasSize } from "./view/viewUtils.js";
14
+ import { initializeViewData } from "./genomeSpy/viewDataInit.js";
14
15
  import UnitView from "./view/unitView.js";
15
16
 
16
- import WebGLHelper, {
17
- framebufferToDataUrl,
18
- readPickingPixel,
19
- } from "./gl/webGLHelper.js";
20
- import Rectangle from "./view/layout/rectangle.js";
21
- import BufferedViewRenderingContext from "./view/renderingContext/bufferedViewRenderingContext.js";
22
- import CompositeViewRenderingContext from "./view/renderingContext/compositeViewRenderingContext.js";
23
- import InteractionEvent from "./utils/interactionEvent.js";
24
- import Point from "./view/layout/point.js";
17
+ import WebGLHelper from "./gl/webGLHelper.js";
25
18
  import Animator from "./utils/animator.js";
26
19
  import DataFlow from "./data/dataFlow.js";
27
- import { buildDataFlow } from "./view/flowBuilder.js";
28
- import { optimizeDataFlow } from "./data/flowOptimizer.js";
29
20
  import GenomeStore from "./genome/genomeStore.js";
30
21
  import BmFontManager from "./fonts/bmFontManager.js";
31
22
  import fasta from "./data/formats/fasta.js";
32
- import { VISIT_STOP } from "./view/view.js";
33
- import Inertia, { makeEventTemplate } from "./utils/inertia.js";
34
23
  import refseqGeneTooltipHandler from "./tooltip/refseqGeneTooltipHandler.js";
35
24
  import dataTooltipHandler from "./tooltip/dataTooltipHandler.js";
36
25
  import { invalidatePrefix } from "./utils/propertyCacher.js";
37
26
  import { VIEW_ROOT_NAME, ViewFactory } from "./view/viewFactory.js";
38
- import { reconfigureScales } from "./view/scaleResolution.js";
39
- import createBindingInputs from "./utils/inputBinding.js";
40
- import { isStillZooming } from "./view/zoom.js";
41
- import { createFramebufferInfo } from "twgl.js";
27
+ import InteractionController from "./genomeSpy/interactionController.js";
28
+ import RenderCoordinator from "./genomeSpy/renderCoordinator.js";
29
+ import { createViewContext } from "./genomeSpy/viewContextFactory.js";
30
+ import {
31
+ configureViewHierarchy,
32
+ configureViewOpacity,
33
+ } from "./genomeSpy/viewHierarchyConfig.js";
34
+ import { exportCanvas } from "./genomeSpy/canvasExport.js";
42
35
 
43
36
  /**
44
37
  * Events that are broadcasted to all views.
45
- * @typedef {"dataFlowBuilt" | "dataLoaded" | "layout" | "layoutComputed"} BroadcastEventType
38
+ * @typedef {"dataFlowBuilt" | "layout" | "layoutComputed" | "subtreeDataReady"} BroadcastEventType
46
39
  */
47
40
 
48
41
  vegaFormats("fasta", fasta);
49
42
 
50
43
  export default class GenomeSpy {
44
+ /** @type {(() => void)[]} */
45
+ #destructionCallbacks = [];
46
+ /** @type {RenderCoordinator} */
47
+ #renderCoordinator;
48
+ /** @type {LoadingIndicatorManager} */
49
+ #loadingIndicatorManager;
50
+ /** @type {InputBindingManager} */
51
+ #inputBindingManager;
52
+ /** @type {InteractionController} */
53
+ #interactionController;
54
+ /** @type {WebGLHelper} */
55
+ #glHelper;
56
+
57
+ #keyboardListenerManager = new KeyboardListenerManager();
58
+ #eventListeners = new EventListenerRegistry();
59
+ #extraBroadcastListeners = new EventListenerRegistry();
60
+
51
61
  /**
52
62
  * @typedef {import("./view/view.js").default} View
53
63
  * @typedef {import("./spec/view.js").ViewSpec} ViewSpec
@@ -67,9 +77,6 @@ export default class GenomeSpy {
67
77
 
68
78
  options.inputBindingContainer ??= "default";
69
79
 
70
- /** @type {(() => void)[]} */
71
- this._destructionCallbacks = [];
72
-
73
80
  /** Root level configuration object */
74
81
  this.spec = spec;
75
82
 
@@ -91,43 +98,6 @@ export default class GenomeSpy {
91
98
  */
92
99
  this.viewVisibilityPredicate = (view) => view.isVisibleInSpec();
93
100
 
94
- /** @type {BufferedViewRenderingContext} */
95
- this._renderingContext = undefined;
96
- /** @type {BufferedViewRenderingContext} */
97
- this._pickingContext = undefined;
98
-
99
- /** Does picking buffer need to be rendered again */
100
- this._dirtyPickingBuffer = false;
101
-
102
- /**
103
- * Currently hovered mark and datum
104
- * @type {{ mark: import("./marks/mark.js").default, datum: import("./data/flowNode.js").Datum, uniqueId: number }}
105
- */
106
- this._currentHover = undefined;
107
-
108
- this._wheelInertia = new Inertia(this.animator);
109
-
110
- /**
111
- * Keeping track so that these can be cleaned up upon finalization.
112
- * @type {Map<string, (function(KeyboardEvent):void)[]>}
113
- */
114
- this._keyboardListeners = new Map();
115
-
116
- /**
117
- * Listers for exposed high-level events such as click on a mark instance.
118
- * These should probably be in the View class and support bubbling through
119
- * the hierarchy.
120
- *
121
- * @type {Map<string, Set<(event: any) => void>>}
122
- */
123
- this._eventListeners = new Map();
124
-
125
- /**
126
- *
127
- * @type {Map<string, Set<(event: any) => void>>}
128
- */
129
- this._extraBroadcastListeners = new Map();
130
-
131
101
  /** @type {Record<string, import("./tooltip/tooltipHandler.js").TooltipHandler>}> */
132
102
  this.tooltipHandlers = {
133
103
  default: dataTooltipHandler,
@@ -138,20 +108,7 @@ export default class GenomeSpy {
138
108
  /** @type {View} */
139
109
  this.viewRoot = undefined;
140
110
 
141
- /**
142
- * Views that are currently loading data using lazy sources.
143
- *
144
- * @type {Map<View, { status: import("./types/viewContext.js").DataLoadingStatus, detail?: string }>}
145
- */
146
- this._loadingViews = new Map();
147
-
148
- /**
149
- * @type {HTMLElement}
150
- */
151
- this._inputBindingContainer = undefined;
152
-
153
- /** @type {Point} */
154
- this._mouseDownCoords = undefined;
111
+ this.#inputBindingManager = new InputBindingManager(container, options);
155
112
 
156
113
  this.dpr = window.devicePixelRatio;
157
114
  }
@@ -163,37 +120,7 @@ export default class GenomeSpy {
163
120
  }
164
121
 
165
122
  #initializeParameterBindings() {
166
- /** @type {import("lit").TemplateResult[]} */
167
- const inputs = [];
168
-
169
- this.viewRoot.visit((view) => {
170
- const mediator = view.paramMediator;
171
- inputs.push(...createBindingInputs(mediator));
172
- });
173
- const ibc = this.options.inputBindingContainer;
174
-
175
- if (!ibc || ibc == "none" || !inputs.length) {
176
- return;
177
- }
178
-
179
- this._inputBindingContainer = element("div", {
180
- className: "gs-input-bindings",
181
- });
182
-
183
- if (ibc == "default") {
184
- this.container.appendChild(this._inputBindingContainer);
185
- } else if (ibc instanceof HTMLElement) {
186
- ibc.appendChild(this._inputBindingContainer);
187
- } else {
188
- throw new Error("Invalid inputBindingContainer");
189
- }
190
-
191
- if (inputs.length) {
192
- render(
193
- html`<div class="gs-input-binding">${inputs}</div>`,
194
- this._inputBindingContainer
195
- );
196
- }
123
+ this.#inputBindingManager.initialize(this.viewRoot);
197
124
  }
198
125
 
199
126
  /**
@@ -229,11 +156,26 @@ export default class GenomeSpy {
229
156
  }
230
157
 
231
158
  namedSource.dataSource.updateDynamicData(data);
232
- reconfigureScales(namedSource.hosts);
233
159
 
234
160
  this.animator.requestRender();
235
161
  }
236
162
 
163
+ /**
164
+ * @param {string} type
165
+ * @param {(event: any) => void} listener
166
+ */
167
+ addEventListener(type, listener) {
168
+ this.#eventListeners.add(type, listener);
169
+ }
170
+
171
+ /**
172
+ * @param {string} type
173
+ * @param {(event: any) => void} listener
174
+ */
175
+ removeEventListener(type, listener) {
176
+ this.#eventListeners.remove(type, listener);
177
+ }
178
+
237
179
  /**
238
180
  * Broadcast a message to all views
239
181
 
@@ -243,71 +185,7 @@ export default class GenomeSpy {
243
185
  broadcast(type, payload) {
244
186
  const message = { type, payload };
245
187
  this.viewRoot.visit((view) => view.handleBroadcast(message));
246
- this._extraBroadcastListeners
247
- .get(type)
248
- ?.forEach((listener) => listener(message));
249
- }
250
-
251
- /**
252
- * Draw some layers on top of the canvas. It's easier to do fancy spinning
253
- * animations with html elements than with WebGL.
254
- */
255
- _updateLoadingIndicators() {
256
- /** @type {import("lit").TemplateResult[]} */
257
- const indicators = [];
258
-
259
- const isSomethingVisible = () =>
260
- [...this._loadingViews.values()].some(
261
- (v) => v.status == "loading" || v.status == "error"
262
- );
263
-
264
- for (const [view, status] of this._loadingViews) {
265
- const c = view.coords;
266
- if (c) {
267
- const style = {
268
- left: `${c.x}px`,
269
- top: `${c.y}px`,
270
- width: `${c.width}px`,
271
- height: `${c.height}px`,
272
- };
273
- indicators.push(
274
- html`<div style=${styleMap(style)}>
275
- <div class=${status.status}>
276
- ${status.status == "error"
277
- ? html`<span
278
- >Loading
279
- failed${status.detail
280
- ? html`: ${status.detail}`
281
- : nothing}</span
282
- >`
283
- : html`
284
- <img src="${SPINNER}" alt="" />
285
- <span>Loading...</span>
286
- `}
287
- </div>
288
- </div>`
289
- );
290
- }
291
- }
292
-
293
- // Do some hacks to stop css animations of the loading indicators.
294
- // Otherwise they fire animation frames even when their opacity is zero.
295
- // TODO: Instead of this, replace the animated spinners with static images.
296
- // Or even better, once more widely supported, use `allow-discrete`
297
- // https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior
298
- // to enable transition of the display property.
299
- if (isSomethingVisible()) {
300
- this.loadingIndicatorsElement.style.display = "block";
301
- } else {
302
- // TODO: Clear previous timeout
303
- setTimeout(() => {
304
- if (!isSomethingVisible()) {
305
- this.loadingIndicatorsElement.style.display = "none";
306
- }
307
- }, 3000);
308
- }
309
-
310
- render(indicators, this.loadingIndicatorsElement);
188
+ this.#extraBroadcastListeners.emit(type, message);
311
189
  }
312
190
 
313
191
  #setupDpr() {
@@ -317,7 +195,7 @@ export default class GenomeSpy {
317
195
  );
318
196
 
319
197
  const resizeCallback = () => {
320
- this._glHelper.invalidateSize();
198
+ this.#glHelper.invalidateSize();
321
199
  this.dpr = window.devicePixelRatio;
322
200
  dprSetter(this.dpr);
323
201
  this.computeLayout();
@@ -329,7 +207,7 @@ export default class GenomeSpy {
329
207
  // TODO: Size should be observed only if the content is not absolutely sized
330
208
  const resizeObserver = new ResizeObserver(resizeCallback);
331
209
  resizeObserver.observe(this.container);
332
- this._destructionCallbacks.push(() => resizeObserver.disconnect());
210
+ this.#destructionCallbacks.push(() => resizeObserver.disconnect());
333
211
  }
334
212
 
335
213
  /** @type {() => void} */
@@ -351,25 +229,19 @@ export default class GenomeSpy {
351
229
  updatePixelRatio();
352
230
 
353
231
  if (remove) {
354
- this._destructionCallbacks.push(remove);
232
+ this.#destructionCallbacks.push(remove);
355
233
  }
356
234
  }
357
235
 
358
236
  #prepareContainer() {
359
- this.container.classList.add("genome-spy");
360
-
361
- const styleElement = document.createElement("style");
362
- styleElement.innerHTML = css;
363
- this.container.appendChild(styleElement);
364
-
365
- const canvasWrapper = element("div", {
366
- class: "canvas-wrapper",
367
- });
368
- this.container.appendChild(canvasWrapper);
369
-
370
- canvasWrapper.classList.add("loading");
237
+ const {
238
+ canvasWrapper,
239
+ loadingMessageElement,
240
+ loadingIndicatorsElement,
241
+ tooltip,
242
+ } = createContainerUi(this.container);
371
243
 
372
- this._glHelper = new WebGLHelper(
244
+ this.#glHelper = new WebGLHelper(
373
245
  canvasWrapper,
374
246
  () =>
375
247
  this.viewRoot
@@ -379,30 +251,16 @@ export default class GenomeSpy {
379
251
  );
380
252
 
381
253
  // The initial loading message that is shown until the first frame is rendered
382
- this.loadingMessageElement = element("div", {
383
- class: "loading-message",
384
- innerHTML: `<div class="message">Loading<span class="ellipsis">...</span></div>`,
385
- });
386
- canvasWrapper.appendChild(this.loadingMessageElement);
387
-
254
+ this.loadingMessageElement = loadingMessageElement;
388
255
  // A container for loading indicators (for lazy data sources.)
389
256
  // These could alternatively be included in the view hierarchy,
390
257
  // but it's easier this way – particularly if we want to show
391
258
  // some fancy animated spinners.
392
- this.loadingIndicatorsElement = element("div", {
393
- class: "loading-indicators",
394
- });
395
- canvasWrapper.appendChild(this.loadingIndicatorsElement);
396
-
397
- this.tooltip = new Tooltip(this.container);
398
-
399
- this.loadingMessageElement
400
- .querySelector(".message")
401
- .addEventListener("transitionend", () => {
402
- /** @type {HTMLElement} */ (
403
- this.loadingMessageElement
404
- ).style.display = "none";
405
- });
259
+ this.loadingIndicatorsElement = loadingIndicatorsElement;
260
+ this.tooltip = tooltip;
261
+ this.#loadingIndicatorManager = new LoadingIndicatorManager(
262
+ loadingIndicatorsElement
263
+ );
406
264
  }
407
265
 
408
266
  /**
@@ -416,125 +274,91 @@ export default class GenomeSpy {
416
274
  this.container.classList.remove("genome-spy");
417
275
  canvasWrapper.classList.remove("loading");
418
276
 
419
- for (const [type, listeners] of this._keyboardListeners) {
420
- for (const listener of listeners) {
421
- document.removeEventListener(type, listener);
422
- }
423
- }
277
+ this.#keyboardListenerManager.removeAll();
424
278
 
425
- this._destructionCallbacks.forEach((callback) => callback());
279
+ this.#destructionCallbacks.forEach((callback) => callback());
426
280
 
427
- this._glHelper.finalize();
281
+ this.#glHelper.finalize();
428
282
 
429
- this._inputBindingContainer?.remove();
283
+ this.#inputBindingManager.remove();
430
284
 
431
285
  while (this.container.firstChild) {
432
286
  this.container.firstChild.remove();
433
287
  }
434
288
  }
435
289
 
436
- async _prepareViewsAndData() {
290
+ async #prepareViewsAndData() {
291
+ await this.#initializeGenomeStore();
292
+ const context = this.#createViewContext();
293
+ await this.#initializeViewHierarchy(context);
294
+ await initializeViewData(
295
+ this.viewRoot,
296
+ context.dataFlow,
297
+ context.fontManager,
298
+ (flow) => this.broadcast("dataFlowBuilt", flow)
299
+ );
300
+ this.#finalizeViewInitialization(context);
301
+ }
302
+
303
+ async #initializeGenomeStore() {
437
304
  if (this.spec.genome) {
438
305
  this.genomeStore = new GenomeStore(this.spec.baseUrl);
439
306
  await this.genomeStore.initialize(this.spec.genome);
440
307
  }
308
+ }
441
309
 
442
- // eslint-disable-next-line consistent-this
443
- const self = this;
444
-
445
- /** @type {import("./types/viewContext.js").default} */
446
- const context = {
310
+ #createViewContext() {
311
+ return createViewContext({
447
312
  dataFlow: new DataFlow(),
448
- glHelper: this._glHelper,
313
+ glHelper: this.#glHelper,
449
314
  animator: this.animator,
450
315
  genomeStore: this.genomeStore,
451
- fontManager: new BmFontManager(this._glHelper),
452
-
453
- requestLayoutReflow: () => {
454
- // placeholder
455
- },
316
+ fontManager: new BmFontManager(this.#glHelper),
456
317
  updateTooltip: this.updateTooltip.bind(this),
457
318
  getNamedDataFromProvider: this.getNamedDataFromProvider.bind(this),
458
- getCurrentHover: () => this._currentHover,
459
-
460
- setDataLoadingStatus: (view, status, detail) => {
461
- this._loadingViews.set(view, { status, detail });
462
- this._updateLoadingIndicators();
463
- },
464
-
319
+ getCurrentHover: () =>
320
+ this.#interactionController.getCurrentHover(),
321
+ setDataLoadingStatus: (view, status, detail) =>
322
+ this.#loadingIndicatorManager.setDataLoadingStatus(
323
+ view,
324
+ status,
325
+ detail
326
+ ),
465
327
  addKeyboardListener: (type, listener) => {
466
328
  // TODO: Listeners should be called only when the mouse pointer is inside the
467
329
  // container or the app covers the full document.
468
- document.addEventListener(type, listener);
469
- let listeners = this._keyboardListeners.get(type);
470
- if (!listeners) {
471
- listeners = [];
472
- this._keyboardListeners.set(type, listeners);
473
- }
474
- listeners.push(listener);
475
- },
476
-
477
- addBroadcastListener(type, listener) {
478
- const listenersByType = self._extraBroadcastListeners;
479
-
480
- // Copy-paste code. TODO: Refactor into a helper function.
481
- let listeners = listenersByType.get(type);
482
- if (!listeners) {
483
- listeners = new Set();
484
- listenersByType.set(type, listeners);
485
- }
486
-
487
- listeners.add(listener);
488
- },
489
-
490
- removeBroadcastListener(type, listener) {
491
- const listenersByType = self._extraBroadcastListeners;
492
-
493
- listenersByType.get(type)?.delete(listener);
330
+ this.#keyboardListenerManager.add(type, listener);
494
331
  },
495
-
496
- isViewConfiguredVisible: self.viewVisibilityPredicate,
497
-
498
- isViewSpec: (spec) => self.viewFactory.isViewSpec(spec),
499
-
500
- createOrImportView: async function (
332
+ addBroadcastListener: (type, listener) =>
333
+ this.#extraBroadcastListeners.add(type, listener),
334
+ removeBroadcastListener: (type, listener) =>
335
+ this.#extraBroadcastListeners.remove(type, listener),
336
+ isViewConfiguredVisible: this.viewVisibilityPredicate,
337
+ isViewSpec: (spec) => this.viewFactory.isViewSpec(spec),
338
+ createOrImportViewWithContext: (
339
+ ctx,
501
340
  spec,
502
341
  layoutParent,
503
342
  dataParent,
504
343
  defaultName,
505
344
  validator
506
- ) {
507
- return self.viewFactory.createOrImportView(
345
+ ) =>
346
+ this.viewFactory.createOrImportView(
508
347
  spec,
509
- context,
348
+ ctx,
510
349
  layoutParent,
511
350
  dataParent,
512
351
  defaultName,
513
352
  validator
514
- );
515
- },
516
-
517
- highlightView: (view) => {
518
- this.container.querySelector(".view-highlight")?.remove();
519
- if (view) {
520
- const coords = view.coords;
521
- if (coords) {
522
- const div = document.createElement("div");
523
- div.className = "view-highlight";
524
- div.style.position = "absolute";
525
- div.style.left = coords.x + "px";
526
- div.style.top = coords.y + "px";
527
- div.style.width = coords.width + "px";
528
- div.style.height = coords.height + "px";
529
- div.style.border = "1px solid green";
530
- div.style.backgroundColor = "rgba(0, 255, 0, 0.1)";
531
- div.style.pointerEvents = "none";
532
- this.container.appendChild(div);
533
- }
534
- }
535
- },
536
- };
353
+ ),
354
+ highlightView: createViewHighlighter(this.container),
355
+ });
356
+ }
537
357
 
358
+ /**
359
+ * @param {import("./types/viewContext.js").default} context
360
+ */
361
+ async #initializeViewHierarchy(context) {
538
362
  /** @type {ViewSpec & RootConfig} */
539
363
  const rootSpec = this.spec;
540
364
 
@@ -556,96 +380,51 @@ export default class GenomeSpy {
556
380
 
557
381
  this.#initializeParameterBindings();
558
382
 
559
- checkForDuplicateScaleNames(this.viewRoot);
560
-
561
- setImplicitScaleNames(this.viewRoot);
562
-
563
- const views = this.viewRoot.getDescendants();
564
-
565
- // View opacity should be configured after all scales have been resolved.
566
- // Currently this doesn't work if new views are added dynamically.
567
- // TODO: Figure out how to handle dynamic view addition/removal nicely.
568
- views.forEach((view) => view.configureViewOpacity());
383
+ configureViewHierarchy(this.viewRoot);
384
+ configureViewOpacity(this.viewRoot);
569
385
 
570
386
  // We should now have a complete view hierarchy. Let's update the canvas size
571
387
  // and ensure that the loading message is visible.
572
- this._glHelper.invalidateSize();
573
- this.#setupDpr();
574
-
575
- // Collect all unit views to a list because they need plenty of initialization
576
- const unitViews = /** @type {UnitView[]} */ (
577
- views.filter((view) => view instanceof UnitView)
578
- );
579
-
580
- // Build the data flow based on the view hierarchy
581
- const flow = buildDataFlow(this.viewRoot, context.dataFlow);
582
- optimizeDataFlow(flow);
583
- this.broadcast("dataFlowBuilt", flow);
584
-
585
- // @ts-expect-error
586
- if (import.meta.env?.DEV) {
587
- flow.dataSources.forEach((ds) => console.log(ds.subtreeToString()));
588
- }
589
-
590
- // Create encoders (accessors, scales and related metadata)
591
- unitViews.forEach((view) => view.mark.initializeEncoders());
592
-
593
- // Compile shaders, create or load textures, etc.
594
- const graphicsInitialized = Promise.all(
595
- unitViews.map((view) => view.mark.initializeGraphics())
596
- );
597
-
598
- for (const view of unitViews) {
599
- flow.addObserver((collector) => {
600
- view.mark.initializeData();
601
- try {
602
- // Update WebGL buffers
603
- view.mark.updateGraphicsData();
604
- } catch (e) {
605
- e.view = view;
606
- throw e;
607
- }
608
- context.animator.requestRender();
609
- }, view);
610
- }
611
-
612
- // Have to wait until asynchronous font loading is complete.
613
- // Text mark's geometry builder needs font metrics before data can be
614
- // converted into geometries.
615
- // TODO: Make updateGraphicsData async and await font loading there.
616
- await context.fontManager.waitUntilReady();
617
-
618
- // Find all data sources and initiate loading
619
- flow.initialize();
620
- await Promise.all(
621
- flow.dataSources.map((dataSource) => dataSource.load())
622
- );
623
-
624
- // Now that all data have been loaded, the domains may need adjusting
625
- // IMPORTANT TODO: Check that discrete domains and indexers match!!!!!!!!!
626
- reconfigureScales(this.viewRoot);
627
-
628
- // This event is needed by SampleView so that it can extract the sample ids
629
- // from the data once they are loaded.
630
- // TODO: It would be great if this could be attached to the data flow,
631
- // because now this is somewhat a hack and is incompatible with dynamic data
632
- // loading in the future.
633
- this.broadcast("dataLoaded");
388
+ this.#glHelper.invalidateSize();
389
+ this.#renderCoordinator = new RenderCoordinator({
390
+ viewRoot: this.viewRoot,
391
+ glHelper: this.#glHelper,
392
+ getBackground: () => this.spec.background,
393
+ broadcast: this.broadcast.bind(this),
394
+ onLayoutComputed: () =>
395
+ this.#loadingIndicatorManager.updateLayout(),
396
+ });
634
397
 
635
- await graphicsInitialized;
398
+ // Allow early layout requests from view subscriptions created during initialization.
399
+ // Layout will be recomputed anyway once launch completes.
400
+ context.requestLayoutReflow = this.computeLayout.bind(this);
636
401
 
637
- for (const view of unitViews) {
638
- view.mark.finalizeGraphicsInitialization();
639
- }
402
+ this.#setupDpr();
403
+ }
640
404
 
641
- // Allow layout computation
405
+ /**
406
+ * @param {import("./types/viewContext.js").default} context
407
+ */
408
+ #finalizeViewInitialization(context) {
409
+ // Allow layout computation (in case a custom context overrode the early assignment).
642
410
  // eslint-disable-next-line require-atomic-updates
643
411
  context.requestLayoutReflow = this.computeLayout.bind(this);
644
412
 
645
413
  // Invalidate cached sizes to ensure that step-based sizes are current.
646
414
  // TODO: This should be done automatically when the domains of band/point scales are updated.
647
415
  this.viewRoot.visit((view) => invalidatePrefix(view, "size"));
648
- this._glHelper.invalidateSize();
416
+ this.#glHelper.invalidateSize();
417
+
418
+ this.#interactionController = new InteractionController({
419
+ viewRoot: this.viewRoot,
420
+ glHelper: this.#glHelper,
421
+ tooltip: this.tooltip,
422
+ animator: this.animator,
423
+ emitEvent: this.#eventListeners.emit.bind(this.#eventListeners),
424
+ tooltipHandlers: this.tooltipHandlers,
425
+ renderPickingFramebuffer: this.renderPickingFramebuffer.bind(this),
426
+ getDevicePixelRatio: () => this.dpr,
427
+ });
649
428
  }
650
429
 
651
430
  /**
@@ -656,7 +435,7 @@ export default class GenomeSpy {
656
435
  try {
657
436
  this.#prepareContainer();
658
437
 
659
- await this._prepareViewsAndData();
438
+ await this.#prepareViewsAndData();
660
439
 
661
440
  this.registerMouseEvents();
662
441
 
@@ -669,7 +448,10 @@ export default class GenomeSpy {
669
448
  reason.view ? `At "${reason.view.getPathString()}": ` : ""
670
449
  }${reason.toString()}`;
671
450
  console.error(reason.stack);
672
- createMessageBox(this.container, message);
451
+ const handled = this.options.onError?.(reason, this.container);
452
+ if (!handled) {
453
+ createMessageBox(this.container, message);
454
+ }
673
455
 
674
456
  return false;
675
457
  } finally {
@@ -682,271 +464,7 @@ export default class GenomeSpy {
682
464
  }
683
465
 
684
466
  registerMouseEvents() {
685
- const canvas = this._glHelper.canvas;
686
-
687
- // TODO: This function is huge. Refactor this into a separate class
688
- // that would also contain state-related stuff that currently pollute the
689
- // GenomeSpy class.
690
-
691
- let lastWheelEvent = performance.now();
692
-
693
- let longPressTriggered = false;
694
-
695
- /** @param {Event} event */
696
- const listener = (event) => {
697
- const now = performance.now();
698
- const wheeling = now - lastWheelEvent < 200;
699
-
700
- if (event instanceof MouseEvent) {
701
- const rect = canvas.getBoundingClientRect();
702
- const point = new Point(
703
- event.clientX - rect.left - canvas.clientLeft,
704
- event.clientY - rect.top - canvas.clientTop
705
- );
706
-
707
- if (event.type == "mousemove" && !wheeling) {
708
- this.tooltip.handleMouseMove(event);
709
- this._tooltipUpdateRequested = false;
710
-
711
- // Disable picking during dragging. Also postpone picking until
712
- // the user has stopped zooming as reading pixels from the
713
- // picking buffer is slow and ruins smooth animations.
714
- if (event.buttons == 0 && !isStillZooming()) {
715
- this.renderPickingFramebuffer();
716
- this._handlePicking(point.x, point.y);
717
- }
718
- }
719
-
720
- /**
721
- * @param {MouseEvent} event
722
- */
723
- const dispatchEvent = (event) => {
724
- this.viewRoot.propagateInteractionEvent(
725
- new InteractionEvent(point, event)
726
- );
727
-
728
- if (!this._tooltipUpdateRequested) {
729
- this.tooltip.clear();
730
- }
731
- };
732
-
733
- if (event.type != "wheel") {
734
- this._wheelInertia.cancel();
735
- }
736
-
737
- if (
738
- (event.type == "mousedown" || event.type == "mouseup") &&
739
- !isStillZooming()
740
- ) {
741
- // Actually, only needed when clicking on a mark
742
- this.renderPickingFramebuffer();
743
- } else if (event.type == "wheel") {
744
- lastWheelEvent = now;
745
- this._tooltipUpdateRequested = false;
746
-
747
- const wheelEvent = /** @type {WheelEvent} */ (event);
748
-
749
- if (
750
- Math.abs(wheelEvent.deltaX) >
751
- Math.abs(wheelEvent.deltaY)
752
- ) {
753
- // If the viewport is panned (horizontally) using the wheel (touchpad),
754
- // the picking buffer becomes stale and needs redrawing. However, we
755
- // optimize by just clearing the currently hovered item so that snapping
756
- // doesn't work incorrectly when zooming in/out.
757
-
758
- // TODO: More robust solution (handle at higher level such as ScaleResolution's zoom method)
759
- this._currentHover = null;
760
-
761
- this._wheelInertia.cancel();
762
- } else {
763
- // Vertical wheeling zooms.
764
- // We use inertia to generate fake wheel events for smoother zooming
765
-
766
- const template = makeEventTemplate(wheelEvent);
767
-
768
- this._wheelInertia.setMomentum(
769
- wheelEvent.deltaY * (wheelEvent.deltaMode ? 80 : 1),
770
- (delta) => {
771
- const e = new WheelEvent("wheel", {
772
- ...template,
773
- deltaMode: 0,
774
- deltaX: 0,
775
- deltaY: delta,
776
- });
777
- dispatchEvent(e);
778
- }
779
- );
780
-
781
- wheelEvent.preventDefault();
782
- return;
783
- }
784
- }
785
-
786
- // TODO: Should be handled at the view level, not globally
787
- if (event.type == "click") {
788
- if (longPressTriggered) {
789
- return;
790
- }
791
-
792
- const e = this._currentHover
793
- ? {
794
- type: event.type,
795
- viewPath: this._currentHover.mark.unitView
796
- .getLayoutAncestors()
797
- .map((view) => view.name)
798
- .reverse(),
799
- datum: this._currentHover.datum,
800
- }
801
- : {
802
- type: event.type,
803
- viewPath: null,
804
- datum: null,
805
- };
806
-
807
- this._eventListeners
808
- .get("click")
809
- ?.forEach((listener) => listener(e));
810
- }
811
-
812
- if (
813
- event.type != "click" ||
814
- // Suppress click events if the mouse has been dragged
815
- this._mouseDownCoords?.subtract(Point.fromMouseEvent(event))
816
- .length < 3
817
- ) {
818
- dispatchEvent(event);
819
- }
820
- }
821
- };
822
-
823
- [
824
- "mousedown",
825
- "mouseup",
826
- "wheel",
827
- "click",
828
- "mousemove",
829
- "gesturechange",
830
- "contextmenu",
831
- "dblclick",
832
- ].forEach((type) => canvas.addEventListener(type, listener));
833
-
834
- canvas.addEventListener("mousedown", (/** @type {MouseEvent} */ e) => {
835
- this._mouseDownCoords = Point.fromMouseEvent(e);
836
- if (this.tooltip.sticky) {
837
- this.tooltip.sticky = false;
838
- this.tooltip.clear();
839
- // A hack to prevent selection if the tooltip is sticky.
840
- // Let the tooltip be destickified first.
841
- longPressTriggered = true;
842
- } else {
843
- longPressTriggered = false;
844
- }
845
-
846
- const disableTooltip = () => {
847
- document.addEventListener(
848
- "mouseup",
849
- () => this.tooltip.popEnabledState(),
850
- { once: true }
851
- );
852
- this.tooltip.pushEnabledState(false);
853
- };
854
-
855
- // Opening context menu or using modifier keys disables the tooltip
856
- if (e.button == 2 || e.shiftKey || e.ctrlKey || e.metaKey) {
857
- disableTooltip();
858
- } else if (this.tooltip.visible) {
859
- // Make tooltip sticky if the user long-presses
860
- const timeout = setTimeout(() => {
861
- longPressTriggered = true;
862
- this.tooltip.sticky = true;
863
- }, 400);
864
-
865
- const clear = () => clearTimeout(timeout);
866
- document.addEventListener("mouseup", clear, { once: true });
867
- document.addEventListener("mousemove", clear, { once: true });
868
- }
869
- });
870
-
871
- // Prevent text selections etc while dragging
872
- canvas.addEventListener("dragstart", (event) =>
873
- event.stopPropagation()
874
- );
875
- }
876
-
877
- /**
878
- * @param {number} x
879
- * @param {number} y
880
- */
881
- _handlePicking(x, y) {
882
- const dpr = this.dpr;
883
- const pp = readPickingPixel(
884
- this._glHelper.gl,
885
- this._glHelper._pickingBufferInfo,
886
- x * dpr,
887
- y * dpr
888
- );
889
-
890
- const uniqueId = pp[0] | (pp[1] << 8) | (pp[2] << 16) | (pp[3] << 24);
891
-
892
- if (uniqueId == 0) {
893
- this._currentHover = null;
894
- return;
895
- }
896
-
897
- if (uniqueId !== this._currentHover?.uniqueId) {
898
- this._currentHover = null;
899
- }
900
-
901
- if (!this._currentHover) {
902
- this.viewRoot.visit((view) => {
903
- if (view instanceof UnitView) {
904
- if (
905
- view.mark.isPickingParticipant() &&
906
- [...view.facetCoords.values()].some((coords) =>
907
- coords.containsPoint(x, y)
908
- )
909
- ) {
910
- const datum = view
911
- .getCollector()
912
- .findDatumByUniqueId(uniqueId);
913
- if (datum) {
914
- this._currentHover = {
915
- mark: view.mark,
916
- datum,
917
- uniqueId,
918
- };
919
- }
920
- }
921
- if (this._currentHover) {
922
- return VISIT_STOP;
923
- }
924
- }
925
- });
926
- }
927
-
928
- if (this._currentHover) {
929
- const mark = this._currentHover.mark;
930
- this.updateTooltip(this._currentHover.datum, async (datum) => {
931
- if (!mark.isPickingParticipant()) {
932
- return;
933
- }
934
-
935
- const tooltipProps = mark.properties.tooltip;
936
-
937
- if (tooltipProps !== null) {
938
- const handlerName = tooltipProps?.handler ?? "default";
939
- const handler = this.tooltipHandlers[handlerName];
940
- if (!handler) {
941
- throw new Error(
942
- "No such tooltip handler: " + handlerName
943
- );
944
- }
945
-
946
- return handler(datum, mark, tooltipProps?.params);
947
- }
948
- });
949
- }
467
+ this.#interactionController.registerMouseEvents();
950
468
  }
951
469
 
952
470
  /**
@@ -958,14 +476,7 @@ export default class GenomeSpy {
958
476
  * @template T
959
477
  */
960
478
  updateTooltip(datum, converter) {
961
- if (!this._tooltipUpdateRequested || !datum) {
962
- this.tooltip.updateWithDatum(datum, converter);
963
- this._tooltipUpdateRequested = true;
964
- } else {
965
- throw new Error(
966
- "Tooltip has already been updated! Duplicate event handler?"
967
- );
968
- }
479
+ this.#interactionController.updateTooltip(datum, converter);
969
480
  }
970
481
 
971
482
  /**
@@ -983,49 +494,14 @@ export default class GenomeSpy {
983
494
  devicePixelRatio,
984
495
  clearColor = "white"
985
496
  ) {
986
- const helper = this._glHelper;
987
-
988
- logicalWidth ??= helper.getLogicalCanvasSize().width;
989
- logicalHeight ??= helper.getLogicalCanvasSize().height;
990
- devicePixelRatio ??= window.devicePixelRatio ?? 1;
991
-
992
- const gl = helper.gl;
993
-
994
- const width = Math.floor(logicalWidth * devicePixelRatio);
995
- const height = Math.floor(logicalHeight * devicePixelRatio);
996
-
997
- const framebufferInfo = createFramebufferInfo(
998
- gl,
999
- [
1000
- {
1001
- format: gl.RGBA,
1002
- type: gl.UNSIGNED_BYTE,
1003
- minMag: gl.LINEAR,
1004
- wrap: gl.CLAMP_TO_EDGE,
1005
- },
1006
- ],
1007
- width,
1008
- height
1009
- );
1010
-
1011
- const renderingContext = new BufferedViewRenderingContext(
1012
- { picking: false },
1013
- {
1014
- webGLHelper: this._glHelper,
1015
- canvasSize: { width: logicalWidth, height: logicalHeight },
1016
- devicePixelRatio,
1017
- clearColor,
1018
- framebufferInfo,
1019
- }
1020
- );
1021
-
1022
- this.viewRoot.render(
1023
- renderingContext,
1024
- Rectangle.create(0, 0, logicalWidth, logicalHeight)
1025
- );
1026
- renderingContext.render();
1027
-
1028
- const pngUrl = framebufferToDataUrl(gl, framebufferInfo, "image/png");
497
+ const pngUrl = exportCanvas({
498
+ glHelper: this.#glHelper,
499
+ viewRoot: this.viewRoot,
500
+ logicalWidth,
501
+ logicalHeight,
502
+ devicePixelRatio,
503
+ clearColor,
504
+ });
1029
505
 
1030
506
  // Clean up
1031
507
  this.computeLayout();
@@ -1034,74 +510,20 @@ export default class GenomeSpy {
1034
510
  return pngUrl;
1035
511
  }
1036
512
 
1037
- computeLayout() {
1038
- const root = this.viewRoot;
1039
- if (!root) {
1040
- return;
1041
- }
1042
-
1043
- this.broadcast("layout");
1044
-
1045
- const canvasSize = this._glHelper.getLogicalCanvasSize();
1046
-
1047
- if (isNaN(canvasSize.width) || isNaN(canvasSize.height)) {
1048
- // TODO: Figure out what causes this
1049
- console.log(
1050
- `NaN in canvas size: ${canvasSize.width}x${canvasSize.height}. Skipping computeLayout().`
1051
- );
1052
- return;
1053
- }
1054
-
1055
- const commonOptions = {
1056
- webGLHelper: this._glHelper,
1057
- canvasSize,
1058
- devicePixelRatio: window.devicePixelRatio ?? 1,
1059
- };
1060
-
1061
- this._renderingContext = new BufferedViewRenderingContext(
1062
- { picking: false },
1063
- {
1064
- ...commonOptions,
1065
- clearColor: this.spec.background,
1066
- }
1067
- );
1068
- this._pickingContext = new BufferedViewRenderingContext(
1069
- { picking: true },
1070
- {
1071
- ...commonOptions,
1072
- framebufferInfo: this._glHelper._pickingBufferInfo,
1073
- }
1074
- );
1075
-
1076
- root.render(
1077
- new CompositeViewRenderingContext(
1078
- this._renderingContext,
1079
- this._pickingContext
1080
- ),
1081
- // Canvas should now be sized based on the root view or the container
1082
- Rectangle.create(0, 0, canvasSize.width, canvasSize.height)
1083
- );
1084
-
1085
- // The view coordinates may have not been known during the initial data loading.
1086
- // Thus, update them so that possible error messages are shown in the correct place.
1087
- this._updateLoadingIndicators();
513
+ getLogicalCanvasSize() {
514
+ return this.#glHelper.getLogicalCanvasSize();
515
+ }
1088
516
 
1089
- this.broadcast("layoutComputed");
517
+ computeLayout() {
518
+ this.#renderCoordinator.computeLayout();
1090
519
  }
1091
520
 
1092
521
  renderAll() {
1093
- this._renderingContext?.render();
1094
-
1095
- this._dirtyPickingBuffer = true;
522
+ this.#renderCoordinator.renderAll();
1096
523
  }
1097
524
 
1098
525
  renderPickingFramebuffer() {
1099
- if (!this._dirtyPickingBuffer) {
1100
- return;
1101
- }
1102
-
1103
- this._pickingContext.render();
1104
- this._dirtyPickingBuffer = false;
526
+ this.#renderCoordinator.renderPickingFramebuffer();
1105
527
  }
1106
528
 
1107
529
  getSearchableViews() {
@@ -1116,7 +538,7 @@ export default class GenomeSpy {
1116
538
  }
1117
539
 
1118
540
  getNamedScaleResolutions() {
1119
- /** @type {Map<string, import("./view/scaleResolution.js").default>} */
541
+ /** @type {Map<string, import("./scales/scaleResolution.js").default>} */
1120
542
  const resolutions = new Map();
1121
543
  this.viewRoot.visit((view) => {
1122
544
  for (const resolution of Object.values(view.resolutions.scale)) {
@@ -1128,34 +550,3 @@ export default class GenomeSpy {
1128
550
  return resolutions;
1129
551
  }
1130
552
  }
1131
-
1132
- /**
1133
- *
1134
- * @param {HTMLElement} container
1135
- * @param {string} message
1136
- */
1137
- function createMessageBox(container, message) {
1138
- // Uh, need a templating thingy
1139
- const messageBox = document.createElement("div");
1140
- messageBox.className = "message-box";
1141
- const messageText = document.createElement("div");
1142
- messageText.textContent = message;
1143
- messageBox.appendChild(messageText);
1144
- container.appendChild(messageBox);
1145
- }
1146
-
1147
- /**
1148
- * @param {string} tag
1149
- * @param {Record<string, any>} attrs
1150
- */
1151
- function element(tag, attrs) {
1152
- const el = document.createElement(tag);
1153
- for (const [key, value] of Object.entries(attrs)) {
1154
- if (["innerHTML", "innerText", "className"].includes(key)) {
1155
- // @ts-ignore
1156
- el[key] = value;
1157
- }
1158
- el.setAttribute(key, value);
1159
- }
1160
- return el;
1161
- }