@genome-spy/core 0.74.0 → 0.75.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 (79) hide show
  1. package/dist/bundle/index.es.js +4660 -4468
  2. package/dist/bundle/index.js +81 -81
  3. package/dist/schema.json +220 -12
  4. package/dist/src/data/sources/dataUtils.d.ts +25 -0
  5. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  6. package/dist/src/data/sources/dataUtils.js +23 -0
  7. package/dist/src/data/sources/inlineSource.js +2 -2
  8. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  9. package/dist/src/data/sources/urlSource.js +8 -3
  10. package/dist/src/encoder/encoder.d.ts +2 -2
  11. package/dist/src/encoder/encoder.d.ts.map +1 -1
  12. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  13. package/dist/src/genome/scaleLocus.js +8 -3
  14. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
  15. package/dist/src/genomeSpy/interactionController.js +91 -51
  16. package/dist/src/gl/dataToVertices.d.ts +12 -14
  17. package/dist/src/gl/dataToVertices.d.ts.map +1 -1
  18. package/dist/src/gl/dataToVertices.js +116 -95
  19. package/dist/src/gl/glslScaleGenerator.d.ts +3 -0
  20. package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
  21. package/dist/src/gl/glslScaleGenerator.js +10 -8
  22. package/dist/src/gl/vertexRangeIndex.d.ts +23 -0
  23. package/dist/src/gl/vertexRangeIndex.d.ts.map +1 -0
  24. package/dist/src/gl/vertexRangeIndex.js +150 -0
  25. package/dist/src/marks/mark.d.ts +1 -1
  26. package/dist/src/paramRuntime/expressionCompiler.d.ts +2 -1
  27. package/dist/src/paramRuntime/expressionCompiler.d.ts.map +1 -1
  28. package/dist/src/paramRuntime/expressionCompiler.js +3 -2
  29. package/dist/src/paramRuntime/expressionRef.d.ts +4 -1
  30. package/dist/src/paramRuntime/expressionRef.d.ts.map +1 -1
  31. package/dist/src/paramRuntime/expressionRef.js +10 -3
  32. package/dist/src/paramRuntime/graphRuntime.d.ts.map +1 -1
  33. package/dist/src/paramRuntime/graphRuntime.js +15 -6
  34. package/dist/src/paramRuntime/paramRuntime.d.ts +8 -2
  35. package/dist/src/paramRuntime/paramRuntime.d.ts.map +1 -1
  36. package/dist/src/paramRuntime/paramRuntime.js +10 -5
  37. package/dist/src/paramRuntime/types.d.ts +1 -0
  38. package/dist/src/paramRuntime/types.d.ts.map +1 -1
  39. package/dist/src/paramRuntime/types.js +1 -0
  40. package/dist/src/paramRuntime/viewParamRuntime.d.ts +5 -4
  41. package/dist/src/paramRuntime/viewParamRuntime.d.ts.map +1 -1
  42. package/dist/src/paramRuntime/viewParamRuntime.js +17 -6
  43. package/dist/src/scale/scale.d.ts.map +1 -1
  44. package/dist/src/scale/scale.js +1 -0
  45. package/dist/src/scales/domainPlanner.d.ts +57 -11
  46. package/dist/src/scales/domainPlanner.d.ts.map +1 -1
  47. package/dist/src/scales/domainPlanner.js +182 -83
  48. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  49. package/dist/src/scales/scaleInstanceManager.js +7 -2
  50. package/dist/src/scales/scalePropsResolver.d.ts +3 -3
  51. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -1
  52. package/dist/src/scales/scalePropsResolver.js +28 -5
  53. package/dist/src/scales/scaleResolution.d.ts +12 -1
  54. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  55. package/dist/src/scales/scaleResolution.js +171 -18
  56. package/dist/src/screenshotExport.d.ts +23 -0
  57. package/dist/src/screenshotExport.d.ts.map +1 -0
  58. package/dist/src/screenshotExport.js +44 -0
  59. package/dist/src/screenshotHarness.d.ts.map +1 -1
  60. package/dist/src/screenshotHarness.js +26 -24
  61. package/dist/src/spec/axis.d.ts +2 -2
  62. package/dist/src/spec/channel.d.ts +4 -4
  63. package/dist/src/spec/data.d.ts +12 -0
  64. package/dist/src/spec/scale.d.ts +13 -1
  65. package/dist/src/utils/expression.d.ts +16 -8
  66. package/dist/src/utils/expression.d.ts.map +1 -1
  67. package/dist/src/utils/expression.js +291 -11
  68. package/dist/src/view/flowBuilder.d.ts +1 -1
  69. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  70. package/dist/src/view/flowBuilder.js +11 -7
  71. package/dist/src/view/resolutionPlanner.d.ts +9 -0
  72. package/dist/src/view/resolutionPlanner.d.ts.map +1 -0
  73. package/dist/src/view/resolutionPlanner.js +302 -0
  74. package/dist/src/view/unitView.d.ts +1 -1
  75. package/dist/src/view/unitView.d.ts.map +1 -1
  76. package/dist/src/view/unitView.js +5 -152
  77. package/dist/src/view/view.d.ts.map +1 -1
  78. package/dist/src/view/view.js +2 -1
  79. package/package.json +2 -2
