@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,23 @@
1
+ /* https://stackoverflow.com/a/41699140/1547896 */
2
+
3
+ export function escapeHtml(str) {
4
+ var map = {
5
+ "&": "&",
6
+ "<": "&lt;",
7
+ ">": "&gt;",
8
+ '"': "&quot;",
9
+ "'": "&#039;",
10
+ };
11
+ return str.replace(/[&<>"']/g, (m) => map[m]);
12
+ }
13
+
14
+ export function decodeHtml(str) {
15
+ var map = {
16
+ "&amp;": "&",
17
+ "&lt;": "<",
18
+ "&gt;": ">",
19
+ "&quot;": '"',
20
+ "&#039;": "'",
21
+ };
22
+ return str.replace(/&amp;|&lt;|&gt;|&quot;|&#039;/g, (m) => map[m]);
23
+ }
@@ -0,0 +1,13 @@
1
+ import * as html from "./html";
2
+
3
+ test("Escape HTML", () => {
4
+ expect(html.escapeHtml('< "x" & "y" >')).toEqual(
5
+ "&lt; &quot;x&quot; &amp; &quot;y&quot; &gt;"
6
+ );
7
+ });
8
+
9
+ test("Decode HTML", () => {
10
+ expect(
11
+ html.decodeHtml("&lt; &quot;x&quot; &amp; &quot;y&quot; &gt;")
12
+ ).toEqual('< "x" & "y" >');
13
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Assigns unique values an index number in the order they are encountered.
3
+ *
4
+ * TODO: What about undefined?
5
+ *
6
+ * @template T
7
+ */
8
+ export default function createIndexer() {
9
+ let counter = 0;
10
+
11
+ /** @type {Map<T, number>} */
12
+ const values = new Map();
13
+
14
+ /** @param {T} value */
15
+ const indexer = (value) => {
16
+ let index = values.get(value);
17
+ if (index === undefined) {
18
+ index = counter++;
19
+ values.set(value, index);
20
+ }
21
+ return index;
22
+ };
23
+
24
+ /** @param {Iterable<T>} iterable */
25
+ indexer.addAll = (iterable) => {
26
+ for (const value of iterable) {
27
+ indexer(value);
28
+ }
29
+ };
30
+
31
+ /** @param {number} value */
32
+ indexer.invert = (value) => {
33
+ for (const entry of values.entries()) {
34
+ if (entry[1] == value) {
35
+ return entry[0];
36
+ }
37
+ }
38
+ };
39
+
40
+ indexer.domain = () => [...values.keys()];
41
+
42
+ return indexer;
43
+ }
@@ -0,0 +1,46 @@
1
+ import createIndexer from "./indexer";
2
+
3
+ test("Index values one by one", () => {
4
+ const indexer = createIndexer();
5
+
6
+ expect(indexer("a")).toEqual(0);
7
+ expect(indexer("b")).toEqual(1);
8
+ expect(indexer("c")).toEqual(2);
9
+ expect(indexer("a")).toEqual(0);
10
+ expect(indexer("c")).toEqual(2);
11
+ });
12
+
13
+ test("Index multiple values (predefined domain)", () => {
14
+ const indexer = createIndexer();
15
+
16
+ indexer.addAll(["a", "b", "c"]);
17
+
18
+ expect(indexer("d")).toEqual(3);
19
+ expect(indexer("a")).toEqual(0);
20
+ expect(indexer("b")).toEqual(1);
21
+ expect(indexer("c")).toEqual(2);
22
+ expect(indexer("d")).toEqual(3);
23
+ });
24
+
25
+ test("Indexer inverts index numbers", () => {
26
+ const indexer = createIndexer();
27
+
28
+ indexer.addAll(["a", "b", "c"]);
29
+
30
+ expect(indexer.invert(0)).toEqual("a");
31
+ expect(indexer.invert(1)).toEqual("b");
32
+ expect(indexer.invert(2)).toEqual("c");
33
+ expect(indexer.invert(3)).toBeUndefined();
34
+ });
35
+
36
+ test("Indexer return correct domain", () => {
37
+ const indexer = createIndexer();
38
+
39
+ expect(indexer("a")).toEqual(0);
40
+ expect(indexer("b")).toEqual(1);
41
+ expect(indexer("c")).toEqual(2);
42
+ expect(indexer("a")).toEqual(0);
43
+ expect(indexer("c")).toEqual(2);
44
+
45
+ expect(indexer.domain()).toEqual(["a", "b", "c"]);
46
+ });
@@ -0,0 +1,124 @@
1
+ import { lerp } from "vega-util";
2
+
3
+ /**
4
+ * Creates some inertia, mainly for zooming with a mechanical mouse wheel
5
+ */
6
+ export default class Inertia {
7
+ /**
8
+ * @param {import("./animator").default} animator
9
+ * @param {boolean} [disabled] Just call the callback directly
10
+ */
11
+ constructor(animator, disabled) {
12
+ this.animator = animator;
13
+ this.disabled = !!disabled;
14
+ this.damping = 0.015;
15
+ this.acceleration = 0.3; // per event
16
+ /** Use acceleration if the momentum step is greater than X */
17
+ this.accelerationThreshold = 100;
18
+ this.lowerLimit = 0.5; // When to stop updating
19
+ this.loop = false;
20
+
21
+ this.momentum = 0;
22
+ this.timestamp = 0;
23
+ /** @type {function(number):void} */
24
+ this.callback = null;
25
+
26
+ this._transitionCallback = this.animate.bind(this);
27
+ this.clear();
28
+ }
29
+
30
+ clear() {
31
+ /** @type {number} */
32
+ this.momentum = 0;
33
+ this.timestamp = null;
34
+ this.loop = null;
35
+ this.callback = null;
36
+ }
37
+
38
+ cancel() {
39
+ if (this.loop) {
40
+ this.animator.cancelTransition(this._transitionCallback);
41
+ this.clear();
42
+ }
43
+ }
44
+
45
+ /**
46
+ *
47
+ * @param {number} value
48
+ * @param {function(number):void} callback
49
+ */
50
+ setMomentum(value, callback) {
51
+ if (this.disabled) {
52
+ callback(value);
53
+ return;
54
+ }
55
+
56
+ // This may have some use in the future to improve the behavior of
57
+ // a mechanical mouse wheel:
58
+ // https://github.com/w3c/uievents/issues/181
59
+
60
+ if (value * this.momentum < 0) {
61
+ this.momentum = 0; // Stop if the direction changes
62
+ } else if (Math.abs(value) > this.accelerationThreshold) {
63
+ this.momentum = lerp([this.momentum, value], this.acceleration);
64
+ } else {
65
+ this.momentum = value;
66
+ }
67
+
68
+ this.callback = callback;
69
+
70
+ if (!this.loop) {
71
+ this.animate();
72
+ }
73
+ }
74
+
75
+ /**
76
+ *
77
+ * @param {number} [timestamp]
78
+ */
79
+ animate(timestamp) {
80
+ this.callback(this.momentum); // TODO: This is actually a delta, should take the elapsed time into account
81
+
82
+ const timeDelta = timestamp - this.timestamp || 0;
83
+ this.timestamp = timestamp;
84
+
85
+ const velocity = Math.abs(this.momentum);
86
+
87
+ this.momentum =
88
+ Math.sign(this.momentum) *
89
+ Math.max(
90
+ 0,
91
+ velocity - ((velocity * this.damping) ** 1.5 + 0.04) * timeDelta
92
+ );
93
+
94
+ if (Math.abs(this.momentum) > this.lowerLimit) {
95
+ this.loop = true;
96
+ this.animator.requestTransition(this._transitionCallback);
97
+ } else {
98
+ this.clear();
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {T} event
105
+ * @template T
106
+ */
107
+ export function makeEventTemplate(event) {
108
+ /** @type {Partial<Record<keyof T, any>>} */
109
+ const template = {};
110
+ const acceptedTypes = ["string", "number", "boolean"];
111
+ const rejectedProps = ["wheelDelta", "wheelDeltaX", "wheelDeltaY"];
112
+
113
+ // eslint-disable-next-line guard-for-in
114
+ for (const key in event) {
115
+ const k = /** @type {keyof T} */ (key);
116
+ if (
117
+ !rejectedProps.includes(key) &&
118
+ acceptedTypes.includes(typeof event[k])
119
+ ) {
120
+ template[k] = /** @type {any} */ (event[k]);
121
+ }
122
+ }
123
+ return template;
124
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * This class wraps a MouseEvent (or similar) and allows for
3
+ * its propagation through the view hierarchy in a similar manner
4
+ * as in the DOM.
5
+ */
6
+ export default class InteractionEvent {
7
+ /**
8
+ *
9
+ * @param {import("./layout/point").default} point Event coordinates
10
+ * inside the visualization canvas.
11
+ * @param {UIEvent} uiEvent The event to be wrapped
12
+ */
13
+ constructor(point, uiEvent) {
14
+ this.point = point;
15
+ this.uiEvent = uiEvent;
16
+ this.stopped = false;
17
+
18
+ /**
19
+ * The target is known only in the bubbling phase
20
+ *
21
+ * @type {import("../view/view").default}
22
+ */
23
+ this.target = undefined;
24
+ }
25
+
26
+ stopPropagation() {
27
+ this.stopped = true;
28
+ }
29
+
30
+ get type() {
31
+ return this.uiEvent.type;
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Iterates a nested Map structure created by d3-array's group() function.
3
+ *
4
+ * Yields arrays that contain the compound key and the grouped data items.
5
+ *
6
+ * @param {Map<any, any>} map The root
7
+ * @param {any[]} [path] The path so far.
8
+ * @returns {Generator<[any[], any[]]>}
9
+ */
10
+ export default function* iterateNestedMaps(map, path = []) {
11
+ for (const [key, value] of map.entries()) {
12
+ if (value instanceof Map) {
13
+ for (const m of iterateNestedMaps(value, [...path, key])) {
14
+ yield m;
15
+ }
16
+ } else {
17
+ // TODO: Could recycle compound key arrays for better performance
18
+ yield [[...path, key], value];
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,32 @@
1
+ import { group } from "d3-array";
2
+ import iterateNestedMaps from "./iterateNestedMaps";
3
+
4
+ const data = [
5
+ { name: "jim", amount: "34.0", date: "11/12/2015" },
6
+ { name: "carl", amount: "120.11", date: "11/12/2015" },
7
+ { name: "stacy", amount: "12.01", date: "01/04/2016" },
8
+ { name: "stacy", amount: "34.05", date: "01/04/2016" },
9
+ { name: "stacy", amount: "1.5", date: "02/04/2016" },
10
+ ];
11
+
12
+ const groups = group(
13
+ data,
14
+ (d) => d.name,
15
+ (d) => d.date
16
+ );
17
+
18
+ test("iterateNestedMaps iterates correctly", () => {
19
+ const expected = [
20
+ [["jim", "11/12/2015"], [data[0]]],
21
+ [["carl", "11/12/2015"], [data[1]]],
22
+ [
23
+ ["stacy", "01/04/2016"],
24
+ [data[2], data[3]],
25
+ ],
26
+ [["stacy", "02/04/2016"], [data[4]]],
27
+ ];
28
+
29
+ const result = [...iterateNestedMaps(groups)];
30
+
31
+ expect(result).toEqual(expected);
32
+ });
@@ -0,0 +1,42 @@
1
+ import FlatQueue from "flatqueue";
2
+
3
+ /**
4
+ * Returns an iterator that merges multiple sorted arrays.
5
+ *
6
+ * @param {T[][]} arrays
7
+ * @param {function(T):number} [accessor]
8
+ * @template T
9
+ */
10
+ export default function* kWayMerge(arrays, accessor = (x) => +x) {
11
+ // https://www.wikiwand.com/en/K-way_merge_algorithm
12
+
13
+ // This could be optimized by implementing a tournament tree or
14
+ // by adding replaceTop to the Heap.
15
+ // https://docs.python.org/2/library/heapq.html#heapq.heapreplace
16
+
17
+ const k = arrays.length;
18
+
19
+ const heap = new FlatQueue();
20
+ const pointers = new Array(k).fill(0);
21
+
22
+ for (const [i, array] of arrays.entries()) {
23
+ if (array.length) {
24
+ heap.push(i, accessor(array[0]));
25
+ }
26
+ }
27
+
28
+ let i = 0;
29
+ while ((i = heap.pop()) !== undefined) {
30
+ const array = arrays[i];
31
+ let pointer = pointers[i];
32
+ const element = array[pointer++];
33
+
34
+ yield element;
35
+
36
+ if (pointer < array.length) {
37
+ const newValue = accessor(array[pointer]);
38
+ heap.push(i, newValue);
39
+ pointers[i] = pointer;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ import kWayMerge from "./kWayMerge";
2
+
3
+ test("k-way merge merges multiple sorted arrays", () => {
4
+ /** @type {{a: number}[][]} */
5
+ const arrays = [];
6
+
7
+ for (let a = 0; a < 20; a++) {
8
+ /** @type {{a: number}[]} */
9
+ const array = [];
10
+ arrays.push(array);
11
+
12
+ let x = 0;
13
+ for (let i = 0; i < a; i++) {
14
+ x += Math.floor(Math.random() * 10);
15
+ array.push({ a: x });
16
+ }
17
+ }
18
+
19
+ const sorted = arrays.flat().sort((a, b) => a.a - b.a);
20
+
21
+ /** @type {function(any):number} */
22
+ const accessor = (d) => d.a;
23
+
24
+ expect([...kWayMerge(arrays, accessor)]).toEqual(sorted);
25
+ });