@genome-spy/core 0.65.0 → 0.67.0

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