@@ -8,6 +8,7 @@ import {
8
8
  isString,
9
9
  ascending,
10
10
  lerp,
11
+ span,
11
12
  } from "vega-util";
12
13
  import { format as d3format } from "d3-format";
13
14
  import smoothstep from "./smoothstep.js";
@@ -100,41 +101,314 @@ const functionContext = {
100
101
  sort(/** @type {Array<any>} */ seq) {
101
102
  return array(seq).slice().sort(ascending);
102
103
  },
104
+ /**
105
+ * Returns the midpoint of an ordered extent, such as [min, max].
106
+ * The helper uses the first and last elements and does not sort.
107
+ */
108
+ center(/** @type {Array<any>} */ seq) {
109
+ const values = array(seq);
110
+ return (values[0] + values[values.length - 1]) / 2;
111
+ },
112
+ span(/** @type {Array<any>} */ seq) {
113
+ return span(seq);
114
+ },
103
115
  smoothstep,
104
116
  };
105
117
 
106
118
  /**
107
119
  * @param {typeof codegenExpression} codegen
120
+ * @param {ScaleHelperCompileContext} context
108
121
  */
109
- function buildFunctions(codegen) {
110
- const fn = functions(codegen);
122
+ function buildFunctions(codegen, context) {
123
+ const fn = /** @type {any} */ (functions(codegen));
111
124
  for (const name in functionContext) {
112
125
  fn[name] = `this.${name}`;
113
126
  }
127
+
128
+ // Replace the public helper names with custom codegen hooks so we can bind
129
+ // a scale resolution once during compilation instead of looking it up for
130
+ // every datum.
131
+ for (const kind of /** @type {const} */ ([
132
+ "scale",
133
+ "invert",
134
+ "domain",
135
+ "range",
136
+ ])) {
137
+ fn[kind] = (
138
+ /** @type {any[]} */
139
+ args
140
+ ) => buildScaleHelperCall(codegen, context, kind, args);
141
+ }
142
+
114
143
  return fn;
115
144
  }
116
145
 
117
- const cg = codegenExpression({
118
- forbidden: [],
119
- allowed: ["datum", "undefined"],
120
- globalvar: "globalObject",
121
- fieldvar: "datum",
122
- functions: buildFunctions,
123
- });
124
-
125
146
  /**
126
147
  * @typedef { object } ExpressionProps
127
148
  * @prop { string[] } fields
128
149
  * @prop { string[] } globals
129
150
  * @prop { string } code
151
+ * @prop { import("../paramRuntime/types.js").ParamRef<any>[] } [scaleDependencies]
130
152
  *
131
153
  * @typedef { ((datum?: import("../data/flowNode.js").Datum) => any) & ExpressionProps } ExpressionFunction
132
154
  *
155
+ * @typedef {object} ExpressionCompileContext
156
+ * @prop {(channel: string) => import("../scales/scaleResolution.js").default | undefined} [resolveScaleResolution]
157
+ *
158
+ * @typedef {ExpressionCompileContext & {
159
+ * globalvar: string,
160
+ * globalObject: Record<string, any>,
161
+ * getScaleHelper: (kind: "scale" | "invert" | "domain" | "range", channel: string, resolution: import("../scales/scaleResolution.js").default) => { codeName: string, dependency: import("../paramRuntime/types.js").ParamRef<any> }
162
+ * }} ScaleHelperCompileContext
163
+ */
164
+
165
+ /**
166
+ * @param {typeof codegenExpression} codegen
167
+ * @param {ScaleHelperCompileContext} context
168
+ * @param {"scale" | "invert" | "domain" | "range"} kind
169
+ * @param {any[]} args
170
+ * @returns {string}
171
+ */
172
+ function buildScaleHelperCall(codegen, context, kind, args) {
173
+ if (args.length === 0) {
174
+ throw new Error(
175
+ `Scale helper "${kind}" requires a literal channel name.`
176
+ );
177
+ }
178
+
179
+ if ((kind === "scale" || kind === "invert") && args.length < 2) {
180
+ throw new Error(
181
+ `Scale helper "${kind}" requires a channel name and a value.`
182
+ );
183
+ }
184
+
185
+ const channel = getLiteralString(args[0]);
186
+ if (!channel) {
187
+ throw new Error(
188
+ `Scale helper "${kind}" requires a literal channel name.`
189
+ );
190
+ }
191
+
192
+ const resolution = context.resolveScaleResolution?.(channel);
193
+ if (!resolution) {
194
+ throw new Error(
195
+ `Unknown scale channel "${channel}" in expression helper "${kind}".`
196
+ );
197
+ }
198
+
199
+ const helper = context.getScaleHelper(kind, channel, resolution);
200
+ const remainingArgs = args
201
+ .slice(1)
202
+ .map((arg) => codegen(arg))
203
+ .join(",");
204
+ return `${context.globalvar}["${helper.codeName}"](${remainingArgs})`;
205
+ }
206
+
207
+ /**
208
+ * @param {any} node
209
+ * @returns {string | undefined}
210
+ */
211
+ function getLiteralString(node) {
212
+ return node?.type === "Literal" && typeof node.value === "string"
213
+ ? node.value
214
+ : undefined;
215
+ }
216
+
217
+ /**
218
+ * @param {"scale" | "invert" | "domain" | "range"} kind
219
+ * @param {import("../scales/scaleResolution.js").default} resolution
220
+ * @returns {(...args: any[]) => any}
221
+ */
222
+ function createScaleHelperFunction(kind, resolution) {
223
+ /** @type {(fn: () => any) => any} */
224
+ const run = (fn) => runWithActiveScaleResolution(resolution, kind, fn);
225
+
226
+ if (kind === "domain") {
227
+ // `vega-scale` returns a fresh array here. That is fine for
228
+ // batch-invariant expressions, but callers should avoid using it in a
229
+ // per-datum hot path unless they really need the full extent array.
230
+ return () => run(() => resolution.getDomain());
231
+ }
232
+ if (kind === "range") {
233
+ // Same allocation caveat as `domain()`: the underlying scale getter
234
+ // returns a copied array, so repeated per-row calls will allocate.
235
+ return () => run(() => resolution.getScale().range());
236
+ }
237
+ if (kind === "scale") {
238
+ return (value) => run(() => resolution.getScale()(value));
239
+ }
240
+ if (kind === "invert") {
241
+ return (value) =>
242
+ run(() => /** @type {any} */ (resolution.getScale()).invert(value));
243
+ }
244
+ throw new Error("Unknown scale helper: " + kind);
245
+ }
246
+
247
+ /** @type {WeakSet<object>} */
248
+ const activeScaleHelperResolutions = new WeakSet();
249
+
250
+ /**
251
+ * @template T
252
+ * @param {import("../scales/scaleResolution.js").default} resolution
253
+ * @param {"scale" | "invert" | "domain" | "range"} kind
254
+ * @param {() => T} fn
255
+ * @returns {T}
256
+ */
257
+ function runWithActiveScaleResolution(resolution, kind, fn) {
258
+ if (activeScaleHelperResolutions.has(resolution)) {
259
+ throw new Error(
260
+ `Scale helper cycle detected while evaluating ${kind}("${resolution.channel}").`
261
+ );
262
+ }
263
+
264
+ activeScaleHelperResolutions.add(resolution);
265
+ try {
266
+ return fn();
267
+ } finally {
268
+ activeScaleHelperResolutions.delete(resolution);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * @param {"scale" | "invert" | "domain" | "range"} kind
274
+ * @param {string} channel
275
+ * @param {import("../scales/scaleResolution.js").default} resolution
276
+ * @param {string} codeName
277
+ * @returns {import("../paramRuntime/types.js").ParamRef<any> & { rank: number }}
278
+ */
279
+ function createScaleDependency(kind, channel, resolution, codeName) {
280
+ /** @type {Set<() => void>} */
281
+ const listeners = new Set();
282
+
283
+ const notify = () => {
284
+ for (const listener of listeners) {
285
+ listener();
286
+ }
287
+ };
288
+
289
+ const attach = () => {
290
+ if (kind === "domain") {
291
+ resolution.addEventListener("domain", notify);
292
+ } else if (kind === "range") {
293
+ resolution.addEventListener("range", notify);
294
+ } else {
295
+ resolution.addEventListener("domain", notify);
296
+ resolution.addEventListener("range", notify);
297
+ }
298
+ };
299
+
300
+ const detach = () => {
301
+ if (kind === "domain") {
302
+ resolution.removeEventListener("domain", notify);
303
+ } else if (kind === "range") {
304
+ resolution.removeEventListener("range", notify);
305
+ } else {
306
+ resolution.removeEventListener("domain", notify);
307
+ resolution.removeEventListener("range", notify);
308
+ }
309
+ };
310
+
311
+ return {
312
+ // The dependency is a lightweight invalidation token. It does not need
313
+ // to expose a separate scale value; the bound helper closure already
314
+ // closes over the actual resolution. The ref exists so the expression
315
+ // graph can subscribe to scale changes like any other reactive input.
316
+ id: `scale:${channel}:${codeName}`,
317
+ name: `scale(${channel})`,
318
+ kind: "derived",
319
+ rank: 0,
320
+ get() {
321
+ return resolution.getScale();
322
+ },
323
+ subscribe(listener) {
324
+ const wasEmpty = listeners.size === 0;
325
+ listeners.add(listener);
326
+ if (wasEmpty) {
327
+ attach();
328
+ }
329
+ return () => {
330
+ const removed = listeners.delete(listener);
331
+ if (removed && listeners.size === 0) {
332
+ detach();
333
+ }
334
+ };
335
+ },
336
+ };
337
+ }
338
+
339
+ /**
133
340
  * @param {string} expr
341
+ * @param {Record<string, any>} globalObject
342
+ * @param {ExpressionCompileContext} context
343
+ *
134
344
  * @returns {ExpressionFunction}
135
345
  */
136
- export default function createFunction(expr, globalObject = {}) {
346
+ export default function createFunction(expr, globalObject = {}, context = {}) {
137
347
  try {
348
+ // Each scale helper call is rewritten once at compile time into a
349
+ // cached closure that targets a specific scale resolution.
350
+ /** @type {Map<string, import("../paramRuntime/types.js").ParamRef<any>>} */
351
+ const scaleDependenciesByChannel = new Map();
352
+ // A helper may appear multiple times in one expression. Cache both the
353
+ // generated closure and the synthetic dependency ref so repeated calls
354
+ // share the same reactive identity.
355
+ /** @type {Map<string, { codeName: string, dependency: import("../paramRuntime/types.js").ParamRef<any> }>} */
356
+ const helperEntries = new Map();
357
+ let nextScaleHelperId = 1;
358
+
359
+ /**
360
+ * @type {ExpressionCompileContext & {
361
+ * globalvar: string,
362
+ * globalObject: Record<string, any>,
363
+ * getScaleHelper: (kind: "scale" | "invert" | "domain" | "range", channel: string, resolution: import("../scales/scaleResolution.js").default) => { codeName: string, dependency: import("../paramRuntime/types.js").ParamRef<any> }
364
+ * }}
365
+ */
366
+ const helperContext = {
367
+ ...context,
368
+ globalvar: "globalObject",
369
+ globalObject,
370
+ getScaleHelper(kind, channel, resolution) {
371
+ // Helper instances are keyed by helper kind + channel so
372
+ // `domain("x")` and `scale("x", ...)` share the same
373
+ // resolution binding but keep separate generated closures.
374
+ const key = kind + ":" + channel;
375
+ const cached = helperEntries.get(key);
376
+ if (cached) {
377
+ return cached;
378
+ }
379
+
380
+ let dependency = scaleDependenciesByChannel.get(channel);
381
+ if (!dependency) {
382
+ dependency = createScaleDependency(
383
+ kind,
384
+ channel,
385
+ resolution,
386
+ "__scale_dependency_" + nextScaleHelperId++
387
+ );
388
+ scaleDependenciesByChannel.set(channel, dependency);
389
+ }
390
+
391
+ const codeName = "__scale_helper_" + nextScaleHelperId++;
392
+ const entry = { codeName, dependency };
393
+ helperEntries.set(key, entry);
394
+ // Store the concrete helper implementation on the global object
395
+ // used by the generated expression function.
396
+ globalObject[codeName] = createScaleHelperFunction(
397
+ kind,
398
+ resolution
399
+ );
400
+ return entry;
401
+ },
402
+ };
403
+
404
+ const cg = codegenExpression({
405
+ forbidden: [],
406
+ allowed: ["datum", "undefined"],
407
+ globalvar: "globalObject",
408
+ fieldvar: "datum",
409
+ functions: (visitor) => buildFunctions(visitor, helperContext),
410
+ });
411
+
138
412
  const parsed = parseExpression(expr);
139
413
  const generatedCode = cg(parsed);
140
414
 
@@ -157,6 +431,12 @@ export default function createFunction(expr, globalObject = {}) {
157
431
  exprFunction.fields = generatedCode.fields;
158
432
  exprFunction.globals = generatedCode.globals;
159
433
  exprFunction.code = generatedCode.code;
434
+ // Reactive bookkeeping lives outside the generated expression body.
435
+ // The expression runtime subscribes to these refs and invalidates the
436
+ // compiled expression when the referenced scale changes.
437
+ exprFunction.scaleDependencies = Array.from(
438
+ scaleDependenciesByChannel.values()
439
+ );
160
440
 
161
441
  return exprFunction;
162
442
  } catch (e) {
@@ -15,7 +15,7 @@ export function buildDataFlow(root: import("./view.js").default<import("../spec/
15
15
  */
16
16
  export function linearizeLocusAccess(view: import("./unitView.js").default): {
17
17
  transforms: import("../data/flowNode.js").default[];
18
- rewrittenEncoding: import("../spec/channel.js").Encoding;
18
+ rewrittenEncoding: Record<string, any>;
19
19
  /**
20
20
  * Should be called after the whole flow has been created in order to
21
21
  * not disrupt inheritance of encodings
@@ -1 +1 @@
1
- {"version":3,"file":"flowBuilder.d.ts","sourceRoot":"","sources":["../../../src/view/flowBuilder.js"],"names":[],"mappings":"AAyBA;;;;;;GAMG;AACH,oHALW,QAAQ,eAER,CAAC,IAAI,iEAAM,KAAK,OAAO,gCACvB,CAAC,IAAI,iEAAM,KAAK,OAAO,YAoPjC;AAuBD;;;;;;GAMG;AACH,2CAFW,OAAO,eAAe,EAAE,OAAO;;;IAoH5B;;;OAGG;;EAYhB;AAgCD;;;;;;GAMG;AACH,4BAFwB,CAAC,SAAZ,qCAAU,cAFZ,CAAC,iBACA,uCAAW;;;0BAwBG,OAAO,CAAC,QAAQ,CAAC,OAAO,qBAAqB,EAAE,KAAK,CAAC,CAAC;EAkB/E;qBAxfoB,qBAAqB;sBAPpB,sBAAsB"}
1
+ {"version":3,"file":"flowBuilder.d.ts","sourceRoot":"","sources":["../../../src/view/flowBuilder.js"],"names":[],"mappings":"AAyBA;;;;;;GAMG;AACH,oHALW,QAAQ,eAER,CAAC,IAAI,iEAAM,KAAK,OAAO,gCACvB,CAAC,IAAI,iEAAM,KAAK,OAAO,YAoPjC;AAuBD;;;;;;GAMG;AACH,2CAFW,OAAO,eAAe,EAAE,OAAO;;;IAwH5B;;;OAGG;;EAYhB;AAgCD;;;;;;GAMG;AACH,4BAFwB,CAAC,SAAZ,qCAAU,cAFZ,CAAC,iBACA,uCAAW;;;0BAwBG,OAAO,CAAC,QAAQ,CAAC,OAAO,qBAAqB,EAAE,KAAK,CAAC,CAAC;EAkB/E;qBA5foB,qBAAqB;sBAPpB,sBAAsB"}
@@ -312,7 +312,7 @@ export function linearizeLocusAccess(view) {
312
312
  /** @type {FlowNode[]} */
313
313
  const transforms = [];
314
314
 
315
- /** @type {Encoding} */
315
+ /** @type {Record<string, any>} */
316
316
  const rewrittenEncoding = {};
317
317
 
318
318
  // Use mark.encoding so we see the same channel defs that encoders consume,
@@ -374,12 +374,16 @@ export function linearizeLocusAccess(view) {
374
374
 
375
375
  // Prefer using the spec directly because getEncoding() returns inherited props too.
376
376
  // TODO: I think this is not robust enough. Needs more work...
377
- /** @type {any} */
377
+ /** @type {Record<string, any>} */
378
+ const currentEncoding =
379
+ view.spec.encoding?.[channel] ??
380
+ configuredEncoding[channel] ??
381
+ encoding[channel] ??
382
+ {};
383
+
384
+ /** @type {Record<string, any>} */
378
385
  const newFieldDef = {
379
- ...(view.spec.encoding?.[channel] ??
380
- configuredEncoding[channel] ??
381
- encoding[channel] ??
382
- {}),
386
+ ...currentEncoding,
383
387
  field: linearizedField,
384
388
  };
385
389
  delete newFieldDef.chrom;
@@ -435,7 +439,7 @@ export function linearizeLocusAccess(view) {
435
439
 
436
440
  /**
437
441
  * @param {import("./unitView.js").default} view
438
- * @param {import("../spec/channel.js").Encoding} [encoding]
442
+ * @param {Record<string, any>} [encoding]
439
443
  * @returns {import("../spec/transform.js").CompareParams}
440
444
  */
441
445
  function getCompareParamsForView(view, encoding) {
@@ -0,0 +1,9 @@
1
+ export function resolveViewResolutions(view: import("./unitView.js").default, type?: import("../spec/view.js").ResolutionTarget): void;
2
+ export type ResolutionMember = {
3
+ view: import("./unitView.js").default;
4
+ channel: import("../spec/channel.js").Channel;
5
+ channelDef: import("../spec/channel.js").ChannelDefWithScale;
6
+ targetChannel: import("../spec/channel.js").ChannelWithScale;
7
+ };
8
+ export type ScaleResolutionMemberMap = Map<import("../scales/scaleResolution.js").default, ResolutionMember[]>;
9
+ //# sourceMappingURL=resolutionPlanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolutionPlanner.d.ts","sourceRoot":"","sources":["../../../src/view/resolutionPlanner.js"],"names":[],"mappings":"AA8RO,6CAHI,OAAO,eAAe,EAAE,OAAO,SAC/B,OAAO,iBAAiB,EAAE,gBAAgB,QAiBpD;;UAhSS,OAAO,eAAe,EAAE,OAAO;aAC/B,OAAO,oBAAoB,EAAE,OAAO;gBACpC,OAAO,oBAAoB,EAAE,mBAAmB;mBAChD,OAAO,oBAAoB,EAAE,gBAAgB;;uCAI1C,GAAG,CAAC,OAAO,8BAA8B,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC"}