@genome-spy/core 0.14.2 → 0.18.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 (70) hide show
  1. package/LICENSE +7 -7
  2. package/README.md +16 -0
  3. package/dist/index.js +42 -42
  4. package/dist/{genome-spy-schema.json → schema.json} +1824 -652
  5. package/dist/style.css +1 -1
  6. package/package.json +9 -7
  7. package/src/data/sources/dataUtils.js +50 -3
  8. package/src/data/sources/dynamicCallbackSource.js +2 -1
  9. package/src/data/sources/dynamicSource.js +2 -1
  10. package/src/data/sources/inlineSource.js +3 -5
  11. package/src/data/sources/namedSource.js +3 -7
  12. package/src/data/sources/urlSource.js +1 -1
  13. package/src/data/transforms/aggregate.js +1 -0
  14. package/src/data/transforms/flattenDelimited.js +6 -0
  15. package/src/data/transforms/regexFold.js +1 -1
  16. package/src/data/transforms/stack.js +3 -3
  17. package/src/embedApi.d.ts +59 -0
  18. package/src/encoder/accessor.js +6 -6
  19. package/src/encoder/encoder.js +47 -22
  20. package/src/genome/scaleIndex.d.ts +38 -0
  21. package/src/genome/scaleIndex.js +18 -52
  22. package/src/genome/scaleLocus.d.ts +11 -0
  23. package/src/genome/scaleLocus.js +12 -16
  24. package/src/genomeSpy.js +1 -1
  25. package/src/gl/dataToVertices.js +14 -6
  26. package/src/gl/includes/fp64-utils.js +10 -0
  27. package/src/gl/includes/scales.glsl +2 -0
  28. package/src/gl/webGLHelper.js +3 -1
  29. package/src/index.js +6 -28
  30. package/src/marks/link.js +1 -12
  31. package/src/marks/mark.js +27 -5
  32. package/src/marks/markUtils.js +41 -25
  33. package/src/marks/pointMark.js +5 -2
  34. package/src/marks/rule.js +11 -2
  35. package/src/scale/glslScaleGenerator.js +16 -29
  36. package/src/scale/scale.js +10 -0
  37. package/src/scale/ticks.js +11 -6
  38. package/src/spec/channel.d.ts +343 -43
  39. package/src/spec/data.d.ts +14 -3
  40. package/src/spec/root.d.ts +3 -8
  41. package/src/spec/scale.d.ts +18 -1
  42. package/src/spec/view.d.ts +12 -6
  43. package/src/tooltip/refseqGeneTooltipHandler.js +1 -0
  44. package/src/types/filetypes.d.ts +10 -0
  45. package/src/types/internmap.d.ts +22 -0
  46. package/src/types/vega-loader.d.ts +1 -0
  47. package/src/utils/addBaseUrl.js +19 -0
  48. package/src/utils/addBaseUrl.test.js +21 -0
  49. package/src/utils/arrayUtils.js +12 -6
  50. package/src/utils/cloner.js +5 -3
  51. package/src/utils/concatIterables.js +2 -2
  52. package/src/utils/domainArray.js +0 -8
  53. package/src/utils/propertyCoalescer.js +9 -4
  54. package/src/view/axisResolution.js +11 -6
  55. package/src/view/axisView.js +8 -5
  56. package/src/view/decoratorView.js +6 -3
  57. package/src/view/facetView.js +3 -0
  58. package/src/view/flowBuilder.js +2 -1
  59. package/src/view/renderingContext/svgViewRenderingContext.js +7 -3
  60. package/src/view/scaleResolution.js +52 -32
  61. package/src/view/testUtils.js +7 -4
  62. package/src/view/unitView.js +15 -9
  63. package/src/view/view.js +10 -8
  64. package/src/view/viewFactory.js +2 -0
  65. package/src/view/viewUtils.js +4 -5
  66. package/src/options.d.ts +0 -15
  67. package/src/utils/fisheye.js +0 -60
  68. package/src/utils/html.js +0 -23
  69. package/src/utils/html.test.js +0 -13
  70. package/src/view/channel.js +0 -5
