@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,69 @@
1
+ import PileupTransform from "./pileup";
2
+ import { processData } from "../flowTestUtils";
3
+
4
+ // TODO: Test for lane preferences
5
+
6
+ /**
7
+ * @typedef {import("../../spec/transform").PileupParams} PileupParams
8
+ */
9
+
10
+ const reads = [
11
+ [0, 4],
12
+ [1, 3],
13
+ [2, 6],
14
+ [4, 8],
15
+ [8, 10],
16
+ [11, 14],
17
+ [11, 13],
18
+ [11, 12],
19
+ [15, 18],
20
+ [16, 18],
21
+ [17, 18],
22
+ ].map((d) => ({
23
+ start: d[0],
24
+ end: d[1],
25
+ }));
26
+
27
+ const lanes = [0, 1, 2, 1, 0, 0, 1, 2, 0, 1, 2];
28
+
29
+ /** @type {PileupParams} */
30
+ const params = {
31
+ type: "pileup",
32
+ start: "start",
33
+ end: "end",
34
+ };
35
+
36
+ /**
37
+ * @param {PileupParams} params
38
+ * @param {any[]} data
39
+ */
40
+ function pileupTransform(params, data) {
41
+ const t = new PileupTransform(params);
42
+ t.initialize();
43
+ return processData(t, data);
44
+ }
45
+
46
+ test("Pileup transform produces correct pileup", () => {
47
+ const piledUp = lanes.map((d, i) => ({
48
+ ...reads[i],
49
+ lane: d,
50
+ }));
51
+
52
+ expect(pileupTransform(params, reads)).toEqual(piledUp);
53
+ });
54
+
55
+ test("Pileup transform produces correct pileup with consecutive contigs", () => {
56
+ // Simulate data having multiple chromosomes, sorted by [chrom, pos].
57
+ // Piling should handle suddenly decreasing start positions by freeing all
58
+ // reserved lanes.
59
+
60
+ const repeatedReads = [...reads, ...reads];
61
+ const repeatedLanes = [...lanes, ...lanes];
62
+
63
+ const piledUp = repeatedLanes.map((d, i) => ({
64
+ ...repeatedReads[i],
65
+ lane: d,
66
+ }));
67
+
68
+ expect(pileupTransform(params, repeatedReads)).toEqual(piledUp);
69
+ });
@@ -0,0 +1,41 @@
1
+ import { accessorName } from "vega-util";
2
+ import { field } from "../../utils/field";
3
+ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode";
4
+
5
+ /**
6
+ * @typedef {import("../../spec/transform").ProjectParams} ProjectParams
7
+ */
8
+ export default class ProjectTransform extends FlowNode {
9
+ get behavior() {
10
+ return BEHAVIOR_CLONES;
11
+ }
12
+
13
+ /**
14
+ *
15
+ * @param {ProjectParams} params
16
+ */
17
+ constructor(params) {
18
+ super();
19
+
20
+ if (params.as && params.as.length != params.fields.length) {
21
+ throw new Error(`"fields" and "as" have unequal lengths!`);
22
+ }
23
+
24
+ // TODO: "If unspecified, all fields will be copied using their existing names."
25
+
26
+ const accessors = params.fields.map((f) => field(f));
27
+ const as = params.as ? params.as : accessors.map(accessorName);
28
+
29
+ /**
30
+ * @param {any} datum
31
+ */
32
+ this.handle = (datum) => {
33
+ /** @type {Record<string, any>} */
34
+ const projected = {};
35
+ for (let i = 0; i < accessors.length; i++) {
36
+ projected[as[i]] = accessors[i](datum);
37
+ }
38
+ this._propagate(projected);
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,31 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import ProjectTransform from "./project";
3
+
4
+ test("Project", () => {
5
+ const data = [
6
+ {
7
+ foo: "FOO",
8
+ bar: "BAR",
9
+ baz: { a: "A" },
10
+ },
11
+ ];
12
+
13
+ /** @param {import("./project").ProjectParams} params */
14
+ const p = (params) => processData(new ProjectTransform(params), data);
15
+
16
+ expect(p({ type: "project", fields: ["bar"] })).toEqual([{ bar: "BAR" }]);
17
+
18
+ expect(
19
+ p({ type: "project", fields: ["bar", "foo"], as: ["xBar", "xFoo"] })
20
+ ).toEqual([{ xBar: "BAR", xFoo: "FOO" }]);
21
+
22
+ expect(p({ type: "project", fields: ["baz.a"] })).toEqual([
23
+ { "baz.a": "A" },
24
+ ]);
25
+
26
+ expect(p({ type: "project", fields: ["baz.a"], as: ["a"] })).toEqual([
27
+ { a: "A" },
28
+ ]);
29
+
30
+ expect(() => p({ type: "project", fields: ["bar"], as: [] })).toThrow();
31
+ });
@@ -0,0 +1,61 @@
1
+ import { isString } from "vega-util";
2
+ import { field } from "../../utils/field";
3
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
4
+
5
+ /**
6
+ * @typedef {import("../../spec/transform").RegexExtractParams} RegexExtractParams
7
+ */
8
+
9
+ export default class RegexExtractTransform extends FlowNode {
10
+ get behavior() {
11
+ return BEHAVIOR_MODIFIES;
12
+ }
13
+
14
+ /**
15
+ * @param {RegexExtractParams} params
16
+ */
17
+ constructor(params) {
18
+ super();
19
+
20
+ const re = new RegExp(params.regex);
21
+ const as = typeof params.as == "string" ? [params.as] : params.as;
22
+ const accessor = field(params.field);
23
+
24
+ /**
25
+ *
26
+ * @param {any} datum
27
+ */
28
+ this.handle = (datum) => {
29
+ const value = accessor(datum);
30
+ if (isString(value)) {
31
+ const m = value.match(re);
32
+
33
+ if (m) {
34
+ if (m.length - 1 != as.length) {
35
+ throw new Error(
36
+ 'The number of RegEx groups and the length of "as" do not match!'
37
+ );
38
+ }
39
+
40
+ for (let i = 0; i < as.length; i++) {
41
+ datum[as[i]] = m[i + 1];
42
+ }
43
+ } else if (params.skipInvalidInput) {
44
+ for (let i = 0; i < as.length; i++) {
45
+ datum[as[i]] = undefined;
46
+ }
47
+ } else {
48
+ throw new Error(
49
+ `"${value}" does not match the given regex: ${re.toString()}`
50
+ );
51
+ }
52
+ } else if (!params.skipInvalidInput) {
53
+ throw new Error(
54
+ `Trying to match a non-string field. Encountered type: ${typeof value}, field content: "${value}".`
55
+ );
56
+ }
57
+
58
+ this._propagate(datum);
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,66 @@
1
+ import { processData } from "../flowTestUtils";
2
+
3
+ import RegexExtractTransform from "./regexExtract";
4
+
5
+ /**
6
+ * @param {import("./regexExtract").RegexExtractParams} params
7
+ * @param {any[]} data
8
+ */
9
+ function transform(params, data) {
10
+ return processData(new RegexExtractTransform(params), data);
11
+ }
12
+
13
+ describe("RegexExtractTransform", () => {
14
+ const rows = [{ a: "12-34" }, { a: "23-45" }];
15
+
16
+ /** @type {import("./regexExtract").RegexExtractParams} */
17
+ const params = {
18
+ type: "regexExtract",
19
+ regex: "^(\\d+)-(\\d+)$",
20
+ field: "a",
21
+ as: ["b", "c"],
22
+ };
23
+
24
+ test("Valid config and input", () => {
25
+ expect(transform(params, rows)).toEqual([
26
+ { a: "12-34", b: "12", c: "34" },
27
+ { a: "23-45", b: "23", c: "45" },
28
+ ]);
29
+ });
30
+
31
+ test("Invalid config", () => {
32
+ /** @type {import("./regexExtract").RegexExtractParams} */
33
+ const config2 = {
34
+ type: "regexExtract",
35
+ regex: "^(\\d+)-(\\d+)$",
36
+ field: "a",
37
+ as: ["b", "c", "d"],
38
+ };
39
+
40
+ expect(() => transform(config2, rows)).toThrow();
41
+ });
42
+
43
+ test("Invalid data", () => {
44
+ const rows2 = [{ a: "12--34" }];
45
+
46
+ expect(() => transform(params, rows2)).toThrow();
47
+ });
48
+
49
+ test("Invalid, non-string data", () => {
50
+ const rows2 = [{ a: 123 }];
51
+
52
+ expect(() => transform(params, rows2)).toThrow();
53
+ });
54
+
55
+ test("Skip invalid or non-string data", () => {
56
+ const rows2 = [{ a: 123 }, { a: "xyzzy" }, { a: "12-34" }];
57
+
58
+ expect(transform({ ...params, skipInvalidInput: true }, rows2)).toEqual(
59
+ [
60
+ { a: 123, b: undefined, c: undefined },
61
+ { a: "xyzzy", b: undefined, c: undefined },
62
+ { a: "12-34", b: "12", c: "34" },
63
+ ]
64
+ );
65
+ });
66
+ });
@@ -0,0 +1,141 @@
1
+ import { asArray } from "../../utils/arrayUtils";
2
+ import FlowNode, { BEHAVIOR_CLONES, isFileBatch } from "../flowNode";
3
+
4
+ /**
5
+ * Folds fields using a regex
6
+ *
7
+ * See: https://vega.github.io/vega/docs/transforms/fold/
8
+ *
9
+ * @typedef {import("../../spec/transform").RegexFoldParams} RegexFoldParams
10
+ */
11
+ export default class RegexFoldTransform extends FlowNode {
12
+ get behavior() {
13
+ return BEHAVIOR_CLONES;
14
+ }
15
+
16
+ /**
17
+ * @param {RegexFoldParams} params
18
+ */
19
+ constructor(params) {
20
+ super();
21
+
22
+ const columnRegex = asArray(params.columnRegex).map(
23
+ (re) => new RegExp(re)
24
+ );
25
+ // TODO: Consider using named groups
26
+ const as = asArray(params.asValue);
27
+
28
+ if (columnRegex.length != as.length) {
29
+ throw new Error('Lengths of "columnRegex" and "as" are not equal!');
30
+ }
31
+
32
+ const skipRegex = params.skipRegex
33
+ ? new RegExp(params.skipRegex)
34
+ : undefined;
35
+
36
+ const sampleKey = params.asKey || "sample";
37
+
38
+ /** @type {[string, string[]][]} */
39
+ let sampleAttrs;
40
+
41
+ /** @type {string[]} */
42
+ let includedColumns;
43
+
44
+ /** @type {(datum: any, sampleId: string) => Record<string, any>} */
45
+ let create;
46
+
47
+ /**
48
+ * @param {any} datum
49
+ */
50
+ const detectColumns = (datum) => {
51
+ const colNames = Object.keys(datum);
52
+
53
+ /** @type {Map<string, string[]>} */
54
+ const sampleColMap = new Map();
55
+
56
+ for (const [i, re] of columnRegex.entries()) {
57
+ for (const colName of colNames) {
58
+ const sampleId = re.exec(colName)?.[1];
59
+ if (sampleId !== undefined) {
60
+ let attrs = sampleColMap.get(sampleId);
61
+ if (!attrs) {
62
+ attrs = [];
63
+ sampleColMap.set(sampleId, attrs);
64
+ }
65
+
66
+ attrs[i] = colName;
67
+ }
68
+ }
69
+ }
70
+
71
+ sampleAttrs = [...sampleColMap.entries()];
72
+
73
+ includedColumns = colNames.filter(
74
+ (colName) =>
75
+ !columnRegex.some((re) => re.test(colName)) &&
76
+ !(skipRegex && skipRegex.test(colName))
77
+ );
78
+
79
+ const props = [
80
+ ...includedColumns.map(
81
+ (prop) =>
82
+ JSON.stringify(prop) +
83
+ ": datum[" +
84
+ JSON.stringify(prop) +
85
+ "]"
86
+ ),
87
+ JSON.stringify(sampleKey) + ": sampleId",
88
+ ...as.map((a) => JSON.stringify(a) + ": null"),
89
+ ];
90
+
91
+ // eslint-disable-next-line no-new-func
92
+ create = /** @type {any} */ (
93
+ new Function(
94
+ "datum",
95
+ "sampleId",
96
+ "return {\n" + props.join(",\n") + "\n};"
97
+ )
98
+ );
99
+ };
100
+
101
+ /**
102
+ * @param {any} datum
103
+ */
104
+ const doRegexFold = (datum) => {
105
+ if (!sampleAttrs) {
106
+ detectColumns(datum);
107
+ }
108
+
109
+ for (const [sampleId, attrs] of sampleAttrs) {
110
+ const tidyRow = create(datum, sampleId);
111
+ for (let i = 0; i < attrs.length; i++) {
112
+ tidyRow[as[i]] = datum[attrs[i]];
113
+ }
114
+
115
+ this._propagate(tidyRow);
116
+ }
117
+ };
118
+
119
+ /**
120
+ * @param {any} datum
121
+ */
122
+ const detectAndHandle = (datum) => {
123
+ detectColumns(datum);
124
+ doRegexFold(datum);
125
+ this.handle = doRegexFold;
126
+ };
127
+
128
+ this.handle = detectAndHandle;
129
+
130
+ /**
131
+ *
132
+ * @param {import("../flowNode").FlowBatch} flowBatch
133
+ */
134
+ this.beginBatch = (flowBatch) => {
135
+ if (isFileBatch(flowBatch)) {
136
+ this.handle = detectAndHandle;
137
+ }
138
+ super.beginBatch(flowBatch);
139
+ };
140
+ }
141
+ }
@@ -0,0 +1,159 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import RegexFoldTransform from "./regexFold";
3
+
4
+ describe("RegexFold", () => {
5
+ test("Transform single variable", () => {
6
+ const sampleData = [
7
+ {
8
+ row: 1,
9
+ sample1_a: "r1s1a",
10
+ sample2_a: "r1s2a",
11
+ },
12
+ {
13
+ row: 2,
14
+ sample1_a: "r2s1a",
15
+ sample2_a: "r2s2a",
16
+ },
17
+ ];
18
+
19
+ /** @type { import("../../spec/transform").RegexFoldParams } */
20
+ const singleGatherConfig = {
21
+ type: "regexFold",
22
+ columnRegex: "^(.*)_a$",
23
+ asValue: "a",
24
+ };
25
+
26
+ const result = processData(
27
+ new RegexFoldTransform(singleGatherConfig),
28
+ sampleData
29
+ );
30
+
31
+ expect(result).toEqual([
32
+ {
33
+ row: 1,
34
+ sample: "sample1",
35
+ a: "r1s1a",
36
+ },
37
+ {
38
+ row: 1,
39
+ sample: "sample2",
40
+ a: "r1s2a",
41
+ },
42
+ {
43
+ row: 2,
44
+ sample: "sample1",
45
+ a: "r2s1a",
46
+ },
47
+ {
48
+ row: 2,
49
+ sample: "sample2",
50
+ a: "r2s2a",
51
+ },
52
+ ]);
53
+ });
54
+
55
+ test("Transform single variable and skip specific columns", () => {
56
+ const sampleData = [
57
+ {
58
+ row: 1,
59
+ sample1_a: "r1s1a",
60
+ sample2_a: "r1s2a",
61
+ },
62
+ {
63
+ row: 2,
64
+ sample1_a: "r2s1a",
65
+ sample2_a: "r2s2a",
66
+ },
67
+ ];
68
+
69
+ /** @type { import("../../spec/transform").RegexFoldParams } */
70
+ const singleGatherConfig = {
71
+ type: "regexFold",
72
+ columnRegex: "^(.*)_a$",
73
+ asValue: "a",
74
+ skipRegex: "^row$",
75
+ };
76
+
77
+ const result = processData(
78
+ new RegexFoldTransform(singleGatherConfig),
79
+ sampleData
80
+ );
81
+
82
+ expect(result).toEqual([
83
+ {
84
+ sample: "sample1",
85
+ a: "r1s1a",
86
+ },
87
+ {
88
+ sample: "sample2",
89
+ a: "r1s2a",
90
+ },
91
+ {
92
+ sample: "sample1",
93
+ a: "r2s1a",
94
+ },
95
+ {
96
+ sample: "sample2",
97
+ a: "r2s2a",
98
+ },
99
+ ]);
100
+ });
101
+
102
+ test("Transform multiple variables", () => {
103
+ const sampleData = [
104
+ {
105
+ row: 1,
106
+ sample1_a: "r1s1a",
107
+ sample2_a: "r1s2a",
108
+ sample1_b: "r1s1b",
109
+ sample2_b: "r1s2b",
110
+ },
111
+ {
112
+ row: 2,
113
+ sample1_a: "r2s1a",
114
+ sample2_a: "r2s2a",
115
+ sample1_b: "r2s1b",
116
+ sample2_b: "r2s2b",
117
+ },
118
+ ];
119
+
120
+ /** @type { import("../../spec/transform").RegexFoldParams } */
121
+ const singleGatherConfig = {
122
+ type: "regexFold",
123
+ columnRegex: ["^(.*)_a$", "^(.*)_b$"],
124
+ asValue: ["a", "b"],
125
+ };
126
+
127
+ const result = processData(
128
+ new RegexFoldTransform(singleGatherConfig),
129
+ sampleData
130
+ );
131
+
132
+ expect(result).toEqual([
133
+ {
134
+ row: 1,
135
+ sample: "sample1",
136
+ a: "r1s1a",
137
+ b: "r1s1b",
138
+ },
139
+ {
140
+ row: 1,
141
+ sample: "sample2",
142
+ a: "r1s2a",
143
+ b: "r1s2b",
144
+ },
145
+ {
146
+ row: 2,
147
+ sample: "sample1",
148
+ a: "r2s1a",
149
+ b: "r2s1b",
150
+ },
151
+ {
152
+ row: 2,
153
+ sample: "sample2",
154
+ a: "r2s2a",
155
+ b: "r2s2b",
156
+ },
157
+ ]);
158
+ });
159
+ });
@@ -0,0 +1,101 @@
1
+ import FlowNode from "../flowNode";
2
+
3
+ /**
4
+ * A reservoir sampler, based on: https://www.wikiwand.com/en/Reservoir_sampling
5
+ *
6
+ * @typedef {import("../../spec/transform").SampleParams} SampleParams
7
+ */
8
+ export default class SampleTransform extends FlowNode {
9
+ /**
10
+ *
11
+ * @param {SampleParams} params
12
+ */
13
+ constructor(params) {
14
+ super();
15
+
16
+ this.k = params.size || 500;
17
+ this.reset();
18
+ }
19
+
20
+ reset() {
21
+ super.reset();
22
+
23
+ /** @type {any[]} */
24
+ this.reservoir = [];
25
+ /** @type {number} */
26
+ this.W = undefined;
27
+ this.ingester = this._initialIngester;
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {any} item
33
+ */
34
+ _initialIngester(item) {
35
+ this.reservoir.push(item);
36
+
37
+ if (this.reservoir.length == this.k) {
38
+ this.W = Math.exp(Math.log(Math.random()) / this.k);
39
+ this.i = this.k;
40
+ this.next = this.i;
41
+ this.ingester = this._finalIngester;
42
+ this._setNextStop();
43
+ }
44
+ }
45
+
46
+ /**
47
+ *
48
+ * @param {any} item
49
+ */
50
+ _finalIngester(item) {
51
+ if (++this.i == this.next) {
52
+ this.reservoir[Math.floor(Math.random() * this.k)] = item;
53
+ this.W *= Math.exp(Math.log(Math.random()) / this.k);
54
+ this._setNextStop();
55
+ }
56
+ }
57
+
58
+ _setNextStop() {
59
+ this.next +=
60
+ Math.floor(Math.log(Math.random()) / Math.log(1 - this.W)) + 1;
61
+ }
62
+
63
+ /**
64
+ *
65
+ * @param {import("../flowNode").Datum} datum
66
+ */
67
+ handle(datum) {
68
+ this.ingester(datum);
69
+ }
70
+
71
+ complete() {
72
+ for (const datum of this.reservoir) {
73
+ this._propagate(datum);
74
+ }
75
+
76
+ super.complete();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * A convenience function that uses SampleTransform to sample an iterable.
82
+ *
83
+ * @param {number} n
84
+ * @param {Iterable<T>} iterable
85
+ * @param {function(T):R} accessor
86
+ * @returns {R[]}
87
+ * @template T
88
+ * @template R
89
+ */
90
+ export function sampleIterable(n, iterable, accessor) {
91
+ const sampler = new SampleTransform({
92
+ type: "sample",
93
+ size: n,
94
+ });
95
+ for (const d of iterable) {
96
+ sampler.handle(accessor(d));
97
+ }
98
+ sampler.complete();
99
+
100
+ return sampler.reservoir;
101
+ }