igv 3.5.1 → 3.5.3

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.
package/dist/igv.js CHANGED
@@ -8708,7 +8708,7 @@
8708
8708
  const response = await fetch(endPoint);
8709
8709
  let json = await response.json();
8710
8710
  if (json.error && json.error.code === 404) {
8711
- let scope = "https://www.googleapis.com/auth/drive.readonly";
8711
+ let scope = "https://www.googleapis.com/auth/drive.file";
8712
8712
  const access_token = await getAccessToken(scope);
8713
8713
  if (access_token) {
8714
8714
  const response = await fetch(endPoint, {
@@ -8890,6 +8890,10 @@
8890
8890
 
8891
8891
  class IGVXhr {
8892
8892
 
8893
+ UCSC_HOST = "hgdownload.soe.ucsc.edu"
8894
+ UCSC_BACKUP_HOST = "genome-browser.s3.us-east-1.amazonaws.com"
8895
+
8896
+
8893
8897
  constructor() {
8894
8898
  this.apiKey = undefined;
8895
8899
  this.googleThrottle = new Throttle({
@@ -8897,6 +8901,7 @@
8897
8901
  });
8898
8902
  this.RANGE_WARNING_GIVEN = false;
8899
8903
  this.oauth = new Oauth();
8904
+ this.corsProxy = undefined;
8900
8905
  }
8901
8906
 
8902
8907
  setApiKey(key) {
@@ -8921,7 +8926,7 @@
8921
8926
  * @param options
8922
8927
  * @returns {Promise<Uint8Array>}
8923
8928
  */
8924
- async loadByteArray(url, options) {
8929
+ async loadByteArray(url, options) {
8925
8930
  const arraybuffer = await this.loadArrayBuffer(url, options);
8926
8931
  let plain;
8927
8932
  if (isgzipped(arraybuffer)) {
@@ -8966,7 +8971,7 @@
8966
8971
 
8967
8972
  if (isFile(url)) {
8968
8973
  return this._loadFileSlice(url, options)
8969
- } else if (typeof url.startsWith === 'function') { // Test for string
8974
+ } else if (isString$3(url)) {
8970
8975
  if (url.startsWith("data:")) {
8971
8976
  const buffer = decodeDataURI$1(url).buffer;
8972
8977
  if (options.range) {
@@ -8996,6 +9001,7 @@
8996
9001
 
8997
9002
  const self = this;
8998
9003
  const _url = url; // The unmodified URL, needed in case of an oAuth retry
9004
+ const {host} = parseUri(_url);
8999
9005
 
9000
9006
  url = mapUrl$1(url);
9001
9007
 
@@ -9076,8 +9082,10 @@
9076
9082
 
9077
9083
  xhr.onload = async function (event) {
9078
9084
 
9085
+ const isFileProtocol = url.toLowerCase().startsWith("file:");
9086
+
9079
9087
  // when the url points to a local file, the status is 0
9080
- if (xhr.status === 0 || (xhr.status >= 200 && xhr.status <= 300)) {
9088
+ if ((xhr.status >= 200 && xhr.status <= 300) || (isFileProtocol && xhr.status === 0)) {
9081
9089
  if ("HEAD" === options.method) {
9082
9090
  // Support fetching specific headers. Attempting to fetch all headers can be problematic with CORS
9083
9091
  const headers = options.requestedHeaders || ['content-length'];
@@ -9093,7 +9101,7 @@
9093
9101
  // For small files a range starting at 0 can return the whole file => 200
9094
9102
  // Provide just the slice we asked for, throw out the rest quietly
9095
9103
  // If file is large warn user
9096
- if (xhr.response.length > 100000 && !self.RANGE_WARNING_GIVEN) {
9104
+ if (xhr.response.length > 1000000 && !self.RANGE_WARNING_GIVEN) {
9097
9105
  alert(`Warning: Range header ignored for URL: ${url}. This can have severe performance impacts.`);
9098
9106
  }
9099
9107
  resolve(xhr.response.slice(range.start, range.start + range.size));
@@ -9110,20 +9118,29 @@
9110
9118
  tryGoogleAuth();
9111
9119
 
9112
9120
  } else {
9121
+ const error = new Error(`Error accessing resource: ${url} status: ${xhr.status}`);
9113
9122
  if (xhr.status === 403) {
9114
9123
  handleError("Access forbidden: " + url);
9124
+ } else if (host === self.UCSC_HOST) {
9125
+ tryUcscBackup(self.UCSC_HOST, self.UCSC_BACKUP_HOST, error);
9126
+ } else if (xhr.status === 0 && self.corsProxy && !options.corsProxyRetried) {
9127
+ tryCorsProxy(error);
9115
9128
  } else {
9116
- handleError(xhr.status);
9129
+ handleError(error);
9117
9130
  }
9118
9131
  }
9119
9132
  };
9120
9133
 
9121
-
9122
9134
  xhr.onerror = function (event) {
9135
+ const error = new Error(`Error accessing resource: ${url} status: ${xhr.status}`);
9123
9136
  if (isGoogleURL(url) && !options.retries) {
9124
9137
  tryGoogleAuth();
9138
+ } else if (host === self.UCSC_HOST) {
9139
+ tryUcscBackup(self.UCSC_HOST, self.UCSC_BACKUP_HOST, error);
9140
+ } else if (self.corsProxy && !options.corsProxyRetried) {
9141
+ tryCorsProxy(error);
9125
9142
  } else {
9126
- handleError("Error accessing resource: " + url + " Status: " + xhr.status);
9143
+ handleError(error);
9127
9144
  }
9128
9145
  };
9129
9146
 
@@ -9155,6 +9172,27 @@
9155
9172
  }
9156
9173
  }
9157
9174
 
9175
+ async function tryCorsProxy(error) {
9176
+ options.corsProxyRetried = true;
9177
+ const proxyUrl = self.corsProxy + (_url.includes("?") ? "&" : "?") + "url=" + encodeURIComponent(_url);
9178
+ try {
9179
+ const result = await self._loadURL(proxyUrl, options);
9180
+ resolve(result);
9181
+ } catch (e) {
9182
+ handleError(error);
9183
+ }
9184
+ }
9185
+
9186
+ async function tryUcscBackup(UCSC_HOST, UCSC_BACKUP_HOST, error) {
9187
+ const backupUrl = _url.replace(UCSC_HOST, UCSC_BACKUP_HOST);
9188
+ try {
9189
+ const result = await self._loadURL(backupUrl, options);
9190
+ resolve(result);
9191
+ } catch (e) {
9192
+ handleError(error);
9193
+ }
9194
+ }
9195
+
9158
9196
  async function tryGoogleAuth() {
9159
9197
  try {
9160
9198
  const accessToken = await fetchGoogleAccessToken(_url);
@@ -9173,6 +9211,8 @@
9173
9211
  }
9174
9212
  }
9175
9213
  }
9214
+
9215
+
9176
9216
  })
9177
9217
 
9178
9218
  }
@@ -9245,20 +9285,29 @@
9245
9285
  }
9246
9286
 
9247
9287
  /**
9248
- * This method should only be called when it is known the server supports HEAD requests. It is used to recover
9249
- * from 416 errors from out-of-spec WRT range request servers. Notably Globus.
9250
- * * *
9288
+ * Return the content length of the file at the given URL. This is not guaranteed to succeed, some servers
9289
+ * do not support or allow the content-length header.
9290
+ *
9251
9291
  * @param url
9252
9292
  * @param options
9253
9293
  * @returns {Promise<unknown>}
9254
9294
  */
9255
9295
  async getContentLength(url, options) {
9256
- options = options || {};
9257
- options.method = 'HEAD';
9258
- options.requestedHeaders = ['content-length'];
9259
- const headerMap = await this._loadURL(url, options);
9260
- const contentLengthString = headerMap['content-length'];
9261
- return contentLengthString ? Number.parseInt(contentLengthString) : 0
9296
+ if (isFile(url)) {
9297
+ return url.size
9298
+ } else {
9299
+ try {
9300
+ options = options || {};
9301
+ options.method = 'HEAD';
9302
+ options.requestedHeaders = ['content-length'];
9303
+ const headerMap = await this._loadURL(url, options);
9304
+ const contentLengthString = headerMap['content-length'];
9305
+ return contentLengthString ? Number.parseInt(contentLengthString) : 0
9306
+ } catch (e) {
9307
+ console.error(e);
9308
+ return -1
9309
+ }
9310
+ }
9262
9311
  }
9263
9312
 
9264
9313
  }
@@ -20670,6 +20719,7 @@
20670
20719
 
20671
20720
  switch (config.format) {
20672
20721
  case "vcf":
20722
+ case "vcftabix":
20673
20723
  return new VcfParser(config)
20674
20724
  case "seg" :
20675
20725
  return new SegParser("seg")
@@ -21281,6 +21331,7 @@
21281
21331
  }
21282
21332
  }
21283
21333
 
21334
+ // Base class for feature sources. Subclasses must implement getFeatures().
21284
21335
  class BaseFeatureSource {
21285
21336
 
21286
21337
  constructor(genome) {
@@ -21342,59 +21393,10 @@
21342
21393
  }
21343
21394
  }
21344
21395
 
21345
- async previousFeature(chr, position, direction, visibilityWindow) {
21346
-
21347
- let chromosomeNames = this.genome.chromosomeNames || [chr];
21348
- let idx = chromosomeNames.indexOf(chr);
21349
- if (idx < 0) return // This shouldn't happen
21350
-
21351
- // Look ahead (or behind) in 10 kb intervals, but no further than visibilityWindow
21352
- const window = Math.min(10000, visibilityWindow || 10000);
21353
- let queryStart = direction ? position : Math.max(position - window, 0);
21354
- while (idx < chromosomeNames.length && idx >= 0) {
21355
- chr = chromosomeNames[idx];
21356
- const chromosome = this.genome.getChromosome(chr);
21357
- const chromosomeEnd = chromosome.bpLength;
21358
- while (queryStart < chromosomeEnd && queryStart >= 0) {
21359
- let queryEnd = Math.min(position, queryStart + window);
21360
- const featureList = await this.getFeatures({chr, start: queryStart, end: queryEnd, visibilityWindow});
21361
- if (featureList) {
21362
-
21363
- const compare = (o1, o2) => o1.start - o2.start + o1.end - o2.end;
21364
- const sortedList = Array.from(featureList);
21365
- sortedList.sort(compare);
21366
-
21367
- // Search for next or previous feature relative to centers. We use a linear search because the
21368
- // feature is likely to be near the first or end of the list
21369
- let idx = direction ? 0 : sortedList.length - 1;
21370
- while(idx >= 0 && idx < sortedList.length) {
21371
- const f = sortedList[idx];
21372
- const center = (f.start + f.end) / 2;
21373
- if(direction) {
21374
- if(center > position) return f
21375
- idx++;
21376
- } else {
21377
- if(center < position) return f
21378
- idx--;
21379
- }
21380
- }
21381
- }
21382
- queryStart = direction ? queryEnd : queryStart - window;
21383
- }
21384
- if (direction) {
21385
- idx++;
21386
- queryStart = 0;
21387
- position = 0;
21388
- } else {
21389
- idx--;
21390
- if (idx < 0) break
21391
- const prevChromosome = this.genome.getChromosome(chromosomeNames[idx]);
21392
- position = prevChromosome.bpLength;
21393
- queryStart = position - window;
21394
- }
21395
- }
21396
+ // Subclasses must implement
21397
+ async getFeatures({chr, start, end, bpPerPixel, visibilityWindow}) {
21398
+ throw new Error("getFeatures not implemented")
21396
21399
  }
21397
-
21398
21400
  }
21399
21401
 
21400
21402
  const GZIP_FLAG = 0x1;
@@ -26533,6 +26535,88 @@
26533
26535
  }
26534
26536
  }
26535
26537
 
26538
+ /**
26539
+ * A feature source for a "list" file. A list file is a text file with two columns, chromosome and URL. It was
26540
+ * created for the 1KG genotype files, which are split by chromosome, and at the moment that is the only use case.
26541
+ */
26542
+
26543
+ class ListFeatureSource extends BaseFeatureSource {
26544
+
26545
+ constructor(config, genome) {
26546
+ super(genome);
26547
+ this.config = config;
26548
+ this.featureSourceMap = null;
26549
+ this.header = null;
26550
+ }
26551
+
26552
+ async getHeader() {
26553
+
26554
+ if (!this.header) {
26555
+
26556
+ if (!this.featureSourceMap) {
26557
+ await this.init();
26558
+ }
26559
+ // Return the header from the first feature source. It is assumed that all sources have a common header.
26560
+ const firstFS = this.featureSourceMap.values().next().value;
26561
+ if (firstFS && firstFS.getHeader) {
26562
+ this.header = firstFS.getHeader();
26563
+ } else {
26564
+ this.header = Promise.resolve(undefined);
26565
+ }
26566
+ }
26567
+
26568
+ return this.header
26569
+
26570
+ }
26571
+
26572
+ async getFeatures({chr, start, end, bpPerPixel, visibilityWindow}) {
26573
+
26574
+ if (!this.featureSourceMap) {
26575
+ await this.init();
26576
+ }
26577
+ const fs = this.featureSourceMap.get(chr);
26578
+ if (fs) {
26579
+ return fs.getFeatures({chr, start, end, bpPerPixel, visibilityWindow})
26580
+ } else {
26581
+ return []
26582
+ }
26583
+ }
26584
+
26585
+ async init() {
26586
+ this.featureSourceMap = new Map();
26587
+
26588
+ const options = buildOptions(this.config);
26589
+ const data = await igvxhr.loadByteArray(this.config.url, options);
26590
+ const dataWrapper = getDataWrapper(data);
26591
+
26592
+ let line;
26593
+ while ((line = dataWrapper.nextLine()) !== undefined) {
26594
+ const trimmed = line.trim();
26595
+ if (!trimmed.startsWith('#')) {
26596
+ const tokens = trimmed.split(/\s+/);
26597
+ if (tokens.length > 1) {
26598
+ const chr = tokens[0];
26599
+ const path = tokens[1];
26600
+ const sourceConfig = Object.assign({}, this.config);
26601
+ sourceConfig.url = path;
26602
+ if (path.endsWith(".vcf.gz")) {
26603
+ sourceConfig.format = "vcf";
26604
+ sourceConfig.indexURL = path + ".tbi";
26605
+ }
26606
+ this.featureSourceMap.set(chr, FeatureSource(sourceConfig, this.genome));
26607
+ }
26608
+ }
26609
+ }
26610
+ }
26611
+
26612
+ supportWholeGenome() {
26613
+ return false
26614
+ }
26615
+ }
26616
+
26617
+ // chrY https://1000genomes.s3.amazonaws.com/release/20130502/ALL.chrY.phase3_integrated_v1b.20130502.genotypes.vcf.gz
26618
+ // chrX https://1000genomes.s3.amazonaws.com/release/20130502/ALL.chrX.phase3_shapeit2_mvncall_integrated_v1b.20130502.genotypes.vcf.gz
26619
+
26536
26620
  const bbFormats = new Set(['bigwig', 'bw', 'bigbed', 'bb', 'biginteract', 'biggenepred', 'bignarrowpeak']);
26537
26621
 
26538
26622
  function FeatureSource(config, genome) {
@@ -26547,6 +26631,9 @@
26547
26631
  return new TDFSource(config, genome)
26548
26632
  } else if ("gbk" === format) {
26549
26633
  return new GenbankFeatureSource(config, genome)
26634
+ } else if ("vcf.list" === format) {
26635
+ // This is a text file with two columns: <chr> <url to vcf>
26636
+ return new ListFeatureSource(config, genome)
26550
26637
  } else {
26551
26638
  return new TextFeatureSource(config, genome)
26552
26639
  }
@@ -27017,14 +27104,14 @@
27017
27104
  if (phase) {
27018
27105
  stringA = sequenceInterval.getSequence(phaseExtentStart, phaseExtentEnd);
27019
27106
 
27020
- if (undefined === stringA) {
27107
+ if (!stringA) {
27021
27108
  return undefined
27022
27109
  }
27023
27110
 
27024
27111
  [ss, ee] = [getEonStart(riteExon), getEonStart(riteExon) + (3 - phase)];
27025
27112
  stringB = sequenceInterval.getSequence(ss, ee);
27026
27113
 
27027
- if (undefined === stringB) {
27114
+ if (!stringB) {
27028
27115
  return undefined
27029
27116
  }
27030
27117
 
@@ -27036,7 +27123,7 @@
27036
27123
  if (remainder) {
27037
27124
  stringB = sequenceInterval.getSequence(remainder.start, remainder.end);
27038
27125
 
27039
- if (undefined === stringB) {
27126
+ if (!stringB) {
27040
27127
  return undefined
27041
27128
  }
27042
27129
 
@@ -27044,7 +27131,7 @@
27044
27131
  const leftEnd = getExonEnd(leftExon);
27045
27132
  stringA = sequenceInterval.getSequence(leftEnd - leftPhase, leftEnd);
27046
27133
 
27047
- if (undefined === stringA) {
27134
+ if (!stringA) {
27048
27135
  return undefined
27049
27136
  }
27050
27137
 
@@ -30409,6 +30496,19 @@
30409
30496
  const filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30410
30497
  "cpgIslandExtUnmasked", "windowMasker"]);
30411
30498
 
30499
+ const vizModeMap = new Map([
30500
+ ["pack", "EXPANDED"],
30501
+ ["full", "EXPANDED"],
30502
+ ["squish", "SQUISHED"],
30503
+ ["dense", "COLLAPSED"]
30504
+ ]);
30505
+ const typeFormatMap = new Map([
30506
+ ["vcftabix", "vcf"],
30507
+ ["vcfphasedtrio", "vcf"],
30508
+ ["bigdbsnp", "bigbed"],
30509
+ ["genepred", "refgene"]
30510
+ ]);
30511
+
30412
30512
  class TrackDbHub {
30413
30513
 
30414
30514
  constructor(trackStanzas, groupStanzas) {
@@ -30554,14 +30654,13 @@
30554
30654
  */
30555
30655
  #getTrackConfig(t) {
30556
30656
 
30557
- const format = t.format;
30657
+ const format = typeFormatMap.get(t.format) || t.format;
30558
30658
 
30559
30659
  const config = {
30560
30660
  "id": t.getProperty("track"),
30561
30661
  "name": t.getProperty("shortLabel"),
30562
30662
  "format": format,
30563
- "url": t.getProperty("bigDataUrl"),
30564
- "displayMode": t.displayMode,
30663
+ "url": t.getProperty("bigDataUrl")
30565
30664
  };
30566
30665
 
30567
30666
  if ("vcfTabix" === format) {
@@ -30611,9 +30710,14 @@
30611
30710
 
30612
30711
  }
30613
30712
  if (t.hasProperty("itemRgb")) ;
30614
- if ("hide" === t.getProperty("visibility")) {
30615
- // TODO -- this not supported yet
30616
- config.visible = false;
30713
+ if(t.hasProperty("visibility")) {
30714
+ if ("hide" === t.getProperty("visibility")) {
30715
+ // TODO -- this not supported yet
30716
+ config.visible = false;
30717
+ }
30718
+ else {
30719
+ config.displayMode = vizModeMap.get(t.getProperty("visibility")) || "COLLAPSED";
30720
+ }
30617
30721
  }
30618
30722
  if (t.hasProperty("url")) {
30619
30723
  config.infoURL = t.getProperty("url");
@@ -37794,6 +37898,8 @@
37794
37898
  case "tdf":
37795
37899
  return "wig"
37796
37900
  case "vcf":
37901
+ case "vcftabix":
37902
+ case "vcf.list":
37797
37903
  return "variant"
37798
37904
  case "seg":
37799
37905
  return "seg"
@@ -37848,7 +37954,7 @@
37848
37954
  } else if ("bam" === config.type) {
37849
37955
  config.type = "alignment";
37850
37956
  config.format = "bam";
37851
- } else if ("vcf" === config.type) {
37957
+ } else if ("vcf" === config.type || "vcftabix" === config.type) {
37852
37958
  config.type = "variant";
37853
37959
  config.format = "vcf";
37854
37960
  } else if ("t2d" === config.type) {
@@ -38128,7 +38234,7 @@
38128
38234
  const groupIndeces = NULL_GROUP !== this.groupBy ?
38129
38235
  this.sampleKeys.map(sample => this.getGroupIndex(sample)) : undefined;
38130
38236
  return {
38131
- names: this.sampleKeys,
38237
+ names: this.sampleKeys || [],
38132
38238
  height: this.sampleHeight,
38133
38239
  yOffset: 0,
38134
38240
  groups: this.groups,
@@ -42831,6 +42937,49 @@
42831
42937
  const end = Number.parseInt(se[1].replace(/,/g, ""));
42832
42938
  return new Locus({chr, start, end})
42833
42939
  }
42940
+
42941
+ /**
42942
+ * Return true if the locus string represents a single base, e.g. "chr1:12345" or "chr1:12345-12345"
42943
+ * @param locus
42944
+ * @returns {boolean}
42945
+ */
42946
+ static isSingleBaseLocusString(locus) {
42947
+
42948
+ if (!locus || typeof locus !== 'string') {
42949
+ return false
42950
+ }
42951
+
42952
+ const parts = locus.split(':');
42953
+ if (parts.length <= 1) {
42954
+ return false
42955
+ }
42956
+
42957
+ const range = parts[1].replace(/,/g, '');
42958
+ if (!range) {
42959
+ return false
42960
+ }
42961
+
42962
+ const rangeParts = range.split('-');
42963
+ const startString = rangeParts[0];
42964
+ const start = parseInt(startString, 10);
42965
+
42966
+ if (String(start) !== startString || !Number.isInteger(start)) {
42967
+ return false
42968
+ }
42969
+
42970
+ if (rangeParts.length === 1) {
42971
+ return true
42972
+ }
42973
+
42974
+ const endString = rangeParts[1];
42975
+ const end = parseInt(endString, 10);
42976
+
42977
+ if (String(end) !== endString || !Number.isInteger(end)) {
42978
+ return false
42979
+ }
42980
+
42981
+ return start === end
42982
+ }
42834
42983
  }
42835
42984
 
42836
42985
  /*!
@@ -62782,7 +62931,8 @@ ${indent}columns: ${matrix.columns}
62782
62931
  }
62783
62932
 
62784
62933
  get supportsWholeGenome() {
62785
- return !this.config.indexURL || this.config.supportsWholeGenome === true
62934
+ const sourceSupportsWG = typeof this.featureSource.supportsWholeGenome === 'function' && this.featureSource.supportsWholeGenome();
62935
+ return sourceSupportsWG || this.config.supportsWholeGenome === true
62786
62936
  }
62787
62937
 
62788
62938
  get color() {
@@ -62855,7 +63005,7 @@ ${indent}columns: ${matrix.columns}
62855
63005
  const yOffset = TOP_MARGIN + nVariantRows * (variantHeight + vGap);
62856
63006
 
62857
63007
  return {
62858
- names: this.sampleKeys,
63008
+ names: this.sampleKeys || [],
62859
63009
  yOffset,
62860
63010
  height,
62861
63011
  // groups: this.groups,
@@ -66986,7 +67136,7 @@ ${indent}columns: ${matrix.columns}
66986
67136
  })
66987
67137
  }
66988
67138
 
66989
- const _version = "3.5.1";
67139
+ const _version = "3.5.3";
66990
67140
  function version() {
66991
67141
  return _version
66992
67142
  }
@@ -74241,6 +74391,12 @@ ${indent}columns: ${matrix.columns}
74241
74391
 
74242
74392
  await this.loadTrackList(nonLocalTrackConfigurations);
74243
74393
 
74394
+ // If an initial locus is defined and represents a single basedo a "search" here. This will force micro
74395
+ // adjustments after width of track column(s) is known. This can be an issue when the center gide is shown
74396
+ // Without this adjustment the single base would be off center by a few pixels.
74397
+ if (session.locus && Locus.isSingleBaseLocusString(session.locus)) {
74398
+ await this.search(session.locus);
74399
+ }
74244
74400
  }
74245
74401
 
74246
74402
  cleanHouseForSession() {
@@ -74527,6 +74683,10 @@ ${indent}columns: ${matrix.columns}
74527
74683
  config = JSON.parse(config);
74528
74684
  }
74529
74685
 
74686
+ if(config.format && config.format.toLowerCase() === 'sampleinfo') {
74687
+ return this.loadSampleInfo(config)
74688
+ }
74689
+
74530
74690
  let track;
74531
74691
  try {
74532
74692
  track = await this.createTrack(config);
@@ -76303,6 +76463,10 @@ ${indent}columns: ${matrix.columns}
76303
76463
  return igvxhr.setOauthToken(accessToken, host)
76304
76464
  }
76305
76465
 
76466
+ function setCORSProxy(proxyURL) {
76467
+ igvxhr.corsProxy = proxyURL;
76468
+ }
76469
+
76306
76470
  // Backward compatibility
76307
76471
  const oauth = igvxhr.oauth;
76308
76472
 
@@ -76319,6 +76483,7 @@ ${indent}columns: ${matrix.columns}
76319
76483
  visibilityChange,
76320
76484
  setGoogleOauthToken,
76321
76485
  setOauthToken,
76486
+ setCORSProxy,
76322
76487
  oauth,
76323
76488
  version,
76324
76489
  setApiKey,