@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,63 @@
1
+ import { range } from "d3-array";
2
+ import { topK, topKSlice } from "./topK";
3
+
4
+ test("topK returns top k numbers in priority order", () => {
5
+ /** @param {number} x */
6
+ const priorityAccessor = (x) => x;
7
+
8
+ expect(topK([1, 2, 3], 3, priorityAccessor)).toEqual([3, 2, 1]);
9
+ expect(topK([1, 2, 3], 1, priorityAccessor)).toEqual([3]);
10
+ expect(topK([1, 2, 3], 6, priorityAccessor)).toEqual([3, 2, 1]);
11
+ expect(topK([1, 2, 3, 4, 5, 6], 3, priorityAccessor)).toEqual([6, 5, 4]);
12
+ expect(topK([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3, priorityAccessor)).toEqual([
13
+ 9, 8, 7,
14
+ ]);
15
+ expect(topK([1, 1, 1], 3, priorityAccessor)).toEqual([1, 1, 1]);
16
+ });
17
+
18
+ test("topK returns top k objects in priority order", () => {
19
+ /** @param {{priority: number}} d */
20
+ const priorityAccessor = (d) => d.priority;
21
+
22
+ expect(
23
+ topK(
24
+ [0, 9, 1, 8, 2, 7, 3, 6, 4, 5].map((x) => ({ priority: x })),
25
+ 3,
26
+ priorityAccessor
27
+ )
28
+ ).toEqual([9, 8, 7].map((x) => ({ priority: x })));
29
+ });
30
+
31
+ test("topK returns top k objects in priority order with large datasets", () => {
32
+ /** @param {number} x */
33
+ const priorityAccessor = (x) => x;
34
+
35
+ const n = 10000;
36
+ const bigArray = range(n).map((x) => Math.floor(Math.random() * 100));
37
+ const sortedBigArray = bigArray.slice().sort((a, b) => b - a);
38
+
39
+ for (let k = 0; k < 13000; k += 1000) {
40
+ expect(topK(bigArray, k, priorityAccessor)).toEqual(
41
+ sortedBigArray.slice(0, k)
42
+ );
43
+ }
44
+ });
45
+
46
+ test("topKSlice returns top k indexes in priority order", () => {
47
+ expect(topKSlice([0, 1, 2], 3)).toEqual([2, 1, 0]);
48
+ expect(topKSlice([1, 2, 3], 3)).toEqual([2, 1, 0]);
49
+ expect(topKSlice([0, 1, 2], 1)).toEqual([2]);
50
+ expect(topKSlice([0, 1, 2], 6)).toEqual([2, 1, 0]);
51
+ expect(topKSlice([0, 1, 2, 3, 4, 5], 3)).toEqual([5, 4, 3]);
52
+ expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3)).toEqual([1, 3, 5]);
53
+ expect(new Set(topKSlice([1, 1, 1, 2, 2, 2], 3))).toEqual(
54
+ new Set([3, 4, 5])
55
+ );
56
+ });
57
+
58
+ test("topKSlice returns top k indexes from a slice in priority order", () => {
59
+ expect(topKSlice([0, 1, 2, 3, 4, 5], 2, 1, 5)).toEqual([4, 3]);
60
+ expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3, 1, 5)).toEqual([
61
+ 1, 3, 4,
62
+ ]);
63
+ });
@@ -0,0 +1,74 @@
1
+ /** @param {number} ms */
2
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+
4
+ /**
5
+ * @param {TransitionOptions} options
6
+ *
7
+ * @typedef {Object} TransitionOptions
8
+ * @prop {number} [from] default: 0
9
+ * @prop {number} [to] default: 1
10
+ * @prop {number} [duration] in milliseconds, default: 1000
11
+ * @prop {number} [delay] milliseconds to wait before the transition starts, default: 0
12
+ * @prop {function(number):void} onUpdate
13
+ * @prop {function(number):number} [easingFunction] default: linear
14
+ * @prop {function(function(number):void):void} [requestAnimationFrame]
15
+ * default: window.requestAnimationFrame
16
+ * @prop {AbortSignal} [signal]
17
+ */
18
+ export default function transition(options) {
19
+ const requestAnimationFrame =
20
+ options.requestAnimationFrame || window.requestAnimationFrame;
21
+
22
+ const signal = options.signal;
23
+
24
+ const makePromise = () =>
25
+ new Promise((resolve, reject) => {
26
+ if (signal?.aborted) {
27
+ return reject("aborted");
28
+ }
29
+
30
+ const beginTimestamp = performance.now();
31
+ const endTimestamp = beginTimestamp + (options.duration || 1000);
32
+
33
+ const from = typeof options.from == "number" ? options.from : 0;
34
+ const to = typeof options.to == "number" ? options.to : 1;
35
+ const ease = options.easingFunction || ((x) => x);
36
+
37
+ /** @param {number} x */
38
+ const toUnit = (x) =>
39
+ (x - beginTimestamp) / (endTimestamp - beginTimestamp);
40
+
41
+ /** @param {number} x */
42
+ const toRange = (x) => x * (to - from) + from;
43
+
44
+ /** @param {number} x */
45
+ const clamp = (x) => Math.max(0, Math.min(1, x));
46
+
47
+ /** @param {number} stamp */
48
+ const step = (stamp) => {
49
+ if (signal?.aborted) {
50
+ reject("aborted");
51
+ } else {
52
+ options.onUpdate(toRange(ease(clamp(toUnit(stamp)))));
53
+ if (stamp < endTimestamp) {
54
+ requestAnimationFrame(step);
55
+ } else {
56
+ options.onUpdate(toRange(ease(1)));
57
+ resolve();
58
+ }
59
+ }
60
+ };
61
+
62
+ requestAnimationFrame(step);
63
+ });
64
+
65
+ if (options.delay) {
66
+ if (signal?.aborted) {
67
+ return Promise.reject("aborted");
68
+ }
69
+
70
+ return wait(options.delay).then(makePromise);
71
+ } else {
72
+ return makePromise();
73
+ }
74
+ }
@@ -0,0 +1,189 @@
1
+ import clientPoint from "../point";
2
+ import { html, render } from "lit-html";
3
+ import { peek } from "../arrayUtils";
4
+
5
+ export const SUPPRESS_TOOLTIP_CLASS_NAME = "gs-suppress-tooltip";
6
+
7
+ export default class Tooltip {
8
+ /**
9
+ * @param {HTMLElement} container
10
+ */
11
+ constructor(container) {
12
+ this.container = container;
13
+
14
+ this.element = document.createElement("div");
15
+ this.element.className = "tooltip";
16
+ this._visible = true;
17
+ this.container.appendChild(this.element);
18
+
19
+ /** @type {any} */
20
+ this._previousTooltipDatum = undefined;
21
+
22
+ this.enabledStack = [true];
23
+
24
+ this._penaltyUntil = 0;
25
+ /** @type {[number, number]} */
26
+ this._lastCoords = undefined;
27
+
28
+ this._previousMove = 0;
29
+
30
+ this.clear();
31
+ }
32
+
33
+ /**
34
+ * @param {boolean} visible
35
+ */
36
+ set visible(visible) {
37
+ if (visible != this._visible) {
38
+ this.element.style.display = visible ? null : "none";
39
+ this._visible = visible;
40
+ }
41
+ }
42
+
43
+ get visible() {
44
+ return this._visible;
45
+ }
46
+
47
+ get enabled() {
48
+ return peek(this.enabledStack) ?? true;
49
+ }
50
+
51
+ /**
52
+ * @param {boolean} enabled True if tooltip is enabled (allowed to be shown)
53
+ */
54
+ pushEnabledState(enabled) {
55
+ this.enabledStack.push(enabled);
56
+ if (!enabled) {
57
+ this.visible = false;
58
+ }
59
+ }
60
+
61
+ popEnabledState() {
62
+ this.enabledStack.pop();
63
+ }
64
+
65
+ /**
66
+ * @param {MouseEvent} mouseEvent
67
+ */
68
+ handleMouseMove(mouseEvent) {
69
+ this.mouseCoords = clientPoint(this.container, mouseEvent);
70
+
71
+ const now = performance.now();
72
+
73
+ // Prevent the tooltip from flashing briefly before it becomes penalized
74
+ // because of a quickly moving mouse pointer
75
+ if (
76
+ !this.visible &&
77
+ !this._isPenalty() &&
78
+ now - this._previousMove > 500
79
+ ) {
80
+ this._penaltyUntil = now + 70;
81
+ }
82
+
83
+ // Disable the tooltip for a while if the mouse is being moved very quickly.
84
+ // Makes the tooltip less annoying.
85
+ // TODO: Should calculate speed: pixels per millisecond or something
86
+ if (
87
+ this._lastCoords &&
88
+ distance(this.mouseCoords, this._lastCoords) > 20
89
+ ) {
90
+ this._penaltyUntil = now + 400;
91
+ }
92
+
93
+ this._lastCoords = this.mouseCoords;
94
+
95
+ if (this.visible) {
96
+ this.updatePlacement();
97
+ }
98
+
99
+ this._previousMove = now;
100
+ }
101
+
102
+ updatePlacement() {
103
+ /** Space between pointer and tooltip box */
104
+ const spacing = 20;
105
+
106
+ const [mouseX, mouseY] = this.mouseCoords;
107
+
108
+ let x = mouseX + spacing;
109
+ if (x > this.container.clientWidth - this.element.offsetWidth) {
110
+ x = mouseX - spacing - this.element.offsetWidth;
111
+ }
112
+ this.element.style.left = x + "px";
113
+
114
+ this.element.style.top =
115
+ Math.min(
116
+ mouseY + spacing,
117
+ this.container.clientHeight - this.element.offsetHeight
118
+ ) + "px";
119
+ }
120
+
121
+ /**
122
+ * @param {string | import("lit").TemplateResult | HTMLElement} content
123
+ */
124
+ setContent(content) {
125
+ if (!content || !this.enabled || this._isPenalty()) {
126
+ if (this.visible) {
127
+ render("", this.element);
128
+ this.visible = false;
129
+ }
130
+ this._previousTooltipDatum = undefined;
131
+ return;
132
+ }
133
+
134
+ render(content, this.element);
135
+
136
+ this.visible = true;
137
+
138
+ this.updatePlacement();
139
+ }
140
+
141
+ clear() {
142
+ this._previousTooltipDatum = undefined;
143
+ this.setContent(undefined);
144
+ }
145
+
146
+ /**
147
+ * Updates the tooltip if the provided datum differs from the previous one.
148
+ * Otherwise this is nop.
149
+ *
150
+ * @param {T} datum
151
+ * @param {function(T):Promise<string | HTMLElement | import("lit").TemplateResult>} [converter]
152
+ * @template T
153
+ */
154
+ updateWithDatum(datum, converter) {
155
+ if (datum !== this._previousTooltipDatum) {
156
+ this._previousTooltipDatum = datum;
157
+ if (!converter) {
158
+ converter = (d) =>
159
+ Promise.resolve(html` ${JSON.stringify(d)} `);
160
+ }
161
+
162
+ converter(datum)
163
+ .then((result) => this.setContent(result))
164
+ .catch((error) => {
165
+ if (error !== "debounced") {
166
+ throw error;
167
+ }
168
+ });
169
+ }
170
+ }
171
+
172
+ _isPenalty() {
173
+ return this._penaltyUntil && this._penaltyUntil > performance.now();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Calculate euclidean distance
179
+ *
180
+ * @param {number[]} a
181
+ * @param {number[]} b
182
+ */
183
+ function distance(a, b) {
184
+ let sum = 0;
185
+ for (let i = 0; i < a.length; i++) {
186
+ sum += (a[i] - b[i]) ** 2;
187
+ }
188
+ return Math.sqrt(sum);
189
+ }
@@ -0,0 +1,22 @@
1
+ const protoRe = /^([A-Za-z]+:)?\/\//;
2
+
3
+ /**
4
+ * Append a relative or absolute url to a base url.
5
+ * The base part is omitted if the append part is absolute.
6
+ *
7
+ * @param {function():string} baseAccessor
8
+ * @param {string} append
9
+ */
10
+ export function appendToBaseUrl(baseAccessor, append) {
11
+ if (append && protoRe.test(append)) {
12
+ return append;
13
+ }
14
+
15
+ const base = baseAccessor();
16
+
17
+ if (base && append) {
18
+ return base.endsWith("/") ? base + append : base + "/" + append;
19
+ }
20
+
21
+ return base ?? append;
22
+ }
@@ -0,0 +1,24 @@
1
+ import { isBoolean, isNumber, isString } from "vega-util";
2
+
3
+ /** A set of typical NA values */
4
+ export const NAs = new Set(["", "NA", ".", "-"]);
5
+
6
+ /**
7
+ * Tests whether the given array of strings can be interpreted as a numeric vector
8
+ *
9
+ * @param {string[]} values
10
+ */
11
+ export function inferNumeric(values) {
12
+ return values
13
+ .filter((value) => typeof value == "string")
14
+ .filter((value) => !NAs.has(value))
15
+ .every((value) => /^[+-]?\d+(\.\d*)?$/.test(value));
16
+ }
17
+
18
+ /**
19
+ * @param {any} value
20
+ * @returns {value is string | number | boolean}
21
+ */
22
+ export function isScalar(value) {
23
+ return isString(value) || isNumber(value) || isBoolean(value);
24
+ }
@@ -0,0 +1,12 @@
1
+ import * as vt from "./variableTools";
2
+
3
+ test("InferNumerality", () => {
4
+ expect(vt.inferNumeric(["0", "1", "2.2", "-4"])).toBeTruthy();
5
+ expect(vt.inferNumeric(["0", ...vt.NAs.values()])).toBeTruthy();
6
+ expect(vt.inferNumeric([])).toBeTruthy();
7
+
8
+ expect(vt.inferNumeric(["0", "x"])).toBeFalsy();
9
+ expect(vt.inferNumeric(["0", " "])).toBeFalsy();
10
+ expect(vt.inferNumeric(["0", "1,2"])).toBeFalsy();
11
+ expect(vt.inferNumeric(["0", "20x"])).toBeFalsy();
12
+ });
@@ -0,0 +1,135 @@
1
+ import { isString } from "vega-util";
2
+ import {
3
+ getChannelDefWithScale,
4
+ getPrimaryChannel,
5
+ isExprDef,
6
+ isFieldDef,
7
+ isSecondaryChannel,
8
+ } from "../encoder/encoder";
9
+ import { peek } from "../utils/arrayUtils";
10
+ import coalesce from "../utils/coalesce";
11
+
12
+ import mergeObjects from "../utils/mergeObjects";
13
+ import { getCachedOrCall } from "../utils/propertyCacher";
14
+
15
+ /**
16
+ *
17
+ * @typedef { import("./unitView").default} UnitView
18
+ */
19
+ export default class AxisResolution {
20
+ /**
21
+ * @param {import("../spec/channel").Channel} channel
22
+ */
23
+ constructor(channel) {
24
+ this.channel = channel;
25
+ /** @type {import("./scaleResolution").ResolutionMember[]} The involved views */
26
+ this.members = [];
27
+ }
28
+
29
+ get scaleResolution() {
30
+ return peek(this.members)?.view.getScaleResolution(this.channel);
31
+ }
32
+
33
+ /**
34
+ * N.B. This is expected to be called in depth-first order, AFTER the
35
+ * scales have been resolved.
36
+ *
37
+ * @param {UnitView} view
38
+ * @param {import("../spec/channel").Channel} channel TODO: Do something for this
39
+ */
40
+ pushUnitView(view, channel) {
41
+ const newScaleResolution = view.getScaleResolution(this.channel);
42
+
43
+ if (!newScaleResolution) {
44
+ throw new Error("Cannot find a scale resolution!");
45
+ }
46
+
47
+ if (
48
+ this.scaleResolution &&
49
+ newScaleResolution !== this.scaleResolution
50
+ ) {
51
+ throw new Error("Shared axes must have a shared scale!");
52
+ }
53
+
54
+ this.members.push({ view, channel });
55
+ }
56
+
57
+ getAxisProps() {
58
+ return getCachedOrCall(this, "axisProps", () => {
59
+ const propArray = this.members.map(
60
+ (member) =>
61
+ getChannelDefWithScale(member.view, member.channel).axis
62
+ );
63
+
64
+ if (
65
+ propArray.length > 0 &&
66
+ propArray.some((props) => props === null)
67
+ ) {
68
+ // No axis whatsoever is wanted
69
+ return null;
70
+ } else {
71
+ return /** @type { import("../spec/axis").Axis} */ (
72
+ mergeObjects(
73
+ propArray.filter((props) => props !== undefined),
74
+ "axis",
75
+ ["title"]
76
+ )
77
+ );
78
+ }
79
+ });
80
+ }
81
+
82
+ getTitle() {
83
+ /** @param {import("./scaleResolution").ResolutionMember} member} */
84
+ const computeTitle = (member) => {
85
+ const channelDef = getChannelDefWithScale(
86
+ member.view,
87
+ member.channel
88
+ );
89
+
90
+ // Retain nulls as they indicate that no title should be shown
91
+
92
+ return {
93
+ member,
94
+ explicitTitle: coalesce(
95
+ channelDef.axis?.title,
96
+ channelDef.title
97
+ ),
98
+ implicitTitle: coalesce(
99
+ isFieldDef(channelDef) ? channelDef.field : undefined,
100
+ isExprDef(channelDef) ? channelDef.expr : undefined
101
+ ),
102
+ };
103
+ };
104
+
105
+ const titles = this.members.map(computeTitle);
106
+
107
+ // Skip implicit secondary channel titles if the primary channel has an explicit title
108
+ const filteredTitles = titles.filter((title) => {
109
+ if (
110
+ isSecondaryChannel(title.member.channel) &&
111
+ !title.explicitTitle
112
+ ) {
113
+ const primaryChannel = getPrimaryChannel(title.member.channel);
114
+ return (
115
+ titles.find(
116
+ (title2) =>
117
+ title2.member.view == title.member.view &&
118
+ title2.member.channel == primaryChannel
119
+ )?.explicitTitle === undefined
120
+ );
121
+ }
122
+ return true;
123
+ });
124
+
125
+ const uniqueTitles = new Set(
126
+ filteredTitles
127
+ .map((title) =>
128
+ coalesce(title.explicitTitle, title.implicitTitle)
129
+ )
130
+ .filter(isString)
131
+ );
132
+
133
+ return uniqueTitles.size ? [...uniqueTitles].join(", ") : null;
134
+ }
135
+ }