@genome-spy/core 0.41.0 → 0.42.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 (72) hide show
  1. package/dist/bundle/index--cKb-dKG.js +615 -0
  2. package/dist/bundle/{index-gn8bhQ8w.js → index-d7k3kkin.js} +365 -366
  3. package/dist/bundle/index.es.js +4077 -3900
  4. package/dist/bundle/index.js +111 -69
  5. package/dist/schema.json +58 -6
  6. package/dist/src/data/sources/dynamic/axisGenomeSource.js +1 -1
  7. package/dist/src/data/sources/dynamic/axisTickSource.js +3 -3
  8. package/dist/src/data/sources/dynamic/bamSource.d.ts +3 -21
  9. package/dist/src/data/sources/dynamic/bamSource.d.ts.map +1 -1
  10. package/dist/src/data/sources/dynamic/bamSource.js +38 -55
  11. package/dist/src/data/sources/dynamic/bigBedSource.d.ts +2 -38
  12. package/dist/src/data/sources/dynamic/bigBedSource.d.ts.map +1 -1
  13. package/dist/src/data/sources/dynamic/bigBedSource.js +14 -71
  14. package/dist/src/data/sources/dynamic/bigWigSource.d.ts +4 -42
  15. package/dist/src/data/sources/dynamic/bigWigSource.d.ts.map +1 -1
  16. package/dist/src/data/sources/dynamic/bigWigSource.js +23 -60
  17. package/dist/src/data/sources/dynamic/gff3Source.d.ts.map +1 -1
  18. package/dist/src/data/sources/dynamic/gff3Source.js +1 -0
  19. package/dist/src/data/sources/dynamic/indexedFastaSource.d.ts +2 -20
  20. package/dist/src/data/sources/dynamic/indexedFastaSource.d.ts.map +1 -1
  21. package/dist/src/data/sources/dynamic/indexedFastaSource.js +23 -41
  22. package/dist/src/data/sources/dynamic/singleAxisLazySource.d.ts +23 -4
  23. package/dist/src/data/sources/dynamic/singleAxisLazySource.d.ts.map +1 -1
  24. package/dist/src/data/sources/dynamic/singleAxisLazySource.js +29 -4
  25. package/dist/src/data/sources/dynamic/singleAxisWindowedSource.d.ts +60 -0
  26. package/dist/src/data/sources/dynamic/singleAxisWindowedSource.d.ts.map +1 -0
  27. package/dist/src/data/sources/dynamic/singleAxisWindowedSource.js +152 -0
  28. package/dist/src/data/sources/dynamic/tabixSource.d.ts +6 -40
  29. package/dist/src/data/sources/dynamic/tabixSource.d.ts.map +1 -1
  30. package/dist/src/data/sources/dynamic/tabixSource.js +29 -78
  31. package/dist/src/data/transforms/regexFold.d.ts.map +1 -1
  32. package/dist/src/data/transforms/regexFold.js +8 -0
  33. package/dist/src/data/transforms/regexFold.test.js +28 -0
  34. package/dist/src/genomeSpy.d.ts +14 -0
  35. package/dist/src/genomeSpy.d.ts.map +1 -1
  36. package/dist/src/genomeSpy.js +114 -8
  37. package/dist/src/gl/webGLHelper.d.ts +6 -21
  38. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  39. package/dist/src/gl/webGLHelper.js +7 -38
  40. package/dist/src/img/90-ring-with-bg.svg +1 -0
  41. package/dist/src/img/README.md +5 -0
  42. package/dist/src/marks/link.d.ts +7 -0
  43. package/dist/src/marks/link.d.ts.map +1 -1
  44. package/dist/src/marks/link.js +72 -37
  45. package/dist/src/marks/text.d.ts.map +1 -1
  46. package/dist/src/marks/text.js +16 -17
  47. package/dist/src/spec/data.d.ts +28 -13
  48. package/dist/src/spec/mark.d.ts +0 -8
  49. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  50. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  51. package/dist/src/styles/genome-spy.css.js +33 -4
  52. package/dist/src/styles/genome-spy.scss +40 -4
  53. package/dist/src/types/viewContext.d.ts +9 -0
  54. package/dist/src/utils/binnedIndex.d.ts +2 -0
  55. package/dist/src/utils/binnedIndex.d.ts.map +1 -1
  56. package/dist/src/utils/binnedIndex.js +59 -10
  57. package/dist/src/utils/binnedIndex.test.js +46 -0
  58. package/dist/src/view/gridView.d.ts.map +1 -1
  59. package/dist/src/view/gridView.js +2 -0
  60. package/dist/src/view/layerView.d.ts.map +1 -1
  61. package/dist/src/view/layerView.js +2 -0
  62. package/dist/src/view/unitView.d.ts +0 -6
  63. package/dist/src/view/unitView.d.ts.map +1 -1
  64. package/dist/src/view/unitView.js +2 -9
  65. package/dist/src/view/view.d.ts +6 -0
  66. package/dist/src/view/view.d.ts.map +1 -1
  67. package/dist/src/view/view.js +11 -0
  68. package/package.json +3 -3
  69. package/dist/bundle/index-Cbz74kpR.js +0 -638
  70. package/dist/src/data/sources/dynamic/windowedMixin.d.ts +0 -32
  71. package/dist/src/data/sources/dynamic/windowedMixin.d.ts.map +0 -1
  72. package/dist/src/data/sources/dynamic/windowedMixin.js +0 -53
