@genome-spy/core 0.65.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 (232) 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 +17587 -16593
  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/flowInit.js +2 -2
  16. package/dist/src/data/sources/lazy/axisTickSource.js +1 -1
  17. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
  18. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  19. package/dist/src/data/sources/lazy/singleAxisLazySource.js +10 -3
  20. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  21. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +5 -1
  22. package/dist/src/data/transforms/filterScoredLabels.d.ts +1 -1
  23. package/dist/src/data/transforms/filterScoredLabels.d.ts.map +1 -1
  24. package/dist/src/data/transforms/filterScoredLabels.js +1 -1
  25. package/dist/src/data/transforms/linearizeGenomicCoordinate.d.ts.map +1 -1
  26. package/dist/src/data/transforms/linearizeGenomicCoordinate.js +2 -1
  27. package/dist/src/encoder/encoder.d.ts +1 -1
  28. package/dist/src/encoder/encoder.d.ts.map +1 -1
  29. package/dist/src/encoder/encoder.js +1 -1
  30. package/dist/src/genome/scaleLocus.d.ts +39 -0
  31. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  32. package/dist/src/genome/scaleLocus.js +76 -0
  33. package/dist/src/genomeSpy/canvasExport.d.ts +19 -0
  34. package/dist/src/genomeSpy/canvasExport.d.ts.map +1 -0
  35. package/dist/src/genomeSpy/canvasExport.js +66 -0
  36. package/dist/src/genomeSpy/containerUi.d.ts +17 -0
  37. package/dist/src/genomeSpy/containerUi.d.ts.map +1 -0
  38. package/dist/src/genomeSpy/containerUi.js +78 -0
  39. package/dist/src/genomeSpy/eventListenerRegistry.d.ts +19 -0
  40. package/dist/src/genomeSpy/eventListenerRegistry.d.ts.map +1 -0
  41. package/dist/src/genomeSpy/eventListenerRegistry.js +38 -0
  42. package/dist/src/genomeSpy/inputBindingManager.d.ts +14 -0
  43. package/dist/src/genomeSpy/inputBindingManager.d.ts.map +1 -0
  44. package/dist/src/genomeSpy/inputBindingManager.js +63 -0
  45. package/dist/src/genomeSpy/interactionController.d.ts +40 -0
  46. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -0
  47. package/dist/src/genomeSpy/interactionController.js +371 -0
  48. package/dist/src/genomeSpy/keyboardListenerManager.d.ts +10 -0
  49. package/dist/src/genomeSpy/keyboardListenerManager.d.ts.map +1 -0
  50. package/dist/src/genomeSpy/keyboardListenerManager.js +31 -0
  51. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +15 -0
  52. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -0
  53. package/dist/src/genomeSpy/loadingIndicatorManager.js +92 -0
  54. package/dist/src/genomeSpy/renderCoordinator.d.ts +22 -0
  55. package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -0
  56. package/dist/src/genomeSpy/renderCoordinator.js +118 -0
  57. package/dist/src/genomeSpy/viewContextFactory.d.ts +18 -0
  58. package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -0
  59. package/dist/src/genomeSpy/viewContextFactory.js +79 -0
  60. package/dist/src/genomeSpy/viewDataInit.d.ts +12 -0
  61. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -0
  62. package/dist/src/genomeSpy/viewDataInit.js +41 -0
  63. package/dist/src/genomeSpy/viewHierarchyConfig.d.ts +14 -0
  64. package/dist/src/genomeSpy/viewHierarchyConfig.d.ts.map +1 -0
  65. package/dist/src/genomeSpy/viewHierarchyConfig.js +24 -0
  66. package/dist/src/genomeSpy/viewHighlight.d.ts +5 -0
  67. package/dist/src/genomeSpy/viewHighlight.d.ts.map +1 -0
  68. package/dist/src/genomeSpy/viewHighlight.js +30 -0
  69. package/dist/src/genomeSpy.d.ts +16 -71
  70. package/dist/src/genomeSpy.d.ts.map +1 -1
  71. package/dist/src/genomeSpy.js +179 -745
  72. package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
  73. package/dist/src/gl/webGLHelper.d.ts +2 -2
  74. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  75. package/dist/src/gl/webGLHelper.js +4 -4
  76. package/dist/src/index.d.ts.map +1 -1
  77. package/dist/src/index.js +2 -12
  78. package/dist/src/marks/mark.d.ts.map +1 -1
  79. package/dist/src/marks/mark.js +4 -2
  80. package/dist/src/{view → scales}/axisResolution.d.ts +9 -16
  81. package/dist/src/scales/axisResolution.d.ts.map +1 -0
  82. package/dist/src/{view → scales}/axisResolution.js +29 -18
  83. package/dist/src/scales/axisResolution.test.d.ts.map +1 -0
  84. package/dist/src/scales/scaleDomainAggregator.d.ts +57 -0
  85. package/dist/src/scales/scaleDomainAggregator.d.ts.map +1 -0
  86. package/dist/src/scales/scaleDomainAggregator.js +162 -0
  87. package/dist/src/scales/scaleDomainAggregator.test.d.ts +2 -0
  88. package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +1 -0
  89. package/dist/src/scales/scaleInstanceManager.d.ts +40 -0
  90. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -0
  91. package/dist/src/scales/scaleInstanceManager.js +313 -0
  92. package/dist/src/scales/scaleInstanceManager.test.d.ts +2 -0
  93. package/dist/src/scales/scaleInstanceManager.test.d.ts.map +1 -0
  94. package/dist/src/scales/scaleInteractionController.d.ts +73 -0
  95. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -0
  96. package/dist/src/scales/scaleInteractionController.js +336 -0
  97. package/dist/src/scales/scaleInteractionController.test.d.ts +2 -0
  98. package/dist/src/scales/scaleInteractionController.test.d.ts.map +1 -0
  99. package/dist/src/scales/scalePropsResolver.d.ts +23 -0
  100. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -0
  101. package/dist/src/scales/scalePropsResolver.js +74 -0
  102. package/dist/src/{view → scales}/scaleResolution.d.ts +52 -35
  103. package/dist/src/scales/scaleResolution.d.ts.map +1 -0
  104. package/dist/src/scales/scaleResolution.js +658 -0
  105. package/dist/src/scales/scaleResolution.test.d.ts.map +1 -0
  106. package/dist/src/scales/scaleResolutionConstants.d.ts +6 -0
  107. package/dist/src/scales/scaleResolutionConstants.d.ts.map +1 -0
  108. package/dist/src/scales/scaleResolutionConstants.js +5 -0
  109. package/dist/src/scales/scaleRules.d.ts +16 -0
  110. package/dist/src/scales/scaleRules.d.ts.map +1 -0
  111. package/dist/src/scales/scaleRules.js +103 -0
  112. package/dist/src/scales/scaleRules.test.d.ts +2 -0
  113. package/dist/src/scales/scaleRules.test.d.ts.map +1 -0
  114. package/dist/src/spec/channel.d.ts +13 -18
  115. package/dist/src/spec/scale.d.ts +6 -0
  116. package/dist/src/types/embedApi.d.ts +5 -0
  117. package/dist/src/types/scaleResolutionApi.d.ts +1 -1
  118. package/dist/src/view/concatView.d.ts +18 -0
  119. package/dist/src/view/concatView.d.ts.map +1 -1
  120. package/dist/src/view/concatView.js +73 -0
  121. package/dist/src/view/concatView.test.d.ts +2 -0
  122. package/dist/src/view/concatView.test.d.ts.map +1 -0
  123. package/dist/src/view/containerMutationHelper.d.ts +74 -0
  124. package/dist/src/view/containerMutationHelper.d.ts.map +1 -0
  125. package/dist/src/view/containerMutationHelper.js +114 -0
  126. package/dist/src/view/containerView.d.ts +0 -7
  127. package/dist/src/view/containerView.d.ts.map +1 -1
  128. package/dist/src/view/containerView.js +0 -10
  129. package/dist/src/view/facetView.d.ts.map +1 -1
  130. package/dist/src/view/facetView.js +0 -15
  131. package/dist/src/view/gridView/gridChild.d.ts +11 -0
  132. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  133. package/dist/src/view/gridView/gridChild.js +32 -6
  134. package/dist/src/view/gridView/gridView.d.ts +39 -1
  135. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  136. package/dist/src/view/gridView/gridView.js +106 -48
  137. package/dist/src/view/gridView/gridView.test.d.ts +2 -0
  138. package/dist/src/view/gridView/gridView.test.d.ts.map +1 -0
  139. package/dist/src/view/gridView/scrollbar.d.ts +39 -8
  140. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  141. package/dist/src/view/gridView/scrollbar.js +184 -69
  142. package/dist/src/view/layerView.d.ts +14 -0
  143. package/dist/src/view/layerView.d.ts.map +1 -1
  144. package/dist/src/view/layerView.js +66 -0
  145. package/dist/src/view/layerView.test.d.ts +2 -0
  146. package/dist/src/view/layerView.test.d.ts.map +1 -0
  147. package/dist/src/view/testUtils.d.ts.map +1 -1
  148. package/dist/src/view/testUtils.js +2 -1
  149. package/dist/src/view/unitView.d.ts.map +1 -1
  150. package/dist/src/view/unitView.js +24 -34
  151. package/dist/src/view/view.d.ts +6 -6
  152. package/dist/src/view/view.d.ts.map +1 -1
  153. package/dist/src/view/view.js +4 -4
  154. package/package.json +2 -2
  155. package/dist/bundle/browser-txUcLy2H.js +0 -123
  156. package/dist/bundle/index-BQpbYrv4.js +0 -1712
  157. package/dist/bundle/index-BhtHKLUo.js +0 -73
  158. package/dist/bundle/index-CCe8rnZz.js +0 -716
  159. package/dist/bundle/index-DhcU-Gk-.js +0 -1487
  160. package/dist/src/data/collector.test.js +0 -138
  161. package/dist/src/data/dataFlow.test.js +0 -38
  162. package/dist/src/data/flow.test.js +0 -81
  163. package/dist/src/data/flowInit.test.js +0 -413
  164. package/dist/src/data/flowNode.test.js +0 -50
  165. package/dist/src/data/flowOptimizer.test.js +0 -209
  166. package/dist/src/data/formats/fasta.test.js +0 -27
  167. package/dist/src/data/sources/inlineSource.test.js +0 -63
  168. package/dist/src/data/sources/sequenceSource.test.js +0 -81
  169. package/dist/src/data/transforms/aggregate.test.js +0 -134
  170. package/dist/src/data/transforms/clone.test.js +0 -11
  171. package/dist/src/data/transforms/coverage.test.js +0 -238
  172. package/dist/src/data/transforms/filter.test.js +0 -20
  173. package/dist/src/data/transforms/flatten.test.js +0 -96
  174. package/dist/src/data/transforms/flattenDelimited.test.js +0 -90
  175. package/dist/src/data/transforms/flattenSequence.test.js +0 -34
  176. package/dist/src/data/transforms/formula.test.js +0 -25
  177. package/dist/src/data/transforms/identifier.test.js +0 -92
  178. package/dist/src/data/transforms/pileup.test.js +0 -70
  179. package/dist/src/data/transforms/project.test.js +0 -32
  180. package/dist/src/data/transforms/regexExtract.test.js +0 -70
  181. package/dist/src/data/transforms/regexFold.test.js +0 -201
  182. package/dist/src/data/transforms/sample.test.js +0 -38
  183. package/dist/src/data/transforms/stack.test.js +0 -91
  184. package/dist/src/encoder/accessor.test.js +0 -162
  185. package/dist/src/encoder/encoder.test.js +0 -105
  186. package/dist/src/genome/genome.test.js +0 -268
  187. package/dist/src/genome/genomes.test.js +0 -8
  188. package/dist/src/genome/scaleIndex.test.js +0 -78
  189. package/dist/src/genome/scaleLocus.test.js +0 -4
  190. package/dist/src/scale/scale.test.js +0 -326
  191. package/dist/src/scale/ticks.test.js +0 -46
  192. package/dist/src/selection/selection.test.js +0 -14
  193. package/dist/src/utils/addBaseUrl.test.js +0 -30
  194. package/dist/src/utils/binnedIndex.test.js +0 -201
  195. package/dist/src/utils/cloner.test.js +0 -35
  196. package/dist/src/utils/coalesce.test.js +0 -16
  197. package/dist/src/utils/concatIterables.test.js +0 -8
  198. package/dist/src/utils/domainArray.test.js +0 -130
  199. package/dist/src/utils/indexer.test.js +0 -49
  200. package/dist/src/utils/interactionEvent.test.js +0 -35
  201. package/dist/src/utils/iterateNestedMaps.test.js +0 -33
  202. package/dist/src/utils/kWayMerge.test.js +0 -30
  203. package/dist/src/utils/mergeObjects.test.js +0 -42
  204. package/dist/src/utils/numberExtractor.test.js +0 -6
  205. package/dist/src/utils/propertyCacher.test.js +0 -89
  206. package/dist/src/utils/propertyCoalescer.test.js +0 -25
  207. package/dist/src/utils/radixSort.test.js +0 -51
  208. package/dist/src/utils/reservationMap.test.js +0 -20
  209. package/dist/src/utils/ringBuffer.test.js +0 -39
  210. package/dist/src/utils/topK.test.js +0 -54
  211. package/dist/src/utils/trees.test.js +0 -135
  212. package/dist/src/utils/url.test.js +0 -28
  213. package/dist/src/utils/variableTools.test.js +0 -13
  214. package/dist/src/view/axisResolution.d.ts.map +0 -1
  215. package/dist/src/view/axisResolution.test.d.ts.map +0 -1
  216. package/dist/src/view/axisResolution.test.js +0 -206
  217. package/dist/src/view/flowBuilder.test.js +0 -125
  218. package/dist/src/view/gridView/selectionRect.test.js +0 -87
  219. package/dist/src/view/layout/flexLayout.test.js +0 -323
  220. package/dist/src/view/layout/grid.test.js +0 -71
  221. package/dist/src/view/layout/rectangle.test.js +0 -192
  222. package/dist/src/view/paramMediator.test.js +0 -282
  223. package/dist/src/view/scaleResolution.d.ts.map +0 -1
  224. package/dist/src/view/scaleResolution.js +0 -1059
  225. package/dist/src/view/scaleResolution.test.d.ts.map +0 -1
  226. package/dist/src/view/scaleResolution.test.js +0 -645
  227. package/dist/src/view/view.test.js +0 -245
  228. package/dist/src/view/viewDispose.test.js +0 -110
  229. package/dist/src/view/viewFactory.test.js +0 -25
  230. package/dist/src/view/viewUtils.test.js +0 -87
  231. /package/dist/src/{view → scales}/axisResolution.test.d.ts +0 -0
  232. /package/dist/src/{view → scales}/scaleResolution.test.d.ts +0 -0
