@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,746 @@
1
+ import { validTicks, tickValues, tickFormat, tickCount } from "../scale/ticks";
2
+ import LayerView from "./layerView";
3
+ import { isNumber } from "vega-util";
4
+ import smoothstep from "../utils/smoothstep";
5
+ import { shallowArrayEquals } from "../utils/arrayUtils";
6
+ import { FlexDimensions } from "../utils/layout/flexLayout";
7
+ import DynamicCallbackSource from "../data/sources/dynamicCallbackSource";
8
+
9
+ const CHROM_LAYER_NAME = "chromosome_ticks_and_labels";
10
+
11
+ /**
12
+ * @typedef {import("../spec/channel").PositionalChannel} PositionalChannel
13
+ * @typedef {import("../spec/view").GeometricDimension} GeometricDimension
14
+ */
15
+
16
+ /** @type {Record<PositionalChannel, GeometricDimension>} */
17
+ const CHANNEL_DIMENSIONS = {
18
+ x: "width",
19
+ y: "height",
20
+ };
21
+
22
+ /**
23
+ * @param {PositionalChannel} channel
24
+ * @returns {PositionalChannel}
25
+ */
26
+ function getPerpendicularChannel(channel) {
27
+ return channel == "x" ? "y" : "x";
28
+ }
29
+
30
+ /** @type {Record<PositionalChannel, AxisOrient[]>} */
31
+ const CHANNEL_ORIENTS = {
32
+ x: ["bottom", "top"],
33
+ y: ["left", "right"],
34
+ };
35
+
36
+ /** @type {Record<AxisOrient, PositionalChannel>} */
37
+ const ORIENT_CHANNELS = Object.fromEntries(
38
+ Object.entries(CHANNEL_ORIENTS)
39
+ .map(([channel, slots]) => slots.map((slot) => [slot, channel]))
40
+ .flat(1)
41
+ );
42
+ /**
43
+ * @param {AxisOrient} slot
44
+ */
45
+ function orient2channel(slot) {
46
+ return ORIENT_CHANNELS[slot];
47
+ }
48
+
49
+ /**
50
+ * An internal view that renders an axis.
51
+ *
52
+ * TODO: Implement grid
53
+ *
54
+ * @typedef {import("../spec/view").LayerSpec} LayerSpec
55
+ * @typedef {import("./view").default} View
56
+ * @typedef {import("../spec/axis").Axis} Axis
57
+ * @typedef {import("../spec/axis").GenomeAxis} GenomeAxis
58
+ * @typedef {import("../spec/axis").AxisOrient} AxisOrient
59
+ * @typedef {import("../utils/layout/flexLayout").SizeDef} SizeDef
60
+ *
61
+ * @typedef {Axis & { extent: number }} AugmentedAxis
62
+ */
63
+ export default class AxisView extends LayerView {
64
+ /**
65
+ * @param {Axis} axisProps
66
+ * @param {import("./viewUtils").ViewContext} context
67
+ * @param {string} type Data type (quantitative, ..., locus)
68
+ * @param {import("./containerView").default} parent
69
+ */
70
+ constructor(axisProps, type, context, parent) {
71
+ // Now the presence of genomeAxis is based on field type, not scale type.
72
+ // TODO: Use scale instead. However, it would make the initialization much more
73
+ // complex because scales are not available before scale resolution.
74
+ const genomeAxis = type == "locus";
75
+
76
+ // TODO: Compute extent
77
+
78
+ /** @type {Axis | GenomeAxis} */
79
+ const fullAxisProps = {
80
+ ...(genomeAxis ? defaultGenomeAxisProps : defaultAxisProps),
81
+ ...getDefaultAngleAndAlign(type, axisProps),
82
+ ...axisProps,
83
+ };
84
+
85
+ super(
86
+ genomeAxis
87
+ ? createGenomeAxis(fullAxisProps)
88
+ : createAxis(fullAxisProps),
89
+ context,
90
+ parent,
91
+ `axis_${axisProps.orient}`
92
+ );
93
+
94
+ this.axisProps = fullAxisProps;
95
+
96
+ /** Axis should be updated before next render */
97
+ this.axisUpdateRequested = true;
98
+
99
+ this._addBroadcastHandler("layout", () => {
100
+ this.axisUpdateRequested = true;
101
+ });
102
+
103
+ /** @type {any[]} */
104
+ this.previousScaleDomain = [];
105
+
106
+ /** @type {number} TODO: Take from scal*/
107
+ this.axisLength = undefined;
108
+
109
+ /** @type {TickDatum[]} */
110
+ this.ticks = [];
111
+
112
+ this.tickSource = new DynamicCallbackSource(() => this.ticks);
113
+
114
+ if (genomeAxis) {
115
+ const channel = orient2channel(this.axisProps.orient);
116
+ const genome = this.getScaleResolution(channel).getGenome();
117
+ this.findChildByName(CHROM_LAYER_NAME).getDynamicDataSource = () =>
118
+ new DynamicCallbackSource(() => genome.chromosomes);
119
+ }
120
+ }
121
+
122
+ getOrient() {
123
+ return this.axisProps.orient;
124
+ }
125
+
126
+ getSize() {
127
+ /** @type {SizeDef} */
128
+ const perpendicularSize = { px: this.getPerpendicularSize() };
129
+
130
+ /** @type {SizeDef} */
131
+ const mainSize = { grow: 1 };
132
+
133
+ if (ORIENT_CHANNELS[this.axisProps.orient] == "x") {
134
+ return new FlexDimensions(mainSize, perpendicularSize);
135
+ } else {
136
+ return new FlexDimensions(perpendicularSize, mainSize);
137
+ }
138
+ }
139
+
140
+ getPerpendicularSize() {
141
+ return getExtent(this.axisProps);
142
+ }
143
+
144
+ getDynamicDataSource() {
145
+ return this.tickSource;
146
+ }
147
+
148
+ _updateAxisData() {
149
+ // TODO: This could be a transform that generates ticks on the fly
150
+ // Would allow for unlimited customization.
151
+
152
+ const channel = orient2channel(this.axisProps.orient);
153
+ const scale = this.getScaleResolution(channel).getScale();
154
+ const currentScaleDomain = scale.domain();
155
+
156
+ if (
157
+ shallowArrayEquals(currentScaleDomain, this.previousScaleDomain) &&
158
+ !this.axisUpdateRequested
159
+ ) {
160
+ // TODO: Instead of scale comparison, register an observer to Resolution
161
+ return;
162
+ }
163
+ this.previousScaleDomain = currentScaleDomain;
164
+
165
+ const oldTicks = this.ticks;
166
+ const newTicks = generateTicks(
167
+ this.axisProps,
168
+ scale,
169
+ this.axisLength,
170
+ oldTicks
171
+ );
172
+
173
+ if (newTicks !== oldTicks) {
174
+ this.ticks = newTicks;
175
+ this.tickSource.loadSynchronously();
176
+ }
177
+
178
+ this.axisUpdateRequested = false;
179
+ }
180
+
181
+ onBeforeRender() {
182
+ super.onBeforeRender();
183
+ this._updateAxisData();
184
+ }
185
+
186
+ /**
187
+ * @param {import("./renderingContext/viewRenderingContext").default} context
188
+ * @param {import("../utils/layout/rectangle").default} coords
189
+ * @param {import("./view").RenderingOptions} [options]
190
+ */
191
+ render(context, coords, options = {}) {
192
+ if (!this.isVisible()) {
193
+ return;
194
+ }
195
+
196
+ this.axisLength =
197
+ coords[CHANNEL_DIMENSIONS[orient2channel(this.getOrient())]];
198
+
199
+ super.render(context, coords, options);
200
+ }
201
+
202
+ isPickingSupported() {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @param {Axis} axisProps
209
+ */
210
+ function getExtent(axisProps) {
211
+ const mainChannel = orient2channel(axisProps.orient);
212
+
213
+ /** @type {number} */
214
+ let extent = (axisProps.ticks && axisProps.tickSize) || 0;
215
+
216
+ if (axisProps.labels) {
217
+ extent += axisProps.labelPadding;
218
+ if (mainChannel == "x") {
219
+ extent += axisProps.labelFontSize;
220
+ } else {
221
+ extent += 30; // TODO: Measure label lengths!
222
+ }
223
+ }
224
+ if (axisProps.title) {
225
+ extent += axisProps.titlePadding + axisProps.titleFontSize;
226
+ }
227
+
228
+ // TODO: Include chrom ticks and labels!
229
+
230
+ extent = Math.min(
231
+ axisProps.maxExtent || Infinity,
232
+ Math.max(axisProps.minExtent || 0, extent)
233
+ );
234
+
235
+ return extent;
236
+ }
237
+
238
+ /**
239
+ * @param {Axis} axisProps
240
+ * @param {any} scale
241
+ * @param {number} axisLength Length of axis in pixels
242
+ * @param {TickDatum[]} [oldTicks] Reuse the old data if the tick values are identical
243
+ * @returns {TickDatum[]}
244
+ *
245
+ * @typedef {object} TickDatum
246
+ * @prop {number} value
247
+ * @prop {string} label
248
+ */
249
+ function generateTicks(axisProps, scale, axisLength, oldTicks = []) {
250
+ /**
251
+ * Make ticks more dense in small plots
252
+ *
253
+ * @param {number} length
254
+ */
255
+ const tickSpacing = (length) => 25 + 60 * smoothstep(100, 700, length);
256
+
257
+ let count = isNumber(axisProps.tickCount)
258
+ ? axisProps.tickCount
259
+ : Math.round(axisLength / tickSpacing(axisLength));
260
+
261
+ count = tickCount(scale, count, axisProps.tickMinStep);
262
+
263
+ const values = axisProps.values
264
+ ? validTicks(scale, axisProps.values, count)
265
+ : tickValues(scale, count);
266
+
267
+ if (
268
+ shallowArrayEquals(
269
+ values,
270
+ oldTicks,
271
+ (v) => v,
272
+ (d) => d.value
273
+ )
274
+ ) {
275
+ return oldTicks;
276
+ } else {
277
+ const format = tickFormat(scale, count, axisProps.format);
278
+
279
+ return values.map((x) => ({ value: x, label: format(x) }));
280
+ }
281
+ }
282
+
283
+ // Based on: https://vega.github.io/vega-lite/docs/axis.html
284
+ // TODO: The defaults should be taken from config (theme)
285
+ /** @type {Axis} */
286
+ const defaultAxisProps = {
287
+ values: null,
288
+
289
+ minExtent: 20,
290
+ maxExtent: Infinity,
291
+ offset: 0, // TODO: Implement
292
+
293
+ domain: true,
294
+ domainWidth: 1,
295
+ domainColor: "gray",
296
+ domainDash: null,
297
+ domainDashOffset: 0,
298
+ domainCap: "square", // Make 1px caps crisp
299
+
300
+ ticks: true,
301
+ tickSize: 5,
302
+ tickWidth: 1,
303
+ tickColor: "gray",
304
+ tickDash: null,
305
+ tickDashOffset: 0,
306
+ tickCap: "square", // Make 1px caps crisp
307
+
308
+ // TODO: tickBand
309
+
310
+ tickCount: null,
311
+ tickMinStep: null,
312
+
313
+ labels: true,
314
+ labelAlign: "center",
315
+ labelBaseline: "middle",
316
+ labelPadding: 4,
317
+ labelFontSize: 10,
318
+ labelLimit: 180, // TODO
319
+ labelColor: "black",
320
+ format: null,
321
+
322
+ titleColor: "black",
323
+ titleFont: "sans-serif",
324
+ titleFontSize: 10,
325
+ titlePadding: 3,
326
+
327
+ // TODO: titleX, titleY, titleAngle, titleAlign, etc
328
+ };
329
+
330
+ /**
331
+ * @param {string} type
332
+ * @param {Axis} axisProps
333
+ */
334
+ function getDefaultAngleAndAlign(type, axisProps) {
335
+ const orient = axisProps.orient;
336
+ const discrete = type == "nominal" || type == "ordinal";
337
+
338
+ /** @type {import("../spec/font").Align} */
339
+ let align = "center";
340
+ /** @type {import("../spec/font").Baseline} */
341
+ let baseline = "middle";
342
+
343
+ /** @type {number} */
344
+ let angle =
345
+ axisProps.labelAngle ??
346
+ ((orient == "top" || orient == "bottom") && discrete ? -90 : 0);
347
+
348
+ // TODO: Setting labelAngle of left or right axis to 90 or -90 should center the labels
349
+
350
+ switch (orient) {
351
+ case "left":
352
+ align = "right";
353
+ break;
354
+ case "right":
355
+ align = "left";
356
+ break;
357
+ case "top":
358
+ case "bottom":
359
+ if (Math.abs(angle) > 30) {
360
+ align = angle > 0 === (orient == "bottom") ? "left" : "right";
361
+ baseline = "middle";
362
+ } else {
363
+ baseline = orient == "top" ? "alphabetic" : "top";
364
+ }
365
+ break;
366
+ default:
367
+ }
368
+
369
+ return {
370
+ labelAlign: align,
371
+ labelAngle: angle,
372
+ labelBaseline: baseline,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * @param {Axis} axisProps
378
+ * @returns {LayerSpec}
379
+ */
380
+ function createAxis(axisProps) {
381
+ // TODO: Ensure that no channels except the positional ones are shared
382
+
383
+ const ap = { ...axisProps, extent: getExtent(axisProps) };
384
+
385
+ const main = orient2channel(ap.orient);
386
+ const secondary = getPerpendicularChannel(main);
387
+
388
+ const offsetDirection =
389
+ ap.orient == "bottom" || ap.orient == "right" ? 1 : -1;
390
+
391
+ const anchor = ap.orient == "bottom" || ap.orient == "left" ? 1 : 0;
392
+
393
+ /**
394
+ * @return {import("../spec/view").UnitSpec}
395
+ */
396
+ const createDomain = () => ({
397
+ name: "domain",
398
+ data: { values: [0] },
399
+ mark: {
400
+ type: "rule",
401
+ clip: false,
402
+ strokeDash: ap.domainDash,
403
+ strokeCap: ap.domainCap,
404
+ color: ap.domainColor,
405
+ [secondary]: anchor,
406
+ size: ap.domainWidth,
407
+ },
408
+ });
409
+
410
+ /**
411
+ * @return {import("../spec/view").UnitSpec}
412
+ */
413
+ const createLabels = () => ({
414
+ name: "labels",
415
+ mark: {
416
+ type: "text",
417
+ clip: false,
418
+ align: ap.labelAlign,
419
+ angle: ap.labelAngle,
420
+ baseline: ap.labelBaseline,
421
+ [secondary + "Offset"]:
422
+ (ap.tickSize + ap.labelPadding) * offsetDirection,
423
+ [secondary]: anchor,
424
+ size: ap.labelFontSize,
425
+ color: ap.labelColor,
426
+ minBufferSize: 1500, // to prevent GPU buffer reallocation when zooming
427
+ dynamicData: true,
428
+ },
429
+ encoding: {
430
+ [main]: { field: "value", type: "quantitative" },
431
+ text: { field: "label", type: "quantitative" },
432
+ },
433
+ });
434
+
435
+ /**
436
+ * @return {import("../spec/view").UnitSpec}
437
+ */
438
+ const createTicks = () => ({
439
+ name: "ticks",
440
+ mark: {
441
+ type: "rule",
442
+ clip: false,
443
+ strokeDash: ap.tickDash,
444
+ strokeCap: ap.tickCap,
445
+ color: ap.tickColor,
446
+ size: ap.tickWidth,
447
+ minBufferSize: 300,
448
+ dynamicData: true,
449
+ },
450
+ encoding: {
451
+ [secondary]: { value: anchor },
452
+ [secondary + "2"]: {
453
+ value: anchor - (ap.tickSize / ap.extent) * (anchor ? 1 : -1),
454
+ },
455
+ },
456
+ });
457
+
458
+ /**
459
+ * @return {import("../spec/view").UnitSpec}
460
+ */
461
+ const createTitle = () => ({
462
+ name: "title",
463
+ data: { values: [0] },
464
+ mark: {
465
+ type: "text",
466
+ clip: false,
467
+ align: "center",
468
+ baseline: ap.orient == "bottom" ? "bottom" : "top",
469
+ angle: [0, 90, 0, -90][
470
+ ["top", "right", "bottom", "left"].indexOf(ap.orient)
471
+ ],
472
+ text: ap.title,
473
+ color: ap.titleColor,
474
+ [main]: 0.5,
475
+ [secondary]: 1 - anchor,
476
+ },
477
+ });
478
+
479
+ /**
480
+ * @return {import("../spec/view").LayerSpec}
481
+ */
482
+ const createTicksAndLabels = () => {
483
+ /** @type {LayerSpec} */
484
+ const spec = {
485
+ name: "ticks_and_labels",
486
+ encoding: {
487
+ [main]: { field: "value", type: "quantitative" },
488
+ },
489
+ layer: [],
490
+ };
491
+
492
+ if (ap.ticks) {
493
+ spec.layer.push(createTicks());
494
+ }
495
+
496
+ if (ap.labels) {
497
+ spec.layer.push(createLabels());
498
+ }
499
+
500
+ return spec;
501
+ };
502
+
503
+ /** @type {LayerSpec} */
504
+ const axisSpec = {
505
+ [CHANNEL_DIMENSIONS[
506
+ getPerpendicularChannel(orient2channel(ap.orient))
507
+ ]]: ap.extent,
508
+ data: { dynamicCallbackSource: true },
509
+ layer: [],
510
+ };
511
+
512
+ if (ap.domain) {
513
+ axisSpec.layer.push(createDomain());
514
+ }
515
+
516
+ if (ap.ticks || ap.labels) {
517
+ axisSpec.layer.push(createTicksAndLabels());
518
+ }
519
+
520
+ if (ap.title) {
521
+ axisSpec.layer.push(createTitle());
522
+ }
523
+
524
+ return axisSpec;
525
+ }
526
+
527
+ /** @type {import("../spec/axis").GenomeAxis} */
528
+ const defaultGenomeAxisProps = {
529
+ ...defaultAxisProps,
530
+
531
+ chromTicks: true,
532
+ chromTickSize: 18,
533
+ chromTickWidth: 1,
534
+ chromTickColor: "#989898",
535
+ chromTickDash: [4, 2],
536
+ chromTickDashOffset: 1,
537
+
538
+ chromLabels: true,
539
+ chromLabelFontSize: 13,
540
+ chromLabelFontWeight: "normal",
541
+ chromLabelFontStyle: "normal",
542
+ chromLabelColor: "black",
543
+ chromLabelAlign: "left",
544
+ chromLabelPadding: 7,
545
+ // TODO: chromLabelAngle
546
+ };
547
+
548
+ /**
549
+ * @param {GenomeAxis} axisProps
550
+ * @returns {LayerSpec}
551
+ */
552
+ export function createGenomeAxis(axisProps) {
553
+ const ap = { ...axisProps, extent: getExtent(axisProps) };
554
+
555
+ const main = orient2channel(ap.orient);
556
+ const secondary = getPerpendicularChannel(main);
557
+
558
+ const anchor = ap.orient == "bottom" || ap.orient == "left" ? 1 : 0;
559
+
560
+ /**
561
+ * @return {import("../spec/view").UnitSpec}
562
+ */
563
+ const createChromosomeTicks = () => ({
564
+ name: "chromosome_ticks",
565
+ mark: {
566
+ type: "rule",
567
+ strokeDash: axisProps.chromTickDash,
568
+ strokeDashOffset: axisProps.chromTickDashOffset,
569
+ [secondary]: anchor,
570
+ [secondary + "2"]:
571
+ anchor - (ap.chromTickSize / ap.extent) * (anchor ? 1 : -1),
572
+ color: axisProps.chromTickColor,
573
+ size: ap.chromTickWidth,
574
+ dynamicData: true,
575
+ },
576
+ });
577
+
578
+ /**
579
+ * @return {import("../spec/view").UnitSpec}
580
+ */
581
+ const createChromosomeLabels = () => {
582
+ /** @type {Partial<import("../spec/mark").MarkConfig>} */
583
+ let chromLabelMarkProps;
584
+ switch (ap.orient) {
585
+ case "top":
586
+ chromLabelMarkProps = {
587
+ y: 0,
588
+ angle: 0,
589
+ paddingX: 4,
590
+ dy: -ap.chromLabelPadding,
591
+ viewportEdgeFadeWidthLeft: 20,
592
+ viewportEdgeFadeWidthRight: 20,
593
+ viewportEdgeFadeDistanceRight: -10,
594
+ viewportEdgeFadeDistanceLeft: -20,
595
+ };
596
+ break;
597
+ case "bottom":
598
+ chromLabelMarkProps = {
599
+ y: 1,
600
+ angle: 0,
601
+ paddingX: 4,
602
+ dy: ap.chromLabelPadding + ap.chromLabelFontSize * 0.73, // A hack to align baseline with other labels
603
+ viewportEdgeFadeWidthLeft: 20,
604
+ viewportEdgeFadeWidthRight: 20,
605
+ viewportEdgeFadeDistanceRight: -10,
606
+ viewportEdgeFadeDistanceLeft: -20,
607
+ };
608
+ break;
609
+ case "left":
610
+ chromLabelMarkProps = {
611
+ x: 1,
612
+ angle: -90,
613
+ paddingY: 4,
614
+ dy: -ap.chromLabelPadding,
615
+ viewportEdgeFadeWidthBottom: 20,
616
+ viewportEdgeFadeWidthTop: 20,
617
+ viewportEdgeFadeDistanceBottom: -20,
618
+ viewportEdgeFadeDistanceTop: -10,
619
+ };
620
+ break;
621
+ case "right":
622
+ chromLabelMarkProps = {
623
+ x: 0,
624
+ angle: 90,
625
+ align: "right",
626
+ paddingY: 4,
627
+ dy: -ap.chromLabelPadding,
628
+ };
629
+ break;
630
+ default:
631
+ chromLabelMarkProps = {};
632
+ }
633
+
634
+ /** @type {import("../spec/view").UnitSpec} */
635
+ const labels = {
636
+ name: "chromosome_labels",
637
+ mark: {
638
+ type: "text",
639
+ size: ap.chromLabelFontSize,
640
+ font: ap.chromLabelFont,
641
+ fontWeight: ap.chromLabelFontWeight,
642
+ fontStyle: ap.chromLabelFontStyle,
643
+ color: ap.chromLabelColor,
644
+ align: axisProps.chromLabelAlign,
645
+ baseline: "alphabetic",
646
+ clip: false,
647
+ dynamicData: true,
648
+ ...chromLabelMarkProps,
649
+ },
650
+ encoding: {
651
+ [main + "2"]: { field: "continuousEnd", type: "locus" },
652
+ text: { field: "name", type: "ordinal" },
653
+ },
654
+ };
655
+ return labels;
656
+ };
657
+
658
+ /** @type {Axis} */
659
+ let fixedAxisProps;
660
+ switch (ap.orient) {
661
+ case "bottom":
662
+ case "top":
663
+ fixedAxisProps = {};
664
+ break;
665
+ case "left":
666
+ fixedAxisProps = {
667
+ labelAngle: -90,
668
+ labelAlign: "center",
669
+ labelPadding: 6,
670
+ };
671
+ break;
672
+ case "right":
673
+ fixedAxisProps = {
674
+ labelAngle: 90,
675
+ labelAlign: "center",
676
+ labelPadding: 6,
677
+ };
678
+ break;
679
+ default:
680
+ fixedAxisProps = {};
681
+ }
682
+
683
+ // Create an ordinary axis
684
+ const axisSpec = createAxis({
685
+ ...axisProps,
686
+ ...fixedAxisProps,
687
+ // TODO: Allow the user to override fixedAxisProps
688
+ });
689
+
690
+ if (axisProps.chromTicks || axisProps.chromLabels) {
691
+ /** @type {import("../spec/view").LayerSpec} */
692
+ const chromLayerSpec = {
693
+ // TODO: Configuration
694
+ name: CHROM_LAYER_NAME,
695
+ data: { dynamicCallbackSource: true },
696
+ encoding: {
697
+ // TODO: { chrom: "name", type: "locus" } // without pos = pos is 0
698
+ [main]: { field: "continuousStart", type: "locus", band: 0 },
699
+ },
700
+ layer: [],
701
+ };
702
+
703
+ if (axisProps.chromTicks) {
704
+ chromLayerSpec.layer.push(createChromosomeTicks());
705
+ }
706
+
707
+ if (axisProps.chromLabels) {
708
+ chromLayerSpec.layer.push(createChromosomeLabels());
709
+
710
+ /** @type {import("../spec/mark").MarkConfig} */
711
+ let labelMarkSpec;
712
+
713
+ // TODO: Simplify the following mess
714
+ axisSpec.layer
715
+ .filter((view) => view.name == "ticks_and_labels")
716
+ .forEach((/** @type {LayerSpec} */ view) =>
717
+ view.layer
718
+ .filter((view) => view.name == "labels")
719
+ .forEach(
720
+ (
721
+ /** @type {import("../spec/view").UnitSpec} */ view
722
+ ) => {
723
+ labelMarkSpec =
724
+ /** @type {import("../spec/mark").MarkConfig} */ (
725
+ view.mark
726
+ );
727
+ }
728
+ )
729
+ );
730
+
731
+ if (labelMarkSpec) {
732
+ if (ap.orient == "top" || ap.orient == "bottom") {
733
+ labelMarkSpec.viewportEdgeFadeWidthLeft = 30;
734
+ labelMarkSpec.viewportEdgeFadeDistanceLeft = 40;
735
+ } else {
736
+ labelMarkSpec.viewportEdgeFadeWidthBottom = 30;
737
+ labelMarkSpec.viewportEdgeFadeDistanceBottom = 40;
738
+ }
739
+ }
740
+ }
741
+
742
+ axisSpec.layer.push(chromLayerSpec);
743
+ }
744
+
745
+ return axisSpec;
746
+ }