@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,369 @@
1
+ import { isDiscrete } from "vega-scale";
2
+ import createIndexer from "../utils/indexer";
3
+
4
+ /**
5
+ * @typedef {Object} EncoderMetadata
6
+ * @prop {boolean} constant True if the accessor returns the same value for all objects
7
+ * @prop {boolean} constantValue True the encoder returns a "value" without a scale
8
+ * @prop {function} invert
9
+ * @prop {VegaScale} [scale]
10
+ * @prop {import("./accessor").Accessor} accessor
11
+ * @prop {function(any):number} [indexer] Converts ordinal values to index numbers
12
+ * @prop {import("../view/viewUtils").ChannelDef} channelDef
13
+ * @prop {function(function):void} applyMetadata Copies metadata to the target function
14
+ *
15
+ * @typedef {(function(object):(string|number)) & EncoderMetadata} Encoder
16
+ * @typedef {(function(object):number) & EncoderMetadata} NumberEncoder
17
+ *
18
+ * @typedef {object} ScaleMetadata
19
+ * @prop {string} type Scale type
20
+ * @prop {boolean} fp64 Whether to use emulated 64 bit floating point in WebGL
21
+ *
22
+ * @typedef {(
23
+ import("d3-scale").ScaleContinuousNumeric<any, any> |
24
+ import("d3-scale").ScaleLinear<any, any> |
25
+ import("d3-scale").ScalePower<any, any> |
26
+ import("d3-scale").ScaleLogarithmic<any, any> |
27
+ import("d3-scale").ScaleSymLog<any, any> |
28
+ import("d3-scale").ScaleIdentity |
29
+ import("d3-scale").ScaleTime<any, any> |
30
+ import("d3-scale").ScaleSequential<any> |
31
+ import("d3-scale").ScaleDiverging<any> |
32
+ import("d3-scale").ScaleQuantize<any> |
33
+ import("d3-scale").ScaleQuantile<any> |
34
+ import("d3-scale").ScaleThreshold<any, any> |
35
+ import("d3-scale").ScaleOrdinal<any, any> |
36
+ import("d3-scale").ScaleBand<any> |
37
+ import("d3-scale").ScalePoint<any>
38
+ )} D3Scale
39
+ *
40
+ * @typedef {D3Scale & ScaleMetadata} VegaScale
41
+ *
42
+ * @typedef {import("../spec/channel").Channel} Channel
43
+ */
44
+
45
+ /**
46
+ * Creates an object that contains encoders for every channel of a mark
47
+ *
48
+ * TODO: This should actually receive the mark as parameter
49
+ *
50
+ * TODO: This method should have a test. But how to mock Mark...
51
+ *
52
+ * @param {import("../marks/mark").default} mark
53
+ * @param {import("../spec/channel").Encoding} [encoding] Taken from the mark if not provided
54
+ * @returns {Partial<Record<Channel, Encoder>>}
55
+ */
56
+ export default function createEncoders(mark, encoding) {
57
+ /** @type {Partial<Record<Channel, Encoder>>} */
58
+ const encoders = {};
59
+
60
+ if (!encoding) {
61
+ encoding = mark.encoding;
62
+ }
63
+
64
+ for (const [channel, channelDef] of Object.entries(encoding)) {
65
+ if (!channelDef) {
66
+ continue;
67
+ }
68
+
69
+ const resolution = mark.unitView.getScaleResolution(
70
+ (isChannelDefWithScale(channelDef) &&
71
+ channelDef.resolutionChannel) ||
72
+ channel
73
+ );
74
+
75
+ encoders[channel] = createEncoder(
76
+ encoding[channel],
77
+ resolution?.getScale(),
78
+ mark.unitView.getAccessor(channel),
79
+ channel
80
+ );
81
+ }
82
+
83
+ return encoders;
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param {import("../view/viewUtils").ChannelDef} channelDef
89
+ * @param {any} scale
90
+ * @param {import("./accessor").Accessor} accessor
91
+ * @param {Channel} channel
92
+ * @returns {Encoder}
93
+ */
94
+ export function createEncoder(channelDef, scale, accessor, channel) {
95
+ /** @type {Encoder} */
96
+ let encoder;
97
+
98
+ if (isValueDef(channelDef)) {
99
+ encoder = /** @type {Encoder} */ ((datum) => channelDef.value);
100
+ encoder.constant = true;
101
+ encoder.constantValue = true;
102
+ encoder.accessor = undefined;
103
+ } else if (accessor) {
104
+ if (channel == "text") {
105
+ // TODO: Define somewhere channels that don't use a scale
106
+ encoder = /** @type {Encoder} */ ((datum) => undefined);
107
+ encoder.accessor = accessor;
108
+ encoder.constant = accessor.constant;
109
+ } else {
110
+ if (!scale) {
111
+ throw new Error(
112
+ `Missing scale! "${channel}": ${JSON.stringify(channelDef)}`
113
+ );
114
+ }
115
+
116
+ encoder = /** @type {Encoder} */ (
117
+ (datum) => scale(accessor(datum))
118
+ );
119
+
120
+ if (isDiscrete(scale.type)) {
121
+ // TODO: pass the found values back to the scale/resolution
122
+ const indexer = createIndexer();
123
+ indexer.addAll(scale.domain());
124
+ encoder.indexer = (d) => indexer(accessor(d));
125
+ }
126
+
127
+ encoder.constant = accessor.constant;
128
+ encoder.accessor = accessor;
129
+ encoder.scale = scale;
130
+ }
131
+ } else {
132
+ throw new Error(
133
+ `Missing value or accessor (field, expr, datum) on channel "${channel}": ${JSON.stringify(
134
+ channelDef
135
+ )}`
136
+ );
137
+ }
138
+
139
+ // TODO: Modifier should be inverted too
140
+ encoder.invert = scale
141
+ ? (value) => scale.invert(value)
142
+ : (value) => {
143
+ throw new Error(
144
+ "No scale available, cannot invert: " +
145
+ JSON.stringify(channelDef)
146
+ );
147
+ };
148
+
149
+ // Just to provide a convenient access to the config
150
+ encoder.channelDef = channelDef;
151
+
152
+ /** @param {Encoder} target */
153
+ encoder.applyMetadata = (target) => {
154
+ for (const prop in encoder) {
155
+ if (prop in encoder) {
156
+ target[prop] = encoder[prop];
157
+ }
158
+ }
159
+ return target;
160
+ };
161
+
162
+ return encoder;
163
+ }
164
+
165
+ /**
166
+ * TODO: Move to a more generic place
167
+ *
168
+ * @param {import("../spec/channel").ChannelDef} channelDef
169
+ * @returns {channelDef is import("../spec/channel").ValueDef}
170
+ */
171
+ export function isValueDef(channelDef) {
172
+ return channelDef && "value" in channelDef;
173
+ }
174
+
175
+ /**
176
+ * @param {import("../spec/channel").ChannelDef} channelDef
177
+ * @returns {channelDef is import("../spec/channel").FieldDef}
178
+ */
179
+ export function isFieldDef(channelDef) {
180
+ return channelDef && "field" in channelDef;
181
+ }
182
+
183
+ /**
184
+ * @param {import("../spec/channel").ChannelDef} channelDef
185
+ * @returns {channelDef is import("../spec/channel").DatumDef}
186
+ */
187
+ export function isDatumDef(channelDef) {
188
+ return channelDef && "datum" in channelDef;
189
+ }
190
+
191
+ /**
192
+ * @param {import("../spec/channel").ChannelDef} channelDef
193
+ * @returns {channelDef is import("../spec/channel").ChannelDefWithScale}
194
+ */
195
+ export function isChannelDefWithScale(channelDef) {
196
+ return (
197
+ isFieldDef(channelDef) ||
198
+ isDatumDef(channelDef) ||
199
+ isExprDef(channelDef) ||
200
+ isChromPosDef(channelDef)
201
+ );
202
+ }
203
+
204
+ /**
205
+ * @param {import("../view/unitView").default} view
206
+ * @param {Channel} channel
207
+ */
208
+ export function getChannelDefWithScale(view, channel) {
209
+ const channelDef = view.mark.encoding[channel];
210
+ if (isChannelDefWithScale(channelDef)) {
211
+ return channelDef;
212
+ } else {
213
+ throw new Error("Not a channel def with scale!");
214
+ }
215
+ }
216
+
217
+ /**
218
+ * @param {import("../spec/channel").ChannelDef} channelDef
219
+ * @returns {channelDef is import("../spec/channel").ChromPosDef}
220
+ */
221
+ export function isChromPosDef(channelDef) {
222
+ return channelDef && "chrom" in channelDef;
223
+ }
224
+
225
+ /**
226
+ * @param {import("../spec/channel").ChannelDef} channelDef
227
+ * @returns {channelDef is import("../spec/channel").ExprDef}
228
+ */
229
+ export function isExprDef(channelDef) {
230
+ return channelDef && "expr" in channelDef;
231
+ }
232
+
233
+ /**
234
+ * @type {Channel[]}
235
+ */
236
+ export const primaryPositionalChannels = ["x", "y"];
237
+
238
+ /**
239
+ * Map primary channels to secondarys
240
+ *
241
+ * @type {Partial<Record<Channel, Channel>>}
242
+ */
243
+ export const secondaryChannels = {
244
+ x: "x2",
245
+ y: "y2",
246
+ size: "size2",
247
+ color: "color2",
248
+ };
249
+
250
+ /**
251
+ * Map secondary channels to primaries
252
+ *
253
+ * @type {Record<Channel, Channel>}
254
+ */
255
+ export const primaryChannels = Object.fromEntries(
256
+ Object.entries(secondaryChannels).map((entry) => [entry[1], entry[0]])
257
+ );
258
+
259
+ /**
260
+ *
261
+ * @param {string} channel
262
+ */
263
+ export function isSecondaryChannel(channel) {
264
+ return channel in primaryChannels;
265
+ }
266
+
267
+ /**
268
+ * Return the matching secondary channel or throws if one does not exist.
269
+ *
270
+ * @param {Channel} primaryChannel
271
+ */
272
+ export function getSecondaryChannel(primaryChannel) {
273
+ const secondary = secondaryChannels[primaryChannel];
274
+ if (secondary) {
275
+ return secondary;
276
+ } else {
277
+ throw new Error(`${primaryChannel} has no secondary channel!`);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Finds the primary channel for the provided channel, which may be
283
+ * the primary or secondary.
284
+ *
285
+ * @param {Channel} maybeSecondary
286
+ */
287
+ export function getPrimaryChannel(maybeSecondary) {
288
+ return primaryChannels[maybeSecondary] || maybeSecondary;
289
+ }
290
+
291
+ /**
292
+ * Returns an array that contains the given channel and its secondary channel if one exists.
293
+ *
294
+ * @param {Channel} channel
295
+ */
296
+ export function getChannelWithSecondarys(channel) {
297
+ return secondaryChannels[channel]
298
+ ? [channel, secondaryChannels[channel]]
299
+ : [channel];
300
+ }
301
+
302
+ /**
303
+ * @param {Channel} channel
304
+ */
305
+ export function isPositionalChannel(channel) {
306
+ return primaryPositionalChannels.includes(getPrimaryChannel(channel));
307
+ }
308
+
309
+ /**
310
+ * @param {Channel} channel
311
+ */
312
+ export function isColorChannel(channel) {
313
+ return ["color", "fill", "stroke"].includes(getPrimaryChannel(channel));
314
+ }
315
+
316
+ /**
317
+ * Returns true if the channel has a discrete range.
318
+ *
319
+ * @param {Channel} channel
320
+ */
321
+ export function isDiscreteChannel(channel) {
322
+ return ["shape", "squeeze"].includes(channel);
323
+ }
324
+
325
+ /**
326
+ * Returns valid discrete values for a discrete channel.
327
+ *
328
+ * @param {Channel} channel
329
+ * @returns {any[]}
330
+ */
331
+ export function getDiscreteRange(channel) {
332
+ // TODO: This is not easily extendable. Figure out a more dynamic approach.
333
+ switch (channel) {
334
+ case "shape":
335
+ return [
336
+ "circle",
337
+ "square",
338
+ "triangle-up",
339
+ "cross",
340
+ "diamond",
341
+ "triangle-down",
342
+ "triangle-right",
343
+ "triangle-left",
344
+ ];
345
+ default:
346
+ }
347
+ }
348
+
349
+ /**
350
+ * @param {Channel} channel
351
+ * @returns {function(any):number}
352
+ */
353
+ export function getDiscreteRangeMapper(channel) {
354
+ if (!isDiscreteChannel(channel)) {
355
+ throw new Error("Not a discrete channel: " + channel);
356
+ }
357
+
358
+ const valueMap = new Map(
359
+ getDiscreteRange(channel).map((value, i) => [value, i])
360
+ );
361
+
362
+ return (value) => {
363
+ const mapped = valueMap.get(value);
364
+ if (mapped !== undefined) {
365
+ return mapped;
366
+ }
367
+ throw new Error(`Invalid value for "${channel}" channel: ${value}`);
368
+ };
369
+ }
@@ -0,0 +1,97 @@
1
+ import AccessorFactory from "./accessor";
2
+ import { scale as vegaScale } from "vega-scale";
3
+
4
+ import { createEncoder } from "./encoder";
5
+
6
+ describe("Encoder", () => {
7
+ /** @type {Record<string, import("../view/viewUtils").ChannelDef>} */
8
+ const encodingSpecs = {
9
+ x: { value: 0 },
10
+ y: { field: "a" },
11
+ z: { datum: 2 },
12
+ size: { value: 5 },
13
+ };
14
+
15
+ const scaleLinear = vegaScale("linear");
16
+
17
+ /** @type {Record<string, import("./encoder").VegaScale>} */
18
+ const scales = {
19
+ y: scaleLinear().domain([0, 10]),
20
+ z: scaleLinear().domain([0, 20]),
21
+ };
22
+
23
+ const accessorFactory = new AccessorFactory();
24
+
25
+ /** @param {Record<string, import("../view/viewUtils").ChannelDef>} encoding */
26
+ function createEncoders(encoding) {
27
+ /** @type {Record<string, import("./encoder").Encoder>} */
28
+ const encoders = {};
29
+ for (const [channel, channelDef] of Object.entries(encoding)) {
30
+ encoders[channel] = createEncoder(
31
+ channelDef,
32
+ scales[channel],
33
+ accessorFactory.createAccessor(encodingSpecs[channel]),
34
+ channel
35
+ );
36
+ }
37
+ return encoders;
38
+ }
39
+
40
+ const datum = {
41
+ a: 5,
42
+ b: 6,
43
+ c: "Pink Floyd",
44
+ };
45
+
46
+ test("Throws on a broken spec", () =>
47
+ expect(() => createEncoders({ x: {} })).toThrow());
48
+
49
+ const encoders = createEncoders(encodingSpecs);
50
+
51
+ test("The encoder object contains all channels", () =>
52
+ expect(
53
+ ["x", "y", "z", "size"].every(
54
+ (channel) => typeof encoders[channel] === "function"
55
+ )
56
+ ).toBeTruthy());
57
+
58
+ test("Returns a value", () => expect(encoders.x(datum)).toEqual(0));
59
+
60
+ test("Encodes and returns a constant using a scale", () =>
61
+ expect(encoders.z(datum)).toBeCloseTo(0.1));
62
+
63
+ test("Accesses a field and uses a scale", () =>
64
+ expect(encoders.y(datum)).toBeCloseTo(0.5));
65
+
66
+ /*
67
+ test("Accesses a field on a secondary channel and uses the scale from the primary", () =>
68
+ expect(encoders.y2(datum)).toBeCloseTo(0.6));
69
+ */
70
+
71
+ test("Constant encoder is annotated", () => {
72
+ expect(encoders.y.constant).toBeFalsy();
73
+ expect(encoders.z.constant).toBeTruthy();
74
+ expect(encoders.size.constant).toBeTruthy();
75
+ });
76
+
77
+ test("Constant value encoder is annotated", () => {
78
+ expect(encoders.y.constantValue).toBeFalsy();
79
+ expect(encoders.z.constantValue).toBeFalsy();
80
+ expect(encoders.size.constantValue).toBeTruthy();
81
+ });
82
+
83
+ test("Inverts a value", () => {
84
+ expect(encoders.y.invert(0.5)).toBeCloseTo(5);
85
+ expect(encoders.z.invert(0.5)).toBeCloseTo(10);
86
+ // A value, no scale, can't invert
87
+ expect(() => encoders.size.invert(123)).toThrow();
88
+ });
89
+
90
+ test("Accessors are provided", () => {
91
+ expect(encoders.y.accessor).toBeDefined();
92
+ expect(encoders.z.accessor).toBeDefined();
93
+ expect(encoders.x.accessor).toBeUndefined();
94
+ });
95
+
96
+ // TODO: Test indexer
97
+ });