@@ -1,43 +1,37 @@
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 { initializeViewData } from "./genomeSpy/viewDataInit.js";
16
15
  import UnitView from "./view/unitView.js";
17
16
 
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";
17
+ import WebGLHelper from "./gl/webGLHelper.js";
27
18
  import Animator from "./utils/animator.js";
28
19
  import DataFlow from "./data/dataFlow.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 createBindingInputs from "./utils/inputBinding.js";
39
- import { isStillZooming } from "./view/zoom.js";
40
- 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";
41
35
 
42
36
  /**
43
37
  * Events that are broadcasted to all views.
@@ -47,6 +41,23 @@ import { createFramebufferInfo } from "twgl.js";
47
41
  vegaFormats("fasta", fasta);
48
42
 
49
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
+
50
61
  /**
51
62
  * @typedef {import("./view/view.js").default} View
52
63
  * @typedef {import("./spec/view.js").ViewSpec} ViewSpec
@@ -66,9 +77,6 @@ export default class GenomeSpy {
66
77
 
67
78
  options.inputBindingContainer ??= "default";
68
79
 
69
- /** @type {(() => void)[]} */
70
- this._destructionCallbacks = [];
71
-
72
80
  /** Root level configuration object */
73
81
  this.spec = spec;
74
82
 
@@ -90,43 +98,6 @@ export default class GenomeSpy {
90
98
  */
91
99
  this.viewVisibilityPredicate = (view) => view.isVisibleInSpec();
92
100
 
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
101
  /** @type {Record<string, import("./tooltip/tooltipHandler.js").TooltipHandler>}> */
131
102
  this.tooltipHandlers = {
132
103
  default: dataTooltipHandler,
@@ -137,20 +108,7 @@ export default class GenomeSpy {
137
108
  /** @type {View} */
138
109
  this.viewRoot = undefined;
139
110
 
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;
111
+ this.#inputBindingManager = new InputBindingManager(container, options);
154
112
 
155
113
  this.dpr = window.devicePixelRatio;
156
114
  }
@@ -162,37 +120,7 @@ export default class GenomeSpy {
162
120
  }
163
121
 
164
122
  #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
- }
123
+ this.#inputBindingManager.initialize(this.viewRoot);
196
124
  }