@@ -23,8 +23,8 @@ import {
23
23
  isColorChannel,
24
24
  isDiscreteChannel,
25
25
  isPositionalChannel,
26
+ isPrimaryPositionalChannel,
26
27
  isSecondaryChannel,
27
- primaryPositionalChannels,
28
28
  } from "../encoder/encoder";
29
29
  import {
30
30
  isChromosomalLocus,
@@ -34,6 +34,7 @@ import { NominalDomain } from "../utils/domainArray";
34
34
  import { easeQuadInOut } from "d3-ease";
35
35
  import { interpolateZoom } from "d3-interpolate";
36
36
  import { shallowArrayEquals } from "../utils/arrayUtils";
37
+ import { isScaleLocus } from "../genome/scaleLocus";
37
38
 
38
39
  export const QUANTITATIVE = "quantitative";
39
40
  export const ORDINAL = "ordinal";
@@ -41,6 +42,15 @@ export const NOMINAL = "nominal";
41
42
  export const LOCUS = "locus"; // Humdum, should this be "genomic"?
42
43
  export const INDEX = "index";
43
44
 
45
+ /**
46
+ * @template {Channel}[T=Channel]
47
+ * @typedef {{view: import("./unitView").default, channel: T}} ResolutionMember
48
+ * @typedef {import("./unitView").default} UnitView
49
+ * @typedef {import("../encoder/encoder").VegaScale} VegaScale
50
+ * @typedef {import("../utils/domainArray").DomainArray} DomainArray
51
+ * @typedef {import("../genome/genome").ChromosomalLocus} ChromosomalLocus
52
+ *
53
+ */
44
54
  /**
45
55
  * Resolution takes care of merging domains and scales from multiple views.
46
56
  * This class also provides some utility methods for zooming the scales etc..
@@ -50,12 +60,6 @@ export const INDEX = "index";
50
60
  * @typedef {import("./scaleResolutionApi").ScaleResolutionApi} ScaleResolutionApi
51
61
  * @implements {ScaleResolutionApi}
52
62
  *
53
- * @typedef {{view: import("./unitView").default, channel: Channel}} ResolutionMember
54
- * @typedef {import("./unitView").default} UnitView
55
- * @typedef {import("../encoder/encoder").VegaScale} VegaScale
56
- * @typedef {import("../utils/domainArray").DomainArray} DomainArray
57
- * @typedef {import("../genome/genome").ChromosomalLocus} ChromosomalLocus
58
- *
59
63
  * @typedef {import("../spec/channel").Channel} Channel
60
64
  * @typedef {import("../spec/scale").Scale} Scale
61
65
  * @typedef {import("../spec/scale").NumericDomain} NumericDomain
@@ -323,7 +327,7 @@ export default class ScaleResolution {
323
327
  const scale = createScale(props);
324
328
  this._scale = scale;
325
329
 
326
- if (scale.type == "locus") {
330
+ if (isScaleLocus(scale)) {
327
331
  scale.genome(this.getGenome());
328
332
  }
329
333
 
@@ -366,7 +370,7 @@ export default class ScaleResolution {
366
370
  }
367
371
 
368
372
  isZoomable() {
369
- if (!primaryPositionalChannels.includes(this.channel)) {
373
+ if (!isPrimaryPositionalChannel(this.channel)) {
370
374
  return false;
371
375
  }
372
376
 
@@ -400,13 +404,14 @@ export default class ScaleResolution {
400
404
  const oldDomain = scale.domain();
401
405
  let newDomain = [...oldDomain];
402
406
 
407
+ // @ts-expect-error
403
408
  const anchor = scale.invert(scaleAnchor);
404
409
 
405
410
  if (this.getScaleProps().reverse) {
406
411
  pan = -pan;
407
412
  }
408
413
 
409
- // TODO: log, pow, symlog, ...
414
+ // TODO: symlog
410
415
  switch (scale.type) {
411
416
  case "linear":
412
417
  case "index":
@@ -419,22 +424,33 @@ export default class ScaleResolution {
419
424
  newDomain = zoomLog(newDomain, anchor, scaleFactor);
420
425
  break;
421
426
  case "pow":
422
- case "sqrt":
423
- newDomain = panPow(newDomain, pan || 0, scale.exponent());
427
+ case "sqrt": {
428
+ const powScale =
429
+ /** @type {import("d3-scale").ScalePower<number, number>} */ (
430
+ scale
431
+ );
432
+ newDomain = panPow(newDomain, pan || 0, powScale.exponent());
424
433
  newDomain = zoomPow(
425
434
  newDomain,
426
435
  anchor,
427
436
  scaleFactor,
428
- scale.exponent()
437
+ powScale.exponent()
429
438
  );
430
439
  break;
440
+ }
431
441
  default:
