@genome-spy/core 0.14.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 (226) hide show
  1. package/dist/index.js +224 -0
  2. package/dist/style.css +1 -0
  3. package/package.json +54 -0
  4. package/src/data/collector.js +178 -0
  5. package/src/data/collector.test.js +82 -0
  6. package/src/data/dataFlow.js +109 -0
  7. package/src/data/dataFlow.test.js +3 -0
  8. package/src/data/facetNode.js +17 -0
  9. package/src/data/flow.test.js +71 -0
  10. package/src/data/flowBatch.d.ts +40 -0
  11. package/src/data/flowNode.js +283 -0
  12. package/src/data/flowNode.test.js +49 -0
  13. package/src/data/flowOptimizer.js +117 -0
  14. package/src/data/flowOptimizer.test.js +192 -0
  15. package/src/data/flowTestUtils.js +63 -0
  16. package/src/data/formats/fasta.js +32 -0
  17. package/src/data/formats/fasta.test.js +26 -0
  18. package/src/data/sources/dataSource.js +22 -0
  19. package/src/data/sources/dataSourceFactory.js +24 -0
  20. package/src/data/sources/dataUtils.js +31 -0
  21. package/src/data/sources/dynamicCallbackSource.js +56 -0
  22. package/src/data/sources/dynamicSource.js +36 -0
  23. package/src/data/sources/inlineSource.js +69 -0
  24. package/src/data/sources/inlineSource.test.js +55 -0
  25. package/src/data/sources/namedSource.js +74 -0
  26. package/src/data/sources/sequenceSource.js +46 -0
  27. package/src/data/sources/sequenceSource.test.js +45 -0
  28. package/src/data/sources/urlSource.js +74 -0
  29. package/src/data/transforms/aggregate.js +69 -0
  30. package/src/data/transforms/clone.js +40 -0
  31. package/src/data/transforms/clone.test.js +10 -0
  32. package/src/data/transforms/coverage.js +187 -0
  33. package/src/data/transforms/coverage.test.js +122 -0
  34. package/src/data/transforms/filter.js +37 -0
  35. package/src/data/transforms/filter.test.js +17 -0
  36. package/src/data/transforms/filterScoredLabels.js +134 -0
  37. package/src/data/transforms/flattenCompressedExons.js +57 -0
  38. package/src/data/transforms/flattenDelimited.js +68 -0
  39. package/src/data/transforms/flattenDelimited.test.js +86 -0
  40. package/src/data/transforms/flattenSequence.js +39 -0
  41. package/src/data/transforms/flattenSequence.test.js +33 -0
  42. package/src/data/transforms/formula.js +39 -0
  43. package/src/data/transforms/formula.test.js +18 -0
  44. package/src/data/transforms/identifier.js +108 -0
  45. package/src/data/transforms/identifier.test.js +82 -0
  46. package/src/data/transforms/linearizeGenomicCoordinate.js +101 -0
  47. package/src/data/transforms/measureText.js +44 -0
  48. package/src/data/transforms/pileup.js +128 -0
  49. package/src/data/transforms/pileup.test.js +69 -0
  50. package/src/data/transforms/project.js +41 -0
  51. package/src/data/transforms/project.test.js +31 -0
  52. package/src/data/transforms/regexExtract.js +61 -0
  53. package/src/data/transforms/regexExtract.test.js +66 -0
  54. package/src/data/transforms/regexFold.js +141 -0
  55. package/src/data/transforms/regexFold.test.js +159 -0
  56. package/src/data/transforms/sample.js +101 -0
  57. package/src/data/transforms/sample.test.js +37 -0
  58. package/src/data/transforms/stack.js +137 -0
  59. package/src/data/transforms/stack.test.js +90 -0
  60. package/src/data/transforms/transformFactory.js +60 -0
  61. package/src/encoder/accessor.js +82 -0
  62. package/src/encoder/accessor.test.js +46 -0
  63. package/src/encoder/encoder.js +369 -0
  64. package/src/encoder/encoder.test.js +97 -0
  65. package/src/fonts/Lato-Regular.json +1267 -0
  66. package/src/fonts/Lato-Regular.png +0 -0
  67. package/src/fonts/OFL.txt +93 -0
  68. package/src/fonts/README.md +3 -0
  69. package/src/fonts/bmFont.d.ts +58 -0
  70. package/src/fonts/bmFontManager.js +357 -0
  71. package/src/fonts/bmFontMetrics.js +108 -0
  72. package/src/genome/genome.js +305 -0
  73. package/src/genome/genome.test.js +152 -0
  74. package/src/genome/genomeStore.js +54 -0
  75. package/src/genome/locusFormat.js +31 -0
  76. package/src/genome/scaleIndex.js +199 -0
  77. package/src/genome/scaleIndex.test.js +61 -0
  78. package/src/genome/scaleLocus.js +112 -0
  79. package/src/genome/scaleLocus.test.js +3 -0
  80. package/src/genomeSpy.js +753 -0
  81. package/src/gl/arrayBuilder.js +199 -0
  82. package/src/gl/dataToVertices.js +621 -0
  83. package/src/gl/includes/common.glsl +63 -0
  84. package/src/gl/includes/fp64-arithmetic.glsl +187 -0
  85. package/src/gl/includes/fp64-utils.js +132 -0
  86. package/src/gl/includes/picking.fragment.glsl +3 -0
  87. package/src/gl/includes/picking.vertex.glsl +29 -0
  88. package/src/gl/includes/sampleFacet.glsl +107 -0
  89. package/src/gl/includes/scales.glsl +79 -0
  90. package/src/gl/includes/scales_fp64.glsl +30 -0
  91. package/src/gl/link.fragment.glsl +18 -0
  92. package/src/gl/link.vertex.glsl +111 -0
  93. package/src/gl/point.fragment.glsl +123 -0
  94. package/src/gl/point.vertex.glsl +128 -0
  95. package/src/gl/rect.fragment.glsl +51 -0
  96. package/src/gl/rect.vertex.glsl +114 -0
  97. package/src/gl/rule.fragment.glsl +52 -0
  98. package/src/gl/rule.vertex.glsl +89 -0
  99. package/src/gl/text.fragment.glsl +31 -0
  100. package/src/gl/text.vertex.glsl +246 -0
  101. package/src/gl/webGLHelper.js +490 -0
  102. package/src/img/bowtie.svg +1 -0
  103. package/src/img/genomespy-favicon.svg +34 -0
  104. package/src/index.html +11 -0
  105. package/src/index.js +151 -0
  106. package/src/marks/link.js +189 -0
  107. package/src/marks/mark.js +867 -0
  108. package/src/marks/markUtils.js +109 -0
  109. package/src/marks/pointMark.js +279 -0
  110. package/src/marks/rectMark.js +236 -0
  111. package/src/marks/rule.js +231 -0
  112. package/src/marks/text.js +274 -0
  113. package/src/options.d.ts +9 -0
  114. package/src/scale/colorUtils.js +184 -0
  115. package/src/scale/glslScaleGenerator.js +462 -0
  116. package/src/scale/scale.js +441 -0
  117. package/src/scale/scale.test.js +323 -0
  118. package/src/scale/ticks.js +198 -0
  119. package/src/scale/ticks.test.js +39 -0
  120. package/src/singlePageApp.js +13 -0
  121. package/src/spec/axis.d.ts +296 -0
  122. package/src/spec/channel.d.ts +127 -0
  123. package/src/spec/data.d.ts +185 -0
  124. package/src/spec/font.d.ts +15 -0
  125. package/src/spec/genome.d.ts +35 -0
  126. package/src/spec/mark.d.ts +432 -0
  127. package/src/spec/root.d.ts +22 -0
  128. package/src/spec/scale.d.ts +265 -0
  129. package/src/spec/tooltip.d.ts +9 -0
  130. package/src/spec/transform.d.ts +479 -0
  131. package/src/spec/view.d.ts +215 -0
  132. package/src/styles/genome-spy.scss +153 -0
  133. package/src/tooltip/dataTooltipHandler.js +59 -0
  134. package/src/tooltip/refseqGeneTooltipHandler.js +77 -0
  135. package/src/tooltip/tooltipHandler.ts +12 -0
  136. package/src/types/filetypes.d.ts +4 -0
  137. package/src/types/flatqueue.d.ts +53 -0
  138. package/src/types/glsl.d.ts +4 -0
  139. package/src/types/object.d.ts +21 -0
  140. package/src/types/vega-scale.d.ts +60 -0
  141. package/src/utils/animator.js +83 -0
  142. package/src/utils/arrayUtils.js +55 -0
  143. package/src/utils/binnedRangeIndex.js +83 -0
  144. package/src/utils/clamp.js +8 -0
  145. package/src/utils/cloner.js +32 -0
  146. package/src/utils/cloner.test.js +23 -0
  147. package/src/utils/coalesce.js +11 -0
  148. package/src/utils/coalesce.test.js +15 -0
  149. package/src/utils/concatIterables.js +26 -0
  150. package/src/utils/concatIterables.test.js +7 -0
  151. package/src/utils/debounce.js +37 -0
  152. package/src/utils/domainArray.js +224 -0
  153. package/src/utils/domainArray.test.js +129 -0
  154. package/src/utils/eerp.js +13 -0
  155. package/src/utils/expression.js +32 -0
  156. package/src/utils/field.js +28 -0
  157. package/src/utils/fisheye.js +60 -0
  158. package/src/utils/formatObject.js +31 -0
  159. package/src/utils/html.js +23 -0
  160. package/src/utils/html.test.js +13 -0
  161. package/src/utils/indexer.js +43 -0
  162. package/src/utils/indexer.test.js +46 -0
  163. package/src/utils/inertia.js +124 -0
  164. package/src/utils/interactionEvent.js +33 -0
  165. package/src/utils/iterateNestedMaps.js +21 -0
  166. package/src/utils/iterateNestedMaps.test.js +32 -0
  167. package/src/utils/kWayMerge.js +42 -0
  168. package/src/utils/kWayMerge.test.js +25 -0
  169. package/src/utils/layout/flexLayout.js +336 -0
  170. package/src/utils/layout/flexLayout.test.js +296 -0
  171. package/src/utils/layout/padding.js +107 -0
  172. package/src/utils/layout/point.js +23 -0
  173. package/src/utils/layout/rectangle.js +282 -0
  174. package/src/utils/layout/rectangle.test.js +171 -0
  175. package/src/utils/mergeObjects.js +99 -0
  176. package/src/utils/mergeObjects.test.js +41 -0
  177. package/src/utils/numberExtractor.js +24 -0
  178. package/src/utils/numberExtractor.test.js +5 -0
  179. package/src/utils/point.js +14 -0
  180. package/src/utils/propertyCacher.js +70 -0
  181. package/src/utils/propertyCacher.test.js +84 -0
  182. package/src/utils/propertyCoalescer.js +37 -0
  183. package/src/utils/propertyCoalescer.test.js +21 -0
  184. package/src/utils/reservationMap.js +103 -0
  185. package/src/utils/reservationMap.test.js +19 -0
  186. package/src/utils/scaleNull.js +19 -0
  187. package/src/utils/setOperations.js +75 -0
  188. package/src/utils/smoothstep.js +10 -0
  189. package/src/utils/throttle.js +34 -0
  190. package/src/utils/topK.js +76 -0
  191. package/src/utils/topK.test.js +63 -0
  192. package/src/utils/transition.js +74 -0
  193. package/src/utils/ui/tooltip.js +189 -0
  194. package/src/utils/url.js +22 -0
  195. package/src/utils/variableTools.js +24 -0
  196. package/src/utils/variableTools.test.js +12 -0
  197. package/src/view/axisResolution.js +135 -0
  198. package/src/view/axisResolution.test.js +200 -0
  199. package/src/view/axisView.js +746 -0
  200. package/src/view/channel.js +5 -0
  201. package/src/view/concatView.js +296 -0
  202. package/src/view/containerView.js +141 -0
  203. package/src/view/decoratorView.js +510 -0
  204. package/src/view/facetView.js +488 -0
  205. package/src/view/flowBuilder.js +362 -0
  206. package/src/view/flowBuilder.test.js +124 -0
  207. package/src/view/importView.js +19 -0
  208. package/src/view/layerView.js +60 -0
  209. package/src/view/rendering.d.ts +44 -0
  210. package/src/view/renderingContext/compositeViewRenderingContext.js +51 -0
  211. package/src/view/renderingContext/deferredViewRenderingContext.js +174 -0
  212. package/src/view/renderingContext/layoutRecorderViewRenderingContext.js +128 -0
  213. package/src/view/renderingContext/simpleViewRenderingContext.js +62 -0
  214. package/src/view/renderingContext/svgViewRenderingContext.js +121 -0
  215. package/src/view/renderingContext/viewRenderingContext.js +41 -0
  216. package/src/view/scaleResolution.js +756 -0
  217. package/src/view/scaleResolution.test.js +571 -0
  218. package/src/view/scaleResolutionApi.d.ts +40 -0
  219. package/src/view/testUtils.js +48 -0
  220. package/src/view/unitView.js +368 -0
  221. package/src/view/view.js +589 -0
  222. package/src/view/view.test.js +213 -0
  223. package/src/view/viewContext.d.ts +57 -0
  224. package/src/view/viewFactory.js +179 -0
  225. package/src/view/viewFactory.test.js +16 -0
  226. package/src/view/viewUtils.js +420 -0