197
125
 
198
126
  /**
@@ -232,6 +160,22 @@ export default class GenomeSpy {
232
160
  this.animator.requestRender();
233
161
  }
234
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
+
235
179
  /**
236
180
  * Broadcast a message to all views
237
181
 
@@ -241,71 +185,7 @@ export default class GenomeSpy {
241
185
  broadcast(type, payload) {
242
186
  const message = { type, payload };
243
187
  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);
188
+ this.#extraBroadcastListeners.emit(type, message);
309
189
  }
310
190
 
311
191
  #setupDpr() {
@@ -315,7 +195,7 @@ export default class GenomeSpy {
315
195
  );
316
196
 
317
197
  const resizeCallback = () => {
318
- this._glHelper.invalidateSize();
198
+ this.#glHelper.invalidateSize();
319
199
  this.dpr = window.devicePixelRatio;
320
200
  dprSetter(this.dpr);
321
201
  this.computeLayout();
@@ -327,7 +207,7 @@ export default class GenomeSpy {
327
207
  // TODO: Size should be observed only if the content is not absolutely sized
328
208
  const resizeObserver = new ResizeObserver(resizeCallback);
329
209
  resizeObserver.observe(this.container);
330
- this._destructionCallbacks.push(() => resizeObserver.disconnect());
210
+ this.#destructionCallbacks.push(() => resizeObserver.disconnect());
331
211
  }
332
212
 
333
213
  /** @type {() => void} */