@@ -1,57 +1,23 @@
1
- declare const TabixSource_base: {
2
- new (...args: any[]): {
3
- [x: string]: any;
4
- lastQuantizedInterval: number[]; /**
5
- * @param {import("../../../spec/data.js").TabixData} params
6
- * @param {import("../../../view/view.js").default} view
7
- */
8
- quantizeInterval(interval: number[], windowSize: number): number[];
9
- checkAndUpdateLastInterval(interval: number[]): boolean;
10
- };
11
- } & typeof SingleAxisLazySource;
12
1
  /**
13
2
  * @template T
3
+ * @abstract
14
4
  */
15
- export default class TabixSource<T> extends TabixSource_base {
5
+ export default class TabixSource<T> extends SingleAxisWindowedSource {
16
6
  /**
17
7
  * @param {import("../../../spec/data.js").TabixData} params
18
8
  * @param {import("../../../view/view.js").default} view
19
9
  */
20
10
  constructor(params: import("../../../spec/data.js").TabixData, view: import("../../../view/view.js").default);
21
- /** Keep track of the order of the requests */
22
- lastRequestId: number;
23
- /** @type {import("@gmod/tabix").TabixIndexedFile} */
24
- tbiIndex: import("@gmod/tabix").TabixIndexedFile;
25
11
  params: import("../../../spec/data.js").TabixData;
26
- /**
27
- * Listen to the domain change event and update data when the covered windows change.
28
- *
29
- * @param {number[]} domain Linearized domain
30
- */
31
- onDomainChanged(domain: number[]): Promise<void>;
32
12
  initializedPromise: Promise<any>;
33
- /**
34
- * Listen to the domain change event and update data when the covered windows change.
35
- *
36
- * @param {number[]} interval linearized domain
37
- */
38
- doRequest(interval: number[]): Promise<void>;
39
- /**
40
- *
41
- * @param {number[]} interval
42
- */
43
- getFeatures(interval: number[]): Promise<{
44
- requestId: number;
45
- abort: () => void;
46
- features: T[];
47
- }>;
48
13
  /**
49
14
  * @abstract
15
+ * @protected
50
16
  * @param {string[]} lines
51
17
  * @returns {T[]}
52
18
  */
53
- _parseFeatures(lines: string[]): T[];
19
+ protected _parseFeatures(lines: string[]): T[];
20
+ #private;
54
21
  }
55
- import SingleAxisLazySource from "./singleAxisLazySource.js";
56
- export {};
22
+ import SingleAxisWindowedSource from "./singleAxisWindowedSource.js";
57
23
  //# sourceMappingURL=tabixSource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tabixSource.d.ts","sourceRoot":"","sources":["../../../../../src/data/sources/dynamic/tabixSource.js"],"names":[],"mappings":";;;yCAeI;;;WAGG;;;;;AAbP;;GAEG;AACH;IAOI;;;OAGG;IACH,oBAHW,OAAO,uBAAuB,EAAE,SAAS,QACzC,OAAO,uBAAuB,EAAE,OAAO,EAmDjD;IA3DD,8CAA8C;IAC9C,sBAAkB;IAElB,qDAAqD;IACrD,UADW,OAAO,aAAa,EAAE,gBAAgB,CACxC;IAiBL,kDAAgC;IAwCpC;;;;OAIG;IACH,wBAFW,MAAM,EAAE,iBAclB;IA3CG,iCAuBE;IAsBN;;;;OAIG;IACH,oBAFW,MAAM,EAAE,iBAWlB;IAED;;;OAGG;IACH,sBAFW,MAAM,EAAE;;;;OAoClB;IAED;;;;OAIG;IACH,sBAHW,MAAM,EAAE,GACN,CAAC,EAAE,CAIf;CACJ;iCAzJgC,2BAA2B"}
1
+ {"version":3,"file":"tabixSource.d.ts","sourceRoot":"","sources":["../../../../../src/data/sources/dynamic/tabixSource.js"],"names":[],"mappings":"AAGA;;;GAGG;AACH;IAII;;;OAGG;IACH,oBAHW,OAAO,uBAAuB,EAAE,SAAS,QACzC,OAAO,uBAAuB,EAAE,OAAO,EA8CjD;IAhCG,kDAAgC;IAQhC,iCAuBE;IAoCN;;;;;OAKG;IACH,gCAHW,MAAM,EAAE,GACN,CAAC,EAAE,CAKf;;CACJ;qCAvGoC,+BAA+B"}
@@ -1,17 +1,13 @@
1
- import SingleAxisLazySource from "./singleAxisLazySource.js";
2
- import windowedMixin from "./windowedMixin.js";
3
- import { debounce } from "../../../utils/debounce.js";
4
1
  import addBaseUrl from "../../../utils/addBaseUrl.js";
2
+ import SingleAxisWindowedSource from "./singleAxisWindowedSource.js";
5
3
 
6
4
  /**
7
5
  * @template T
6
+ * @abstract
8
7
  */
9
- export default class TabixSource extends windowedMixin(SingleAxisLazySource) {
10
- /** Keep track of the order of the requests */
11
- lastRequestId = 0;
12
-
8
+ export default class TabixSource extends SingleAxisWindowedSource {
13
9
  /** @type {import("@gmod/tabix").TabixIndexedFile} */
14
- tbiIndex;
10
+ #tbiIndex;
15
11
 
16
12
  /**
17
13
  * @param {import("../../../spec/data.js").TabixData} params
@@ -22,7 +18,8 @@ export default class TabixSource extends windowedMixin(SingleAxisLazySource) {
22
18
  const paramsWithDefaults = {
23
19
  channel: "x",
24
20
  windowSize: 3_000_000,
25
- debounceDomainChange: 200,
21
+ debounce: 200,
22
+ debounceMode: "domain",
26
23
  ...params,
27
24
  };
28
25
 
@@ -34,13 +31,7 @@ export default class TabixSource extends windowedMixin(SingleAxisLazySource) {
34
31
  throw new Error("No URL provided for TabixSource");
35
32
  }
36
33
 
37
- if (this.params.debounceDomainChange > 0) {
38
- this.onDomainChanged = debounce(
39
- this.onDomainChanged.bind(this),
40
- this.params.debounceDomainChange,
41
- false
42
- );
43
- }
34
+ this.setupDebouncing(this.params);
44
35
 
45
36
  this.initializedPromise = new Promise((resolve) => {
46
37
  Promise.all([
@@ -56,7 +47,7 @@ export default class TabixSource extends windowedMixin(SingleAxisLazySource) {
56
47
  const withBase = (/** @type {string} */ uri) =>
57
48
  new RemoteFile(addBaseUrl(uri, this.view.getBaseUrl()));
58
49
 
59
- this.tbiIndex = new TabixIndexedFile({
50
+ this.#tbiIndex = new TabixIndexedFile({
60
51
  filehandle: withBase(this.params.url),
61
52
  tbiFilehandle: withBase(
62
53
  this.params.indexUrl ?? this.params.url + ".tbi"
@@ -68,87 +59,47 @@ export default class TabixSource extends windowedMixin(SingleAxisLazySource) {
68
59
  });
69
60
  }
70
61
 
71
- /**
72
- * Listen to the domain change event and update data when the covered windows change.
73
- *
74
- * @param {number[]} domain Linearized domain
75
- */
76
- async onDomainChanged(domain) {
77
- const windowSize = this.params.windowSize;
78
-
79
- if (domain[1] - domain[0] > windowSize) {
80
- return;
81
- }
82
-
83
- const quantizedInterval = this.quantizeInterval(domain, windowSize);
84
-
85
- if (this.checkAndUpdateLastInterval(quantizedInterval)) {
86
- this.doRequest(quantizedInterval);
87
- }
88
- }
89
-
90
62
  /**
91
63
  * Listen to the domain change event and update data when the covered windows change.
92
64
  *
93
65
  * @param {number[]} interval linearized domain
94
66
  */
95
- async doRequest(interval) {
96
- const featureResponse = await this.getFeatures(interval);
97
-
98
- // Discard late responses
99
- if (featureResponse.requestId < this.lastRequestId) {
100
- return;
101
- }
102
-
103
- this.publishData(featureResponse.features);
104
- }
105
-
106
- /**
107
- *
108
- * @param {number[]} interval
109
- */
110
- async getFeatures(interval) {
111
- await this.initializedPromise;
112
-
113
- let requestId = ++this.lastRequestId;
114
-
115
- // TODO: Abort previous requests
116
- const abortController = new AbortController();
117
-
118
- const discreteChromosomeIntervals =
119
- this.genome.continuousToDiscreteChromosomeIntervals(interval);
120
-
121
- // TODO: Error handling
122
- const featuresWithChrom = await Promise.all(
123
- discreteChromosomeIntervals.map(async (d) => {
67
+ async loadInterval(interval) {
68
+ const featureChunks = await this.discretizeAndLoad(
69
+ interval,
70
+ async (discreteInterval, signal) => {
124
71
  /** @type {string[]} */
125
72
  const lines = [];
126
73
 
127
- await this.tbiIndex.getLines(d.chrom, d.startPos, d.endPos, {
128
- lineCallback: (line) => {
129
- lines.push(line);
130
- },
131
- signal: abortController.signal,
132
- });
74
+ await this.#tbiIndex.getLines(
75
+ discreteInterval.chrom,
76
+ discreteInterval.startPos,
77
+ discreteInterval.endPos,
78
+ {
79
+ lineCallback: (line) => {
80
+ lines.push(line);
81
+ },
82
+ signal,
83
+ }
84
+ );
133
85
 
134
- // Hmm. It's silly that we have to first collect individual lines and then join them.
135
86
  return this._parseFeatures(lines);
136
- })
87
+ }
137
88
  );
138
89
 
139
- return {
140
- requestId,
141
- abort: () => abortController.abort(),
142
- features: featuresWithChrom.flat(), // TODO: Use batches, not flat
143
- };
90
+ if (featureChunks) {
91
+ this.publishData(featureChunks);
92
+ }
144
93
  }
145
94
 
146
95
  /**
147
96
  * @abstract
97
+ * @protected
148
98
  * @param {string[]} lines
149
99
  * @returns {T[]}
150
100
  */
151
101
  _parseFeatures(lines) {
102
+ // Override me
152
103
  return [];
153
104
  }
154
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"regexFold.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/regexFold.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IAKI;;OAEG;IACH,oBAFW,OAAO,yBAAyB,EAAE,eAAe,EA2H3D;IAfO,gBALO,GAAG,UAKe;CAgBpC;qBAzIsD,gBAAgB"}
1
+ {"version":3,"file":"regexFold.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/regexFold.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IAKI;;OAEG;IACH,oBAFW,OAAO,yBAAyB,EAAE,eAAe,EAmI3D;IAfO,gBALO,GAAG,UAKe;CAgBpC;qBAjJsD,gBAAgB"}
@@ -48,6 +48,14 @@ export default class RegexFoldTransform extends FlowNode {
48
48
  const detectColumns = (datum) => {
49
49
  const colNames = /** @type {string[]} */ (Object.keys(datum));
50
50
 
51
+ for (const re of columnRegex) {
52
+ if (!colNames.some((colName) => re.test(colName))) {
53
+ throw new Error(
54
+ `No columns matching the regex ${re.toString()} found in the data!`
55
+ );
56
+ }
57
+ }
58
+
51
59
  /** @type {Map<string, string[]>} */
52
60
  const sampleColMap = new Map();
53
61
 
@@ -157,4 +157,32 @@ describe("RegexFold", () => {
157
157
  },
158
158
  ]);
159
159
  });
160
+
161
+ test("Throws error if no columns match the regex", () => {
162
+ const sampleData = [
163
+ {
164
+ row: 1,
165
+ sample1_a: "r1s1a",
166
+ sample2_a: "r1s2a",
167
+ },
168
+ {
169
+ row: 2,
170
+ sample1_a: "r2s1a",
171
+ sample2_a: "r2s2a",
172
+ },
173
+ ];
174
+
175
+ /** @type { import("../../spec/transform.js").RegexFoldParams } */
176
+ const singleGatherConfig = {
177
+ type: "regexFold",
178
+ columnRegex: "^(.*)_c$",
179
+ asValue: "a",
180
+ };
181
+
182
+ expect(() =>
183
+ processData(new RegexFoldTransform(singleGatherConfig), sampleData)
184
+ ).toThrowError(
185
+ "No columns matching the regex /^(.*)_c$/ found in the data!"
186
+ );
187
+ });
160
188
  });