432
- throw new Error("Unsupported scale type: " + scale.type);
442
+ throw new Error(
443
+ "Zooming is not implemented for: " + scale.type
444
+ );
433
445
  }
434
446
 
435
447
  // TODO: Use the zoomTo method. Move clamping etc there.
436
448
  if (this._zoomExtent) {
437
- newDomain = clampRange(newDomain, ...this._zoomExtent);
449
+ newDomain = clampRange(
450
+ newDomain,
451
+ this._zoomExtent[0],
452
+ this._zoomExtent[1]
453
+ );
438
454
  }
439
455
 
440
456
  if ([0, 1].some((i) => newDomain[i] != oldDomain[i])) {
@@ -491,9 +507,8 @@ export default class ScaleResolution {
491
507
  },
492
508
  });
493
509
  */
494
- const interpolator = interpolateZoom.rho(0.7)(
495
- [fc, 0, fw],
496
- [tc, 0, tw]
510
+ const interpolator = interpolateZoom([fc, 0, fw], [tc, 0, tw]).rho(
511
+ 0.7
497
512
  );
498
513
  await animator.transition({
499
514
  duration: (duration / 1000) * interpolator.duration,
@@ -672,12 +687,13 @@ export default class ScaleResolution {
672
687
  *
673
688
  * @param {Channel} channel
674
689
  * @param {string} dataType
690
+ * @returns {import("../spec/scale").ScaleType}
675
691
  */
676
692
  function getDefaultScaleType(channel, dataType) {
677
693
  // TODO: Band scale, Bin-Quantitative
678
694
 
679
- if ([INDEX, LOCUS].includes(dataType)) {
680
- if (primaryPositionalChannels.includes(channel)) {
695
+ if (dataType == INDEX || dataType == LOCUS) {
696
+ if (isPrimaryPositionalChannel(channel)) {
681
697
  return dataType;
682
698
  } else {
683
699
  // TODO: Also explicitly set scales should be validated
@@ -688,13 +704,11 @@ function getDefaultScaleType(channel, dataType) {
688
704
  }
689
705
 
690
706
  /**
691
- * @type {Partial<Record<Channel, string[]>>}
707
+ * @type {Partial<Record<Channel, (import("../spec/scale").ScaleType | undefined)[]>>}
692
708
  * Default types: nominal, ordinal, quantitative.
693
709
  * undefined = incompatible, "null" = disabled (pass-thru)
694
710
  */
695
711
  const defaults = {
696
- uniqueId: ["null", undefined, undefined],
697
- facetIndex: ["null", undefined, undefined],
698
712
  x: ["band", "band", "linear"],
699
713
  y: ["band", "band", "linear"],
700
714
  size: [undefined, "point", "linear"],
@@ -706,16 +720,24 @@ function getDefaultScaleType(channel, dataType) {
706
720
  stroke: ["ordinal", "ordinal", "linear"],
707
721
  strokeWidth: [undefined, undefined, "linear"],
708
722
  shape: ["ordinal", "ordinal", undefined],
709
- sample: ["null", "null", undefined],
710
- semanticScore: [undefined, undefined, "null"],
711
- search: ["null", undefined, undefined],
712
- text: ["null", "null", "null"],
713
723
  dx: [undefined, undefined, "null"],
714
724
  dy: [undefined, undefined, "null"],
715
725
  angle: [undefined, undefined, "linear"],
716
726
  };
717
727
 
718
- const type = defaults[channel]
728
+ /** @type {Channel[]} */
729
+ const typelessChannels = [
730
+ "uniqueId",
731
+ "facetIndex",
732
+ "semanticScore",
733
+ "search",
734
+ "text",
735
+ "sample",
736
+ ];
737
+
738
+ const type = typelessChannels.includes(channel)
739
+ ? "null"
740
+ : defaults[channel]
719
741
  ? defaults[channel][[NOMINAL, ORDINAL, QUANTITATIVE].indexOf(dataType)]
720
742
  : dataType == QUANTITATIVE
721
743
  ? "linear"
@@ -739,10 +761,8 @@ function applyLockedProperties(props, channel) {
739
761
  props.range = [0, 1];
740
762
  }
741
763
 
742
- if (channel == "opacity") {
743
- if (isContinuous(props.type)) {
744
- props.clamp = true;
745
- }
764
+ if (channel == "opacity" && isContinuous(props.type)) {
765
+ props.clamp = true;
746
766
  }
747
767
  }
748
768
 
@@ -15,15 +15,15 @@ import { ViewFactory } from "./viewFactory";
15
15
  /** @type {<V extends View>(spec: ViewSpec, viewClass: { new(...args: any[]): V }, context?: ViewContext) => V} */
16
16
  export function create(spec, viewClass, context = undefined) {
17
17
  const viewTypeRegistry = new ViewFactory();
18
- /** @type {ViewContext} */
19
- const c = {
18
+
19
+ const c = /** @type {ViewContext} */ ({
20
20
  ...(context || {}),
21
21
  accessorFactory: new AccessorFactory(),
22
22
 
23
23
  createView: function (spec, parent, defaultName) {
24
24
  return viewTypeRegistry.createView(spec, c, parent, defaultName);
25
25
  },
26
- };
26
+ });
27
27
 
28
28
  const view = c.createView(spec, null, "root");
29
29
 
@@ -40,7 +40,10 @@ export async function createAndInitialize(
40
40
  viewClass,
41
41
  context = undefined
42
42
  ) {
43
- context = { ...(context || {}), dataFlow: new DataFlow() };
43
+ context = /** @type {ViewContext} */ ({
44
+ ...(context || {}),
45
+ dataFlow: new DataFlow(),
46
+ });
44
47
  const view = create(spec, viewClass, context);
45
48
  resolveScalesAndAxes(view);
46
49
  await initializeData(view, context.dataFlow);
@@ -170,7 +170,18 @@ export default class UnitView extends ContainerView {
170
170
  : new AxisResolution(targetChannel);
171
171
  }
172
172
 
173
- view.resolutions[type][targetChannel].pushUnitView(this, channel);
173
+ // Looks silly, but keeps type checking happy
174
+ if (isPositionalChannel(channel)) {
175
+ view.resolutions[type][targetChannel].pushUnitView(
176
+ this,
177
+ channel
178
+ );
179
+ } else if (type == "scale") {
180
+ view.resolutions[type][targetChannel].pushUnitView(
181
+ this,
182
+ channel
183
+ );
184
+ }
174
185
  }
175
186
  }
176
187
 
@@ -223,16 +234,11 @@ export default class UnitView extends ContainerView {
223
234
  }
224
235
 
225
236
  const channelDef = this.mark.encoding[channel];
237
+ // TODO: Broken. Fix.
226
238
  if (!isChannelDefWithScale(channelDef)) {
227
239
  throw new Error("The channel has no scale, cannot get domain!");
228
240
  }
229
241
 
230
- const type = channelDef.type;
231
- if (!type) {
232
- throw new Error(`No data type for channel "${channel}"!`);
233
- // TODO: Support defaults
234
- }
235
-
236
242
  return channelDef;
237
243
  }
238
244
 
@@ -252,7 +258,7 @@ export default class UnitView extends ContainerView {
252
258
  channelDef.resolutionChannel ?? channel
253
259
  );
254
260
  return createDomain(
255
- channelDef.type,
261
+ channelDef.type ?? "nominal",
256
262
  // Chrom/pos must be linearized first
257
263
  scaleResolution.fromComplexInterval(specDomain)
258
264
  );
@@ -274,7 +280,7 @@ export default class UnitView extends ContainerView {
274
280
  */
275
281
  extractDataDomain(channel) {
276
282
  const channelDef = this._validateDomainQuery(channel);
277
- const type = channelDef.type;
283
+ const type = channelDef.type ?? "nominal"; // TODO: Should check that this is a channel without scale
278
284
 
279
285
  /** @param {Channel} channel */
280
286
  const extract = (channel) => {
package/src/view/view.js CHANGED
@@ -142,13 +142,10 @@ export default class View {
142
142
  */
143
143
  getSizeFromSpec() {
144
144
  /**
145
- *
146
145
  * @param {"width" | "height"} dimension
147
146
  * @return {SizeDef}
148
147
  */
149
148
  const handleSize = (dimension) => {
150
- /** @type {SizeDef} */
151
- let sizeDef;
152
149
  let value = this.spec[dimension];
153
150
 
154
151
  if (isStepSize(value)) {
@@ -172,22 +169,27 @@ export default class View {
172
169
  );
173
170
  }
174
171
 
172
+ // TODO: Type guards maybe?
173
+ const _scale =
174
+ /** @type {import("d3-scale").ScaleBand<any> | import("../genome/scaleLocus").ScaleLocus | import("../genome/scaleIndex").ScaleIndex} */ (
175
+ scale
176
+ );
177
+
175
178
  steps = bandSpace(
176
179
  steps,
177
- scale.paddingInner(),
178
- scale.paddingOuter()
180
+ _scale.paddingInner(),
181
+ _scale.paddingOuter()
179
182
  );
180
183
 
181
- sizeDef = { px: steps * stepSize, grow: 0 };
184
+ return { px: steps * stepSize, grow: 0 };
182
185
  } else {
183
186
  throw new Error(
184
187
  "Cannot use 'step' size with missing scale!"
185
188
  );
186
189
  }
187
190
  } else {
188
- sizeDef = (value && parseSizeDef(value)) || { px: 0, grow: 1 };
191
+ return (value && parseSizeDef(value)) ?? { px: 0, grow: 1 };
189
192
  }
190
- return sizeDef;
191
193
  };
192
194
 
193
195
  return this._cache(
@@ -123,8 +123,10 @@ export function isLayerSpec(spec) {
123
123
  export function isFacetSpec(spec) {
124
124
  return (
125
125
  "facet" in spec &&
126
+ // @ts-expect-error
126
127
  isObject(spec.facet) &&
127
128
  "spec" in spec &&
129
+ // @ts-expect-error
128
130
  isObject(spec.spec)
129
131
  );
130
132
  }
@@ -35,10 +35,9 @@ import { rollup } from "d3-array";
35
35
  * @typedef {import("../spec/view").ImportSpec} ImportSpec
36
36
  * @typedef {import("../spec/view").ImportConfig} ImportConfig
37
37
  * @typedef {import("../spec/root").RootSpec} RootSpec
38
- * @typedef {import("../spec/root").RootConfig} RootConfig
39
38
  *
40
- * @typedef {import("../spec/channel").FacetFieldDef} FacetFieldDef
41
39
  * @typedef {import("../spec/view").FacetMapping} FacetMapping
40
+ * @typedef {import("../spec/channel").FacetFieldDef} FacetFieldDef
42
41
  */
43
42
 
44
43
  /**
@@ -253,14 +252,14 @@ export async function initializeData(root, existingFlow) {
253
252
  * @param {View} view
254
253
  */
255
254
  export function findEncodedFields(view) {
256
- /** @type {{view: UnitView, channel: string, field: string, type: string}[]} */
255
+ /** @type {{view: UnitView, channel: import("../spec/channel").Channel, field: import("../spec/channel").Field, type: import("../spec/channel").Type}[]} */
257
256
  const fieldInfos = [];
258
257
 
259
258
  view.visit((view) => {
260
259
  if (view instanceof UnitView) {
261
260
  const encoding = view.getEncoding();
262
261
  for (const [channel, def] of Object.entries(encoding)) {
263
- if (isFieldDef(def)) {
262
+ if (isFieldDef(def) && "type" in def) {
264
263
  fieldInfos.push({
265
264
  view,
266
265
  channel,
@@ -292,7 +291,7 @@ async function loadExternalViewSpec(spec, baseUrl, viewContext) {
292
291
  const url = spec.import.url;
293
292
 
294
293
  const importedSpec = JSON.parse(
295
- await loader.load(url).catch((e) => {
294
+ await loader.load(url).catch((/** @type {Error} */ e) => {
296
295
  throw new Error(
297
296
  `Could not load imported view spec: ${url} \nReason: ${e.message}`
298
297
  );
package/src/options.d.ts DELETED
@@ -1,15 +0,0 @@
1
- import { TooltipHandler } from "./tooltip/tooltipHandler";
2
-
3
- export interface EmbedOptions {
4
- /**
5
- * A function that allows retrieval of named data sources.
6
- *
7
- * TODO: Support dynamic updates, i.e., pushing new data.
8
- */
9
- namedDataProvider?: (name: string) => any[];
10
-
11
- /**
12
- * Custom tooltip handlers. Use "default" to override the default handler
13
- */
14
- tooltipHandlers?: Record<string, TooltipHandler>;
15
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Based on:
3
- * fisheye d3-plugin (c) Mike Bostock
4
- * https://github.com/d3/d3-plugins/blob/master/fisheye/
5
- */
6
- export default function () {
7
- let radius = 200;
8
- let distortion = 2;
9
- let k0 = 1,
10
- k1 = 1;
11
- let focus = 0;
12
-
13
- /**
14
- *
15
- * @param {number} x
16
- */
17
- function fisheye(x) {
18
- const dx = x - focus;
19
- const dd = Math.abs(dx);
20
- if (!dd || dd >= radius) return x;
21
- const k = ((k0 * (1 - Math.exp(-dd * k1))) / dd) * 0.75 + 0.25;
22
- return focus + dx * k;
23
- }
24
-
25
- function rescale() {
26
- k0 = Math.exp(distortion);
27
- k0 = (k0 / (k0 - 1)) * radius;
28
- k1 = distortion / radius;
29
- return fisheye;
30
- }
31
-
32
- /**
33
- * @param {number} _
34
- */
35
- fisheye.radius = function (_) {
36
- if (!arguments.length) return radius;
37
- radius = +_;
38
- return rescale();
39
- };
40
-
41
- /**
42
- * @param {number} _
43
- */
44
- fisheye.distortion = function (_) {
45
- if (!arguments.length) return distortion;
46
- distortion = +_;
47
- return rescale();
48
- };
49
-
50
- /**
51
- * @param {number} _
52
- */
53
- fisheye.focus = function (_) {
54
- if (!arguments.length) return focus;
55
- focus = _;
56
- return fisheye;
57
- };
58
-
59
- return rescale();
60
- }
package/src/utils/html.js DELETED
@@ -1,23 +0,0 @@
1
- /* https://stackoverflow.com/a/41699140/1547896 */
2
-
3
- export function escapeHtml(str) {
4
- var map = {
5
- "&": "&amp;",
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
- }
@@ -1,13 +0,0 @@
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
- });
@@ -1,5 +0,0 @@
1
- export default class Channel {
2
- constructor() {
3
- this.type = null; // x, y, etc
4
- }
5
- }