@@ -349,25 +229,19 @@ export default class GenomeSpy {
349
229
  updatePixelRatio();
350
230
 
351
231
  if (remove) {
352
- this._destructionCallbacks.push(remove);
232
+ this.#destructionCallbacks.push(remove);
353
233
  }
354
234
  }
355
235
 
356
236
  #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");
237
+ const {
238
+ canvasWrapper,
239
+ loadingMessageElement,
240
+ loadingIndicatorsElement,
241
+ tooltip,
242
+ } = createContainerUi(this.container);
369
243
 
370
- this._glHelper = new WebGLHelper(
244
+ this.#glHelper = new WebGLHelper(
371
245
  canvasWrapper,
372
246
  () =>
373
247
  this.viewRoot
@@ -377,30 +251,16 @@ export default class GenomeSpy {
377
251
  );
378
252
 
379
253
  // 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
-
254
+ this.loadingMessageElement = loadingMessageElement;
386
255
  // A container for loading indicators (for lazy data sources.)
387
256
  // These could alternatively be included in the view hierarchy,
388
257
  // but it's easier this way – particularly if we want to show
389
258
  // 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
- });
259
+ this.loadingIndicatorsElement = loadingIndicatorsElement;
260
+ this.tooltip = tooltip;
261
+ this.#loadingIndicatorManager = new LoadingIndicatorManager(
262
+ loadingIndicatorsElement
263
+ );
404
264
  }