@@ -0,0 +1,753 @@
1
+ import scaleLocus from "./genome/scaleLocus";
2
+ import { scale as vegaScale } from "vega-scale";
3
+ import { formats as vegaFormats } from "vega-loader";
4
+
5
+ import "./styles/genome-spy.scss";
6
+ import Tooltip from "./utils/ui/tooltip";
7
+
8
+ import AccessorFactory from "./encoder/accessor";
9
+ import {
10
+ resolveScalesAndAxes,
11
+ addDecorators,
12
+ processImports,
13
+ setImplicitScaleNames,
14
+ } from "./view/viewUtils";
15
+ import UnitView from "./view/unitView";
16
+
17
+ import WebGLHelper from "./gl/webGLHelper";
18
+ import Rectangle from "./utils/layout/rectangle";
19
+ import DeferredViewRenderingContext from "./view/renderingContext/deferredViewRenderingContext";
20
+ import LayoutRecorderViewRenderingContext from "./view/renderingContext/layoutRecorderViewRenderingContext";
21
+ import CompositeViewRenderingContext from "./view/renderingContext/compositeViewRenderingContext";
22
+ import InteractionEvent from "./utils/interactionEvent";
23
+ import Point from "./utils/layout/point";
24
+ import Animator from "./utils/animator";
25
+ import DataFlow from "./data/dataFlow";
26
+ import scaleIndex from "./genome/scaleIndex";
27
+ import { buildDataFlow } from "./view/flowBuilder";
28
+ import { optimizeDataFlow } from "./data/flowOptimizer";
29
+ import scaleNull from "./utils/scaleNull";
30
+ import GenomeStore from "./genome/genomeStore";
31
+ import BmFontManager from "./fonts/bmFontManager";
32
+ import fasta from "./data/formats/fasta";
33
+ import { VISIT_STOP } from "./view/view";
34
+ import Inertia, { makeEventTemplate } from "./utils/inertia";
35
+ import refseqGeneTooltipHandler from "./tooltip/refseqGeneTooltipHandler";
36
+ import dataTooltipHandler from "./tooltip/dataTooltipHandler";
37
+ import { invalidatePrefix } from "./utils/propertyCacher";
38
+ import { ViewFactory } from "./view/viewFactory";
39
+
40
+ /**
41
+ * @typedef {import("./spec/view").UnitSpec} UnitSpec
42
+ * @typedef {import("./spec/view").ViewSpec} ViewSpec
43
+ * @typedef {import("./spec/view").ImportSpec} ImportSpec
44
+ * @typedef {import("./spec/view").VConcatSpec} TrackSpec
45
+ * @typedef {import("./spec/root").RootSpec} RootSpec
46
+ * @typedef {import("./spec/root").RootConfig} RootConfig
47
+ */
48
+
49
+ // Register scaleLocus to Vega-Scale.
50
+ // Loci are discrete but the scale's domain can be adjusted in a continuous manner.
51
+ vegaScale("index", scaleIndex, ["continuous"]);
52
+ vegaScale("locus", scaleLocus, ["continuous"]);
53
+ vegaScale("null", scaleNull, []);
54
+
55
+ vegaFormats("fasta", fasta);
56
+
57
+ /**
58
+ * The actual browser without any toolbars etc
59
+ */
60
+ export default class GenomeSpy {
61
+ /**
62
+ *
63
+ * @param {HTMLElement} container
64
+ * @param {RootSpec} spec
65
+ * @param {import("./options").EmbedOptions} [options]
66
+ */
67
+ constructor(container, spec, options = {}) {
68
+ this.container = container;
69
+
70
+ /** Root level configuration object */
71
+ this.spec = spec;
72
+
73
+ this.accessorFactory = new AccessorFactory();
74
+ this.viewFactory = new ViewFactory();
75
+
76
+ /** @type {(function(string):object[])[]} */
77
+ this.namedDataProviders = [];
78
+
79
+ this.animator = new Animator(() => this.renderAll());
80
+
81
+ /** @type {GenomeStore} */
82
+ this.genomeStore = undefined;
83
+
84
+ /**
85
+ * View visibility is checked using a predicate that can be overridden
86
+ * for more dynamic visibility management.
87
+ *
88
+ * @type {(view: import("./view/view").default) => boolean}
89
+ */
90
+ this.viewVisibilityPredicate = (view) => view.isVisibleInSpec();
91
+
92
+ /** @type {DeferredViewRenderingContext} */
93
+ this._renderingContext = undefined;
94
+ /** @type {DeferredViewRenderingContext} */
95
+ this._pickingContext = undefined;
96
+
97
+ /** Does picking buffer need to be rendered again */
98
+ this._dirtyPickingBuffer = false;
99
+
100
+ /**
101
+ * Currently hovered mark and datum
102
+ * @type {{ mark: import("./marks/Mark").default, datum: import("./data/flowNode").Datum, uniqueId: number }}
103
+ */
104
+ this._currentHover = undefined;
105
+
106
+ this._wheelInertia = new Inertia(this.animator);
107
+
108
+ /**
109
+ * Keeping track so that these can be cleaned up upon finalization.
110
+ * @type {Map<string, (function(KeyboardEvent):void)[]>}
111
+ */
112
+ this._keyboardListeners = new Map();
113
+
114
+ /**
115
+ * Listers for exposed high-level events such as click on a mark instance.
116
+ * These should probably be in the View class and support bubbling through
117
+ * the hierarchy.
118
+ *
119
+ * @type {Map<string, Set<(event: any) => void>>}
120
+ */
121
+ this._eventListeners = new Map();
122
+
123
+ /** @type {Record<string, import("./tooltip/tooltipHandler").TooltipHandler>}> */
124
+ this.tooltipHandlers = {
125
+ default: dataTooltipHandler,
126
+ refseqgene: refseqGeneTooltipHandler,
127
+ ...(options.tooltipHandlers ?? {}),
128
+ };
129
+
130
+ /** @type {import("./view/view").default} */
131
+ this.viewRoot = undefined;
132
+ }
133
+
134
+ /**
135
+ *
136
+ * @param {function(string):any[]} provider
137
+ */
138
+ registerNamedDataProvider(provider) {
139
+ this.namedDataProviders.unshift(provider);
140
+ }
141
+
142
+ /**
143
+ *
144
+ * @param {string} name
145
+ */
146
+ getNamedData(name) {
147
+ for (const provider of this.namedDataProviders) {
148
+ const data = provider(name);
149
+ if (data) {
150
+ return data;
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Broadcast a message to all views
157
+ *
158
+ * @param {string} type
159
+ * @param {any} [payload]
160
+ */
161
+ broadcast(type, payload) {
162
+ const message = { type, payload };
163
+ this.viewRoot.visit((view) => view.handleBroadcast(message));
164
+ }
165
+
166
+ _prepareContainer() {
167
+ this.container.classList.add("genome-spy");
168
+ this.container.classList.add("loading");
169
+
170
+ this._glHelper = new WebGLHelper(this.container, () => {
171
+ if (this.viewRoot) {
172
+ const size = this.viewRoot.getSize();
173
+
174
+ // If a dimension has an absolutely specified size (in pixels), use it for the canvas size.
175
+ // However, if the dimension has a growing component, the canvas should be fit to the
176
+ // container.
177
+ // TODO: Enforce the minimum size (in case of both absolute and growing components).
178
+
179
+ /** @param {import("./utils/layout/flexLayout").SizeDef} dim */
180
+ const f = (dim) => (dim.grow > 0 ? undefined : dim.px);
181
+ return {
182
+ width: f(size.width),
183
+ height: f(size.height),
184
+ };
185
+ }
186
+ });
187
+
188
+ this.loadingMessageElement = document.createElement("div");
189
+ this.loadingMessageElement.className = "loading-message";
190
+ this.loadingMessageElement.innerHTML = `<div class="message">Loading<span class="ellipsis">...</span></div>`;
191
+ this.container.appendChild(this.loadingMessageElement);
192
+
193
+ this.tooltip = new Tooltip(this.container);
194
+
195
+ this.loadingMessageElement
196
+ .querySelector(".message")
197
+ .addEventListener("transitionend", () => {
198
+ /** @type {HTMLElement} */ (
199
+ this.loadingMessageElement
200
+ ).style.display = "none";
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Unregisters all listeners, removes all created dom elements, removes all css classes from the container
206
+ */
207
+ destroy() {
208
+ // TODO: There's a memory leak somewhere
209
+
210
+ this.container.classList.remove("genome-spy");
211
+ this.container.classList.remove("loading");
212
+
213
+ for (const [type, listeners] of this._keyboardListeners) {
214
+ for (const listener of listeners) {
215
+ document.removeEventListener(type, listener);
216
+ }
217
+ }
218
+
219
+ this._glHelper.finalize();
220
+
221
+ while (this.container.firstChild) {
222
+ this.container.firstChild.remove();
223
+ }
224
+ }
225
+
226
+ async _prepareViewsAndData() {
227
+ if (this.spec.genome) {
228
+ this.genomeStore = new GenomeStore(this);
229
+ await this.genomeStore.initialize(this.spec.genome);
230
+ }
231
+
232
+ // eslint-disable-next-line consistent-this
233
+ const self = this;
234
+
235
+ /** @type {import("./view/viewContext").default} */
236
+ const context = {
237
+ dataFlow: new DataFlow(),
238
+ accessorFactory: this.accessorFactory,
239
+ glHelper: this._glHelper,
240
+ animator: this.animator,
241
+ genomeStore: this.genomeStore,
242
+ fontManager: new BmFontManager(this._glHelper),
243
+ requestLayoutReflow: () => {
244
+ // placeholder
245
+ },
246
+ updateTooltip: this.updateTooltip.bind(this),
247
+ getNamedData: this.getNamedData.bind(this),
248
+ getCurrentHover: () => this._currentHover,
249
+
250
+ addKeyboardListener: (type, listener) => {
251
+ // TODO: Listeners should be called only when the mouse pointer is inside the
252
+ // container or the app covers the full document.
253
+ document.addEventListener(type, listener);
254
+ let listeners = this._keyboardListeners.get(type);
255
+ if (!listeners) {
256
+ listeners = [];
257
+ this._keyboardListeners.set(type, listeners);
258
+ }
259
+ listeners.push(listener);
260
+ },
261
+
262
+ isViewVisible: self.viewVisibilityPredicate,
263
+
264
+ isViewSpec: (spec) => self.viewFactory.isViewSpec(spec),
265
+
266
+ createView: function (spec, parent, defaultName) {
267
+ return self.viewFactory.createView(
268
+ spec,
269
+ context,
270
+ parent,
271
+ defaultName
272
+ );
273
+ },
274
+ };
275
+
276
+ /** @type {import("./spec/view").ViewSpec & RootConfig} */
277
+ const rootSpec = this.spec;
278
+
279
+ if (rootSpec.datasets) {
280
+ this.registerNamedDataProvider((name) => rootSpec.datasets[name]);
281
+ }
282
+
283
+ // Create the view hierarchy
284
+ this.viewRoot = context.createView(rootSpec, null, "viewRoot");
285
+
286
+ // Replace placeholder ImportViews with actual views.
287
+ await processImports(this.viewRoot);
288
+
289
+ // Resolve scales, i.e., if possible, pull them towards the root
290
+ resolveScalesAndAxes(this.viewRoot);
291
+ setImplicitScaleNames(this.viewRoot);
292
+
293
+ // Wrap unit or layer views that need axes
294
+ this.viewRoot = addDecorators(this.viewRoot);
295
+
296
+ // We should now have a complete view hierarchy. Let's update the canvas size
297
+ // and ensure that the loading message is visible.
298
+ this._glHelper.invalidateSize();
299
+
300
+ // Collect all unit views to a list because they need plenty of initialization
301
+ /** @type {UnitView[]} */
302
+ const unitViews = [];
303
+ this.viewRoot.visit((view) => {
304
+ if (view instanceof UnitView) {
305
+ unitViews.push(view);
306
+ }
307
+ });
308
+
309
+ // Build the data flow based on the view hierarchy
310
+ const flow = buildDataFlow(this.viewRoot, context.dataFlow);
311
+ optimizeDataFlow(flow);
312
+ this.broadcast("dataFlowBuilt", flow);
313
+
314
+ flow.dataSources.forEach((ds) => console.log(ds.subtreeToString()));
315
+
316
+ // Create encoders (accessors, scales and related metadata)
317
+ unitViews.forEach((view) => view.mark.initializeEncoders());
318
+
319
+ // Compile shaders, create or load textures, etc.
320
+ const graphicsInitialized = Promise.all(
321
+ unitViews.map((view) => view.mark.initializeGraphics())
322
+ );
323
+
324
+ for (const view of unitViews) {
325
+ flow.addObserver((collector) => {
326
+ view.mark.initializeData();
327
+ // Update WebGL buffers
328
+ view.mark.updateGraphicsData();
329
+ }, view);
330
+ }
331
+
332
+ // Have to wait until asynchronous font loading is complete.
333
+ // Text mark's geometry builder needs font metrics before data can be
334
+ // converted into geometries.
335
+ await context.fontManager.waitUntilReady();
336
+
337
+ // Find all data sources and initiate loading
338
+ flow.initialize();
339
+ await Promise.all(
340
+ flow.dataSources.map((dataSource) => dataSource.load())
341
+ );
342
+
343
+ // Now that all data have been loaded, the domains may need adjusting
344
+ this.viewRoot.visit((view) => {
345
+ for (const resolution of Object.values(view.resolutions.scale)) {
346
+ // IMPORTANT TODO: Check that discrete domains and indexers match!!!!!!!!!
347
+ resolution.reconfigure();
348
+ }
349
+ });
350
+
351
+ // This event is needed by SampleView so that it can extract the sample ids
352
+ // from the data once they are loaded.
353
+ // TODO: It would be great if this could be attached to the data flow,
354
+ // because now this is somewhat a hack and is incompatible with dynamic data
355
+ // loading in the future.
356
+ this.broadcast("dataLoaded");
357
+
358
+ await graphicsInitialized;
359
+
360
+ this.viewRoot.visit((view) => {
361
+ for (const resolution of Object.values(view.resolutions.scale)) {
362
+ this._glHelper.createRangeTexture(resolution);
363
+ }
364
+ });
365
+
366
+ for (const view of unitViews) {
367
+ view.mark.finalizeGraphicsInitialization();
368
+ }
369
+
370
+ // Allow layout computation
371
+ // eslint-disable-next-line require-atomic-updates
372
+ context.requestLayoutReflow = this.computeLayout.bind(this);
373
+
374
+ // Invalidate cached sizes to ensure that step-based sizes are current.
375
+ // TODO: This should be done automatically when the domains of band/point scales are updated.
376
+ this.viewRoot.visit((view) => invalidatePrefix(view, "size"));
377
+ this._glHelper.invalidateSize();
378
+ }
379
+
380
+ /**
381
+ * TODO: Come up with a sensible name. And maybe this should be called at the end of the constructor.
382
+ * @returns {Promise<boolean>} true if the launch was successful
383
+ */
384
+ async launch() {
385
+ try {
386
+ this._prepareContainer();
387
+
388
+ await this._prepareViewsAndData();
389
+
390
+ this.registerMouseEvents();
391
+
392
+ this.computeLayout();
393
+ this.animator.requestRender();
394
+
395
+ // Register resize listener after the initial layout computation to prevent
396
+ // incomplete layouts from accidentally polluting any caches related to sizes.
397
+ this._glHelper.addEventListener("resize", () => {
398
+ this.computeLayout();
399
+ // Render immediately, without RAF
400
+ this.renderAll();
401
+ });
402
+
403
+ return true;
404
+ } catch (reason) {
405
+ const message = `${
406
+ reason.view ? `At "${reason.view.getPathString()}": ` : ""
407
+ }${reason.toString()}`;
408
+ console.error(reason.stack);
409
+ createMessageBox(this.container, message);
410
+
411
+ return false;
412
+ } finally {
413
+ this.container.classList.remove("loading");
414
+ // Transition listener doesn't appear to work on observablehq
415
+ window.setTimeout(() => {
416
+ this.loadingMessageElement.style.display = "none";
417
+ }, 2000);
418
+ }
419
+ }
420
+
421
+ registerMouseEvents() {
422
+ const canvas = this._glHelper.canvas;
423
+
424
+ // TODO: This function is huge. Refactor this into a separate class
425
+ // that would also contain state-related stuff that currently pollute the
426
+ // GenomeSpy class.
427
+
428
+ /** @param {Event} event */
429
+ const listener = (event) => {
430
+ if (this.layout && event instanceof MouseEvent) {
431
+ if (event.type == "mousemove") {
432
+ this.tooltip.handleMouseMove(event);
433
+ this._tooltipUpdateRequested = false;
434
+
435
+ if (event.buttons == 0) {
436
+ // Disable during dragging
437
+ this.renderPickingFramebuffer();
438
+ }
439
+ }
440
+
441
+ const rect = canvas.getBoundingClientRect();
442
+ const point = new Point(
443
+ event.clientX - rect.left - canvas.clientLeft,
444
+ event.clientY - rect.top - canvas.clientTop
445
+ );
446
+
447
+ /**
448
+ * @param {MouseEvent} event
449
+ */
450
+ const dispatchEvent = (event) => {
451
+ this.layout.dispatchInteractionEvent(
452
+ new InteractionEvent(point, event)
453
+ );
454
+
455
+ if (!this._tooltipUpdateRequested) {
456
+ this.tooltip.clear();
457
+ }
458
+ };
459
+
460
+ if (event.type != "wheel") {
461
+ this._wheelInertia.cancel();
462
+ }
463
+
464
+ if (event.type == "mousemove") {
465
+ this._handlePicking(point.x, point.y);
466
+ } else if (
467
+ event.type == "mousedown" ||
468
+ event.type == "mouseup"
469
+ ) {
470
+ this.renderPickingFramebuffer();
471
+ } else if (event.type == "wheel") {
472
+ this._tooltipUpdateRequested = false;
473
+
474
+ const wheelEvent = /** @type {WheelEvent} */ (event);
475
+
476
+ if (
477
+ Math.abs(wheelEvent.deltaX) >
478
+ Math.abs(wheelEvent.deltaY)
479
+ ) {
480
+ // If the viewport is panned (horizontally) using the wheel (touchpad),
481
+ // the picking buffer becomes stale and needs redrawing. However, we
482
+ // optimize by just clearing the currently hovered item so that snapping
483
+ // doesn't work incorrectly when zooming in/out.
484
+
485
+ // TODO: More robust solution (handle at higher level such as ScaleResolution's zoom method)
486
+ this._currentHover = null;
487
+
488
+ this._wheelInertia.cancel();
489
+ } else {
490
+ // Vertical wheeling zooms.
491
+ // We use inertia to generate fake wheel events for smoother zooming
492
+
493
+ const template = makeEventTemplate(wheelEvent);
494
+
495
+ this._wheelInertia.setMomentum(
496
+ wheelEvent.deltaY * (wheelEvent.deltaMode ? 80 : 1),
497
+ (delta) => {
498
+ const e = new WheelEvent("wheel", {
499
+ ...template,
500
+ deltaMode: 0,
501
+ deltaX: 0,
502
+ deltaY: delta,
503
+ });
504
+ dispatchEvent(e);
505
+ }
506
+ );
507
+
508
+ wheelEvent.preventDefault();
509
+ return;
510
+ }
511
+ }
512
+
513
+ // TODO: Should be handled at the view level, not globally
514
+ if (event.type == "click") {
515
+ const e = this._currentHover
516
+ ? {
517
+ type: event.type,
518
+ viewPath: [
519
+ ...this._currentHover.mark.unitView.getAncestors(),
520
+ ]
521
+ .map((view) => view.name)
522
+ .reverse(),
523
+ datum: this._currentHover.datum,
524
+ }
525
+ : {
526
+ type: event.type,
527
+ viewPath: null,
528
+ datum: null,
529
+ };
530
+
531
+ this._eventListeners
532
+ .get("click")
533
+ ?.forEach((listener) => listener(e));
534
+ }
535
+
536
+ dispatchEvent(event);
537
+ }
538
+ };
539
+
540
+ [
541
+ "mousedown",
542
+ "mouseup",
543
+ "wheel",
544
+ "click",
545
+ "mousemove",
546
+ "gesturechange",
547
+ "contextmenu",
548
+ ].forEach((type) => canvas.addEventListener(type, listener));
549
+
550
+ canvas.addEventListener("mousedown", () => {
551
+ document.addEventListener(
552
+ "mouseup",
553
+ () => this.tooltip.popEnabledState(),
554
+ { once: true }
555
+ );
556
+ this.tooltip.pushEnabledState(false);
557
+ });
558
+
559
+ // Prevent text selections etc while dragging
560
+ canvas.addEventListener("dragstart", (event) =>
561
+ event.stopPropagation()
562
+ );
563
+ }
564
+
565
+ /**
566
+ * @param {number} x
567
+ * @param {number} y
568
+ */
569
+ _handlePicking(x, y) {
570
+ const pixelValue = this._glHelper.readPickingPixel(x, y);
571
+
572
+ const uniqueId =
573
+ pixelValue[0] | (pixelValue[1] << 8) | (pixelValue[2] << 16);
574
+
575
+ if (uniqueId == 0) {
576
+ this._currentHover = null;
577
+ return;
578
+ }
579
+
580
+ if (uniqueId !== this._currentHover?.uniqueId) {
581
+ this._currentHover = null;
582
+ }
583
+
584
+ if (!this._currentHover) {
585
+ // We are doing an exhaustive search of the data. This is a bit slow with
586
+ // millions of items.
587
+ // TODO: Optimize by indexing or something
588
+
589
+ this.viewRoot.visit((view) => {
590
+ if (view instanceof UnitView) {
591
+ if (view.mark.isPickingParticipant()) {
592
+ const accessor = view.mark.encoders.uniqueId.accessor;
593
+ view.getCollector().visitData((d) => {
594
+ if (accessor(d) == uniqueId) {
595
+ this._currentHover = {
596
+ mark: view.mark,
597
+ datum: d,
598
+ uniqueId,
599
+ };
600
+ }
601
+ });
602
+ }
603
+ if (this._currentHover) {
604
+ return VISIT_STOP;
605
+ }
606
+ }
607
+ });
608
+ }
609
+
610
+ if (this._currentHover) {
611
+ const mark = this._currentHover.mark;
612
+ this.updateTooltip(this._currentHover.datum, async (datum) => {
613
+ if (!mark.isPickingParticipant()) {
614
+ return;
615
+ }
616
+
617
+ const tooltipProps = mark.properties.tooltip;
618
+
619
+ if (tooltipProps !== null) {
620
+ const handlerName = tooltipProps?.handler ?? "default";
621
+ const handler = this.tooltipHandlers[handlerName];
622
+ if (!handler) {
623
+ throw new Error(
624
+ "No such tooltip handler: " + handlerName
625
+ );
626
+ }
627
+
628
+ return handler(datum, mark, tooltipProps?.params);
629
+ }
630
+ });
631
+ }
632
+ }
633
+
634
+ /**
635
+ * This method should be called in a mouseMove handler. If not called, the
636
+ * tooltip will be hidden.
637
+ *
638
+ * @param {T} datum
639
+ * @param {function(T):Promise<string | HTMLElement | import("lit").TemplateResult>} [converter]
640
+ * @template T
641
+ */
642
+ updateTooltip(datum, converter) {
643
+ if (!this._tooltipUpdateRequested || !datum) {
644
+ this.tooltip.updateWithDatum(datum, converter);
645
+ this._tooltipUpdateRequested = true;
646
+ } else {
647
+ throw new Error(
648
+ "Tooltip has already been updated! Duplicate event handler?"
649
+ );
650
+ }
651
+ }
652
+
653
+ computeLayout() {
654
+ const root = this.viewRoot;
655
+ if (!root) {
656
+ return;
657
+ }
658
+
659
+ this.broadcast("layout");
660
+
661
+ const canvasSize = this._glHelper.getLogicalCanvasSize();
662
+
663
+ if (isNaN(canvasSize.width) || isNaN(canvasSize.height)) {
664
+ // TODO: Figure out what causes this
665
+ console.log(
666
+ `NaN in canvas size: ${canvasSize.width}x${canvasSize.height}. Skipping computeLayout().`
667
+ );
668
+ return;
669
+ }
670
+
671
+ this._renderingContext = new DeferredViewRenderingContext(
672
+ {
673
+ picking: false,
674
+ },
675
+ this._glHelper
676
+ );
677
+ this._pickingContext = new DeferredViewRenderingContext(
678
+ {
679
+ picking: true,
680
+ },
681
+ this._glHelper
682
+ );
683
+ const layoutRecorder = new LayoutRecorderViewRenderingContext({});
684
+
685
+ root.render(
686
+ new CompositeViewRenderingContext(
687
+ this._renderingContext,
688
+ this._pickingContext,
689
+ layoutRecorder
690
+ ),
691
+ // Canvas should now be sized based on the root view or the container
692
+ Rectangle.create(0, 0, canvasSize.width, canvasSize.height)
693
+ );
694
+
695
+ this.layout = layoutRecorder.getLayout();
696
+
697
+ this.broadcast("layoutComputed");
698
+ }
699
+
700
+ renderAll() {
701
+ this._renderingContext?.renderDeferred();
702
+
703
+ this._dirtyPickingBuffer = true;
704
+ }
705
+
706
+ renderPickingFramebuffer() {
707
+ if (!this._dirtyPickingBuffer) {
708
+ return;
709
+ }
710
+
711
+ this._pickingContext.renderDeferred();
712
+ this._dirtyPickingBuffer = false;
713
+ }
714
+
715
+ getSearchableViews() {
716
+ /** @type {UnitView[]} */
717
+ const views = [];
718
+ this.viewRoot.visit((view) => {
719
+ if (view instanceof UnitView && view.getAccessor("search")) {
720
+ views.push(view);
721
+ }
722
+ });
723
+ return views;
724
+ }
725
+
726
+ getNamedScaleResolutions() {
727
+ /** @type {Map<string, import("./view/scaleResolution").default>} */
728
+ const resolutions = new Map();
729
+ this.viewRoot.visit((view) => {
730
+ for (const resolution of Object.values(view.resolutions.scale)) {
731
+ if (resolution.name) {
732
+ resolutions.set(resolution.name, resolution);
733
+ }
734
+ }
735
+ });
736
+ return resolutions;
737
+ }
738
+ }
739
+
740
+ /**
741
+ *
742
+ * @param {HTMLElement} container
743
+ * @param {string} message
744
+ */
745
+ function createMessageBox(container, message) {
746
+ // Uh, need a templating thingy
747
+ const messageBox = document.createElement("div");
748
+ messageBox.className = "message-box";
749
+ const messageText = document.createElement("div");
750
+ messageText.textContent = message;
751
+ messageBox.appendChild(messageText);
752
+ container.appendChild(messageBox);
753
+ }