@@ -13,6 +13,8 @@ export default class GenomeSpy {
13
13
  */
14
14
  constructor(container: HTMLElement, spec: import("./spec/root.js").RootSpec, options?: import("./types/embedApi.js").EmbedOptions);
15
15
  container: HTMLElement;
16
+ /** @type {(() => void)[]} */
17
+ _destructionCallbacks: (() => void)[];
16
18
  /** Root level configuration object */
17
19
  spec: import("./spec/root.js").RootSpec;
18
20
  accessorFactory: AccessorFactory;
@@ -68,6 +70,12 @@ export default class GenomeSpy {
68
70
  /** @type {View} */
69
71
  viewRoot: import("./view/view.js").default;
70
72
  _paramBroker: ParamBroker;
73
+ /**
74
+ * Views that are currently loading data using lazy sources.
75
+ *
76
+ * @type {Map<View, boolean>}
77
+ */
78
+ _loadingViews: Map<import("./view/view.js").default, boolean>;
71
79
  /**
72
80
  *
73
81
  * @param {(name: string) => any[]} provider
@@ -90,9 +98,15 @@ export default class GenomeSpy {
90
98
  * @param {any} [payload]
91
99
  */
92
100
  broadcast(type: BroadcastEventType, payload?: any): void;
101
+ /**
102
+ * Draw some layers on top of the canvas. It's easier to do fancy spinning
103
+ * animations with html elements than with WebGL.
104
+ */
105
+ _updateLoadingIndicators(): void;
93
106
  _prepareContainer(): void;
94
107
  _glHelper: WebGLHelper;
95
108
  loadingMessageElement: HTMLDivElement;
109
+ loadingIndicatorsElement: HTMLDivElement;
96
110
  tooltip: Tooltip;
97
111
  /**
98
112
  * Unregisters all listeners, removes all created dom elements, removes all css classes from the container
@@ -1 +1 @@
1
- {"version":3,"file":"genomeSpy.d.ts","sourceRoot":"","sources":["../../src/genomeSpy.js"],"names":[],"mappings":"AA0CA;IACI;;;;;OAKG;IAEH;;;;;OAKG;IACH,uBAJW,WAAW,qDAEX,OAAO,qBAAqB,EAAE,YAAY,EA+EpD;IA5EG,uBAA0B;IAM1B,sCAAsC;IACtC,wCAAgB;IAEhB,iCAA4C;IAC5C,yBAAoC;IAEpC,4CAA4C;IAC5C,oBADW,QAAU,MAAM,KAAE,MAAM,EAAE,CAAC,EAAE,CACZ;IAE5B,mBAAoD;IAEpD,0BAA0B;IAC1B,aADW,WAAW,CACM;IAE5B;;;;;OAKG;IACH,qEAF0B,OAAO,CAE8B;IAE/D,2CAA2C;IAC3C,mBADW,4BAA4B,CACL;IAClC,2CAA2C;IAC3C,iBADW,4BAA4B,CACP;IAEhC,oDAAoD;IACpD,6BAAgC;IAEhC;;;OAGG;IACH,eAFU;QAAE,IAAI,EAAE,OAAO,iBAAiB,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,oBAAoB,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAEpF;IAE9B,uBAA+C;IAE/C;;;OAGG;IACH,oBAFU,IAAI,MAAM,EAAE,QAAU,aAAa,KAAE,IAAI,CAAC,EAAE,CAAC,CAEpB;IAEnC;;;;;;OAMG;IACH,yCAFkC,GAAG,KAAK,IAAI,GAEd;IAEhC;;;OAGG;IACH,kDAFkC,GAAG,KAAK,IAAI,GAEL;IAEzC,oFAAoF;IACpF,iBADW,OAAO,MAAM,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC,CAK9E;IAED,mBAAmB;IACnB,2CAAyB;IAEzB,0BAAqC;IAGzC;;;OAGG;IACH,2CAFkB,MAAM,KAAK,GAAG,EAAE,QAIjC;IAED;;OAEG;IACH,+BAFW,MAAM,YAShB;IAED;;;;OAIG;IACH,sBAHW,MAAM,QACN,GAAG,EAAE,QAaf;IAED;;;;;OAKG;IACH,gBAHW,kBAAkB,YAClB,GAAG,QAQb;IAED,0BA2BC;IAvBG,uBAOC;IAED,sCAA0D;IAK1D,iBAA0C;IAW9C;;OAEG;IACH,gBAiBC;IAED,sCAyMC;IAED;;;OAGG;IACH,UAFa,QAAQ,OAAO,CAAC,CAqC5B;IAED,4BA6IC;IAjIe,iCAAoC;IAmIpD;;;OAGG;IACH,kBAHW,MAAM,KACN,MAAM,QAiEhB;IAED;;;;;;;OAOG;IACH,oDAHuB,QAAQ,MAAM,GAAG,WAAW,GAAG,OAAO,KAAK,EAAE,cAAc,CAAC,QAYlF;IAED,sBAyCC;IAED,kBAIC;IAED,iCAOC;IAED,iCASC;IAED,qFAWC;CACJ;;;;iCArvBY,eAAe,GAAG,YAAY,GAAG,QAAQ,GAAG,gBAAgB;4BAhC7C,uBAAuB;4BA0BP,uBAAuB;qBAZ9C,qBAAqB;wBAIlB,yBAAyB;yCARR,yDAAyD;oBAYvD,oBAAoB;wBAMvC,kBAAkB;wBApBlB,qBAAqB;oBAVzB,uBAAuB;qBAQtB,oBAAoB"}
1
+ {"version":3,"file":"genomeSpy.d.ts","sourceRoot":"","sources":["../../src/genomeSpy.js"],"names":[],"mappings":"AA6CA;IACI;;;;;OAKG;IAEH;;;;;OAKG;IACH,uBAJW,WAAW,qDAEX,OAAO,qBAAqB,EAAE,YAAY,EAyFpD;IAtFG,uBAA0B;IAE1B,6BAA6B;IAC7B,uBADW,CAAC,MAAM,IAAI,CAAC,EAAE,CACM;IAM/B,sCAAsC;IACtC,wCAAgB;IAEhB,iCAA4C;IAC5C,yBAAoC;IAEpC,4CAA4C;IAC5C,oBADW,QAAU,MAAM,KAAE,MAAM,EAAE,CAAC,EAAE,CACZ;IAE5B,mBAAoD;IAEpD,0BAA0B;IAC1B,aADW,WAAW,CACM;IAE5B;;;;;OAKG;IACH,qEAF0B,OAAO,CAE8B;IAE/D,2CAA2C;IAC3C,mBADW,4BAA4B,CACL;IAClC,2CAA2C;IAC3C,iBADW,4BAA4B,CACP;IAEhC,oDAAoD;IACpD,6BAAgC;IAEhC;;;OAGG;IACH,eAFU;QAAE,IAAI,EAAE,OAAO,iBAAiB,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,oBAAoB,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAEpF;IAE9B,uBAA+C;IAE/C;;;OAGG;IACH,oBAFU,IAAI,MAAM,EAAE,QAAU,aAAa,KAAE,IAAI,CAAC,EAAE,CAAC,CAEpB;IAEnC;;;;;;OAMG;IACH,yCAFkC,GAAG,KAAK,IAAI,GAEd;IAEhC;;;OAGG;IACH,kDAFkC,GAAG,KAAK,IAAI,GAEL;IAEzC,oFAAoF;IACpF,iBADW,OAAO,MAAM,EAAE,OAAO,6BAA6B,EAAE,cAAc,CAAC,CAK9E;IAED,mBAAmB;IACnB,2CAAyB;IAEzB,0BAAqC;IAErC;;;;OAIG;IACH,8DAA8B;IAGlC;;;OAGG;IACH,2CAFkB,MAAM,KAAK,GAAG,EAAE,QAIjC;IAED;;OAEG;IACH,+BAFW,MAAM,YAShB;IAED;;;;OAIG;IACH,sBAHW,MAAM,QACN,GAAG,EAAE,QAaf;IAED;;;;;OAKG;IACH,gBAHW,kBAAkB,YAClB,GAAG,QAQb;IAED;;;OAGG;IACH,iCAyCC;IAED,0BA0EC;IAtEG,uBAOC;IAyCD,sCAA0D;IAS1D,yCAA6D;IAI7D,iBAA0C;IAW9C;;OAEG;IACH,gBAmBC;IAED,sCA8MC;IAED;;;OAGG;IACH,UAFa,QAAQ,OAAO,CAAC,CA6B5B;IAED,4BA6IC;IAjIe,iCAAoC;IAmIpD;;;OAGG;IACH,kBAHW,MAAM,KACN,MAAM,QAiEhB;IAED;;;;;;;OAOG;IACH,oDAHuB,QAAQ,MAAM,GAAG,WAAW,GAAG,OAAO,KAAK,EAAE,cAAc,CAAC,QAYlF;IAED,sBAyCC;IAED,kBAIC;IAED,iCAOC;IAED,iCASC;IAED,qFAWC;CACJ;;;;iCA51BY,eAAe,GAAG,YAAY,GAAG,QAAQ,GAAG,gBAAgB;4BAhC7C,uBAAuB;4BA0BP,uBAAuB;qBAZ9C,qBAAqB;wBAIlB,yBAAyB;yCARR,yDAAyD;oBAYvD,oBAAoB;wBAMvC,kBAAkB;wBApBlB,qBAAqB;oBAVzB,uBAAuB;qBAQtB,oBAAoB"}
@@ -1,4 +1,7 @@
1
1
  import { formats as vegaFormats } from "vega-loader";
2
+ import { html, render } from "lit-html";
3
+ import { styleMap } from "lit/directives/style-map.js";
4
+ import SPINNER from "./img/90-ring-with-bg.svg";
2
5
 
3
6
  import css from "./styles/genome-spy.css.js";
4
7
  import Tooltip from "./utils/ui/tooltip.js";
@@ -57,6 +60,9 @@ export default class GenomeSpy {
57
60
  constructor(container, spec, options = {}) {
58
61
  this.container = container;
59
62
 
63
+ /** @type {(() => void)[]} */
64
+ this._destructionCallbacks = [];
65
+
60
66
  const styleElement = document.createElement("style");
61
67
  styleElement.innerHTML = css;
62
68
  container.appendChild(styleElement);
@@ -131,6 +137,13 @@ export default class GenomeSpy {
131
137
  this.viewRoot = undefined;
132
138
 
133
139
  this._paramBroker = new ParamBroker();
140
+
141
+ /**
142
+ * Views that are currently loading data using lazy sources.
143
+ *
144
+ * @type {Map<View, boolean>}
145
+ */
146
+ this._loadingViews = new Map();
134
147
  }
135
148
 
136
149
  /**
@@ -185,6 +198,53 @@ export default class GenomeSpy {
185
198
  ?.forEach((listener) => listener(message));
186
199
  }
187
200
 
201
+ /**
202
+ * Draw some layers on top of the canvas. It's easier to do fancy spinning
203
+ * animations with html elements than with WebGL.
204
+ */
205
+ _updateLoadingIndicators() {
206
+ /** @type {import("lit-html").TemplateResult[]} */
207
+ const indicators = [];
208
+
209
+ const isSomethingVisible = () =>
210
+ [...this._loadingViews.values()].some((v) => v);
211
+
212
+ for (const [view, status] of this._loadingViews) {
213
+ const c = view.coords;
214
+ if (c) {
215
+ const style = {
216
+ left: `${c.x}px`,
217
+ top: `${c.y}px`,
218
+ width: `${c.width}px`,
219
+ height: `${c.height}px`,
220
+ };
221
+ indicators.push(
222
+ html`<div style=${styleMap(style)}>
223
+ <div class=${status ? "loading" : ""}>
224
+ <img src="${SPINNER}" alt="" />
225
+ <span>Loading...</span>
226
+ </div>
227
+ </div>`
228
+ );
229
+ }
230
+ }
231
+
232
+ // Do some hacks to stop css animations of the loading indicators.
233
+ // Otherwise they fire animation frames even when their opacity is zero.
234
+ if (isSomethingVisible()) {
235
+ this.loadingIndicatorsElement.style.display = "block";
236
+ } else {
237
+ // TODO: Clear previous timeout
238
+ setTimeout(() => {
239
+ if (!isSomethingVisible()) {
240
+ this.loadingIndicatorsElement.style.display = "none";
241
+ }
242
+ }, 3000);
243
+ }
244
+
245
+ render(indicators, this.loadingIndicatorsElement);
246
+ }
247
+
188
248
  _prepareContainer() {
189
249
  this.container.classList.add("genome-spy");
190
250
  this.container.classList.add("loading");
@@ -198,11 +258,58 @@ export default class GenomeSpy {
198
258
  this.spec.background
199
259
  );
200
260
 
261
+ const resizeCallback = () => {
262
+ this._glHelper.invalidateSize();
263
+ dprSetter(window.devicePixelRatio);
264
+ this.computeLayout();
265
+ // Render immediately, without RAF
266
+ this.renderAll();
267
+ };
268
+
269
+ // TODO: Size should be observed only if the content is not absolutely sized
270
+ const resizeObserver = new ResizeObserver(resizeCallback);
271
+ resizeObserver.observe(this.container);
272
+ this._destructionCallbacks.push(() => resizeObserver.disconnect());
273
+
274
+ const dprSetter = this._paramBroker.allocateSetter("devicePixelRatio");
275
+ dprSetter(window.devicePixelRatio);
276
+
277
+ /** @type {() => void} */
278
+ let remove = null;
279
+
280
+ const updatePixelRatio = () => {
281
+ if (remove != null) {
282
+ remove();
283
+ resizeCallback();
284
+ }
285
+ const media = matchMedia(
286
+ `(resolution: ${window.devicePixelRatio}dppx)`
287
+ );
288
+ media.addEventListener("change", updatePixelRatio);
289
+ remove = () => {
290
+ media.removeEventListener("change", updatePixelRatio);
291
+ };
292
+ };
293
+ updatePixelRatio();
294
+
295
+ if (remove) {
296
+ this._destructionCallbacks.push(remove);
297
+ }
298
+
299
+ // The initial loading message that is shown until the first frame is rendered
201
300
  this.loadingMessageElement = document.createElement("div");
202
301
  this.loadingMessageElement.className = "loading-message";
203
302
  this.loadingMessageElement.innerHTML = `<div class="message">Loading<span class="ellipsis">...</span></div>`;
204
303
  this.container.appendChild(this.loadingMessageElement);
205
304
 
305
+ // A container for loading indicators (for lazy data sources.)
306
+ // These could alternatively be included in the view hierarchy,
307
+ // but it's easier this way – particularly if we want to show
308
+ // some fancy animated spinners.
309
+ this.loadingIndicatorsElement = document.createElement("div");
310
+ this.loadingIndicatorsElement.className = "loading-indicators";
311
+ this.container.appendChild(this.loadingIndicatorsElement);
312
+
206
313
  this.tooltip = new Tooltip(this.container);
207
314
 
208
315
  this.loadingMessageElement
@@ -229,6 +336,8 @@ export default class GenomeSpy {
229
336
  }
230
337
  }
231
338
 
339
+ this._destructionCallbacks.forEach((callback) => callback());
340
+
232
341
  this._glHelper.finalize();
233
342
 
234
343
  while (this.container.firstChild) {
@@ -267,6 +376,11 @@ export default class GenomeSpy {
267
376
  getNamedDataFromProvider: this.getNamedDataFromProvider.bind(this),
268
377
  getCurrentHover: () => this._currentHover,
269
378
 
379
+ setDataLoadingStatus: (view, status) => {
380
+ this._loadingViews.set(view, status);
381
+ this._updateLoadingIndicators();
382
+ },
383
+
270
384
  addKeyboardListener: (type, listener) => {
271
385
  // TODO: Listeners should be called only when the mouse pointer is inside the
272
386
  // container or the app covers the full document.
@@ -454,14 +568,6 @@ export default class GenomeSpy {
454
568
  this.computeLayout();
455
569
  this.animator.requestRender();
456
570
 
457
- // Register resize listener after the initial layout computation to prevent
458
- // incomplete layouts from accidentally polluting any caches related to sizes.
459
- this._glHelper.addEventListener("resize", () => {
460
- this.computeLayout();
461
- // Render immediately, without RAF
462
- this.renderAll();
463
- });
464
-
465
571
  return true;
466
572
  } catch (reason) {
467
573
  const message = `${
@@ -32,16 +32,11 @@ export default class WebGLHelper {
32
32
  }, clearColor?: string);
33
33
  _container: HTMLElement;
34
34
  _sizeSource: () => {
35
- width: number;
36
- height: number;
35
+ width: any;
36
+ height: any;
37
37
  };
38
38
  /** @type {Map<string, WebGLShader>} */
39
39
  _shaderCache: Map<string, WebGLShader>;
40
- /** @type {{ type: string, listener: function}[]} */
41
- _listeners: {
42
- type: string;
43
- listener: Function;
44
- }[];
45
40
  /** @type {WeakMap<import("../view/scaleResolution.js").default, WebGLTexture>} */
46
41
  rangeTextures: WeakMap<import("../view/scaleResolution.js").default, WebGLTexture>;
47
42
  canvas: HTMLCanvasElement;
@@ -49,13 +44,12 @@ export default class WebGLHelper {
49
44
  /** @type {import("twgl.js").AttachmentOptions[]} */
50
45
  _pickingAttachmentOptions: import("twgl.js").AttachmentOptions[];
51
46
  _pickingBufferInfo: import("twgl.js").FramebufferInfo;
52
- _resizeObserver: ResizeObserver;
53
47
  /** @type {[number, number, number, number]} */
54
48
  _clearColor: [number, number, number, number];
55
49
  invalidateSize(): void;
56
50
  _logicalCanvasSize: {
57
- width: number;
58
- height: number;
51
+ width: any;
52
+ height: any;
59
53
  };
60
54
  _updateDpr(): void;
61
55
  dpr: number;
@@ -84,18 +78,9 @@ export default class WebGLHelper {
84
78
  * Returns the canvas size in logical pixels (without devicePixelRatio correction)
85
79
  */
86
80
  getLogicalCanvasSize(): {
87
- width: number;
88
- height: number;
81
+ width: any;
82
+ height: any;
89
83
  };
90
- /**
91
- * @param {"render"|"resize"} eventType
92
- * @param {function} listener
93
- */
94
- addEventListener(eventType: "render" | "resize", listener: Function): void;
95
- /**
96
- * @param {string} eventType
97
- */
98
- _emit(eventType: string): void;
99
84
  /**
100
85
  *
101
86
  * @param {number} x
@@ -1 +1 @@
1
- {"version":3,"file":"webGLHelper.d.ts","sourceRoot":"","sources":["../../../src/gl/webGLHelper.js"],"names":[],"mappings":"AA6bA;;;;GAIG;AACH,kCAJW,sBAAsB,gBACtB,WAAW,kBACX,WAAW;;;;;;EA8CrB;AAED;;;;;GAKG;AACH,0CALW,qBAAqB,WACrB,KAAK,OAAO,SAAS,EAAE,cAAc,EAAE,KAAK,CAAC,OAC7C,MAAM,EAAE,GAAG,eAAe,YAC1B,YAAY,gBAYtB;AAleD;IACI;;;;;;;OAOG;IACH,uBANW,WAAW,eACX,MAAM;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,eAGrC,MAAM,EA2FhB;IAxFG,wBAA2B;IAC3B,mBAPa;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,CAOf;IAE7B,uCAAuC;IACvC,cADW,IAAI,MAAM,EAAE,WAAW,CAAC,CACN;IAE7B,oDAAoD;IACpD,YADW;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,WAAU;KAAC,EAAE,CAC5B;IAEpB,kFAAkF;IAClF,eADW,QAAQ,OAAO,4BAA4B,EAAE,OAAO,EAAE,YAAY,CAAC,CAC5C;IAuClC,0BAAoB;IACpB,2BAAY;IAGZ,oDAAoD;IACpD,2BADW,OAAO,SAAS,EAAE,iBAAiB,EAAE,CAQ/C;IACD,sDAGC;IAMD,gCAGE;IAQF,+CAA+C;IAC/C,aADW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CACZ;IAOnC,uBAIC;IAHG;;;MAAmC;IAKvC,mBAEC;IADG,YAAkC;IAGtC;;;;;OAKG;IACH,oBAHW,MAAM,QACN,MAAM,GAAG,MAAM,EAAE,eA2B3B;IAED,iBAcC;IAED,iBAGC;IAED;;;;OAIG;IACH,oCAFW;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;;;MAQ3C;IAED;;OAEG;IACH;;;MA0BC;IAED;;;OAGG;IACH,4BAHW,QAAQ,GAAC,QAAQ,4BAK3B;IAED;;OAEG;IACH,iBAFW,MAAM,QAQhB;IAED;;;;OAIG;IACH,oBAHW,MAAM,KACN,MAAM,cAwBhB;IAED,iBAOC;IAED;;;;;;;;;OASG;IACH,+BAHW,OAAO,4BAA4B,EAAE,OAAO,WAC5C,OAAO,QA4GjB;CACJ"}
1
+ {"version":3,"file":"webGLHelper.d.ts","sourceRoot":"","sources":["../../../src/gl/webGLHelper.js"],"names":[],"mappings":"AA8ZA;;;;GAIG;AACH,kCAJW,sBAAsB,gBACtB,WAAW,kBACX,WAAW;;;;;;EA8CrB;AAED;;;;;GAKG;AACH,0CALW,qBAAqB,WACrB,KAAK,OAAO,SAAS,EAAE,cAAc,EAAE,KAAK,CAAC,OAC7C,MAAM,EAAE,GAAG,eAAe,YAC1B,YAAY,gBAYtB;AAncD;IACI;;;;;;;OAOG;IACH,uBANW,WAAW,eACX,MAAM;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAC,eAGrC,MAAM,EAmFhB;IAhFG,wBAA2B;IAC3B;;;MAKO;IAEP,uCAAuC;IACvC,cADW,IAAI,MAAM,EAAE,WAAW,CAAC,CACN;IAE7B,kFAAkF;IAClF,eADW,QAAQ,OAAO,4BAA4B,EAAE,OAAO,EAAE,YAAY,CAAC,CAC5C;IAuClC,0BAAoB;IACpB,2BAAY;IAGZ,oDAAoD;IACpD,2BADW,OAAO,SAAS,EAAE,iBAAiB,EAAE,CAQ/C;IACD,sDAGC;IAOD,+CAA+C;IAC/C,aADW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CACZ;IAOnC,uBAIC;IAHG;;;MAAmC;IAKvC,mBAEC;IADG,YAAkC;IAGtC;;;;;OAKG;IACH,oBAHW,MAAM,QACN,MAAM,GAAG,MAAM,EAAE,eA2B3B;IAED,iBAcC;IAED,iBAEC;IAED;;;;OAIG;IACH,oCAFW;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;;;MAQ3C;IAED;;OAEG;IACH;;;MAuBC;IAED;;;;OAIG;IACH,oBAHW,MAAM,KACN,MAAM,cAwBhB;IAED,iBAOC;IAED;;;;;;;;;OASG;IACH,+BAHW,OAAO,4BAA4B,EAAE,OAAO,WAC5C,OAAO,QA4GjB;CACJ"}
@@ -39,14 +39,16 @@ export default class WebGLHelper {
39
39
  */
40
40
  constructor(container, sizeSource, clearColor) {
41
41
  this._container = container;
42
- this._sizeSource = sizeSource;
42
+ this._sizeSource =
43
+ sizeSource ??
44
+ (() => ({
45
+ width: undefined,
46
+ height: undefined,
47
+ }));
43
48
 
44
49
  /** @type {Map<string, WebGLShader>} */
45
50
  this._shaderCache = new Map();
46
51
 
47
- /** @type {{ type: string, listener: function}[]} */
48
- this._listeners = [];
49
-
50
52
  /** @type {WeakMap<import("../view/scaleResolution.js").default, WebGLTexture>} */
51
53
  this.rangeTextures = new WeakMap();
52
54
 
@@ -108,16 +110,6 @@ export default class WebGLHelper {
108
110
 
109
111
  this.adjustGl();
110
112
 
111
- // TODO: Size should be observed only if the content is not absolutely sized
112
- this._resizeObserver = new ResizeObserver((entries) => {
113
- this.invalidateSize();
114
- this._emit("resize");
115
- });
116
- this._resizeObserver.observe(this._container);
117
-
118
- // TODO: Observe devicePixelRatio
119
- // https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Monitoring_screen_resolution_or_zoom_level_changes
120
-
121
113
  this._updateDpr();
122
114
 
123
115
  /** @type {[number, number, number, number]} */
@@ -188,7 +180,6 @@ export default class WebGLHelper {
188
180
  }
189
181
 
190
182
  finalize() {
191
- this._resizeObserver.unobserve(this._container);
192
183
  this.canvas.remove();
193
184
  }
194
185
 
@@ -214,10 +205,7 @@ export default class WebGLHelper {
214
205
  }
215
206
 
216
207
  // TODO: The size should never be smaller than the minimum content size!
217
- const contentSize = this._sizeSource?.() ?? {
218
- width: undefined,
219
- height: undefined,
220
- };
208
+ const contentSize = this._sizeSource();
221
209
 
222
210
  const cs = window.getComputedStyle(this._container, null);
223
211
  const width =
@@ -236,25 +224,6 @@ export default class WebGLHelper {
236
224
  return this._logicalCanvasSize;
237
225
  }
238
226
 
239
- /**
240
- * @param {"render"|"resize"} eventType
241
- * @param {function} listener
242
- */
243
- addEventListener(eventType, listener) {
244
- this._listeners.push({ type: eventType, listener });
245
- }
246
-
247
- /**
248
- * @param {string} eventType
249
- */
250
- _emit(eventType) {
251
- for (const entry of this._listeners) {
252
- if (entry.type === eventType) {
253
- entry.listener();
254
- }
255
- }
256
- }
257
-
258
227
  /**
259
228
  *
260
229
  * @param {number} x
@@ -0,0 +1 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_ajPY{transform-origin:center;animation:spinner_AtaB .75s infinite linear}@keyframes spinner_AtaB{100%{transform:rotate(360deg)}}</style><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/><path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" class="spinner_ajPY"/></svg>
@@ -0,0 +1,5 @@
1
+ # Image assets
2
+
3
+ ## Spinner svg
4
+
5
+ Spinner: 90-ring-with-bg.svg, MIT License, https://github.com/n3r4zzurr0/svg-spinners