405
265
 
406
266
  /**
@@ -414,128 +274,91 @@ export default class GenomeSpy {
414
274
  this.container.classList.remove("genome-spy");
415
275
  canvasWrapper.classList.remove("loading");
416
276
 
417
- for (const [type, listeners] of this._keyboardListeners) {
418
- for (const listener of listeners) {
419
- document.removeEventListener(type, listener);
420
- }
421
- }
277
+ this.#keyboardListenerManager.removeAll();
422
278
 
423
- this._destructionCallbacks.forEach((callback) => callback());
279
+ this.#destructionCallbacks.forEach((callback) => callback());
424
280
 
425
- this._glHelper.finalize();
281
+ this.#glHelper.finalize();
426
282
 
427
- this._inputBindingContainer?.remove();
283
+ this.#inputBindingManager.remove();
428
284
 
429
285
  while (this.container.firstChild) {
430
286
  this.container.firstChild.remove();
431
287
  }
432
288
  }
433
289
 
434
- 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() {
435
304
  if (this.spec.genome) {
436
305
  this.genomeStore = new GenomeStore(this.spec.baseUrl);
437
306
  await this.genomeStore.initialize(this.spec.genome);
438
307
  }
308
+ }
439
309
 
440
- // eslint-disable-next-line consistent-this
441
- const self = this;
442
-
443
- /** @type {import("./types/viewContext.js").default} */
444
- const context = {
310
+ #createViewContext() {
311
+ return createViewContext({
445
312
  dataFlow: new DataFlow(),
446
- glHelper: this._glHelper,
313
+ glHelper: this.#glHelper,
447
314
  animator: this.animator,
448
315
  genomeStore: this.genomeStore,
449
- fontManager: new BmFontManager(this._glHelper),
450
-
451
- requestLayoutReflow: () => {
452
- // placeholder
453
- },
316
+ fontManager: new BmFontManager(this.#glHelper),
454
317
  updateTooltip: this.updateTooltip.bind(this),
455
318
  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
-
319
+ getCurrentHover: () =>
320
+ this.#interactionController.getCurrentHover(),
321
+ setDataLoadingStatus: (view, status, detail) =>
322
+ this.#loadingIndicatorManager.setDataLoadingStatus(
323
+ view,
324
+ status,
325
+ detail
326
+ ),
463
327
  addKeyboardListener: (type, listener) => {
464
328
  // TODO: Listeners should be called only when the mouse pointer is inside the
465
329
  // 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);
473
- },
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);
330
+ this.#keyboardListenerManager.add(type, listener);
486
331
  },
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 (
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,
499
340
  spec,
500
341
  layoutParent,
501
342
  dataParent,
502
343
  defaultName,
503
344
  validator
504
- ) {
505
- return self.viewFactory.createOrImportView(
345
+ ) =>
346
+ this.viewFactory.createOrImportView(
506
347
  spec,
507
- context,
348
+ ctx,
508
349
  layoutParent,
509
350
  dataParent,
510
351
  defaultName,
511
352
  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
- };
353
+ ),
354
+ highlightView: createViewHighlighter(this.container),
355
+ });
356
+ }
538
357
 
358
+ /**
359
+ * @param {import("./types/viewContext.js").default} context
360
+ */
361
+ async #initializeViewHierarchy(context) {
539
362
  /** @type {ViewSpec & RootConfig} */
540
363
  const rootSpec = this.spec;
541
364
 
@@ -557,47 +380,51 @@ export default class GenomeSpy {
557
380
 
558
381
  this.#initializeParameterBindings();
559
382
 
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());
383
+ configureViewHierarchy(this.viewRoot);
384
+ configureViewOpacity(this.viewRoot);
570
385
 
571
386
  // We should now have a complete view hierarchy. Let's update the canvas size
572
387
  // 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();
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
+ });
587
397
 
588
- // Find all data sources and initiate loading.
589
- await loadViewSubtreeData(this.viewRoot, new Set(dataFlow.dataSources));
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);
590
401
 
591
- await finalizeSubtreeGraphics(graphicsPromises);
402
+ this.#setupDpr();
403
+ }
592
404
 
593
- // 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).
594
410
  // eslint-disable-next-line require-atomic-updates
595
411
  context.requestLayoutReflow = this.computeLayout.bind(this);
596
412
 
597
413
  // Invalidate cached sizes to ensure that step-based sizes are current.
598
414
  // TODO: This should be done automatically when the domains of band/point scales are updated.
599
415
  this.viewRoot.visit((view) => invalidatePrefix(view, "size"));
600
- 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
+ });
601
428
  }
602
429
 
603
430
  /**
@@ -608,7 +435,7 @@ export default class GenomeSpy {
608
435
  try {
609
436
  this.#prepareContainer();
610
437
 
611
- await this._prepareViewsAndData();
438
+ await this.#prepareViewsAndData();
612
439
 
613
440
  this.registerMouseEvents();
614
441
 
@@ -621,7 +448,10 @@ export default class GenomeSpy {
621
448
  reason.view ? `At "${reason.view.getPathString()}": ` : ""
622
449
  }${reason.toString()}`;
623
450
  console.error(reason.stack);
624
- createMessageBox(this.container, message);
451
+ const handled = this.options.onError?.(reason, this.container);
452
+ if (!handled) {
453
+ createMessageBox(this.container, message);
454
+ }
625
455
 
626
456
  return false;
627
457
  } finally {
@@ -634,276 +464,7 @@ export default class GenomeSpy {
634
464
  }
635
465
 
636
466
  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;
851
- return;
852
- }
853
-
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;
893
-
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
- }
902
-
903
- return handler(datum, mark, tooltipProps?.params);
904
- }
905
- });
906
- }
467
+ this.#interactionController.registerMouseEvents();
907
468
  }
908
469
 
909
470
  /**
@@ -915,14 +476,7 @@ export default class GenomeSpy {
915
476
  * @template T
916
477
  */
917
478
  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
- }
479
+ this.#interactionController.updateTooltip(datum, converter);
926
480
  }
927
481
 
928
482
  /**
@@ -940,49 +494,14 @@ export default class GenomeSpy {
940
494
  devicePixelRatio,
941
495
  clearColor = "white"
942
496
  ) {
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");
497
+ const pngUrl = exportCanvas({
498
+ glHelper: this.#glHelper,
499
+ viewRoot: this.viewRoot,
500
+ logicalWidth,
501
+ logicalHeight,
502
+ devicePixelRatio,
503
+ clearColor,
504
+ });
986
505
 
987
506
  // Clean up
988
507
  this.computeLayout();
@@ -991,74 +510,20 @@ export default class GenomeSpy {
991
510
  return pngUrl;
992
511
  }
993
512
 
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();
513
+ getLogicalCanvasSize() {
514
+ return this.#glHelper.getLogicalCanvasSize();
515
+ }
1045
516
 
1046
- this.broadcast("layoutComputed");
517
+ computeLayout() {
518
+ this.#renderCoordinator.computeLayout();
1047
519
  }
1048
520
 
1049
521
  renderAll() {
1050
- this._renderingContext?.render();
1051
-
1052
- this._dirtyPickingBuffer = true;
522
+ this.#renderCoordinator.renderAll();
1053
523
  }
1054
524
 
1055
525
  renderPickingFramebuffer() {
1056
- if (!this._dirtyPickingBuffer) {
1057
- return;
1058
- }
1059
-
1060
- this._pickingContext.render();
1061
- this._dirtyPickingBuffer = false;
526
+ this.#renderCoordinator.renderPickingFramebuffer();
1062
527
  }
1063
528
 
1064
529
  getSearchableViews() {
@@ -1073,7 +538,7 @@ export default class GenomeSpy {
1073
538
  }
1074
539
 
1075
540
  getNamedScaleResolutions() {
1076
- /** @type {Map<string, import("./view/scaleResolution.js").default>} */
541
+ /** @type {Map<string, import("./scales/scaleResolution.js").default>} */
1077
542
  const resolutions = new Map();
1078
543
  this.viewRoot.visit((view) => {
1079
544
  for (const resolution of Object.values(view.resolutions.scale)) {
@@ -1085,34 +550,3 @@ export default class GenomeSpy {
1085
550
  return resolutions;
1086
551
  }
1087
552
  }
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
- }