igv 3.2.6 → 3.3.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.
package/dist/igv.esm.js CHANGED
@@ -12874,6 +12874,20 @@ class NonIndexedFasta {
12874
12874
 
12875
12875
  async loadAll() {
12876
12876
 
12877
+
12878
+ const pushChromosome = (current, order) => {
12879
+ const length = current.length || (current.offset + current.seq.length);
12880
+ if (!chrNameSet.has(current.chr)) {
12881
+ this.sequences.set(current.chr, []);
12882
+ this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12883
+ chrNameSet.add(current.chr);
12884
+ } else {
12885
+ const c = this.chromosomes.get(current.chr);
12886
+ c.bpLength = Math.max(c.bpLength, length);
12887
+ }
12888
+ this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12889
+ };
12890
+
12877
12891
  let data;
12878
12892
  if (isDataURL(this.fastaURL)) {
12879
12893
  let bytes = decodeDataURI$1(this.fastaURL);
@@ -12887,70 +12901,62 @@ class NonIndexedFasta {
12887
12901
 
12888
12902
  const chrNameSet = new Set();
12889
12903
  const lines = splitLines$2(data);
12890
- const len = lines.length;
12891
- let lineNo = 0;
12892
12904
  let order = 0;
12893
- let nextLine;
12894
12905
  let current = {};
12895
- while (lineNo < len) {
12896
- nextLine = lines[lineNo++].trim();
12906
+ for (let nextLine of lines) {
12897
12907
  if (nextLine.startsWith("#") || nextLine.length === 0) ; else if (nextLine.startsWith(">")) {
12898
12908
  // Start the next sequence
12899
- if (current && current.seq) {
12900
- pushChromosome.call(this, current, order++);
12909
+ if (current.seq && current.seq.length > 0) {
12910
+ pushChromosome(current, order++);
12901
12911
  }
12912
+ current.seq = "";
12902
12913
 
12903
12914
  const parts = nextLine.substr(1).split(/\s+/);
12904
12915
 
12905
- // Check for samtools style locus string. This is not perfect, and could fail on weird sequence names
12906
- const nameParts = parts[0].split(':');
12907
- current.chr = nameParts[0];
12908
- current.seq = "";
12909
- current.offset = 0;
12910
- if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12911
- const locusParts = nameParts[1].split('-');
12912
- if (locusParts.length === 2 &&
12913
- /^[0-9]+$/.test(locusParts[0]) &&
12914
- /^[0-9]+$/.test(locusParts[1])) ;
12915
- const from = Number.parseInt(locusParts[0]);
12916
- const to = Number.parseInt(locusParts[1]);
12917
- if (to > from) { // TODO this should be an error
12916
+
12917
+ // Check for @len= token, which is a non-standard extension supporting igv-reports.
12918
+ if (nextLine.includes("@len=")) {
12919
+ const nameParts = parts[0].split(':');
12920
+ current.chr = nameParts[0];
12921
+ if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12922
+
12923
+ const locusParts = nameParts[1].split('-');
12924
+ if (locusParts.length === 2 &&
12925
+ /^[0-9]+$/.test(locusParts[0]) &&
12926
+ /^[0-9]+$/.test(locusParts[1])) ;
12927
+ const from = Number.parseInt(locusParts[0]);
12928
+ Number.parseInt(locusParts[1]);
12918
12929
  current.offset = from - 1;
12919
- }
12920
12930
 
12921
- // Check for chromosome length token
12922
- if (parts.length > 1 && parts[1].startsWith("@len=")) {
12923
- try {
12924
- current.length = parseInt(parts[1].trim().substring(5));
12925
- } catch (e) {
12931
+ // Check for chromosome length token
12932
+ if (parts.length > 1 && parts[1].startsWith("@len=")) {
12933
+ try {
12934
+ current.length = parseInt(parts[1].trim().substring(5));
12935
+ } catch (e) {
12936
+ current.length = undefined;
12937
+ console.error(`Error parsing sequence length for ${nextLine}`);
12938
+ }
12939
+ } else {
12926
12940
  current.length = undefined;
12927
- console.error(`Error parsing sequence length for ${nextLine}`);
12928
12941
  }
12929
- } else {
12930
- current.length = undefined;
12931
12942
  }
12943
+ } else {
12944
+ // No special tokens, a standard FASTA header
12945
+ current.chr = parts[0];
12946
+ current.offset = 0;
12932
12947
  }
12948
+
12933
12949
  } else {
12950
+ // Not a header or comment, so it must be sequence data
12934
12951
  current.seq += nextLine;
12935
12952
  }
12936
- // add last seq
12937
- if (current && current.seq) {
12938
- pushChromosome.call(this, current, order);
12939
- }
12940
12953
  }
12941
12954
 
12942
- function pushChromosome(current, order) {
12943
- const length = current.length || (current.offset + current.seq.length);
12944
- if (!chrNameSet.has(current.chr)) {
12945
- this.sequences.set(current.chr, []);
12946
- this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12947
- chrNameSet.add(current.chr);
12948
- } else {
12949
- const c = this.chromosomes.get(current.chr);
12950
- c.bpLength = Math.max(c.bpLength, length);
12951
- }
12952
- this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12955
+ // Handle the last sequence
12956
+ if (current.seq && current.seq.length > 0) {
12957
+ pushChromosome(current, order);
12953
12958
  }
12959
+
12954
12960
  }
12955
12961
 
12956
12962
  /**
@@ -13715,17 +13721,22 @@ class BPTree {
13715
13721
 
13716
13722
  static magic = 2026540177
13717
13723
  littleEndian = true
13724
+ type = 'BPTree' // Either BPTree or BPChromTree
13718
13725
  nodeCache = new Map()
13719
13726
 
13720
- static async loadBpTree(path, config, startOffset) {
13721
- const bpTree = new BPTree(path, config, startOffset);
13727
+ static async loadBpTree(path, config, startOffset, type, loader) {
13728
+ const bpTree = new BPTree(path, config, startOffset, type, loader);
13722
13729
  return bpTree.init()
13723
13730
  }
13724
13731
 
13725
- constructor(path, config, startOffset) {
13732
+ constructor(path, config, startOffset, type, loader) {
13726
13733
  this.path = path;
13727
13734
  this.config = config;
13728
13735
  this.startOffset = startOffset;
13736
+ if(type) {
13737
+ this.type = type;
13738
+ }
13739
+ this.loader = loader || igvxhr;
13729
13740
  }
13730
13741
 
13731
13742
  async init() {
@@ -13751,69 +13762,22 @@ class BPTree {
13751
13762
  return this
13752
13763
  }
13753
13764
 
13754
- async search(term) {
13755
-
13765
+ getItemCount() {
13756
13766
  if(!this.header) {
13757
- await this.init();
13767
+ throw Error("Header not initialized")
13758
13768
  }
13769
+ return this.header.itemCount
13770
+ }
13759
13771
 
13760
- const {keySize, valSize} = this.header;
13772
+ async search(term) {
13761
13773
 
13762
- if (!(valSize === 16 || valSize === 8)) {
13763
- throw Error(`Unexpected valSize ${valSize}`)
13774
+ if(!this.header) {
13775
+ await this.init();
13764
13776
  }
13765
13777
 
13766
- const readTreeNode = async (offset) => {
13767
-
13768
- if (this.nodeCache.has(offset)) {
13769
- return this.nodeCache.get(offset)
13770
- } else {
13771
-
13772
- let binaryParser = await this.#getParserFor(offset, 4);
13773
- const type = binaryParser.getByte();
13774
- binaryParser.getByte();
13775
- const count = binaryParser.getUShort();
13776
- const items = [];
13777
-
13778
- if (type === 1) {
13779
- // Leaf node
13780
- const size = count * (keySize + valSize);
13781
- binaryParser = await this.#getParserFor(offset + 4, size);
13782
- for (let i = 0; i < count; i++) {
13783
- const key = binaryParser.getFixedLengthString(keySize);
13784
- const offset = binaryParser.getLong();
13785
-
13786
- let value;
13787
- if (valSize === 16) {
13788
- const length = binaryParser.getInt();
13789
- binaryParser.getInt();
13790
- value = {offset, length};
13791
- } else {
13792
- value = {offset};
13793
- }
13794
- items.push({key, value});
13795
- }
13796
- } else {
13797
- // Non leaf node
13798
- const size = count * (keySize + 8);
13799
- binaryParser = await this.#getParserFor(offset + 4, size);
13800
-
13801
- for (let i = 0; i < count; i++) {
13802
- const key = binaryParser.getFixedLengthString(keySize);
13803
- const offset = binaryParser.getLong();
13804
- items.push({key, offset});
13805
- }
13806
- }
13807
-
13808
- const node = {type, count, items};
13809
- this.nodeCache.set(offset, node);
13810
- return node
13811
- }
13812
- };
13813
-
13814
13778
  const walkTreeNode = async (offset) => {
13815
13779
 
13816
- const node = await readTreeNode(offset);
13780
+ const node = await this.readTreeNode(offset);
13817
13781
 
13818
13782
  if (node.type === 1) {
13819
13783
  // Leaf node
@@ -13844,9 +13808,66 @@ class BPTree {
13844
13808
  return walkTreeNode(this.header.nodeOffset)
13845
13809
  }
13846
13810
 
13811
+ async readTreeNode (offset) {
13812
+
13813
+ if (this.nodeCache.has(offset)) {
13814
+ return this.nodeCache.get(offset)
13815
+ } else {
13816
+ let binaryParser = await this.#getParserFor(offset, 4);
13817
+ const type = binaryParser.getByte();
13818
+ binaryParser.getByte();
13819
+ const count = binaryParser.getUShort();
13820
+ const items = [];
13821
+
13822
+ const {keySize, valSize} = this.header;
13823
+
13824
+ if (type === 1) {
13825
+ // Leaf node
13826
+ const size = count * (keySize + valSize);
13827
+ binaryParser = await this.#getParserFor(offset + 4, size);
13828
+ for (let i = 0; i < count; i++) {
13829
+ const key = binaryParser.getFixedLengthString(keySize);
13830
+ let value;
13831
+ if(this.type === 'BPChromTree') {
13832
+ const id = binaryParser.getInt();
13833
+ const size = binaryParser.getInt();
13834
+ value = {id, size};
13835
+ } else {
13836
+ const offset = binaryParser.getLong();
13837
+ if (valSize === 16) {
13838
+ const length = binaryParser.getLong();
13839
+ value = {offset, length};
13840
+ } else {
13841
+ value = {offset};
13842
+ }
13843
+ }
13844
+ items.push({key, value});
13845
+ }
13846
+ } else {
13847
+ // Non leaf node
13848
+ const size = count * (keySize + 8);
13849
+ binaryParser = await this.#getParserFor(offset + 4, size);
13850
+
13851
+ for (let i = 0; i < count; i++) {
13852
+ const key = binaryParser.getFixedLengthString(keySize);
13853
+ const offset = binaryParser.getLong();
13854
+ items.push({key, offset});
13855
+ }
13856
+ }
13857
+
13858
+ const node = {type, count, items};
13859
+ this.nodeCache.set(offset, node);
13860
+ return node
13861
+ }
13862
+ }
13863
+
13847
13864
  async #getParserFor(start, size) {
13848
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13849
- return new BinaryParser$1(new DataView(data), this.littleEndian)
13865
+ try {
13866
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13867
+ return new BinaryParser$1(new DataView(data), this.littleEndian)
13868
+ } catch (e) {
13869
+ console.error(e);
13870
+ }
13850
13871
  }
13851
13872
 
13852
13873
  }
@@ -13872,6 +13893,7 @@ class TwobitSequence {
13872
13893
 
13873
13894
  littleEndian
13874
13895
  metaIndex = new Map()
13896
+ chromosomeNames
13875
13897
 
13876
13898
  constructor(config) {
13877
13899
  this.url = config.twoBitURL || config.fastaURL;
@@ -13964,9 +13986,16 @@ class TwobitSequence {
13964
13986
  return sequenceBases
13965
13987
  }
13966
13988
 
13989
+ /**
13990
+ * Read the internal index of the 2bit file. This is a list of sequence names and their offsets in the file.
13991
+ *
13992
+ * @returns {Promise<Map<any, any>>}
13993
+ * @private
13994
+ */
13967
13995
  async _readIndex() {
13968
13996
 
13969
13997
  const index = new Map();
13998
+ this.chromosomeNames = [];
13970
13999
 
13971
14000
  const loadRange = {start: 0, size: 64};
13972
14001
  let arrayBuffer = await igvxhr.loadArrayBuffer(this.url, {range: loadRange});
@@ -14019,6 +14048,8 @@ class TwobitSequence {
14019
14048
  index.set(name, offset);
14020
14049
 
14021
14050
  estNameLength = Math.floor(estNameLength * (i / (i + 1)) + name.length / (i + 1));
14051
+
14052
+ this.chromosomeNames.push(name);
14022
14053
  }
14023
14054
  return index
14024
14055
  }
@@ -22164,84 +22195,6 @@ function zoomLevelForScale$1(chr, bpPerPixel, genome) {
22164
22195
  return Math.ceil(Math.log(Math.max(0, (chrSize / (bpPerPixel * 700)))) / log2)
22165
22196
  }
22166
22197
 
22167
- /**
22168
- * A ChromTree parses a UCSC bigbed/bigwig "chromosomeTree" section of the header to produce 2 maps,
22169
- * (1) ID -> chromosome names, and its
22170
- * (2) chromsome name -> ID
22171
- *
22172
- * Both maps are needed by IGV
22173
- *
22174
- * The chromosome tree is a B+ index, but is located continguously in memory in the header section of the file. This
22175
- * makes it feasible to parse the whole tree with data from a single fetch. In the end the tree is discarded
22176
- * leaving only the mapps.
22177
- */
22178
- class ChromTree {
22179
-
22180
- constructor(header, nameToID, valueToKey, sumLengths) {
22181
- this.header = header;
22182
- this.nameToId = nameToID;
22183
- this.idToName = valueToKey;
22184
- this.sumLengths = sumLengths;
22185
- }
22186
-
22187
- static parseTree(binaryParser, startOffset, genome = false) {
22188
- {
22189
- const magic = binaryParser.getInt();
22190
- const blockSize = binaryParser.getInt();
22191
- const keySize = binaryParser.getInt();
22192
- const valSize = binaryParser.getInt();
22193
- const itemCount = binaryParser.getLong();
22194
- const reserved = binaryParser.getLong();
22195
-
22196
- const header = {magic, blockSize, keySize, valSize, itemCount, reserved};
22197
- const nameToId = new Map();
22198
- const idToName = [];
22199
- let sumLengths = 0;
22200
- const readTreeNode = (offset) => {
22201
- if (offset >= 0) binaryParser.position = offset;
22202
- const type = binaryParser.getByte();
22203
- binaryParser.getByte();
22204
- const count = binaryParser.getUShort();
22205
-
22206
- if (type === 1) {
22207
- // Leaf node
22208
- for (let i = 0; i < count; i++) {
22209
- let key = binaryParser.getFixedLengthString(keySize);
22210
- let value;
22211
- if (valSize === 8) {
22212
- value = binaryParser.getInt();
22213
- const chromSize = binaryParser.getInt();
22214
- sumLengths += chromSize;
22215
- if (genome) key = genome.getChromosomeName(key); // Translate to canonical chr name
22216
- nameToId.set(key, value);
22217
- idToName[value] = key;
22218
-
22219
- } else {
22220
- throw Error(`Unexpected "valSize" value in chromosome tree. Expected 8, actual value is ${valSize}`)
22221
- }
22222
- }
22223
- } else {
22224
- // non-leaf
22225
- for (let i = 0; i < count; i++) {
22226
- binaryParser.getFixedLengthString(keySize);
22227
- const childOffset = binaryParser.getLong();
22228
- const bufferOffset = childOffset - startOffset;
22229
- const currOffset = binaryParser.position;
22230
- readTreeNode(bufferOffset);
22231
- binaryParser.position = currOffset;
22232
- }
22233
- }
22234
- };
22235
-
22236
- // Recursively walk tree to populate dictionary
22237
- readTreeNode( -1);
22238
-
22239
- return new ChromTree(header, nameToId, idToName, sumLengths)
22240
- }
22241
- }
22242
-
22243
- }
22244
-
22245
22198
  const RPTREE_HEADER_SIZE = 48;
22246
22199
  const RPTREE_NODE_LEAF_ITEM_SIZE = 32; // leaf item size
22247
22200
  const RPTREE_NODE_CHILD_ITEM_SIZE = 24; // child item size
@@ -22252,11 +22205,12 @@ class RPTree {
22252
22205
  littleEndian = true
22253
22206
  nodeCache = new Map()
22254
22207
 
22255
- constructor(path, config, startOffset) {
22208
+ constructor(path, config, startOffset, loader) {
22256
22209
 
22257
22210
  this.path = path;
22258
22211
  this.config = config;
22259
22212
  this.startOffset = startOffset;
22213
+ this.loader = loader || igvxhr;
22260
22214
  }
22261
22215
 
22262
22216
 
@@ -22300,7 +22254,7 @@ class RPTree {
22300
22254
  }
22301
22255
 
22302
22256
  async #getParserFor(start, size) {
22303
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22257
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22304
22258
  return new BinaryParser$1(new DataView(data), this.littleEndian)
22305
22259
  }
22306
22260
 
@@ -22657,6 +22611,165 @@ class Trix {
22657
22611
  }
22658
22612
  }
22659
22613
 
22614
+ class ChromTree {
22615
+
22616
+ nameToId = new Map()
22617
+ idToName = new Map()
22618
+
22619
+ constructor(path, config, startOffset, loader) {
22620
+ this.path = path;
22621
+ this.config = config;
22622
+ this.startOffset = startOffset;
22623
+
22624
+ this.bpTree = new BPTree(path, config, startOffset, 'BPChromTree', loader);
22625
+ }
22626
+
22627
+ async init() {
22628
+ return this.bpTree.init()
22629
+ }
22630
+
22631
+ getItemCount() {
22632
+ return this.bpTree.getItemCount()
22633
+ }
22634
+
22635
+ /**
22636
+ * Return the chromosome ID for the given name. This is the internal chromosome ID for the parent BB file only.
22637
+ * @param {string} chr - The chromosome name.
22638
+ * @returns {number|null} - The chromosome ID or null if not found.
22639
+ */
22640
+ async getIdForName(chr) {
22641
+ if (this.nameToId.has(chr)) {
22642
+ return this.nameToId.get(chr)
22643
+ } else {
22644
+ try {
22645
+ const result = await this.bpTree.search(chr);
22646
+ if (result) {
22647
+ const id = result.id;
22648
+ this.nameToId.set(chr, id);
22649
+ return id
22650
+ } else {
22651
+ return
22652
+ }
22653
+ } catch (error) {
22654
+ throw new Error(error)
22655
+ }
22656
+ }
22657
+ }
22658
+
22659
+ /**
22660
+ * Return the chromosome name for the given ID. This is a potentially expensive operation as it involves
22661
+ * walking the tree until the leaf item for the given name is found. Currently it is used in only 2
22662
+ * situations:
22663
+ * (1) decoding features from a bigbed search-by-name query
22664
+ * (2) decoding bigwig data from the whole genome view
22665
+ * @param {number} id
22666
+ * @return {string|null}
22667
+ */
22668
+ async getNameForId(id) {
22669
+ if (this.idToName.has(id)) {
22670
+ return this.idToName.get(id)
22671
+ } else {
22672
+ const name = await this.searchForName(id);
22673
+ if (name !== null) {
22674
+ this.idToName.set(id, name);
22675
+ return name
22676
+ }
22677
+ }
22678
+ return null
22679
+ }
22680
+
22681
+ /**
22682
+ * Perform a reverse search by traversing the tree starting at the given offset. This is potentially expensive
22683
+ * as it traverses the tree to find the name corresponding to the given ID. It shoud not be used for large trees.
22684
+ *
22685
+ * @param {number} id - The ID to search for.
22686
+ * @returns {string|null} - The name corresponding to the ID, or null if not found.
22687
+ */
22688
+ async searchForName(id) {
22689
+
22690
+ const reverseSearch = async (offset, id) => {
22691
+
22692
+ const node = await this.bpTree.readTreeNode(offset);
22693
+
22694
+ let found = null;
22695
+
22696
+ if (node.type === 1) {
22697
+ // Leaf node
22698
+ for (const item of node.items) {
22699
+ const key = item.key;
22700
+ const itemId = item.value.id;
22701
+ if (itemId === id) {
22702
+ found = key;
22703
+ }
22704
+ // Cache the name and ID for future lookups
22705
+ this.nameToId.set(key, itemId);
22706
+ this.idToName.set(id, itemId);
22707
+ }
22708
+ return found
22709
+ } else {
22710
+ // Non-leaf node
22711
+ for (const item of node.items) {
22712
+ found = await reverseSearch.call(this, item.offset, id);
22713
+ if (found !== null) {
22714
+ break
22715
+ }
22716
+ }
22717
+ }
22718
+ return found
22719
+ };
22720
+
22721
+ try {
22722
+ return reverseSearch.call(this, this.startOffset + 32, id)
22723
+ } catch (error) {
22724
+ throw new Error(error)
22725
+ }
22726
+ }
22727
+
22728
+ /**
22729
+ * Return an estimated length of the genome, which might be the actual length if the number of contigs is small.
22730
+ * This is only used for calculating a default feature visibility window.
22731
+ *
22732
+ * @return {number}
22733
+ */
22734
+ async estimateGenomeSize() {
22735
+ try {
22736
+ const runningTotal = {total: 0, count: 0};
22737
+ await this.accumulateSize(this.startOffset + 32, runningTotal, 10000);
22738
+ const itemCount = this.getItemCount();
22739
+ return (itemCount / runningTotal.count) * runningTotal.total
22740
+
22741
+ } catch (error) {
22742
+ console.error("Error estimating genome size", error);
22743
+ return -1
22744
+ }
22745
+ }
22746
+
22747
+ async accumulateSize(offset, runningTotal, maxCount) {
22748
+
22749
+ const node = await this.bpTree.readTreeNode(offset);
22750
+
22751
+ if (node.type === 1) {
22752
+ // Leaf node
22753
+ for (const item of node.items) {
22754
+ const value = item.value;
22755
+ runningTotal.total += value.size;
22756
+ runningTotal.count += 1;
22757
+ }
22758
+ } else {
22759
+ // Non-leaf node. Items are visited in random order to avoid biasing the estimate
22760
+ const shuffledItems = node.items.slice().sort(() => Math.random() - 0.5);
22761
+ for (const item of shuffledItems) {
22762
+ await this.accumulateSize(item.offset, runningTotal, maxCount);
22763
+ if (runningTotal.count > maxCount) {
22764
+ break
22765
+ }
22766
+ }
22767
+ }
22768
+ return runningTotal
22769
+ }
22770
+
22771
+ }
22772
+
22660
22773
  /*
22661
22774
  * The MIT License (MIT)
22662
22775
  *
@@ -22720,14 +22833,40 @@ class BWReader {
22720
22833
  async preload() {
22721
22834
  const data = await igvxhr.loadArrayBuffer(this.path);
22722
22835
  this.loader = new DataBuffer(data);
22836
+ for (let rpTree of this.rpTreeCache.values()) {
22837
+ rpTree.loader = this.loader;
22838
+ }
22839
+ if (this._searchTrees) {
22840
+ for (let bpTree of this._searchTrees) {
22841
+ bpTree.loader = this.loader;
22842
+ }
22843
+ }
22723
22844
  }
22724
22845
 
22725
- async readWGFeatures(bpPerPixel, windowFunction) {
22846
+ async readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction) {
22847
+
22726
22848
  await this.loadHeader();
22727
- const chrIdx1 = 0;
22728
- const chrIdx2 = this.chromTree.idToName.length - 1;
22729
- const chr1 = this.chromTree.idToName[chrIdx1];
22730
- const chr2 = this.chromTree.idToName[chrIdx2];
22849
+ // Convert the logic to JavaScript
22850
+ let minID = Number.MAX_SAFE_INTEGER;
22851
+ let maxID = -1;
22852
+ let chr1;
22853
+ let chr2;
22854
+
22855
+ for (const chr of wgChromosomeNames) {
22856
+ const id = await this.getIdForChr(chr);
22857
+ if (id === null || id === undefined) {
22858
+ continue
22859
+ }
22860
+ if (id < minID) {
22861
+ minID = id;
22862
+ chr1 = chr;
22863
+ }
22864
+ if (id > maxID) {
22865
+ maxID = id;
22866
+ chr2 = chr;
22867
+ }
22868
+ }
22869
+
22731
22870
  return this.readFeatures(chr1, 0, chr2, Number.MAX_VALUE, bpPerPixel, windowFunction)
22732
22871
  }
22733
22872
 
@@ -22738,8 +22877,8 @@ class BWReader {
22738
22877
 
22739
22878
  await this.loadHeader();
22740
22879
 
22741
- let chrIdx1 = await this.#getIdForChr(chr1);
22742
- let chrIdx2 = await this.#getIdForChr(chr2);
22880
+ const chrIdx1 = await this.getIdForChr(chr1);
22881
+ const chrIdx2 = await this.getIdForChr(chr2);
22743
22882
 
22744
22883
  if (chrIdx1 === undefined || chrIdx2 === undefined) {
22745
22884
  return []
@@ -22798,7 +22937,7 @@ class BWReader {
22798
22937
  } else {
22799
22938
  plain = uint8Array;
22800
22939
  }
22801
- decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, this.chromTree.idToName, windowFunction, this.littleEndian);
22940
+ await decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, windowFunction);
22802
22941
  }
22803
22942
 
22804
22943
  features.sort(function (a, b) {
@@ -22815,29 +22954,30 @@ class BWReader {
22815
22954
  * @param chr
22816
22955
  * @returns {Promise<*>}
22817
22956
  */
22818
- async #getIdForChr(chr) {
22957
+ async getIdForChr(chr) {
22819
22958
 
22820
22959
  if (this.chrAliasTable.has(chr)) {
22821
22960
  chr = this.chrAliasTable.get(chr);
22822
- if (chr === undefined) {
22961
+ if (!chr) {
22823
22962
  return undefined
22824
22963
  }
22825
22964
  }
22826
22965
 
22827
- let chrIdx = this.chromTree.nameToId.get(chr);
22966
+ let chrIdx = await this.chromTree.getIdForName(chr);
22828
22967
 
22829
22968
  // Try alias
22830
22969
  if (chrIdx === undefined && this.genome) {
22831
22970
  const aliasRecord = await this.genome.getAliasRecord(chr);
22832
22971
  let alias;
22833
22972
  if (aliasRecord) {
22834
- const aliases = Object.keys(aliasRecord)
22835
- .filter(k => k !== "start" && k !== "end")
22836
- .map(k => aliasRecord[k])
22837
- .filter(a => this.chromTree.nameToId.has(a));
22838
- if (aliases.length > 0) {
22839
- alias = aliases[0];
22840
- chrIdx = this.chromTree.nameToId.get(aliases[0]);
22973
+ for (let k of Object.keys(aliasRecord)) {
22974
+ if (k === "start" || k === "end") continue
22975
+ alias = aliasRecord[k];
22976
+ if (alias === chr) continue // Already tried this
22977
+ chrIdx = await this.chromTree.getIdForName(alias);
22978
+ if (chrIdx !== undefined) {
22979
+ break
22980
+ }
22841
22981
  }
22842
22982
  }
22843
22983
  this.chrAliasTable.set(chr, alias); // alias may be undefined => no alias exists. Setting prevents repeated attempts
@@ -22924,7 +23064,8 @@ class BWReader {
22924
23064
  this.header.extraIndexOffsets.length > 0) {
22925
23065
  this._searchTrees = [];
22926
23066
  for (let offset of this.header.extraIndexOffsets) {
22927
- const bpTree = await BPTree.loadBpTree(this.path, this.config, offset);
23067
+ const type = undefined;
23068
+ const bpTree = await BPTree.loadBpTree(this.path, this.config, offset, type, this.loader);
22928
23069
  this._searchTrees.push(bpTree);
22929
23070
  }
22930
23071
  }
@@ -23041,15 +23182,13 @@ class BWReader {
23041
23182
  this.totalSummary = new BWTotalSummary(extHeaderParser);
23042
23183
  }
23043
23184
 
23044
- // Chrom data index. The start is known, size is not, but we can estimate it
23045
- const bufferSize = Math.min(200000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset));
23046
- this.chromTree = await this.#readChromTree(header.chromTreeOffset, bufferSize);
23047
- this.chrNames = new Set(this.chromTree.idToName);
23185
+ this.chromTree = new ChromTree(this.path, this.config, header.chromTreeOffset, this.loader);
23186
+ await this.chromTree.init();
23048
23187
 
23049
23188
  // Estimate feature density from dataCount (bigbed only)
23050
- if("bigbed" === this.type) {
23189
+ if ("bigbed" === this.type) {
23051
23190
  const dataCount = await this.#readDataCount(header.fullDataOffset);
23052
- this.featureDensity = dataCount / this.chromTree.sumLengths;
23191
+ this.featureDensity = dataCount / await this.chromTree.estimateGenomeSize();
23053
23192
  }
23054
23193
 
23055
23194
  this.header = header;
@@ -23073,38 +23212,6 @@ class BWReader {
23073
23212
  return binaryParser.getInt()
23074
23213
  }
23075
23214
 
23076
- /**
23077
- * Used when the chromTreeOffset is > fullDataOffset, that is when the chrom tree is not in the initial chunk
23078
- * read for parsing the header. We know the start position, but not the total size of the chrom tree
23079
- *
23080
- * @returns {Promise<void>}
23081
- */
23082
- async #readChromTree(chromTreeOffset, bufferSize) {
23083
-
23084
- let size = bufferSize;
23085
- const load = async () => {
23086
- const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
23087
- range: {
23088
- start: chromTreeOffset,
23089
- size: size
23090
- }
23091
- }));
23092
- const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
23093
- return ChromTree.parseTree(binaryParser, chromTreeOffset, this.genome)
23094
- };
23095
-
23096
- let error;
23097
- while (size < 1000000) {
23098
- try {
23099
- const chromTree = await load();
23100
- return chromTree
23101
- } catch (e) {
23102
- error = e;
23103
- size *= 2;
23104
- }
23105
- }
23106
- throw (error)
23107
- }
23108
23215
 
23109
23216
  async loadExtendedHeader(offset) {
23110
23217
 
@@ -23160,7 +23267,7 @@ class BWReader {
23160
23267
  if (rpTree) {
23161
23268
  return rpTree
23162
23269
  } else {
23163
- rpTree = new RPTree(this.path, this.config, offset);
23270
+ rpTree = new RPTree(this.path, this.config, offset, this.loader);
23164
23271
  await rpTree.init();
23165
23272
  this.rpTreeCache.set(offset, rpTree);
23166
23273
  return rpTree
@@ -23200,9 +23307,7 @@ class BWReader {
23200
23307
  const plain = (this.header.uncompressBuffSize > 0) ? inflate_1$3(uint8Array) : uint8Array;
23201
23308
  const decodeFunction = getBedDataDecoder.call(this);
23202
23309
  const features = [];
23203
- decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
23204
- features, this.chromTree.idToName);
23205
-
23310
+ await decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, features);
23206
23311
  return features
23207
23312
 
23208
23313
  }
@@ -23270,7 +23375,7 @@ function zoomLevelForScale(bpPerPixel, zoomLevelHeaders) {
23270
23375
  }
23271
23376
 
23272
23377
 
23273
- function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23378
+ async function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23274
23379
 
23275
23380
  const binaryParser = new BinaryParser$1(data, littleEndian);
23276
23381
  const chromId = binaryParser.getInt();
@@ -23311,7 +23416,7 @@ function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chr
23311
23416
  else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break
23312
23417
 
23313
23418
  if (Number.isFinite(value)) {
23314
- const chr = chrDict[chromId];
23419
+ const chr = await this.chromTree.getNameForId(chromId);
23315
23420
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23316
23421
  }
23317
23422
  }
@@ -23322,12 +23427,13 @@ function getBedDataDecoder() {
23322
23427
 
23323
23428
  const minSize = 3 * 4 + 1; // Minimum # of bytes required for a bed record
23324
23429
  const decoder = getDecoder(this.header.definedFieldCount, this.header.fieldCount, this.autoSql, this.format);
23325
- return function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23326
- const binaryParser = new BinaryParser$1(data, littleEndian);
23430
+ return async function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray) {
23431
+
23432
+ const binaryParser = new BinaryParser$1(data, this.littleEndian);
23327
23433
  while (binaryParser.remLength() >= minSize) {
23328
23434
 
23329
23435
  const chromId = binaryParser.getInt();
23330
- const chr = chrDict[chromId];
23436
+ const chr = await this.chromTree.getNameForId(chromId);
23331
23437
  const chromStart = binaryParser.getInt();
23332
23438
  const chromEnd = binaryParser.getInt();
23333
23439
  const rest = binaryParser.getString();
@@ -23344,8 +23450,7 @@ function getBedDataDecoder() {
23344
23450
  }
23345
23451
  }
23346
23452
 
23347
-
23348
- function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23453
+ async function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23349
23454
 
23350
23455
  const binaryParser = new BinaryParser$1(data, littleEndian);
23351
23456
  const minSize = 8 * 4; // Minimum # of bytes required for a zoom record
@@ -23377,7 +23482,7 @@ function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, ch
23377
23482
 
23378
23483
 
23379
23484
  if (Number.isFinite(value)) {
23380
- const chr = chrDict[chromId];
23485
+ const chr = await this.chromTree.getNameForId(chromId);
23381
23486
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23382
23487
 
23383
23488
 
@@ -23462,7 +23567,8 @@ class BWSource extends BaseFeatureSource {
23462
23567
 
23463
23568
  let features;
23464
23569
  if ("all" === chr.toLowerCase()) {
23465
- features = isBigWig ? await this.getWGValues(windowFunction, bpPerPixel) : [];
23570
+ const wgChromosomeNames = this.genome.wgChromosomeNames;
23571
+ features = isBigWig && wgChromosomeNames? await this.getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) : [];
23466
23572
  } else {
23467
23573
  features = await this.reader.readFeatures(chr, start, chr, end, bpPerPixel, windowFunction);
23468
23574
  }
@@ -23486,15 +23592,14 @@ class BWSource extends BaseFeatureSource {
23486
23592
 
23487
23593
  }
23488
23594
 
23489
- async getWGValues(windowFunction, bpPerPixel) {
23595
+ async getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) {
23490
23596
 
23491
23597
  const genome = this.genome;
23492
23598
  const cached = this.#wgValues[windowFunction];
23493
23599
  if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
23494
23600
  return cached.values
23495
23601
  } else {
23496
-
23497
- const features = await this.reader.readWGFeatures(bpPerPixel, windowFunction);
23602
+ const features = await this.reader.readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction);
23498
23603
  let wgValues = [];
23499
23604
  for (let f of features) {
23500
23605
  const chr = f.chr;
@@ -28289,7 +28394,7 @@ async function createBlatTrack({sequence, browser, name, title}) {
28289
28394
  features
28290
28395
  };
28291
28396
 
28292
- const track = await browser.loadTrack(trackConfig);
28397
+ const track = (await browser.loadTrackList([trackConfig]))[0];
28293
28398
  track.openTableView();
28294
28399
 
28295
28400
  } catch (e) {
@@ -30341,109 +30446,578 @@ function convertToHubURL(accension) {
30341
30446
  }
30342
30447
  }
30343
30448
 
30344
- /*
30345
- https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30346
- https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30347
- https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30348
- https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30349
- */
30449
+ const parentOverrideProperties = new Set(["visibility", "priority", "group"]);
30350
30450
 
30451
+ const nonInheritableProperties = new Set([
30452
+ "track", "type", "shortLabel", "longLabel", "bigDataUrl",
30453
+ "parent", "superTrack", "priority", "view", "compositeContainer", "compositeTrack"
30454
+ ]);
30351
30455
 
30352
- class Hub {
30353
30456
 
30354
- static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30355
- static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30356
- "cpgIslandExtUnmasked", "windowMasker"])
30457
+ class Stanza {
30458
+
30459
+ properties = new Map()
30460
+
30461
+ constructor(type, name) {
30462
+ this.type = type;
30463
+ this.name = name;
30464
+ }
30465
+
30466
+ setProperty(key, value) {
30467
+ this.properties.set(key, value);
30468
+ }
30469
+
30470
+ getProperty(key) {
30471
+ if (this.properties.has("noInherit")) {
30472
+ return this.properties.get(key)
30473
+ } else if (this.parent && parentOverrideProperties.has(key) && this.parent.hasProperty(key)) {
30474
+ return this.parent.getProperty(key)
30475
+ } else if (this.properties.has(key)) {
30476
+ return this.properties.get(key)
30477
+ } else if (this.parent && !nonInheritableProperties.has(key)) {
30478
+ return this.parent.getProperty(key)
30479
+ } else {
30480
+ return undefined
30481
+ }
30482
+ }
30483
+
30484
+ hasProperty(key) {
30485
+ return this.getProperty(key) !== null && this.getProperty(key) !== undefined
30486
+ }
30357
30487
 
30358
- static async loadHub(url) {
30488
+ hasOwnProperty(key) {
30489
+ return this.properties.has(key)
30490
+ }
30359
30491
 
30360
- const idx = url.lastIndexOf("/");
30361
- const baseURL = url.substring(0, idx + 1);
30362
- const stanzas = await loadStanzas(url);
30363
- let groups;
30364
- if ("genome" === stanzas[1].type) {
30365
- const genome = stanzas[1];
30366
- if (genome.hasProperty("groups")) {
30367
- const groupsTxtURL = baseURL + genome.getProperty("groups");
30368
- groups = await loadStanzas(groupsTxtURL);
30492
+ getOwnProperty(key) {
30493
+ return this.properties.get(key)
30494
+ }
30495
+
30496
+ removeProperty(key) {
30497
+ this.properties.delete(key);
30498
+ }
30499
+
30500
+ get format() {
30501
+ const type = this.getProperty("type");
30502
+ if (type) {
30503
+ // Trim extra bed qualifiers (e.g. bigBed + 4)
30504
+ return firstWord$1(type)
30505
+ }
30506
+ return undefined // unknown type
30507
+ }
30508
+
30509
+ /**
30510
+ * IGV display mode
30511
+ */
30512
+ get displayMode() {
30513
+ let viz = this.getProperty("visibility");
30514
+ if (!viz) {
30515
+ return "COLLAPSED"
30516
+ } else {
30517
+ viz = viz.toLowerCase();
30518
+ switch (viz) {
30519
+ case "dense":
30520
+ return "COLLAPSED"
30521
+ case "pack":
30522
+ return "EXPANDED"
30523
+ case "squish":
30524
+ return "SQUISHED"
30525
+ default:
30526
+ return "COLLAPSED"
30369
30527
  }
30528
+ }
30529
+ }
30530
+ }
30531
+
30532
+
30533
+ function firstWord$1(str) {
30534
+ const idx = str.indexOf(' ');
30535
+ return idx > 0 ? str.substring(0, idx) : str
30536
+ }
30370
30537
 
30371
- // If the genome has a chromSizes file, and it is not too large, set the chromSizesURL property. This will
30372
- // enable whole genome view and the chromosome pulldown
30373
- if (genome.hasProperty("chromSizes")) {
30374
- const chromSizesURL = baseURL + genome.getProperty("chromSizes");
30375
- const l = await getContentLength(chromSizesURL);
30376
- if (l !== null && Number.parseInt(l) < 1000000) {
30377
- genome.setProperty("chromSizesURL", chromSizesURL);
30538
+ class TrackConfigContainer {
30539
+ constructor(name, label, priority, defaultOpen) {
30540
+ this.name = name;
30541
+ this.priority = priority;
30542
+ this.label = label;
30543
+ this.defaultOpen = defaultOpen;
30544
+ this.tracks = [];
30545
+ this.children = [];
30546
+ }
30547
+
30548
+ isEmpty() {
30549
+ return this.tracks.length === 0 &&
30550
+ (!this.children || this.children.length === 0 || this.children.every(child => child.isEmpty()));
30551
+ }
30552
+
30553
+ map(callback) {
30554
+ this.tracks.forEach(callback);
30555
+ this.children.forEach(child => child.map(callback));
30556
+ }
30557
+
30558
+ findTracks(filter) {
30559
+ const found = [];
30560
+ this._find(found, filter);
30561
+ return found;
30562
+ }
30563
+
30564
+ _find(found, filter) {
30565
+ this.tracks.forEach(track => {
30566
+ if (filter(track)) {
30567
+ found.push(track);
30378
30568
  }
30569
+ });
30570
+ this.children.forEach(child => child._find(found, filter));
30571
+ }
30572
+
30573
+ countTracks() {
30574
+ return this.tracks.length + this.children.reduce((count, child) => count + child.countTracks(), 0);
30575
+ }
30576
+
30577
+ countSelectedTracks() {
30578
+ const selectedCount = this.tracks.filter(track => track.visible).length;
30579
+ return selectedCount + this.children.reduce((count, child) => count + child.countSelectedTracks(), 0);
30580
+ }
30581
+
30582
+ trim() {
30583
+ this.children = this.children.filter(child => !child.isEmpty());
30584
+ this.children.forEach(child => child.trim());
30585
+ }
30586
+
30587
+ setTrackVisibility(loadedTrackPaths) {
30588
+ this.tracks.forEach(track => {
30589
+ track.visible = loadedTrackPaths.has(track.url);
30590
+ });
30591
+ this.children.forEach(child => child.setTrackVisibility(loadedTrackPaths));
30592
+ }
30593
+ }
30594
+
30595
+ const supportedTypes = new Set([
30596
+ "bigbed", "bigwig", "biggenepred", "vcftabix", "refgene",
30597
+ "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf", "vcf", "vcfphasedtrio",
30598
+ "bigdbsnp", "rmask", "genepred", "wig", "bedgraph", "interact", "broadpeak", "narrowpeak", "gappedpeak",
30599
+ "gistic", "seg", "mut, bigrmsk"
30600
+ ]);
30601
+
30602
+ const filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30603
+ "cpgIslandExtUnmasked", "windowMasker"]);
30604
+
30605
+ class TrackDbHub {
30606
+
30607
+ constructor(trackStanzas, groupStanzas) {
30608
+ this.groupStanzas = groupStanzas;
30609
+ this.trackStanzas = trackStanzas;
30610
+ }
30611
+
30612
+ findCytobandURL() {
30613
+ for (const t of this.trackStanzas) {
30614
+ if (t.name === "cytoBandIdeo" && t.hasProperty("bigDataUrl")) {
30615
+ return t.getProperty("bigDataUrl")
30379
30616
  }
30380
30617
  }
30618
+ }
30381
30619
 
30382
- // TODO -- categorize extra "user" supplied and other tracks in some distinctive way before including them
30383
- // load includes. Nested includes are not supported
30384
- // for (let s of stanzas.slice()) {
30385
- // if ("include" === s.type) {
30386
- // const includeStanzas = await loadStanzas(baseURL + s.getProperty("include"))
30387
- // for (s of includeStanzas) {
30388
- // s.setProperty("visibility", "hide")
30389
- // stanzas.push(s)
30390
- // }
30391
- // }
30392
- // }
30620
+ getSupportedTrackCount() {
30621
+ let count = 0;
30622
+ for (const t of this.trackStanzas) {
30623
+ if (!filterTracks.has(t.name) &&
30624
+ t.hasProperty("bigDataUrl") &&
30625
+ t.format &&
30626
+ supportedTypes.has(t.format.toLowerCase())) {
30627
+ count++;
30628
+ }
30629
+ }
30630
+ return count
30631
+ }
30632
+
30633
+ getGroupedTrackConfigurations() {
30634
+
30635
+ if (!this.groupTrackConfigs) {
30636
+ this.groupTrackConfigs = [];
30637
+ const trackContainers = new Map();
30393
30638
 
30394
- return new Hub(url, stanzas, groups)
30639
+ // create a container for tracks with no parent
30640
+ const nullContainer = new TrackConfigContainer('', '', 0, true);
30641
+ this.groupTrackConfigs.push(nullContainer);
30642
+
30643
+ const hasGroups = this.groupStanzas && this.groupStanzas.length > 0;
30644
+ if (hasGroups) {
30645
+ for (const groupStanza of this.groupStanzas) {
30646
+ const name = groupStanza.getProperty("name");
30647
+ const defaultOpen = groupStanza.getProperty("defaultIsClosed") === "0";
30648
+ const priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30649
+ const container = new TrackConfigContainer(name, groupStanza.getProperty("label"), priority, defaultOpen);
30650
+ trackContainers.set(name, container);
30651
+ this.groupTrackConfigs.push(container);
30652
+ }
30653
+ }
30654
+
30655
+ for (let s of this.trackStanzas) {
30656
+
30657
+ const isContainer = (s.hasOwnProperty("superTrack") && !s.hasOwnProperty("bigDataUrl")) ||
30658
+ s.hasOwnProperty("compositeTrack") || s.hasOwnProperty("view") ||
30659
+ (s.hasOwnProperty("container") && s.getOwnProperty("container").equals("multiWig"));
30660
+
30661
+ // Find parent, if any. "group" containers can be implicit, all other types should be explicitly
30662
+ // defined before their children
30663
+ let parent;
30664
+
30665
+ if (s.hasOwnProperty("parent")) {
30666
+ parent = trackContainers.get(s.getOwnProperty("parent"));
30667
+ }
30668
+
30669
+ if (!parent && hasGroups && s.hasProperty("group")) {
30670
+ const groupName = s.getProperty("group");
30671
+ if (trackContainers.has(groupName)) {
30672
+ parent = trackContainers.get(groupName);
30673
+ } else {
30674
+ const container = new TrackConfigContainer(groupName, groupName, 1000, true);
30675
+ trackContainers.set(groupName, container);
30676
+ this.groupTrackConfigs.push(container);
30677
+ parent = container;
30678
+ }
30679
+ }
30680
+
30681
+ if (isContainer) {
30682
+
30683
+ const name = s.getProperty("track");
30684
+ const priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30685
+ const defaultOpen = s.getProperty("defaultIsClosed") === "0";
30686
+ const longLabel = s.getOwnProperty("longLabel");
30687
+ const label = longLabel && longLabel.length < 50 ? longLabel : s.getOwnProperty("shortLabel");
30688
+ const container = new TrackConfigContainer(name, label, priority, defaultOpen);
30689
+
30690
+ if (trackContainers.has(name)) {
30691
+ throw new Error(`Duplicate track container: ${name}`)
30692
+ }
30693
+ trackContainers.set(name, container);
30694
+
30695
+ if (parent) {
30696
+ parent.children.push(container);
30697
+ } else {
30698
+ // No parent or a superTrack => promote to top level
30699
+ this.groupTrackConfigs.push(container);
30700
+ }
30701
+ } else if (!filterTracks.has(s.name) &&
30702
+ s.hasProperty("bigDataUrl") &&
30703
+ s.format &&
30704
+ supportedTypes.has(s.format.toLowerCase())) {
30705
+
30706
+ const trackConfig = this.#getTrackConfig(s);
30707
+ if (parent) {
30708
+ parent.tracks.push(trackConfig);
30709
+ } else {
30710
+ nullContainer.tracks.push(trackConfig);
30711
+ }
30712
+ }
30713
+ }
30714
+
30715
+ }
30716
+
30717
+ // Filter empty groups and sort
30718
+ this.groupTrackConfigs.forEach(c => c.trim());
30719
+ this.groupTrackConfigs = this.groupTrackConfigs.filter(t => !t.isEmpty());
30720
+
30721
+ this.groupTrackConfigs.sort((a, b) => a.priority - b.priority);
30722
+ return this.groupTrackConfigs
30395
30723
  }
30396
30724
 
30397
- constructor(url, stanzas, groupStanzas) {
30725
+ /**
30726
+ * Return an array of igv track config objects that satisfy the filter
30727
+ */
30728
+ #getTracksConfigs(filter) {
30729
+ return this.trackStanzas.filter(t => {
30730
+ return supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t))
30731
+ })
30732
+ .map(t => this.#getTrackConfig(t))
30733
+ }
30398
30734
 
30399
- this.url = url;
30400
30735
 
30401
- const idx = url.lastIndexOf("/");
30402
- this.baseURL = url.substring(0, idx + 1);
30736
+ /** example
30737
+ * track gc5Base
30738
+ * shortLabel GC Percent
30739
+ * longLabel GC Percent in 5-Base Windows
30740
+ * group map
30741
+ * visibility full
30742
+ * autoScale Off
30743
+ * maxHeightPixels 128:36:16
30744
+ * graphTypeDefault Bar
30745
+ * gridDefault OFF
30746
+ * windowingFunction Mean
30747
+ * color 0,0,0
30748
+ * altColor 128,128,128
30749
+ * viewLimits 30:70
30750
+ * type bigWig 0 100
30751
+ * bigDataUrl bbi/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base.bw
30752
+ * html html/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base
30753
+ * @param t
30754
+ */
30755
+ #getTrackConfig(t) {
30403
30756
 
30404
- // The first stanza must be type = hub
30405
- if ("hub" === stanzas[0].type) {
30406
- this.hubStanza = stanzas[0];
30407
- } else {
30408
- throw Error("Unexpected hub.txt file -- does the first line start with 'hub'?")
30757
+ const format = t.format;
30758
+
30759
+ const config = {
30760
+ "id": t.getProperty("track"),
30761
+ "name": t.getProperty("shortLabel"),
30762
+ "format": format,
30763
+ "url": t.getProperty("bigDataUrl"),
30764
+ "displayMode": t.displayMode,
30765
+ };
30766
+
30767
+ if ("vcfTabix" === format) {
30768
+ config.indexURL = config.url + ".tbi";
30769
+ }
30770
+
30771
+ if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30772
+ if (config.description) config.description += "<br/>";
30773
+ config.description =
30774
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30775
+ } else if (t.hasProperty("longLabel")) {
30776
+ config.description = t.getProperty("longLabel");
30777
+ }
30778
+
30779
+ if (t.hasProperty("autoScale")) {
30780
+ config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
30781
+ }
30782
+ if (t.hasProperty("maxHeightPixels")) {
30783
+ const tokens = t.getProperty("maxHeightPixels").split(":");
30784
+ config.maxHeight = Number.parseInt(tokens[0]);
30785
+ config.height = Number.parseInt(tokens[1]);
30786
+ config.minHeight = Number.parseInt(tokens[2]);
30787
+ }
30788
+ // TODO -- graphTypeDefault
30789
+ // TODO -- windowingFunction
30790
+ if (t.hasProperty("color")) {
30791
+ const c = t.getProperty("color");
30792
+ config.color = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30793
+ }
30794
+ if (t.hasProperty("altColor")) {
30795
+ const c = t.getProperty("altColor");
30796
+ config.altColor = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30797
+ }
30798
+ if (t.hasProperty("viewLimits")) {
30799
+ const tokens = t.getProperty("viewLimits").split(":");
30800
+ let min, max;
30801
+ if (tokens.length > 1) {
30802
+ min = Number.parseInt(tokens[0]);
30803
+ max = Number.parseInt(tokens[1]);
30804
+ }
30805
+ if (Number.isNaN(max) || Number.isNaN(min)) {
30806
+ console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
30807
+ } else {
30808
+ config.min = min;
30809
+ config.max = max;
30810
+ }
30811
+
30812
+ }
30813
+ if (t.hasProperty("itemRgb")) ;
30814
+ if ("hide" === t.getProperty("visibility")) {
30815
+ // TODO -- this not supported yet
30816
+ config.visible = false;
30409
30817
  }
30410
- if ("on" !== this.hubStanza.getProperty("useOneFile")) {
30411
- throw Error("Only 'useOneFile' hubs are currently supported")
30818
+ if (t.hasProperty("url")) {
30819
+ config.infoURL = t.getProperty("url");
30412
30820
  }
30413
- if (stanzas.length < 2) {
30414
- throw Error("Expected at least 2 stanzas, hub and genome")
30821
+ if (t.hasProperty("searchIndex")) {
30822
+ config.searchIndex = t.getProperty("searchIndex");
30823
+ }
30824
+ if (t.hasProperty("searchTrix")) {
30825
+ config.trixURL = t.getProperty("searchTrix");
30826
+ }
30827
+ if (t.hasProperty("html")) {
30828
+ config.html = t.getProperty("html");
30415
30829
  }
30416
30830
 
30417
- // The second stanza should be a genome
30418
- if ("genome" === stanzas[1].type) {
30419
- this.genomeStanza = stanzas[1];
30420
- } else {
30421
- throw Error(`Unexpected hub file -- expected "genome" stanza but found "${stanzas[1].type}"`)
30831
+ if (t.hasProperty("group")) {
30832
+ config._group = t.getProperty("group");
30833
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
30834
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30835
+ config.order = nextPriority;
30836
+ this.groupPriorityMap.set(config._group, nextPriority);
30837
+ }
30838
+ }
30839
+
30840
+ if (t.hasProperty("metadata")) {
30841
+ config.attributes = parseMetadata(t.getProperty("metadata"));
30842
+ }
30843
+
30844
+ if (t.hasProperty("maxWindowToDraw")) {
30845
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowToDraw"), 10);
30846
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30847
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30848
+ }
30849
+ config.visibilityWindow = maxWindowToDraw;
30422
30850
  }
30423
30851
 
30424
- // Remaining stanzas should be tracks
30425
- this.trackStanzas = [];
30426
- for (let i = 2; i < stanzas.length; i++) {
30427
- if ("track" === stanzas[i].type) {
30428
- this.trackStanzas.push(stanzas[i]);
30852
+ // IGV does not support "maxWindowCoverage" in the same way as UCSC. Use to limit visibility window
30853
+ if (t.hasProperty("maxWindowCoverage")) {
30854
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowCoverage"), 10);
30855
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30856
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30429
30857
  }
30858
+ config.visibilityWindow = maxWindowToDraw;
30430
30859
  }
30431
30860
 
30432
- if (groupStanzas) {
30433
- this.groupStanzas = groupStanzas;
30434
- this.groupPriorityMap = new Map();
30435
- for (let g of groupStanzas) {
30436
- if (g.hasProperty("priority")) {
30437
- this.groupPriorityMap.set(g.getProperty("name"), Number.parseInt(g.getProperty("priority")) * 10);
30861
+ return config
30862
+ }
30863
+ }
30864
+
30865
+ function htmlText(html) {
30866
+ // Assumes a pattern like <span style="color:#C58DAA">Digestive</span>
30867
+ const idx1 = html.indexOf('>');
30868
+ const idx2 = html.indexOf('<', idx1);
30869
+ if (idx1 > 0 && idx2 > idx1) {
30870
+ return html.substring(idx1 + 1, idx2)
30871
+ } else {
30872
+ return html
30873
+ }
30874
+ }
30875
+
30876
+ /**
30877
+ * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4).
30878
+ * Ignore levels > 3
30879
+ *
30880
+ * @param {string} priorityString Priority as a string (e.g. 3.4)
30881
+ * @return {number} A priority as an integer
30882
+ */
30883
+ function getPriority(priorityString) {
30884
+ try {
30885
+ const tokens = priorityString.trim().split(".");
30886
+ let p = parseInt(tokens[0], 10) * 100;
30887
+ if (tokens.length > 1) {
30888
+ p += parseInt(tokens[1], 10) * 10;
30889
+ }
30890
+ if (tokens.length > 2) {
30891
+ p += parseInt(tokens[2], 10);
30892
+ }
30893
+ return p
30894
+ } catch (e) {
30895
+ console.error(`Error parsing priority string: ${priorityString}`, e);
30896
+ return Number.MAX_SAFE_INTEGER
30897
+ }
30898
+ }
30899
+
30900
+ function parseMetadata(metadata) {
30901
+ const attrs = new Map();
30902
+ let lastMetdataLengh = -1;
30903
+ while (metadata && metadata.length > 0) {
30904
+ try {
30905
+ if (metadata.length === lastMetdataLengh) {
30906
+ break
30907
+ }
30908
+ lastMetdataLengh = metadata.length;
30909
+ let idx = metadata.indexOf("=");
30910
+ if (idx === -1 || idx === metadata.length - 1) {
30911
+ break
30912
+ }
30913
+ let idx2;
30914
+ const key = capitalize(stripQuotes$2(metadata.substring(0, idx)));
30915
+ let value;
30916
+
30917
+ if (metadata.charAt(idx + 1) === '"') {
30918
+ idx++;
30919
+ idx2 = metadata.indexOf('" ', idx + 1);
30920
+ value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1);
30921
+ idx2++;
30922
+ } else {
30923
+ idx2 = metadata.indexOf(" ", idx + 1);
30924
+ if (idx2 === -1) {
30925
+ idx2 = metadata.length;
30438
30926
  }
30927
+ value = metadata.substring(idx + 1, idx2);
30928
+ }
30929
+ value = stripQuotes$2(value);
30930
+ if (value.endsWith('"')) {
30931
+ value = value.substring(0, value.length - 1);
30932
+ }
30933
+ if (value.startsWith("<") && value.endsWith(">")) {
30934
+ value = htmlText(value);
30439
30935
  }
30936
+ attrs.set(key, value);
30937
+ if (idx2 === metadata.length) {
30938
+ break
30939
+ }
30940
+ metadata = idx2 > 0 ? metadata.substring(idx2 + 1).trim() : "";
30941
+ } catch (e) {
30942
+ // We don't want to fail parsing the hub due to a failure parsing metadata. Also, we don't want to
30943
+ // overwhelm the log. Metadata is of marginal importance in IGV.
30440
30944
  }
30441
30945
  }
30946
+ return attrs
30947
+ }
30948
+
30949
+ /*
30950
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30951
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30952
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30953
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30954
+ */
30955
+
30956
+ const idMappings = new Map([
30957
+ ["hg38", "GCF_000001405.40"],
30958
+ ["mm39", "GCF_000001635.27"],
30959
+ ["mm10", "GCF_000001635.26"],
30960
+ ["bosTau9", "GCF_002263795.1"],
30961
+ ["canFam4", "GCF_011100685.1"],
30962
+ ["canFam6", "GCF_000002285.5"],
30963
+ ["ce11", "GCF_000002985.6"],
30964
+ ["dm6", "GCF_000001215.4"],
30965
+ ["galGal6", "GCF_000002315.6"],
30966
+ ["gorGor6", "GCF_008122165.1"],
30967
+ ["macFas5", "GCA_000364345.1"],
30968
+ ["panTro6", "GCA_002880755.3"],
30969
+ ["rn6", "GCF_000001895.5"],
30970
+ ["rn7", "GCF_015227675.2"],
30971
+ ["sacCer3", "GCF_000146045.2"],
30972
+ ["sacCer2", "GCF_000146045.2"],
30973
+ ["susScr11", "GCF_000003025.6"],
30974
+ ["taeGut1", "GCF_000002275.3"],
30975
+ ["tetNig2", "GCF_000002275.3"],
30976
+ ["xenTro10", "GCF_000002035.6"],
30977
+ ["xenTro9", "GCF_000002035.6"],
30978
+ ["tair10", "GCF_000001735.4"],
30979
+ ]);
30442
30980
 
30443
- getDefaultPosition() {
30444
- return this.genomeStanza.getProperty("defaultPos")
30981
+ class Hub {
30982
+
30983
+ static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30984
+ static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30985
+ "cpgIslandExtUnmasked", "windowMasker"])
30986
+
30987
+ constructor(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas) {
30988
+
30989
+ this.url = url;
30990
+ this.hubStanza = hubStanza;
30991
+ this.genomeStanzas = genomeStanzas;
30992
+ this.trackStanzas = trackStanzas;
30993
+ this.groupStanzas = groupStanzas;
30994
+ this.trackHubMap = new Map();
30995
+
30996
+ // trackStanzas will not be null if this is a "onefile" hub
30997
+ if (trackStanzas) {
30998
+ const genomeId = genomeStanzas[0].getProperty("genome"); // Assumption here this is a single genome hub
30999
+ this.trackHubMap.set(genomeId, new TrackDbHub(trackStanzas, groupStanzas));
31000
+ }
30445
31001
  }
30446
31002
 
31003
+
31004
+ getName() {
31005
+ return this.hubStanza.getProperty("hub")
31006
+ }
31007
+
31008
+ getShortLabel() {
31009
+ return this.hubStanza.getProperty("shortLabel")
31010
+ }
31011
+
31012
+ getLongLabel() {
31013
+ return this.hubStanza.getProperty("longLabel")
31014
+ }
31015
+
31016
+ getDescriptionUrl() {
31017
+ return this.hubStanza.getProperty("descriptionUrl")
31018
+ }
31019
+
31020
+
30447
31021
  /* Example genome stanza
30448
31022
  genome GCF_000186305.1
30449
31023
  taxId 176946
@@ -30462,133 +31036,126 @@ transBlat dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30462
31036
  isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30463
31037
  */
30464
31038
 
30465
- getGenomeConfig(options = {}) {
30466
- // TODO -- add blat? htmlPath?
31039
+ getGenomeConfig(genomeId) {
31040
+
31041
+ const genomeStanza = genomeId ? this.genomeStanzas.find(s => s.getProperty("genome") === genomeId) : this.genomeStanzas[0];
31042
+ if (!genomeStanza) {
31043
+ throw new Error(`Genome not found in hub: ${genomeId}`)
31044
+ }
31045
+ return this.#getGenomeConfig(genomeStanza)
31046
+ }
31047
+
31048
+ #getGenomeConfig(genomeStanza) {
30467
31049
 
30468
- const id = this.genomeStanza.getProperty("genome");
31050
+ const id = genomeStanza.getProperty("genome");
30469
31051
  const gsName =
30470
31052
  this.hubStanza.getProperty("shortLabel") ||
30471
- this.genomeStanza.getProperty("scientificName") ||
30472
- this.genomeStanza.getProperty("organism") ||
30473
- this.genomeStanza.getProperty("description");
31053
+ genomeStanza.getProperty("scientificName") ||
31054
+ genomeStanza.getProperty("organism") ||
31055
+ genomeStanza.getProperty("description");
30474
31056
  const name = gsName + (gsName ? ` (${id})` : ` ${id}`);
30475
31057
 
30476
31058
  const config = {
30477
- hubURL: this.url,
31059
+
30478
31060
  id: id,
30479
31061
  name: name,
30480
- twoBitURL: this.baseURL + this.genomeStanza.getProperty("twoBitPath"),
31062
+ twoBitURL: genomeStanza.getProperty("twoBitPath"),
30481
31063
  nameSet: "ucsc",
31064
+ hubs: [this.url]
30482
31065
  };
30483
31066
 
30484
- if (this.genomeStanza.hasProperty("chromSizesURL")) {
30485
- config.chromSizesURL = this.genomeStanza.getProperty("chromSizesURL");
31067
+ if (genomeStanza.hasProperty("chromSizes")) {
31068
+ config.chromSizesURL = genomeStanza.getProperty("chromSizes");
30486
31069
  } else {
30487
31070
  config.wholeGenomeView = false;
30488
31071
  config.showChromosomeWidget = false;
30489
31072
  }
30490
31073
 
30491
- if (this.genomeStanza.hasProperty("defaultPos")) {
30492
- const hubLocus = this.genomeStanza.getProperty("defaultPos");
31074
+ if (genomeStanza.hasProperty("defaultPos")) {
31075
+ const hubLocus = genomeStanza.getProperty("defaultPos");
30493
31076
  // Strip out coordinates => whole chromosome view
30494
- if (hubLocus) {
30495
- const idx = hubLocus.lastIndexOf(":");
30496
- config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus;
30497
- }
31077
+ // if (hubLocus) {
31078
+ // const idx = hubLocus.lastIndexOf(":")
31079
+ // config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus
31080
+ // }
31081
+ config.locus = hubLocus;
30498
31082
  }
30499
31083
 
30500
- if (this.genomeStanza.hasProperty("blat")) {
30501
- config.blat = this.baseURL + this.genomeStanza.getProperty("blat");
30502
- }
30503
- if (this.genomeStanza.hasProperty("chromAliasBb")) {
30504
- config.chromAliasBbURL = this.baseURL + this.genomeStanza.getProperty("chromAliasBb");
31084
+ if (genomeStanza.hasProperty("blat")) {
31085
+ config.blat = genomeStanza.getProperty("blat");
30505
31086
  }
30506
- if (this.genomeStanza.hasProperty("chromAlias")) {
30507
- config.aliasURL = this.baseURL + this.genomeStanza.getProperty("chromAlias");
31087
+ if (genomeStanza.hasProperty("chromAliasBb")) {
31088
+ config.chromAliasBbURL = genomeStanza.getProperty("chromAliasBb");
30508
31089
  }
30509
- if (this.genomeStanza.hasProperty("twoBitBptURL")) {
30510
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptURL");
31090
+ if (genomeStanza.hasProperty("chromAlias")) {
31091
+ config.aliasURL = genomeStanza.getProperty("chromAlias");
30511
31092
  }
30512
-
30513
- if (this.genomeStanza.hasProperty("twoBitBptUrl")) {
30514
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl");
31093
+ if (genomeStanza.hasProperty("twoBitBptURL")) {
31094
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptURL");
30515
31095
  }
30516
31096
 
30517
- // chromSizes can take a very long time to load, and is not useful with the default WGV = off
30518
- if (options.includeChromSizes && this.genomeStanza.hasProperty("chromSizes")) {
30519
- config.chromSizesURL = this.baseURL + this.genomeStanza.getProperty("chromSizes");
31097
+ if (genomeStanza.hasProperty("twoBitBptUrl")) {
31098
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptUrl");
30520
31099
  }
30521
31100
 
30522
31101
  if (this.hubStanza.hasProperty("longLabel")) {
30523
31102
  config.description = this.hubStanza.getProperty("longLabel").replace("/", "\n");
30524
31103
  } else {
30525
31104
  config.description = config.id;
30526
- if (this.genomeStanza.hasProperty("description")) {
30527
- config.description += `\n${this.genomeStanza.getProperty("description")}`;
31105
+ if (genomeStanza.hasProperty("description")) {
31106
+ config.description += `\n${genomeStanza.getProperty("description")}`;
30528
31107
  }
30529
- if (this.genomeStanza.hasProperty("organism")) {
30530
- config.description += `\n${this.genomeStanza.getProperty("organism")}`;
31108
+ if (genomeStanza.hasProperty("organism")) {
31109
+ config.description += `\n${genomeStanza.getProperty("organism")}`;
30531
31110
  }
30532
- if (this.genomeStanza.hasProperty("scientificName")) {
30533
- config.description += `\n${this.genomeStanza.getProperty("scientificName")}`;
31111
+ if (genomeStanza.hasProperty("scientificName")) {
31112
+ config.description += `\n${genomeStanza.getProperty("scientificName")}`;
30534
31113
  }
30535
31114
 
30536
- if (this.genomeStanza.hasProperty("htmlPath")) {
30537
- config.infoURL = this.baseURL + this.genomeStanza.getProperty("htmlPath");
31115
+ if (genomeStanza.hasProperty("htmlPath")) {
31116
+ config.infoURL = genomeStanza.getProperty("htmlPath");
30538
31117
  }
30539
31118
  }
30540
31119
 
30541
- // Search for cytoband
30542
- /*
30543
- track cytoBandIdeo
30544
- shortLabel Chromosome Band (Ideogram)
30545
- longLabel Ideogram for Orientation
30546
- group map
30547
- visibility dense
30548
- type bigBed 4 +
30549
- bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb
30550
- */
30551
- const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.hasProperty("bigDataUrl"));
30552
- if (cytoStanza.length > 0) {
30553
- config.cytobandBbURL = this.baseURL + cytoStanza[0].getProperty("bigDataUrl");
30554
- }
30555
-
30556
31120
  // Tracks.
30557
31121
  const filter = (t) => !Hub.filterTracks.has(t.name) && "hide" !== t.getProperty("visibility");
30558
31122
  config.tracks = this.#getTracksConfigs(filter);
30559
31123
 
30560
-
30561
31124
  return config
30562
31125
  }
30563
31126
 
30564
- getGroupedTrackConfigurations() {
30565
-
30566
- // Organize track configs by group
30567
- const trackConfigMap = new Map();
30568
- for (let c of this.#getTracksConfigs()) {
30569
- if (c.name === "cytoBandIdeo") continue
30570
- const groupName = c.group || "other";
30571
- if (trackConfigMap.has(groupName)) {
30572
- trackConfigMap.get(groupName).push(c);
30573
- } else {
30574
- trackConfigMap.set(groupName, [c]);
30575
- }
31127
+ async getGroupedTrackConfigurations(genomeId) {
31128
+ let trackHub = await this.#getTrackDbHub(genomeId);
31129
+ if (!trackHub && idMappings.has(genomeId)) {
31130
+ trackHub = await this.#getTrackDbHub(idMappings.get(genomeId));
30576
31131
  }
31132
+ if (!trackHub) {
31133
+ console.log(`Warning: no trackDB found for genomeId ${genomeId}.`);
31134
+ }
31135
+ return trackHub ? trackHub.getGroupedTrackConfigurations() : []
31136
+ }
30577
31137
 
30578
- // Build group structure
30579
- const groupStanazMap = this.groupStanzas ?
30580
- new Map(this.groupStanzas.map(groupStanza => [groupStanza.getProperty("name"), groupStanza])) :
30581
- new Map();
30582
-
30583
- return Array.from(trackConfigMap.keys()).map(groupName => {
30584
- return {
30585
- label: groupStanazMap.has(groupName) ? groupStanazMap.get(groupName).getProperty("label") : groupName,
30586
- tracks: trackConfigMap.get(groupName)
31138
+ async #getTrackDbHub(genomeId) {
31139
+ let trackHub = this.trackHubMap.get(genomeId);
31140
+ if (!trackHub) {
31141
+ for (let stanza of this.genomeStanzas) {
31142
+ if (genomeId === stanza.getProperty("genome")) {
31143
+ try {
31144
+ const trackDbURL = stanza.getProperty("trackDb");
31145
+ const trackStanzas = await loadStanzas(trackDbURL);
31146
+ trackHub = new TrackDbHub(trackStanzas, this.groupStanzas);
31147
+ this.trackHubMap.set(genomeId, trackHub);
31148
+ } catch (error) {
31149
+ console.error(`Error loading trackDb file: ${stanza.getProperty("trackDb")}`, error);
31150
+ }
31151
+ break
31152
+ }
30587
31153
  }
30588
- })
30589
-
31154
+ }
31155
+ return trackHub
30590
31156
  }
30591
31157
 
31158
+
30592
31159
  /**
30593
31160
  * Return an array of igv track config objects that satisfy the filter
30594
31161
  */
@@ -30626,7 +31193,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30626
31193
  "id": t.getProperty("track"),
30627
31194
  "name": t.getProperty("shortLabel"),
30628
31195
  "format": format,
30629
- "url": this.baseURL + t.getProperty("bigDataUrl"),
31196
+ "url": t.getProperty("bigDataUrl"),
30630
31197
  "displayMode": t.displayMode,
30631
31198
  };
30632
31199
 
@@ -30637,7 +31204,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30637
31204
  if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30638
31205
  if (config.description) config.description += "<br/>";
30639
31206
  config.description =
30640
- `<a target="_blank" href="${this.baseURL + t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
31207
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30641
31208
  } else if (t.hasProperty("longLabel")) {
30642
31209
  config.description = t.getProperty("longLabel");
30643
31210
  }
@@ -30669,7 +31236,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30669
31236
  max = Number.parseInt(tokens[1]);
30670
31237
  }
30671
31238
  if (Number.isNaN(max) || Number.isNaN(min)) {
30672
- console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
31239
+ console.warn(`Unexpected viewLimits value in track line: ${t.getProperty("viewLimits")}`);
30673
31240
  } else {
30674
31241
  config.min = min;
30675
31242
  config.max = max;
@@ -30688,15 +31255,15 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30688
31255
  config.searchIndex = t.getProperty("searchIndex");
30689
31256
  }
30690
31257
  if (t.hasProperty("searchTrix")) {
30691
- config.trixURL = this.baseURL + t.getProperty("searchTrix");
31258
+ config.trixURL = t.getProperty("searchTrix");
30692
31259
  }
30693
31260
 
30694
31261
  if (t.hasProperty("group")) {
30695
- config.group = t.getProperty("group");
30696
- if (this.groupPriorityMap && this.groupPriorityMap.has(config.group)) {
30697
- const nextPriority = this.groupPriorityMap.get(config.group) + 1;
31262
+ config._group = t.getProperty("group");
31263
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
31264
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30698
31265
  config.order = nextPriority;
30699
- this.groupPriorityMap.set(config.group, nextPriority);
31266
+ this.groupPriorityMap.set(config._group, nextPriority);
30700
31267
  }
30701
31268
  }
30702
31269
 
@@ -30705,96 +31272,92 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30705
31272
 
30706
31273
  }
30707
31274
 
30708
- function firstWord(str) {
30709
- const idx = str.indexOf(' ');
30710
- return idx > 0 ? str.substring(0, idx) : str
30711
- }
31275
+ /*
31276
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
31277
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
31278
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
31279
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
31280
+ */
30712
31281
 
30713
- class Stanza {
31282
+ const urlProperties = new Set(["descriptionUrl", "desriptionUrl",
31283
+ "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl",
31284
+ "genomesFile", "trackDb", "groups", "include", "html", "searchTrix", "groups",
31285
+ "chromSizes"]);
30714
31286
 
30715
- properties = new Map()
30716
31287
 
30717
- constructor(type, name) {
30718
- this.type = type;
30719
- this.name = name;
30720
- }
31288
+ const hubCache = new Map();
30721
31289
 
30722
- setProperty(key, value) {
30723
- this.properties.set(key, value);
31290
+ async function loadHub(url) {
31291
+ if (hubCache.has(url)) {
31292
+ return hubCache.get(url)
30724
31293
  }
30725
31294
 
30726
- getProperty(key) {
30727
- if (this.properties.has(key)) {
30728
- return this.properties.get(key)
30729
- } else if (this.parent) {
30730
- return this.parent.getProperty(key)
30731
- } else {
30732
- return undefined
30733
- }
31295
+ const stanzas = await loadStanzas(url);
31296
+ if (stanzas.length < 1) {
31297
+ throw new Error("Empty hub file")
30734
31298
  }
30735
31299
 
30736
- hasProperty(key) {
30737
- if (this.properties.has(key)) {
30738
- return true
30739
- } else if (this.parent) {
30740
- return this.parent.hasProperty(key)
30741
- } else {
30742
- return false
30743
- }
31300
+ const hubStanza = stanzas[0];
31301
+ if (hubStanza.type !== "hub") {
31302
+ throw new Error("First stanza must be a hub stanza")
30744
31303
  }
30745
31304
 
30746
- get format() {
30747
- const type = this.getProperty("type");
30748
- if (type) {
30749
- // Trim extra bed qualifiers (e.g. bigBed + 4)
30750
- return firstWord(type)
31305
+ let genomeStanzas;
31306
+ let trackStanzas;
31307
+ if (hubStanza.getProperty("useOneFile") === "on") {
31308
+ // This is a "onefile" hub, all stanzas are in the same file
31309
+ if (stanzas[1].type !== "genome") {
31310
+ throw new Error("Unexpected hub file -- expected 'genome' stanza but found " + stanzas[1].type)
30751
31311
  }
30752
- return undefined // unknown type
30753
- }
31312
+ const genomeStanza = stanzas[1];
31313
+ genomeStanzas = [genomeStanza];
31314
+ trackStanzas = stanzas.slice(2);
30754
31315
 
30755
- /**
30756
- * IGV display mode
30757
- */
30758
- get displayMode() {
30759
- let viz = this.getProperty("visibility");
30760
- if (!viz) {
30761
- return "COLLAPSED"
30762
- } else {
30763
- viz = viz.toLowerCase();
30764
- switch (viz) {
30765
- case "dense":
30766
- return "COLLAPSED"
30767
- case "pack":
30768
- return "EXPANDED"
30769
- case "squish":
30770
- return "SQUISHED"
30771
- default:
30772
- return "COLLAPSED"
31316
+ // If this is an assembly check chromSizes. This file can be very large, and not needed if whole genome view
31317
+ // is not enabled. Remove it if > 100 kb
31318
+ if (genomeStanza.hasOwnProperty("chromSizes")) {
31319
+ const chromSizes = genomeStanza.getProperty("chromSizes");
31320
+ try {
31321
+ const contentLength = await igvxhr.getContentLength(chromSizes);
31322
+ if (contentLength > 100000) {
31323
+ genomeStanza.removeProperty("chromSizes");
31324
+ }
31325
+ } catch (e) {
31326
+ console.error(`Error getting content length for chromSizes ${chromSizes}`, e);
30773
31327
  }
30774
- }
30775
- }
30776
- }
30777
31328
 
31329
+ }
30778
31330
 
30779
- /**
30780
- * Return the content length of the resource. If the content length cannot be determined return null;
30781
- * @param url
30782
- * @returns {Promise<number|string>}
30783
- */
30784
- async function getContentLength(url) {
30785
- try {
30786
- const response = await fetch(url, {method: 'HEAD'});
30787
- const headers = response.headers;
30788
- if (headers.has("content-length")) {
30789
- return headers.get("content-length")
30790
- } else {
30791
- return null
31331
+ } else {
31332
+ if (!hubStanza.hasProperty("genomesFile")) {
31333
+ throw new Error("hub.txt must specify 'genomesFile'")
30792
31334
  }
30793
- } catch (e) {
30794
- return null
31335
+ genomeStanzas = await loadStanzas(hubStanza.getProperty("genomesFile"));
30795
31336
  }
31337
+
31338
+ // Load group files for all genomes, if any.
31339
+ const uniqGroupURLs = new Set();
31340
+ genomeStanzas.forEach(s => {
31341
+ const groupURL = s.getProperty("groups");
31342
+ if (groupURL) uniqGroupURLs.add(groupURL);
31343
+
31344
+ });
31345
+ const groupStanzas = [];
31346
+ const groupPromises = Array.from(uniqGroupURLs).map(async url => {
31347
+ const stanza = await loadStanzas(url);
31348
+ return stanza
31349
+ });
31350
+ const groupResults = await Promise.all(groupPromises);
31351
+ groupResults.forEach(stanza => groupStanzas.push(...stanza));
31352
+
31353
+ const hub = new Hub(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas);
31354
+
31355
+ hubCache.set(url, hub);
31356
+
31357
+ return hub
30796
31358
  }
30797
31359
 
31360
+
30798
31361
  /**
30799
31362
  * Parse a UCSC file
30800
31363
  * @param url
@@ -30802,38 +31365,92 @@ async function getContentLength(url) {
30802
31365
  */
30803
31366
  async function loadStanzas(url) {
30804
31367
 
30805
- const response = await fetch(url);
30806
- const data = await response.text();
31368
+ const idx = url.lastIndexOf("/");
31369
+ const baseURL = url.substring(0, idx + 1);
31370
+ const host = getHost(url);
31371
+
31372
+ //const response = await fetch(url)
31373
+ const data = await igvxhr.loadString(url, {}); //await response.text()
30807
31374
  const lines = data.split(/\n|\r\n|\r/g);
30808
31375
 
30809
31376
  const nodes = [];
30810
31377
  let currentNode;
30811
31378
  let startNewNode = true;
30812
- for (let line of lines) {
30813
- const indent = indentLevel(line);
30814
- const i = line.indexOf(' ', indent);
30815
- if (i < 0) {
31379
+ for (let i = 0; i < lines.length; i++) {
31380
+
31381
+ let line = lines[i].trim();
31382
+
31383
+ if (line.length == 0) {
30816
31384
  // Break - start a new node
30817
31385
  startNewNode = true;
30818
31386
  } else {
30819
- const key = line.substring(indent, i).trim();
30820
- if (key.startsWith("#")) continue
30821
- const value = line.substring(i + 1).trim();
31387
+ if (line.startsWith("#")) {
31388
+ continue
31389
+ }
31390
+
31391
+ while (line.endsWith('\\')) {
31392
+ i++;
31393
+ if (i >= lines.length) {
31394
+ break
31395
+ }
31396
+ line = line.substring(0, line.length - 1) + lines[i].trim();
31397
+ }
31398
+
31399
+ if (line.startsWith("include")) {
31400
+ const relativeURL = line.substring(8).trim();
31401
+ const includeURL = getDataURL(relativeURL, host, baseURL);
31402
+ const includeStanzas = await loadStanzas(includeURL);
31403
+ for (let s of includeStanzas) {
31404
+ nodes.push(s);
31405
+ }
31406
+ }
31407
+
31408
+
31409
+ const i = line.indexOf(' ');
31410
+ const key = line.substring(0, i).trim();
31411
+ let value = line.substring(i + 1).trim();
31412
+
31413
+ if (key === "type") {
31414
+ // The "type" property contains format and sometimes other information. For example, data range
31415
+ // on a bigwig "type bigWig 0 .5"
31416
+ const tokens = value.split(/\s+/);
31417
+ value = tokens[0];
31418
+ if (value === "bigWig" && tokens.length === 3) {
31419
+ // This is a bigWig with a range
31420
+ const min = tokens[1];
31421
+ const max = tokens[2];
31422
+ if (currentNode) {
31423
+ currentNode.setProperty("min", min);
31424
+ currentNode.setProperty("max", max);
31425
+ }
31426
+ }
31427
+
31428
+ } else if (!["shortLabel", "longLabel", "metadata", "label"].includes(key)) {
31429
+ const tokens = value.split(/\s+/);
31430
+ value = tokens[0];
31431
+ }
31432
+
31433
+ if (urlProperties.has(key) || value.endsWith("URL") || value.endsWith("Url")) {
31434
+ value = getDataURL(value, host, baseURL);
31435
+ }
31436
+
30822
31437
  if (startNewNode) {
30823
- // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks,
30824
- // so track stanzas are flattened
30825
- const newNode = new Stanza(key, value);
30826
- nodes.push(newNode);
30827
- currentNode = newNode;
31438
+ currentNode = new Stanza(key, value);
31439
+ nodes.push(currentNode);
30828
31440
  startNewNode = false;
30829
31441
  }
31442
+
30830
31443
  currentNode.setProperty(key, value);
30831
31444
  }
30832
31445
  }
30833
-
30834
31446
  return resolveParents(nodes)
30835
31447
  }
30836
31448
 
31449
+ function firstWord(str) {
31450
+ const idx = str.indexOf(' ');
31451
+ return idx > 0 ? str.substring(0, idx) : str
31452
+ }
31453
+
30837
31454
  function resolveParents(nodes) {
30838
31455
  const nodeMap = new Map();
30839
31456
  for (let n of nodes) {
@@ -30848,13 +31465,30 @@ function resolveParents(nodes) {
30848
31465
  return nodes
30849
31466
  }
30850
31467
 
30851
- function indentLevel(str) {
30852
- let level = 0;
30853
- for (level = 0; level < str.length; level++) {
30854
- const c = str.charAt(level);
30855
- if (c !== ' ' && c !== '\t') break
31468
+ function getDataURL(url, host, baseURL) {
31469
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("gs://") || url.startsWith("s3://")) {
31470
+ return url
31471
+ } else if (url.startsWith("/")) {
31472
+ return host + url
31473
+ } else {
31474
+ return baseURL + url
30856
31475
  }
30857
- return level
31476
+ }
31477
+
31478
+ function getHost(url) {
31479
+ let host;
31480
+ if (url.startsWith("https://") || url.startsWith("http://")) {
31481
+ try {
31482
+ const tmp = new URL(url);
31483
+ host = `${tmp.protocol}//${tmp.host}`;
31484
+ } catch (e) {
31485
+ console.error("Error parsing base URL host", e);
31486
+ throw e
31487
+ }
31488
+ } else {
31489
+ host = '';
31490
+ }
31491
+ return host
30858
31492
  }
30859
31493
 
30860
31494
  const DEFAULT_GENOMES_URL = "https://igv.org/genomes/genomes.json";
@@ -30938,8 +31572,8 @@ const GenomeUtils = {
30938
31572
  if ((genomeID.startsWith("GCA_") || genomeID.startsWith("GCF_")) && genomeID.length >= 13) {
30939
31573
  try {
30940
31574
  const hubURL = convertToHubURL(genomeID);
30941
- const hub = await Hub.loadHub(hubURL);
30942
- reference = hub.getGenomeConfig();
31575
+ const hub = await loadHub(hubURL);
31576
+ reference = hub.getGenomeConfig(genomeID);
30943
31577
  } catch (e) {
30944
31578
  console.error(e);
30945
31579
  }
@@ -32901,27 +33535,18 @@ const distinctColorsPalette = _18_DistinctColorsViaChatGPT4.map(str => {
32901
33535
  return [ r, g, b ]
32902
33536
  });
32903
33537
 
32904
- const colorForNA = appleCrayonRGB('magnesium');
32905
- const sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors'];
32906
-
32907
33538
  class SampleInfo {
32908
33539
 
32909
33540
  static emptySpaceReplacement = '|'
32910
-
32911
- sampleDictionary = {}
32912
- attributeNames = []
32913
- sampleMappingDictionary = {}
32914
- colorDictionary = {}
32915
- attributeRangeLUT = {}
33541
+ static colorForNA = appleCrayonRGB('magnesium')
33542
+ static sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors']
32916
33543
 
32917
33544
  constructor(browser) {
32918
-
32919
33545
  const found = browser.tracks.some(t => typeof t.getSamples === 'function');
32920
33546
  if (found.length > 0) {
32921
33547
  browser.sampleInfoControl.setButtonVisibility(true);
32922
33548
  }
32923
33549
  this.initialize();
32924
-
32925
33550
  }
32926
33551
 
32927
33552
  initialize() {
@@ -32948,47 +33573,60 @@ class SampleInfo {
32948
33573
 
32949
33574
  getAttributes(sampleName) {
32950
33575
 
32951
- const key = 0 === Object.keys(this.sampleMappingDictionary) ? sampleName : (this.sampleMappingDictionary[sampleName] || sampleName);
33576
+ const key = this.sampleMappingDictionary[sampleName] || sampleName;
32952
33577
  return this.sampleDictionary[key]
32953
33578
  }
32954
33579
 
32955
- async loadSampleInfoFile(path) {
32956
- try {
32957
- const string = await igvxhr.loadString(path);
32958
- this.#processSampleInfoFileAsString(string);
32959
- this.sampleInfoFiles.push(path);
32960
- } catch (e) {
32961
- console.error(e.message);
33580
+ async loadSampleInfo(config) {
33581
+
33582
+ if (config.url) {
33583
+ await this.loadSampleInfoFile(config.url);
33584
+ } else {
33585
+
33586
+ const samples = { ...config };
33587
+ for (const [key, record] of Object.entries(samples)) {
33588
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33589
+ }
33590
+
33591
+ const [ value ] = Object.values(samples);
33592
+ const attributes = Object.keys(value);
33593
+
33594
+ this.loadSampleInfoHelper(attributes, samples);
33595
+
32962
33596
  }
32963
- }
32964
33597
 
32965
- #processSampleInfoFileAsString(string) {
33598
+ this.initialized = true;
33599
+ }
32966
33600
 
32967
- const sectionDictionary = createSectionDictionary(string);
33601
+ loadSampleInfoHelper(attributes, samples){
32968
33602
 
32969
- for (const [header, value] of Object.entries(sectionDictionary)) {
32970
- switch (header) {
32971
- case '#sampleTable':
32972
- this.#accumulateSampleTableDictionary(value);
32973
- break
32974
- case '#sampleMapping':
32975
- this.#accumulateSampleMappingDictionary(value);
32976
- break
32977
- case '#colors':
32978
- this.#accumulateColorScheme(value);
32979
- break
33603
+ // Establish the range of values for each attribute
33604
+ const lut = createAttributeRangeLUT(attributes, samples);
33605
+ accumulateDictionary(this.attributeRangeLUT, lut);
32980
33606
 
33607
+ // Ensure unique attribute names list
33608
+ const currentAttributeNameSet = new Set(this.attributeNames);
33609
+ for (const name of attributes) {
33610
+ if (!currentAttributeNameSet.has(name)) {
33611
+ this.attributeNames.push(name);
32981
33612
  }
32982
33613
  }
32983
33614
 
32984
- this.initialized = true;
33615
+ accumulateDictionary(this.sampleDictionary, samples);
32985
33616
 
32986
33617
  }
32987
33618
 
33619
+ async loadSampleInfoFile(path) {
33620
+ try {
33621
+ const string = await igvxhr.loadString(path);
33622
+ this.#processSampleInfoFileAsString(string);
33623
+ this.sampleInfoFiles.push(path);
33624
+ } catch (e) {
33625
+ console.error(e.message);
33626
+ }
33627
+ }
33628
+
32988
33629
  getAttributeColor(attribute, value) {
32989
- // if (value === 'NA') {
32990
- // console.log(`${ attribute } : ${ value }`)
32991
- // }
32992
33630
 
32993
33631
  let color;
32994
33632
 
@@ -33006,7 +33644,7 @@ class SampleInfo {
33006
33644
 
33007
33645
  } else if (typeof value === "string") {
33008
33646
 
33009
- color = 'NA' === value ? colorForNA : stringToRGBString(value);
33647
+ color = 'NA' === value ? SampleInfo.colorForNA : SampleInfo.stringToRGBString(value);
33010
33648
 
33011
33649
  } else {
33012
33650
 
@@ -33074,6 +33712,27 @@ class SampleInfo {
33074
33712
  return json
33075
33713
  }
33076
33714
 
33715
+ #processSampleInfoFileAsString(string) {
33716
+
33717
+ const sectionDictionary = createSectionDictionary(string);
33718
+
33719
+ for (const [header, value] of Object.entries(sectionDictionary)) {
33720
+ switch (header) {
33721
+ case '#sampleTable':
33722
+ this.#accumulateSampleTableDictionary(value);
33723
+ break
33724
+ case '#sampleMapping':
33725
+ this.#accumulateSampleMappingDictionary(value);
33726
+ break
33727
+ case '#colors':
33728
+ this.#accumulateColorScheme(value);
33729
+ break
33730
+
33731
+ }
33732
+ }
33733
+
33734
+ }
33735
+
33077
33736
  #accumulateSampleTableDictionary(lines) {
33078
33737
 
33079
33738
  // shift array with first item that is 'sample' or 'Linking_id'. Remaining items are attribute names
@@ -33113,22 +33772,11 @@ class SampleInfo {
33113
33772
  } // for (lines)
33114
33773
 
33115
33774
  for (const [key, record] of Object.entries(samples)) {
33116
- samples[key] = toNumericalRepresentation(record);
33775
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33117
33776
  }
33118
33777
 
33119
- // Establish the range of values for each attribute
33120
- const lut = createAttributeRangeLUT(attributes, samples);
33121
- accumulateDictionary(this.attributeRangeLUT, lut);
33122
-
33123
- // Ensure unique attribute names list
33124
- const currentAttributeNameSet = new Set(this.attributeNames);
33125
- for (const name of attributes) {
33126
- if (!currentAttributeNameSet.has(name)) {
33127
- this.attributeNames.push(name);
33128
- }
33129
- }
33778
+ this.loadSampleInfoHelper(attributes, samples);
33130
33779
 
33131
- accumulateDictionary(this.sampleDictionary, samples);
33132
33780
  }
33133
33781
 
33134
33782
  #accumulateSampleMappingDictionary(lines) {
@@ -33229,7 +33877,7 @@ class SampleInfo {
33229
33877
  this.colorDictionary[attribute] = attributeValue => {
33230
33878
 
33231
33879
  if ('NA' === attributeValue) {
33232
- return colorForNA
33880
+ return SampleInfo.colorForNA
33233
33881
  } else {
33234
33882
  const [min, max] = this.attributeRangeLUT[attribute];
33235
33883
  const interpolant = (attributeValue - min) / (max - min);
@@ -33249,8 +33897,34 @@ class SampleInfo {
33249
33897
 
33250
33898
  }
33251
33899
 
33252
- }
33900
+ static toNumericalRepresentation(obj) {
33901
+ const result = Object.assign({}, obj);
33253
33902
 
33903
+ for (const [key, value] of Object.entries(result)) {
33904
+ if (typeof value === 'string' && !isNaN(value)) {
33905
+ result[key] = Number(value);
33906
+ }
33907
+ }
33908
+
33909
+ return result
33910
+ }
33911
+
33912
+ static stringToRGBString(str) {
33913
+ let hash = 0;
33914
+ for (let i = 0; i < str.length; i++) {
33915
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
33916
+ }
33917
+
33918
+ let color = [];
33919
+ for (let i = 0; i < 3; i++) {
33920
+ const value = (hash >> (i * 8)) & 0xff;
33921
+ color.push(value);
33922
+ }
33923
+
33924
+ return `rgb(${color.join(', ')})`
33925
+ }
33926
+
33927
+ }
33254
33928
 
33255
33929
  function createSectionDictionary(string) {
33256
33930
 
@@ -33261,14 +33935,14 @@ function createSectionDictionary(string) {
33261
33935
  let currentHeader;
33262
33936
 
33263
33937
  // If the first line does not start with a section header an initial #sampleTable is implied
33264
- if (!sampleInfoFileHeaders.includes(lines[0])) {
33938
+ if (!SampleInfo.sampleInfoFileHeaders.includes(lines[0])) {
33265
33939
  currentHeader = '#sampleTable';
33266
33940
  dictionary[currentHeader] = [];
33267
33941
  }
33268
33942
 
33269
33943
  for (const line of lines) {
33270
33944
 
33271
- if (sampleInfoFileHeaders.includes(line)) {
33945
+ if (SampleInfo.sampleInfoFileHeaders.includes(line)) {
33272
33946
  currentHeader = line;
33273
33947
  dictionary[currentHeader] = [];
33274
33948
  } else if (currentHeader && false === line.startsWith('#')) {
@@ -33287,7 +33961,6 @@ function accumulateDictionary(accumulator, dictionary) {
33287
33961
  }
33288
33962
  }
33289
33963
 
33290
-
33291
33964
  function createAttributeRangeLUT(names, dictionary) {
33292
33965
 
33293
33966
  const lut = {};
@@ -33333,35 +34006,6 @@ function createAttributeRangeLUT(names, dictionary) {
33333
34006
  return lut
33334
34007
  }
33335
34008
 
33336
- function toNumericalRepresentation(obj) {
33337
-
33338
- const result = Object.assign({}, obj);
33339
-
33340
- for (const [key, value] of Object.entries(result)) {
33341
- if (typeof value === 'string' && !isNaN(value)) {
33342
- result[key] = Number(value);
33343
- }
33344
- }
33345
-
33346
- return result
33347
- }
33348
-
33349
- function stringToRGBString(str) {
33350
-
33351
- let hash = 0;
33352
- for (let i = 0; i < str.length; i++) {
33353
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
33354
- }
33355
-
33356
- let color = [];
33357
- for (let i = 0; i < 3; i++) {
33358
- const value = (hash >> (i * 8)) & 0xff;
33359
- color.push(value);
33360
- }
33361
-
33362
- return `rgb(${color.join(', ')})`
33363
- }
33364
-
33365
34009
  const sampleInfoTileXShim = 8;
33366
34010
  const sampleInfoTileWidth = 16;
33367
34011
 
@@ -33970,7 +34614,11 @@ class SampleNameViewport {
33970
34614
 
33971
34615
  checkCanvas() {
33972
34616
 
33973
- const width = this.browser.sampleNameViewportWidth || 0;
34617
+ let width = 0;
34618
+ if (true === this.browser.showSampleNames) {
34619
+ width = undefined === this.browser.sampleNameViewportWidth ? 0 : this.browser.sampleNameViewportWidth;
34620
+ }
34621
+
33974
34622
  this.ctx.canvas.width = width * window.devicePixelRatio;
33975
34623
  this.ctx.canvas.style.width = `${width}px`;
33976
34624
 
@@ -35082,7 +35730,7 @@ class MergedTrack extends TrackBase {
35082
35730
  let element = document.createElement('div');
35083
35731
  element.textContent = 'Separate tracks';
35084
35732
 
35085
- function click(e) {
35733
+ async function click(e) {
35086
35734
 
35087
35735
  // Capture state which will be nulled when track is removed
35088
35736
  const groupAutoscale = this.autoscale;
@@ -35098,9 +35746,10 @@ class MergedTrack extends TrackBase {
35098
35746
  track.autoscaleGroup = name;
35099
35747
  }
35100
35748
  track.isMergedTrack = false;
35101
- browser.addTrack(track.config, track);
35749
+ browser.addTrack(track);
35102
35750
  }
35103
- browser.updateViews();
35751
+ await browser.updateViews();
35752
+ browser.reorderTracks();
35104
35753
  }
35105
35754
 
35106
35755
  return {element, click}
@@ -35195,7 +35844,7 @@ class OverlayTrackButton extends NavbarButton {
35195
35844
  }
35196
35845
  }
35197
35846
 
35198
- function trackOverlayClickHandler(e) {
35847
+ async function trackOverlayClickHandler(e) {
35199
35848
 
35200
35849
  if (true === isOverlayTrackCriteriaMet(this.browser)) {
35201
35850
 
@@ -35232,9 +35881,9 @@ function trackOverlayClickHandler(e) {
35232
35881
  track.trackView.dispose();
35233
35882
  }
35234
35883
 
35235
- this.browser.addTrack(config, mergedTrack);
35236
- mergedTrack.trackView.updateViews();
35237
-
35884
+ this.browser.addTrack(mergedTrack);
35885
+ await mergedTrack.trackView.updateViews();
35886
+ this.browser.reorderTracks();
35238
35887
  }
35239
35888
 
35240
35889
  }
@@ -36841,14 +37490,45 @@ class InputDialog {
36841
37490
 
36842
37491
 
36843
37492
  present(options, e) {
36844
-
36845
37493
  this.label.textContent = options.label;
36846
37494
  this._input.value = options.value;
36847
37495
  this.callback = options.callback || options.click;
36848
37496
 
36849
- const { top} = e.currentTarget.parentElement.getBoundingClientRect();
36850
- this.container.style.top = `${ top }px`;
36851
- this.container.style.display = 'flex';
37497
+ this.container.style.display = '';
37498
+
37499
+ // Get click coordinates
37500
+ const clickX = e.clientX;
37501
+ const clickY = e.clientY;
37502
+
37503
+ // Get dialog dimensions
37504
+ const dialogWidth = this.container.offsetWidth;
37505
+ const dialogHeight = this.container.offsetHeight;
37506
+
37507
+ // Calculate available space
37508
+ const windowWidth = window.innerWidth;
37509
+ const windowHeight = window.innerHeight;
37510
+
37511
+ // Calculate position to keep dialog on screen
37512
+ let left = clickX;
37513
+ let top = clickY;
37514
+
37515
+ // Adjust horizontal position if dialog would go off screen
37516
+ if (left + dialogWidth > windowWidth) {
37517
+ left = windowWidth - dialogWidth - 10; // 10px padding from edge
37518
+ }
37519
+
37520
+ // Adjust vertical position if dialog would go off screen
37521
+ if (top + dialogHeight > windowHeight) {
37522
+ top = windowHeight - dialogHeight - 10; // 10px padding from edge
37523
+ }
37524
+
37525
+ // Ensure minimum distance from edges
37526
+ left = Math.max(10, left);
37527
+ top = Math.max(10, top);
37528
+
37529
+ // Apply positions
37530
+ this.container.style.left = `${left}px`;
37531
+ this.container.style.top = `${top}px`;
36852
37532
  }
36853
37533
  }
36854
37534
 
@@ -50496,7 +51176,7 @@ class RemoteFile {
50496
51176
 
50497
51177
  let url = this.url.slice(); // slice => copy
50498
51178
  if (this.config.oauthToken) {
50499
- const token = resolveToken(this.config.oauthToken);
51179
+ const token = await resolveToken(this.config.oauthToken);
50500
51180
  headers['Authorization'] = `Bearer ${token}`;
50501
51181
  }
50502
51182
 
@@ -55261,7 +55941,7 @@ function create_nested_array(value, shape) {
55261
55941
  async function openH5File(options) {
55262
55942
 
55263
55943
  // Some clients (notably igv-webapp) pass a File reference in the url field. Fix this
55264
- if(options.url && isBlobLike(options.url)) {
55944
+ if (options.url && isBlobLike(options.url)) {
55265
55945
  options.file = options.url;
55266
55946
  options.url = undefined;
55267
55947
  }
@@ -55291,26 +55971,30 @@ async function openH5File(options) {
55291
55971
 
55292
55972
  async function readExternalIndex(options) {
55293
55973
 
55294
- let indexReader;
55295
- if(options.indexReader) {
55296
- indexReader = options.indexReader;
55297
- }
55298
- else if(options.index) {
55974
+ if (options.index) {
55299
55975
  return options.index
55300
- } else if (options.indexURL) {
55301
- indexReader = new RemoteFile({url: options.indexURL});
55302
- } else if (options.indexPath) {
55303
- indexReader = new NodeLocalFile({path: options.indexPath});
55304
- } else if (options.indexFile) {
55305
- indexReader = new BlobFile({file: options.indexFile});
55306
- }
55307
- if (indexReader) {
55976
+ } else {
55977
+ let indexReader;
55978
+ if (options.indexReader) {
55979
+ indexReader = options.indexReader;
55980
+ } else {
55981
+ let indexOptions;
55982
+ if (options.indexURL) {
55983
+ indexOptions = Object.assign({url: options.indexURL}, options);
55984
+ } else if (options.indexPath) {
55985
+ indexOptions = Object.assign({path: options.indexPath}, options);
55986
+ } else if (options.indexFile) {
55987
+ indexOptions = Object.assign({file: options.indexFile}, options);
55988
+ } else {
55989
+ return undefined
55990
+ }
55991
+ indexReader = getReaderFor(indexOptions);
55992
+ }
55308
55993
  const indexFileContents = await indexReader.read();
55309
55994
  const indexFileJson = new TextDecoder().decode(indexFileContents);
55310
55995
  return JSON.parse(indexFileJson)
55311
- } else {
55312
- return undefined
55313
55996
  }
55997
+
55314
55998
  }
55315
55999
 
55316
56000
 
@@ -55388,9 +56072,9 @@ class HDF5Reader {
55388
56072
  * @param {string} h5_file - path for the pytor file
55389
56073
  * @param {integer} bin_size - bin size
55390
56074
  */
55391
- constructor(h5_file, bin_size=100000){
56075
+ constructor(config, bin_size=100000){
55392
56076
 
55393
- this.h5_file = h5_file;
56077
+ this.config = config;
55394
56078
  this.bin_size = bin_size;
55395
56079
  this.h5_obj = undefined;
55396
56080
  this.pytorKeys = [];
@@ -55400,11 +56084,8 @@ class HDF5Reader {
55400
56084
  async fetch(){
55401
56085
 
55402
56086
  if(!this.h5_obj) {
55403
- this.h5_obj = await openH5File({
55404
- url: this.h5_file,
55405
- fetchSize: 1000000,
55406
- maxSize: 200000000
55407
- });
56087
+ const options = Object.assign(this.config, {fetchSize: 1000000, maxSize: 200000000});
56088
+ this.h5_obj = await openH5File(options);
55408
56089
  }
55409
56090
  return this.h5_obj
55410
56091
  }
@@ -64440,7 +65121,7 @@ class CNVPytorTrack extends TrackBase {
64440
65121
  this.set_available_callers();
64441
65122
 
64442
65123
  } else {
64443
- this.cnvpytor_obj = new HDF5Reader(this.config.url, this.bin_size);
65124
+ this.cnvpytor_obj = new HDF5Reader(this.config, this.bin_size);
64444
65125
  // get chrom list that currently user viewing
64445
65126
  let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
64446
65127
 
@@ -69428,6 +70109,23 @@ class ReferenceFrame {
69428
70109
  return this.genome.getChromosome(this.chr)
69429
70110
  }
69430
70111
 
70112
+ /**
70113
+ * Update reference frame based on new viewport width
70114
+ * @param {number} viewportWidth - The calculated viewport width
70115
+ */
70116
+ updateForViewportWidth(viewportWidth) {
70117
+ const {chr} = this;
70118
+ const {bpLength} = this.getChromosome();
70119
+ const viewportWidthBP = this.toBP(viewportWidth);
70120
+
70121
+ // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
70122
+ if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
70123
+ this.bpPerPixel = bpLength / viewportWidth;
70124
+ } else {
70125
+ this.end = this.start + this.toBP(viewportWidth);
70126
+ }
70127
+ }
70128
+
69431
70129
  getMultiLocusLabelBPLengthOnly(pixels) {
69432
70130
  const margin = '&nbsp';
69433
70131
  const ss = Math.floor(this.start) + 1;
@@ -69513,7 +70211,7 @@ function createReferenceFrameList(loci, genome, browserFlanking, minimumBases, v
69513
70211
  })
69514
70212
  }
69515
70213
 
69516
- const _version = "3.2.6";
70214
+ const _version = "3.3.0";
69517
70215
  function version() {
69518
70216
  return _version
69519
70217
  }
@@ -69557,10 +70255,24 @@ class ChromosomeSelectWidget {
69557
70255
  this.select.setAttribute('name', 'chromosome-select-widget');
69558
70256
  this.container.appendChild(this.select);
69559
70257
 
69560
- this.select.addEventListener('change', () => {
70258
+ this.select.addEventListener('change', async () => {
69561
70259
  this.select.blur();
69562
70260
  if (this.select.value !== '' && maximumSequenceCountExceeded !== this.select.value) {
69563
- browser.search(this.select.value);
70261
+
70262
+ if (this.select.value.trim().toLowerCase() === "all" || this.select.value === "*") {
70263
+ if (browser.genome.wholeGenomeView) {
70264
+ const wgChr = browser.genome.getChromosome("all");
70265
+ return {chr: "all", start: 0, end: wgChr.bpLength}
70266
+ }
70267
+ } else {
70268
+ const chromosome = await browser.genome.loadChromosome(this.select.value);
70269
+ const locusObject = {chr: chromosome.name};
70270
+ if (locusObject.start === undefined && locusObject.end === undefined) {
70271
+ locusObject.start = 0;
70272
+ locusObject.end = chromosome.bpLength;
70273
+ }
70274
+ browser.updateLoci([locusObject]);
70275
+ }
69564
70276
  }
69565
70277
  });
69566
70278
 
@@ -69585,7 +70297,9 @@ class ChromosomeSelectWidget {
69585
70297
  this.genome = genome;
69586
70298
 
69587
70299
  // Start with explicit chromosome name list
69588
- const list = genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) || [];
70300
+ const list = (genome.wgChromosomeNames) ?
70301
+ genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) :
70302
+ [];
69589
70303
 
69590
70304
  if (this.showAllChromosomes && genome.chromosomeNames.length > 1) {
69591
70305
  const exclude = new Set(list);
@@ -70811,7 +71525,7 @@ class ResponsiveNavbar {
70811
71525
  });
70812
71526
 
70813
71527
  this.searchInput.addEventListener('change', () => {
70814
- browser.doSearch(this.searchInput.value);
71528
+ this.doSearch(this.searchInput.value);
70815
71529
  });
70816
71530
 
70817
71531
  const searchIconContainer = document.createElement('div');
@@ -70822,7 +71536,7 @@ class ResponsiveNavbar {
70822
71536
  searchIconContainer.appendChild(searchIcon);
70823
71537
 
70824
71538
  searchIconContainer.addEventListener('click', () => {
70825
- browser.doSearch(this.searchInput.value);
71539
+ this.doSearch(this.searchInput.value);
70826
71540
  });
70827
71541
 
70828
71542
  this.windowSizePanel = new WindowSizePanel(locusSizeGroup, browser);
@@ -70970,6 +71684,22 @@ class ResponsiveNavbar {
70970
71684
  this.navigation.style.display = 'flex';
70971
71685
  }
70972
71686
 
71687
+ /**
71688
+
71689
+ * Search for the locus string -- this function is called from the navbar search box, and is not part of the API.
71690
+ * Wraps ```search``` and presents an error dialog if false.
71691
+ *
71692
+ * @param locus
71693
+ * @param init
71694
+ * @returns {Promise<void>}
71695
+ */
71696
+ async doSearch(locus) {
71697
+ const success = await this.browser.search(locus);
71698
+ if (!success) {
71699
+ this.browser.alert.present(new Error(`Unrecognized locus: <b> ${locus} </b>`));
71700
+ }
71701
+ }
71702
+
70973
71703
  }
70974
71704
 
70975
71705
  function logo() {
@@ -72221,6 +72951,9 @@ class ChromAliasDefaults {
72221
72951
  aliasRecord["_cap"] = cap;
72222
72952
  }
72223
72953
 
72954
+ const chrPrefixAlias = aliasRecord.chr.startsWith("chr") ? aliasRecord.chr.substring(3) : "chr" + aliasRecord.chr;
72955
+ aliasRecord["_chrprefix_"] = chrPrefixAlias;
72956
+
72224
72957
  }
72225
72958
 
72226
72959
  }
@@ -72245,7 +72978,7 @@ class ChromAliasBB {
72245
72978
  }
72246
72979
 
72247
72980
  async preload(chrNames) {
72248
- await this.reader.preload();
72981
+ await this.reader.preload();
72249
72982
  for(let nm of chrNames) {
72250
72983
  await this.search(nm);
72251
72984
  }
@@ -72297,12 +73030,6 @@ class ChromAliasBB {
72297
73030
  }
72298
73031
  return this.aliasRecordCache.get(alias)
72299
73032
  }
72300
-
72301
- async getChromosomeNames() {
72302
- await this.reader.loadHeader();
72303
- return Array.from(this.reader.chrNames)
72304
- }
72305
-
72306
73033
  }
72307
73034
 
72308
73035
  /**
@@ -72327,10 +73054,9 @@ class ChromAliasFile {
72327
73054
  this.genome = genome;
72328
73055
  }
72329
73056
 
72330
- async preload() {
72331
- return this.loadAliases();
73057
+ async preload(chrNames) {
73058
+ // A no-op, this is a text file, no need to preload
72332
73059
  }
72333
-
72334
73060
  /**
72335
73061
  * Return the canonical chromosome name for the alias. If none found return the alias
72336
73062
  *
@@ -72496,7 +73222,7 @@ class CytobandFile {
72496
73222
  for (let line of lines) {
72497
73223
 
72498
73224
  const tokens = line.split("\t");
72499
- const chrName = tokens[0]; //genome.getChromosomeName(tokens[0]) // Note allowance for alias name, not sure why this is needed here
73225
+ const chrName = tokens[0];
72500
73226
  if (!lastChr) lastChr = chrName;
72501
73227
 
72502
73228
  if (chrName !== lastChr) {
@@ -72514,37 +73240,20 @@ class CytobandFile {
72514
73240
  bands.push(new Cytoband(start, end, name, stain));
72515
73241
  }
72516
73242
  }
72517
-
72518
- }
72519
-
72520
- /**
72521
- * Infer genome chromosome names from cytoband data. This should only be used as a last resort
72522
- */
72523
- async getChromosomeNames() {
72524
- if(this.cytobands.size === 0) {
72525
- await this.#loadCytobands();
73243
+ if(bands.length > 0) {
73244
+ this.cytobands.set(lastChr, bands);
72526
73245
  }
72527
- return Array.from(this.cytobands.keys())
72528
- }
72529
73246
 
72530
- /**
72531
- * Infer chromosome objects from cytoband data. This should only be used as last resort.
72532
- */
72533
- async getChromosomes() {
72534
- if(this.cytobands.size === 0) {
72535
- await this.#loadCytobands();
72536
- }
72537
-
72538
- const chromosomes = [];
72539
- let order = 0;
72540
- for(let [chrName, cytoList] of this.cytobands.entries()) {
72541
- chromosomes.push(new Chromosome(chrName, order++, cytoList[cytoList.length - 1].end));
72542
- }
72543
- return chromosomes
72544
73247
  }
72545
73248
 
72546
73249
  }
72547
73250
 
73251
+ const ucsdIDMap = new Map([
73252
+ ["1kg_ref", "hg18"],
73253
+ ["1kg_v37", "hg19"],
73254
+ ["b37", "hg19"]
73255
+ ]);
73256
+
72548
73257
  /**
72549
73258
  * The Genome class represents an assembly and consists of the following elements
72550
73259
  * sequence - Object representing the DNA sequence
@@ -72569,8 +73278,10 @@ class Genome {
72569
73278
  this.config = config;
72570
73279
  this.browser = browser;
72571
73280
  this.id = config.id || generateGenomeID(config);
73281
+ this.ucscID = config.ucscID || ucsdIDMap.get(this.id) || this.id;
73282
+ this.blatDB = config.blatDB || this.ucscID;
72572
73283
  this.name = config.name;
72573
- this.nameSet = config.nameSet;
73284
+ this.nameSet = config.nameSet || 'ucsc';
72574
73285
  }
72575
73286
 
72576
73287
 
@@ -72578,49 +73289,45 @@ class Genome {
72578
73289
 
72579
73290
  const config = this.config;
72580
73291
 
73292
+ // Load sequence
72581
73293
  this.sequence = await loadSequence(config, this.browser);
72582
73294
 
72583
- if (config.chromSizesURL) {
72584
- // a chromSizes file is neccessary for 2bit sequences for whole-genome view
73295
+
73296
+ // Load cytobands. This is optional but required to support the ideogram. Only needed for whole genome view
73297
+ if(false !== config.showIdeogram && false !== config.wholeGenomeView) {
73298
+ if (config.cytobandURL) {
73299
+ this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
73300
+ } else if (config.cytobandBbURL) {
73301
+ this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
73302
+ }
73303
+ }
73304
+
73305
+ // Search for chromosomes, that is an array of chromosome objects containing name and length. This is
73306
+ // optional but required to support whole genome view.
73307
+ if (this.sequence.chromosomes) {
73308
+ this.chromosomes = this.sequence.chromosomes;
73309
+ } else if (config.chromSizesURL) {
72585
73310
  this.chromosomes = await loadChromSizes(config.chromSizesURL);
72586
73311
  } else {
72587
- // if the sequence defines chromosomes use them (fasta does, 2bit does not)
72588
- this.chromosomes = this.sequence.chromosomes || new Map();
73312
+ this.chromosomes = new Map(); // Cache, chromosome are added as they are loaded
72589
73313
  }
72590
73314
 
72591
- if (this.chromosomes.size > 0) {
73315
+ // Search for chromosome names. This is optional but required to support the chromosome pulldown
73316
+ if (this.sequence.chromosomeNames) {
73317
+ this.chromosomeNames = this.sequence.chromosomeNames; // Twobit files can supply chromosome names unless they use an external index
73318
+ } else if (this.chromosomes.size > 0) {
72592
73319
  this.chromosomeNames = Array.from(this.chromosomes.keys());
72593
73320
  }
72594
73321
 
73322
+ // Chromosome alias
72595
73323
  if (config.chromAliasBbURL) {
72596
73324
  this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this);
72597
- if (!this.chromosomeNames) {
72598
- this.chromosomeNames = await this.chromAlias.getChromosomeNames();
72599
- }
72600
73325
  } else if (config.aliasURL) {
72601
73326
  this.chromAlias = new ChromAliasFile(config.aliasURL, Object.assign({}, config), this);
72602
73327
  } else if (this.chromosomeNames) {
72603
73328
  this.chromAlias = new ChromAliasDefaults(this.id, this.chromosomeNames);
72604
73329
  }
72605
73330
 
72606
- if (config.cytobandBbURL) {
72607
- this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
72608
- } else if (config.cytobandURL) {
72609
- this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
72610
- }
72611
-
72612
- // Last resort for chromosome information -- retrieve it from the cytoband source if supported
72613
- if (!this.chromosomeNames && typeof this.cytobandSource.getChromosomeNames === 'function') {
72614
- this.chromosomeNames = await this.cytobandSource.getChromosomeNames();
72615
- }
72616
- if (this.chromosomes.size === 0 && typeof this.cytobandSource.getChromosomes === 'function') {
72617
- const c = await this.cytobandSource.getChromosomes();
72618
- for (let chromosome of c) {
72619
- this.chromosomes.set(c.name, c);
72620
- }
72621
- }
72622
-
72623
-
72624
73331
  if (false !== config.wholeGenomeView && this.chromosomes.size > 0) {
72625
73332
  // Set chromosome order for WG view and chromosome pulldown. If chromosome order is not specified sort
72626
73333
  if (config.chromosomeOrder) {
@@ -72672,9 +73379,9 @@ class Genome {
72672
73379
  getHomeChromosomeName() {
72673
73380
  if (this.showWholeGenomeView() && this.chromosomes.has("all")) {
72674
73381
  return "all"
72675
- } else {
73382
+ } else if (this.chromosomeNames) {
72676
73383
  return this.chromosomeNames[0]
72677
- }
73384
+ } else ;
72678
73385
  }
72679
73386
 
72680
73387
  getChromosomeName(chr) {
@@ -72725,18 +73432,18 @@ class Genome {
72725
73432
  if (!aliasRecord && chr !== chr.toLowerCase()) {
72726
73433
  aliasRecord = await this.chromAlias.search(chr.toLowerCase());
72727
73434
  }
72728
- if(aliasRecord) {
73435
+ if (aliasRecord) {
72729
73436
  // Add some aliases for case insensitivy
72730
73437
  const upper = aliasRecord.chr.toUpperCase();
72731
73438
  const lower = aliasRecord.chr.toLowerCase();
72732
73439
  const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
72733
- if(aliasRecord.chr !== upper) {
73440
+ if (aliasRecord.chr !== upper) {
72734
73441
  aliasRecord["_uppercase"] = upper;
72735
73442
  }
72736
- if(aliasRecord.chr !== lower) {
73443
+ if (aliasRecord.chr !== lower) {
72737
73444
  aliasRecord["_lowercase"] = lower;
72738
73445
  }
72739
- if(aliasRecord.chr !== cap) {
73446
+ if (aliasRecord.chr !== cap) {
72740
73447
  aliasRecord["_cap"] = cap;
72741
73448
  }
72742
73449
  }
@@ -72868,6 +73575,10 @@ class Genome {
72868
73575
  return undefined
72869
73576
  }
72870
73577
  }
73578
+
73579
+ getHubURLs() {
73580
+ return this.config.hubs
73581
+ }
72871
73582
  }
72872
73583
 
72873
73584
  /**
@@ -72910,7 +73621,7 @@ function generateGenomeID(config) {
72910
73621
  }
72911
73622
  }
72912
73623
 
72913
- var igvCss = '.igv-ui-dropdown {\n cursor: default;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2048;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n background-color: white;\n}\n.igv-ui-dropdown > div {\n overflow-y: auto;\n overflow-x: hidden;\n background-color: white;\n}\n.igv-ui-dropdown > div > div {\n padding: 4px;\n width: 100%;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: 1px;\n background-color: white;\n}\n.igv-ui-dropdown > div > div:last-child {\n border-bottom-color: transparent;\n border-bottom-width: 0;\n}\n.igv-ui-dropdown > div > div:hover {\n cursor: pointer;\n background-color: rgba(0, 0, 0, 0.04);\n}\n\n.igv-ui-popover {\n cursor: default;\n position: absolute;\n z-index: 2048;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n background-color: white;\n}\n.igv-ui-popover > div:first-child {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-width: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-popover > div:first-child > div:first-child {\n margin-left: 4px;\n}\n.igv-ui-popover > div:first-child > div:last-child {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-popover > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-popover > div:last-child {\n user-select: text;\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 400px;\n max-width: 800px;\n background-color: white;\n border-bottom-width: 0;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-ui-popover > div:last-child > div {\n margin-left: 4px;\n margin-right: 4px;\n min-width: 220px;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.igv-ui-popover > div:last-child > div > span {\n font-weight: bolder;\n}\n.igv-ui-popover > div:last-child hr {\n width: 100%;\n}\n\n.igv-ui-alert-dialog-container {\n position: fixed;\n top: 20%;\n left: 50%;\n transform: translateX(-50%);\n z-index: 2048;\n box-sizing: content-box;\n width: 400px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n outline: none;\n font-family: \"Open Sans\", sans-serif;\n font-size: 15px;\n font-weight: 400;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:first-child {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-alert-dialog-container > div:first-child div:first-child {\n padding-left: 8px;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body {\n -webkit-user-select: text;\n -moz-user-select: text;\n -ms-user-select: text;\n user-select: text;\n color: #373737;\n width: 100%;\n height: calc(100% - 24px - 64px);\n overflow-y: scroll;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body .igv-ui-alert-dialog-body-copy {\n margin: 16px;\n width: auto;\n height: auto;\n overflow-wrap: break-word;\n word-break: break-word;\n background-color: white;\n border: unset;\n}\n.igv-ui-alert-dialog-container > div:last-child {\n width: 100%;\n margin-bottom: 10px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:last-child div {\n margin: unset;\n width: 40px;\n height: 30px;\n line-height: 30px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n border-color: #2B81AF;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-alert-dialog-container > div:last-child div:hover {\n cursor: pointer;\n border-color: #25597f;\n background-color: #25597f;\n}\n\n.igv-ui-generic-dialog-container {\n box-sizing: content-box;\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n width: 300px;\n height: fit-content;\n padding-bottom: 16px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input > div {\n width: fit-content;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input[type=range] {\n width: 70%;\n -webkit-appearance: none;\n background: linear-gradient(90deg, white, black);\n outline: none;\n margin: 0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input output {\n display: block;\n height: 100%;\n width: 20%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n padding-top: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div {\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n\n.igv-ui-generic-container {\n position: fixed;\n cursor: pointer;\n background-color: white;\n z-index: 2048;\n box-sizing: content-box;\n width: 300px;\n height: 200px;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-ui-generic-container > div:nth-child(1) > div {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-ui-colorpicker-container {\n background-color: #eee;\n height: fit-content;\n flex-direction: column;\n flex-wrap: nowrap;\n}\n.igv-ui-colorpicker-container > div:nth-child(2) {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-colorpicker-container > div:nth-child(3) {\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n padding: 0.5rem;\n font-family: \"Open Sans\", sans-serif;\n font-size: 1rem;\n font-weight: 400;\n width: 100%;\n border-top-style: solid;\n border-top-width: thin;\n border-top-color: #373737;\n}\n.igv-ui-colorpicker-container > div:nth-child(4) {\n width: 100%;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n\n.igv-ui-color-swatch {\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n margin: 2px;\n width: 32px;\n height: 32px;\n border-style: solid;\n border-width: 0;\n border-color: white;\n border-radius: 3px;\n}\n\n.igv-ui-color-swatch-shim {\n cursor: pointer;\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n width: 32px;\n height: 32px;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: white;\n border-radius: 4px;\n}\n\n.igv-ui-dialog {\n z-index: 2048;\n position: fixed;\n width: fit-content;\n height: fit-content;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n background-color: white;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n}\n.igv-ui-dialog .igv-ui-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-dialog .igv-ui-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-dialog .igv-ui-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-dialog .igv-ui-dialog-one-liner {\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin: 8px;\n overflow-wrap: break-word;\n background-color: white;\n font-weight: bold;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel {\n width: 100%;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div {\n margin: 16px;\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child {\n background-color: #5ea4e0;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child {\n background-color: #c4c4c4;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-ui-panel, .igv-ui-panel-row, .igv-ui-panel-column {\n z-index: 2048;\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-panel-column {\n display: flex;\n flex-direction: column;\n}\n\n.igv-ui-panel-row {\n display: flex;\n flex-direction: row;\n}\n\n.igv-ui-textbox {\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-table {\n background-color: white;\n}\n\n.igv-ui-table thead {\n position: sticky;\n top: 0;\n}\n\n.igv-ui-table th {\n text-align: left;\n}\n\n.igv-ui-table td {\n padding-right: 20px;\n}\n\n.igv-ui-table tr:hover {\n background-color: lightblue;\n}\n\n.igv-ui-center-fixed {\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.igv-navbar {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n box-sizing: border-box;\n width: 100%;\n color: #444;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n line-height: 32px;\n padding-left: 8px;\n padding-right: 8px;\n margin-top: 2px;\n margin-bottom: 6px;\n height: 32px;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: #f3f3f3;\n}\n.igv-navbar .igv-navbar-left-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 32px;\n line-height: 32px;\n}\n.igv-navbar .igv-navbar-left-container .igv-logo {\n width: 34px;\n height: 32px;\n margin-right: 8px;\n}\n.igv-navbar .igv-navbar-left-container .igv-current-genome {\n height: 32px;\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n line-height: 32px;\n vertical-align: middle;\n text-align: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n height: 100%;\n width: 125px;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container select {\n display: block;\n cursor: pointer;\n width: 100px;\n height: 75%;\n outline: none;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n margin-left: 8px;\n height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 240px;\n height: 22px;\n line-height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container input.igv-search-input {\n cursor: text;\n width: 85%;\n height: 22px;\n line-height: 22px;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n text-align: left;\n padding-left: 8px;\n margin-right: 8px;\n outline: none;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: white;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container .igv-search-icon-container {\n cursor: pointer;\n height: 16px;\n width: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-windowsize-panel-container {\n margin-left: 4px;\n user-select: none;\n}\n.igv-navbar .igv-navbar-right-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container {\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container-hidden {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget {\n color: #737373;\n font-size: 18px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:nth-child(even) {\n display: block;\n height: fit-content;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget input {\n display: block;\n width: 125px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 {\n color: #737373;\n font-size: 18px;\n height: 32px;\n line-height: 32px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:nth-child(even) {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 input {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-hidden {\n display: none;\n}\n\n.igv-navbar-button {\n display: block;\n box-sizing: unset;\n padding-left: 6px;\n padding-right: 6px;\n height: 18px;\n text-transform: capitalize;\n user-select: none;\n line-height: 18px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 11px;\n font-weight: 200;\n color: #737373;\n background-color: #f3f3f3;\n border-color: #737373;\n border-style: solid;\n border-width: thin;\n border-radius: 6px;\n}\n\n.igv-navbar-button:hover {\n cursor: pointer;\n}\n\n.igv-navbar-button-clicked {\n color: white;\n background-color: #737373;\n}\n\n.igv-navbar-icon-button {\n cursor: pointer;\n position: relative;\n width: 24px;\n height: 24px;\n margin-left: 4px;\n margin-right: 4px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:first-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: -18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:last-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.igv-navbar-text-button {\n cursor: pointer;\n position: relative;\n margin-left: 2px;\n margin-right: 2px;\n border: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar-text-button > div:nth-child(2) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 0;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-text-button > div:nth-child(3) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 42px;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n#igv-text-button-label {\n text-anchor: middle;\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-inactive rect {\n stroke: #737373;\n fill: white;\n}\n.igv-navbar-text-button-svg-inactive text {\n fill: #737373;\n}\n.igv-navbar-text-button-svg-inactive tspan {\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-hover rect {\n stroke: #737373;\n fill: #737373;\n}\n.igv-navbar-text-button-svg-hover text {\n fill: white;\n}\n.igv-navbar-text-button-svg-hover tspan {\n dominant-baseline: middle;\n}\n\n#igv-save-svg-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-svg-group text {\n fill: #737373;\n}\n\n#igv-save-svg-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-svg-group:hover text {\n fill: white;\n}\n\n#igv-save-png-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-png-group text {\n fill: #737373;\n}\n\n#igv-save-png-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-png-group:hover text {\n fill: white;\n}\n\n.igv-zoom-in-notice-container {\n z-index: 256;\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translate(-50%, 0%);\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n background-color: white;\n}\n.igv-zoom-in-notice-container > div {\n padding-left: 4px;\n padding-right: 4px;\n padding-top: 2px;\n padding-bottom: 2px;\n width: 100%;\n height: 100%;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: #3f3f3f;\n}\n\n.igv-zoom-in-notice {\n position: absolute;\n top: 10px;\n left: 50%;\n}\n.igv-zoom-in-notice div {\n position: relative;\n left: -50%;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #3f3f3f;\n background-color: rgba(255, 255, 255, 0.51);\n z-index: 64;\n}\n\n.igv-container-spinner {\n position: absolute;\n top: 90%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 1024;\n width: 24px;\n height: 24px;\n pointer-events: none;\n color: #737373;\n}\n\n.igv-multi-locus-close-button {\n position: absolute;\n top: 2px;\n right: 0;\n padding-left: 2px;\n padding-right: 2px;\n width: 12px;\n height: 12px;\n color: #666666;\n background-color: white;\n z-index: 1000;\n}\n.igv-multi-locus-close-button > svg {\n vertical-align: top;\n}\n\n.igv-multi-locus-close-button:hover {\n cursor: pointer;\n color: #434343;\n}\n\n.igv-multi-locus-ruler-label {\n z-index: 64;\n position: absolute;\n top: 2px;\n left: 0;\n width: 100%;\n height: 12px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-multi-locus-ruler-label > div {\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n color: rgb(16, 16, 16);\n background-color: white;\n}\n.igv-multi-locus-ruler-label > div {\n cursor: pointer;\n}\n\n.igv-multi-locus-ruler-label-square-dot {\n z-index: 64;\n position: absolute;\n left: 50%;\n top: 5%;\n transform: translate(-50%, 0%);\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-multi-locus-ruler-label-square-dot > div:first-child {\n width: 14px;\n height: 14px;\n}\n.igv-multi-locus-ruler-label-square-dot > div:last-child {\n margin-left: 16px;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: rgb(16, 16, 16);\n}\n\n.igv-ruler-sweeper {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 26px;\n bottom: 0;\n left: 0;\n width: 0;\n z-index: 99999;\n background-color: rgba(68, 134, 247, 0.25);\n}\n\n.igv-ruler-tooltip {\n pointer-events: none;\n z-index: 128;\n position: absolute;\n top: 0;\n left: 0;\n width: 1px;\n height: 32px;\n background-color: transparent;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ruler-tooltip > div {\n pointer-events: none;\n width: 128px;\n height: auto;\n padding: 1px;\n color: #373737;\n font-size: 10px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: #373737;\n}\n\n.igv-track-label {\n position: absolute;\n left: 8px;\n top: 8px;\n width: auto;\n height: auto;\n max-width: 50%;\n padding-left: 4px;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n text-align: center;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-color: #444;\n border-radius: 2px;\n border-style: solid;\n border-width: thin;\n background-color: white;\n z-index: 128;\n cursor: pointer;\n}\n\n.igv-track-label:hover,\n.igv-track-label:focus,\n.igv-track-label:active {\n background-color: #e8e8e8;\n}\n\n.igv-track-label-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-top: 4px;\n}\n\n.igv-center-line {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n z-index: 8;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-left-style: dashed;\n border-left-width: thin;\n border-right-style: dashed;\n border-right-width: thin;\n}\n\n.igv-center-line-wide {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(127, 127, 127, 0.51);\n}\n\n.igv-center-line-thin {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(0, 0, 0, 0);\n}\n\n.igv-cursor-guide-horizontal {\n display: none;\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n left: 0;\n right: 0;\n top: 50%;\n height: 1px;\n z-index: 32;\n margin-left: 50px;\n margin-right: 54px;\n border-top-style: dotted;\n border-top-width: thin;\n border-top-color: rgba(127, 127, 127, 0.76);\n}\n\n.igv-cursor-guide-vertical {\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 1px;\n z-index: 32;\n border-left-style: dotted;\n border-left-width: thin;\n border-left-color: rgba(127, 127, 127, 0.76);\n display: none;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-generic-dialog-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n width: 300px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n z-index: 2048;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-generic-dialog-container .igv-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input div {\n width: 30%;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div {\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-generic-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-generic-container > div:nth-child(1) i {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-menu-popup {\n position: absolute;\n top: 0;\n left: 0;\n width: max-content;\n max-width: 400px;\n z-index: 512;\n cursor: pointer;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background: white;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-end;\n text-align: left;\n}\n.igv-menu-popup > div:not(:first-child) {\n width: 100%;\n}\n.igv-menu-popup > div:not(:first-child) > div {\n background: white;\n}\n.igv-menu-popup > div:not(:first-child) > div.context-menu {\n padding-left: 4px;\n padding-right: 4px;\n}\n.igv-menu-popup > div:not(:first-child) > div:last-child {\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-menu-popup > div:not(:first-child) > div:hover {\n background: #efefef;\n}\n\n.igv-menu-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-bottom: 1px;\n padding-top: 1px;\n}\n\n.igv-menu-popup-header {\n position: relative;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-menu-popup-header div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-menu-popup-header div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-menu-popup-check-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 20px;\n margin-right: 4px;\n background-color: transparent;\n}\n.igv-menu-popup-check-container div {\n padding-top: 2px;\n padding-left: 8px;\n}\n.igv-menu-popup-check-container div:first-child {\n position: relative;\n width: 12px;\n height: 12px;\n}\n.igv-menu-popup-check-container div:first-child svg {\n position: absolute;\n width: 12px;\n height: 12px;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-loading-spinner-container {\n z-index: 1024;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 32px;\n height: 32px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-loading-spinner-container > div {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 4px solid rgba(128, 128, 128, 0.5);\n border-top-color: rgb(255, 255, 255);\n animation: spin 1s ease-in-out infinite;\n -webkit-animation: spin 1s ease-in-out infinite;\n}\n\n@keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n@-webkit-keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n.igv-roi-menu {\n position: absolute;\n z-index: 512;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background-color: white;\n width: 192px;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-menu > div:first-child {\n height: 24px;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-roi-menu > div:first-child > div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-roi-menu > div:first-child > div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-menu > div:last-child {\n background-color: white;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: 0;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n text-align: start;\n vertical-align: middle;\n}\n.igv-roi-menu > div:last-child > div {\n height: 24px;\n padding-left: 4px;\n border-bottom-style: solid;\n border-bottom-width: thin;\n border-bottom-color: #7f7f7f;\n}\n.igv-roi-menu > div:last-child > div:not(:first-child):hover {\n cursor: pointer;\n background-color: rgba(127, 127, 127, 0.1);\n}\n.igv-roi-menu > div:last-child div:first-child {\n font-style: italic;\n text-align: center;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.igv-roi-menu > div:last-child > div:last-child {\n border-bottom-width: 0;\n border-bottom-color: transparent;\n}\n\n.igv-roi-placeholder {\n font-style: normal;\n color: rgba(75, 75, 75, 0.6);\n}\n\n.igv-roi-table {\n position: absolute;\n z-index: 1024;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n resize: both;\n overflow: hidden;\n width: min-content;\n max-width: 1600px;\n border-color: #7f7f7f;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n font-weight: 400;\n background-color: white;\n cursor: default;\n}\n.igv-roi-table > div {\n height: 24px;\n font-size: 14px;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n}\n.igv-roi-table > div:first-child {\n border-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-top-width: 0;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-roi-table > div:first-child > div:first-child {\n text-align: center;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n margin-left: 4px;\n margin-right: 4px;\n width: calc(100% - 4px - 12px);\n}\n.igv-roi-table > div:first-child > div:last-child {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7f7f7f;\n}\n.igv-roi-table > div:first-child > div:last-child > svg {\n display: block;\n}\n.igv-roi-table > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-table > .igv-roi-table-description {\n padding: 4px;\n margin-left: 4px;\n word-break: break-all;\n overflow-y: auto;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-goto-explainer {\n margin-top: 5px;\n margin-left: 4px;\n color: #7F7F7F;\n font-style: italic;\n height: 24px;\n border-top: solid lightgray;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-column-titles {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n padding-right: 16px;\n background-color: white;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: #7f7f7f;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n overflow: auto;\n height: 360px;\n flex: 1 1 auto;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: transparent;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row-hover {\n background-color: rgba(0, 0, 0, 0.04);\n}\n.igv-roi-table > div:last-child {\n min-height: 32px;\n height: 32px;\n line-height: 32px;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: transparent;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-width: 0;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n\n.igv-roi-table-row-selected {\n background-color: rgba(0, 0, 0, 0.125);\n}\n\n.igv-roi-table-button {\n cursor: pointer;\n height: 20px;\n user-select: none;\n line-height: 20px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 13px;\n font-weight: 400;\n color: black;\n padding-left: 6px;\n padding-right: 6px;\n background-color: rgb(239, 239, 239);\n border-color: black;\n border-style: solid;\n border-width: thin;\n border-radius: 3px;\n}\n\n.igv-roi-table-button:hover {\n font-weight: 400;\n background-color: rgba(0, 0, 0, 0.13);\n}\n\n.igv-roi-region {\n z-index: 64;\n position: absolute;\n top: 0;\n bottom: 0;\n pointer-events: none;\n overflow: visible;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-region > div {\n position: relative;\n width: 100%;\n height: 8px;\n pointer-events: auto;\n}\n\n.igv-roi-menu-row {\n height: 24px;\n padding-left: 8px;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n background-color: white;\n}\n\n.igv-roi-menu-row-edit-description {\n width: -webkit-fill-available;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n background-color: white;\n padding-left: 4px;\n padding-right: 4px;\n padding-bottom: 4px;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-menu-row-edit-description > label {\n margin-left: 2px;\n margin-bottom: 0;\n display: block;\n width: -webkit-fill-available;\n}\n.igv-roi-menu-row-edit-description > input {\n display: block;\n margin-left: 2px;\n margin-right: 2px;\n margin-bottom: 1px;\n width: -webkit-fill-available;\n}\n\n.picker_wrapper.no_alpha .picker_alpha {\n display: none;\n}\n\n.picker_wrapper.no_editor .picker_editor {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.picker_wrapper.no_cancel .picker_cancel {\n display: none;\n}\n\n.layout_default.picker_wrapper {\n display: flex;\n flex-flow: row wrap;\n justify-content: space-between;\n align-items: stretch;\n font-size: 10px;\n width: 25em;\n padding: 0.5em;\n}\n\n.layout_default.picker_wrapper input, .layout_default.picker_wrapper button {\n font-size: 1rem;\n}\n\n.layout_default.picker_wrapper > * {\n margin: 0.5em;\n}\n\n.layout_default.picker_wrapper::before {\n content: \"\";\n display: block;\n width: 100%;\n height: 0;\n order: 1;\n}\n\n.layout_default .picker_slider, .layout_default .picker_selector {\n padding: 1em;\n}\n\n.layout_default .picker_hue {\n width: 100%;\n}\n\n.layout_default .picker_sl {\n flex: 1 1 auto;\n}\n\n.layout_default .picker_sl::before {\n content: \"\";\n display: block;\n padding-bottom: 100%;\n}\n\n.layout_default .picker_editor {\n order: 1;\n width: 6.5rem;\n}\n\n.layout_default .picker_editor input {\n width: 100%;\n height: 100%;\n}\n\n.layout_default .picker_sample {\n order: 1;\n flex: 1 1 auto;\n}\n\n.layout_default .picker_done, .layout_default .picker_cancel {\n order: 1;\n}\n\n.picker_wrapper {\n box-sizing: border-box;\n background: #f2f2f2;\n box-shadow: 0 0 0 1px silver;\n cursor: default;\n font-family: sans-serif;\n color: #444;\n pointer-events: auto;\n}\n\n.picker_wrapper:focus {\n outline: none;\n}\n\n.picker_wrapper button, .picker_wrapper input {\n box-sizing: border-box;\n border: none;\n box-shadow: 0 0 0 1px silver;\n outline: none;\n}\n\n.picker_wrapper button:focus, .picker_wrapper button:active, .picker_wrapper input:focus, .picker_wrapper input:active {\n box-shadow: 0 0 2px 1px #1e90ff;\n}\n\n.picker_wrapper button {\n padding: 0.4em 0.6em;\n cursor: pointer;\n background-color: #f5f5f5;\n background-image: linear-gradient(0deg, gainsboro, transparent);\n}\n\n.picker_wrapper button:active {\n background-image: linear-gradient(0deg, transparent, gainsboro);\n}\n\n.picker_wrapper button:hover {\n background-color: #fff;\n}\n\n.picker_selector {\n position: absolute;\n z-index: 1;\n display: block;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n border: 2px solid #fff;\n border-radius: 100%;\n box-shadow: 0 0 3px 1px #67b9ff;\n background: currentColor;\n cursor: pointer;\n}\n\n.picker_slider .picker_selector {\n border-radius: 2px;\n}\n\n.picker_hue {\n position: relative;\n background-image: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_sl {\n position: relative;\n box-shadow: 0 0 0 1px silver;\n background-image: linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%), linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%), linear-gradient(90deg, #808080, rgba(128, 128, 128, 0));\n}\n\n.picker_alpha, .picker_sample {\n position: relative;\n background: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em, linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_alpha .picker_selector, .picker_sample .picker_selector {\n background: none;\n}\n\n.picker_editor input {\n font-family: monospace;\n padding: 0.2em 0.4em;\n}\n\n.picker_sample::before {\n content: \"\";\n position: absolute;\n display: block;\n width: 100%;\n height: 100%;\n background: currentColor;\n}\n\n.picker_arrow {\n position: absolute;\n z-index: -1;\n}\n\n.picker_wrapper.popup {\n position: absolute;\n z-index: 2;\n margin: 1.5em;\n}\n\n.picker_wrapper.popup, .picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n background: #f2f2f2;\n box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.4);\n}\n\n.picker_wrapper.popup .picker_arrow {\n width: 3em;\n height: 3em;\n margin: 0;\n}\n\n.picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n z-index: -99;\n}\n\n.picker_wrapper.popup .picker_arrow::before {\n width: 100%;\n height: 100%;\n -webkit-transform: skew(45deg);\n transform: skew(45deg);\n -webkit-transform-origin: 0 100%;\n transform-origin: 0 100%;\n}\n\n.picker_wrapper.popup .picker_arrow::after {\n width: 150%;\n height: 150%;\n box-shadow: none;\n}\n\n.popup.popup_top {\n bottom: 100%;\n left: 0;\n}\n\n.popup.popup_top .picker_arrow {\n bottom: 0;\n left: 0;\n -webkit-transform: rotate(-90deg);\n transform: rotate(-90deg);\n}\n\n.popup.popup_bottom {\n top: 100%;\n left: 0;\n}\n\n.popup.popup_bottom .picker_arrow {\n top: 0;\n left: 0;\n -webkit-transform: rotate(90deg) scale(1, -1);\n transform: rotate(90deg) scale(1, -1);\n}\n\n.popup.popup_left {\n top: 0;\n right: 100%;\n}\n\n.popup.popup_left .picker_arrow {\n top: 0;\n right: 0;\n -webkit-transform: scale(-1, 1);\n transform: scale(-1, 1);\n}\n\n.popup.popup_right {\n top: 0;\n left: 100%;\n}\n\n.popup.popup_right .picker_arrow {\n top: 0;\n left: 0;\n}\n\n.igv-container {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n padding-top: 4px;\n user-select: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n min-height: 160px;\n}\n\n.igv-viewport {\n position: relative;\n margin-top: 5px;\n line-height: 1;\n overflow-x: hidden;\n overflow-y: hidden;\n}\n\n.igv-viewport-content {\n position: relative;\n width: 100%;\n}\n.igv-viewport-content > canvas {\n position: relative;\n display: block;\n}\n\n.igv-column-container {\n position: relative;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n width: 100%;\n}\n\n.igv-column-shim {\n width: 1px;\n margin-left: 2px;\n margin-right: 2px;\n background-color: #545453;\n}\n\n.igv-axis-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 50px;\n}\n.igv-axis-column > div {\n position: relative;\n margin-top: 5px;\n width: 100%;\n}\n.igv-axis-column > div > div {\n z-index: 512;\n position: absolute;\n top: 8px;\n left: 8px;\n width: fit-content;\n height: fit-content;\n background-color: transparent;\n display: grid;\n align-items: start;\n justify-items: center;\n}\n.igv-axis-column > div > div > input {\n display: block;\n margin: unset;\n cursor: pointer;\n}\n\n.igv-column {\n position: relative;\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-info-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-name-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-scrollbar-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 14px;\n}\n.igv-scrollbar-column > div {\n position: relative;\n margin-top: 5px;\n width: 14px;\n}\n.igv-scrollbar-column > div > div {\n cursor: pointer;\n position: absolute;\n top: 0;\n left: 2px;\n width: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: #c4c4c4;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-scrollbar-column > div > div:hover {\n background-color: #c4c4c4;\n}\n\n.igv-track-drag-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 12px;\n background-color: white;\n}\n.igv-track-drag-column > .igv-track-drag-handle {\n z-index: 512;\n position: relative;\n cursor: pointer;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.igv-track-drag-column .igv-track-drag-handle-color {\n background-color: #c4c4c4;\n}\n.igv-track-drag-column .igv-track-drag-handle-hover-color {\n background-color: #787878;\n}\n.igv-track-drag-column .igv-track-drag-handle-selected-color {\n background-color: #0963fa;\n}\n.igv-track-drag-column > .igv-track-drag-shim {\n position: relative;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n}\n\n.igv-gear-menu-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 28px;\n}\n.igv-gear-menu-column > div {\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n margin-top: 5px;\n width: 100%;\n background: white;\n}\n.igv-gear-menu-column > div > div {\n position: relative;\n margin-top: 4px;\n width: 16px;\n height: 16px;\n color: #7F7F7F;\n}\n.igv-gear-menu-column > div > div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-vertical-center {\n margin: 0 !important;\n top: 50% !important;\n -ms-transform: translateY(-50%) !important;\n transform: translateY(-50%) !important;\n}\n\n/*# sourceMappingURL=igv.css.map */\n';
73624
+ var igvCss = '.igv-ui-dropdown {\n cursor: default;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2048;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n background-color: white;\n}\n.igv-ui-dropdown > div {\n overflow-y: auto;\n overflow-x: hidden;\n background-color: white;\n}\n.igv-ui-dropdown > div > div {\n padding: 4px;\n width: 100%;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: 1px;\n background-color: white;\n}\n.igv-ui-dropdown > div > div:last-child {\n border-bottom-color: transparent;\n border-bottom-width: 0;\n}\n.igv-ui-dropdown > div > div:hover {\n cursor: pointer;\n background-color: rgba(0, 0, 0, 0.04);\n}\n\n.igv-ui-popover {\n cursor: default;\n position: absolute;\n z-index: 2048;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: 1px;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n background-color: white;\n}\n.igv-ui-popover > div:first-child {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-width: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-popover > div:first-child > div:first-child {\n margin-left: 4px;\n}\n.igv-ui-popover > div:first-child > div:last-child {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-popover > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-popover > div:last-child {\n user-select: text;\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 400px;\n max-width: 800px;\n background-color: white;\n border-bottom-width: 0;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-ui-popover > div:last-child > div {\n margin-left: 4px;\n margin-right: 4px;\n min-width: 220px;\n overflow-x: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.igv-ui-popover > div:last-child > div > span {\n font-weight: bolder;\n}\n.igv-ui-popover > div:last-child hr {\n width: 100%;\n}\n\n.igv-ui-alert-dialog-container {\n position: fixed;\n top: 20%;\n left: 50%;\n transform: translateX(-50%);\n z-index: 2048;\n box-sizing: content-box;\n width: 400px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n outline: none;\n font-family: \"Open Sans\", sans-serif;\n font-size: 15px;\n font-weight: 400;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:first-child {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-alert-dialog-container > div:first-child div:first-child {\n padding-left: 8px;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body {\n -webkit-user-select: text;\n -moz-user-select: text;\n -ms-user-select: text;\n user-select: text;\n color: #373737;\n width: 100%;\n height: calc(100% - 24px - 64px);\n overflow-y: scroll;\n}\n.igv-ui-alert-dialog-container .igv-ui-alert-dialog-body .igv-ui-alert-dialog-body-copy {\n margin: 16px;\n width: auto;\n height: auto;\n overflow-wrap: break-word;\n word-break: break-word;\n background-color: white;\n border: unset;\n}\n.igv-ui-alert-dialog-container > div:last-child {\n width: 100%;\n margin-bottom: 10px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-ui-alert-dialog-container > div:last-child div {\n margin: unset;\n width: 40px;\n height: 30px;\n line-height: 30px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n border-color: #2B81AF;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-alert-dialog-container > div:last-child div:hover {\n cursor: pointer;\n border-color: #25597f;\n background-color: #25597f;\n}\n\n.igv-ui-generic-dialog-container {\n box-sizing: content-box;\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n width: 300px;\n height: fit-content;\n padding-bottom: 16px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input > div {\n width: fit-content;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input input[type=range] {\n width: 70%;\n -webkit-appearance: none;\n background: linear-gradient(90deg, white, black);\n outline: none;\n margin: 0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-input output {\n display: block;\n height: 100%;\n width: 20%;\n font-size: 16px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n padding-top: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div {\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-generic-dialog-container .igv-ui-generic-dialog-ok-cancel > div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n\n.igv-ui-generic-container {\n position: fixed;\n cursor: pointer;\n background-color: white;\n z-index: 2048;\n box-sizing: content-box;\n width: 300px;\n height: 200px;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-ui-generic-container > div:nth-child(1) > div {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-ui-colorpicker-container {\n background-color: #eee;\n height: fit-content;\n flex-direction: column;\n flex-wrap: nowrap;\n}\n.igv-ui-colorpicker-container > div:nth-child(2) {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ui-colorpicker-container > div:nth-child(3) {\n box-sizing: border-box;\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n padding: 0.5rem;\n font-family: \"Open Sans\", sans-serif;\n font-size: 1rem;\n font-weight: 400;\n width: 100%;\n border-top-style: solid;\n border-top-width: thin;\n border-top-color: #373737;\n}\n.igv-ui-colorpicker-container > div:nth-child(4) {\n width: 100%;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n\n.igv-ui-color-swatch {\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n margin: 2px;\n width: 32px;\n height: 32px;\n border-style: solid;\n border-width: 0;\n border-color: white;\n border-radius: 3px;\n}\n\n.igv-ui-color-swatch-shim {\n cursor: pointer;\n position: relative;\n box-sizing: content-box;\n display: flex;\n flex-flow: row;\n flex-wrap: wrap;\n justify-content: center;\n align-items: center;\n width: 32px;\n height: 32px;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: white;\n border-radius: 4px;\n}\n\n.igv-ui-dialog {\n z-index: 2048;\n position: fixed;\n width: fit-content;\n height: fit-content;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n background-color: white;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n}\n.igv-ui-dialog .igv-ui-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-ui-dialog .igv-ui-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-ui-dialog .igv-ui-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-ui-dialog .igv-ui-dialog-one-liner {\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin: 8px;\n overflow-wrap: break-word;\n background-color: white;\n font-weight: bold;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel {\n width: 100%;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div {\n margin: 16px;\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child {\n background-color: #5ea4e0;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child {\n background-color: #c4c4c4;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-ui-dialog .igv-ui-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-ui-dialog .igv-ui-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-ui-panel, .igv-ui-panel-row, .igv-ui-panel-column {\n z-index: 2048;\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-panel-column {\n display: flex;\n flex-direction: column;\n}\n\n.igv-ui-panel-row {\n display: flex;\n flex-direction: row;\n}\n\n.igv-ui-textbox {\n background-color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n}\n\n.igv-ui-table {\n background-color: white;\n}\n\n.igv-ui-table thead {\n position: sticky;\n top: 0;\n}\n\n.igv-ui-table th {\n text-align: left;\n}\n\n.igv-ui-table td {\n padding-right: 20px;\n}\n\n.igv-ui-table tr:hover {\n background-color: lightblue;\n}\n\n.igv-ui-center-fixed {\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n.igv-navbar {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n box-sizing: border-box;\n width: 100%;\n color: #444;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n line-height: 32px;\n padding-left: 8px;\n padding-right: 8px;\n margin-top: 2px;\n margin-bottom: 6px;\n height: 32px;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: #f3f3f3;\n}\n.igv-navbar .igv-navbar-left-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 32px;\n line-height: 32px;\n}\n.igv-navbar .igv-navbar-left-container .igv-logo {\n width: 34px;\n height: 32px;\n margin-right: 8px;\n}\n.igv-navbar .igv-navbar-left-container .igv-current-genome {\n height: 32px;\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n line-height: 32px;\n vertical-align: middle;\n text-align: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n height: 100%;\n width: 125px;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-chromosome-select-widget-container select {\n display: block;\n cursor: pointer;\n width: 100px;\n height: 75%;\n outline: none;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n margin-left: 8px;\n height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 240px;\n height: 22px;\n line-height: 22px;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container input.igv-search-input {\n cursor: text;\n width: 85%;\n height: 22px;\n line-height: 22px;\n font-size: 12px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n text-align: left;\n padding-left: 8px;\n margin-right: 8px;\n outline: none;\n border-style: solid;\n border-radius: 3px;\n border-width: thin;\n border-color: #bfbfbf;\n background-color: white;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-search-container .igv-search-icon-container {\n cursor: pointer;\n height: 16px;\n width: 16px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar .igv-navbar-left-container .igv-navbar-genomic-location .igv-locus-size-group .igv-windowsize-panel-container {\n margin-left: 4px;\n user-select: none;\n}\n.igv-navbar .igv-navbar-right-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container {\n position: relative;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-navbar-toggle-button-container-hidden {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n height: 100%;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget {\n color: #737373;\n font-size: 18px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget div:nth-child(even) {\n display: block;\n height: fit-content;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget input {\n display: block;\n width: 125px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 {\n color: #737373;\n font-size: 18px;\n height: 32px;\n line-height: 32px;\n margin-left: 8px;\n user-select: none;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div {\n cursor: pointer;\n margin-left: unset;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:first-child {\n height: 20px;\n width: 20px;\n margin-left: unset;\n margin-right: 4px;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:last-child {\n height: 20px;\n width: 20px;\n margin-left: 4px;\n margin-right: unset;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 div:nth-child(even) {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 input {\n width: 0;\n height: 0;\n display: none;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-900 svg {\n display: block;\n}\n.igv-navbar .igv-navbar-right-container .igv-zoom-widget-hidden {\n display: none;\n}\n\n.igv-navbar-button {\n display: block;\n box-sizing: unset;\n padding-left: 6px;\n padding-right: 6px;\n height: 18px;\n text-transform: capitalize;\n user-select: none;\n line-height: 18px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 11px;\n font-weight: 200;\n color: #737373;\n background-color: #f3f3f3;\n border-color: #737373;\n border-style: solid;\n border-width: thin;\n border-radius: 6px;\n}\n\n.igv-navbar-button:hover {\n cursor: pointer;\n}\n\n.igv-navbar-button-clicked {\n color: white;\n background-color: #737373;\n}\n\n.igv-navbar-icon-button {\n cursor: pointer;\n position: relative;\n width: 24px;\n height: 24px;\n margin-left: 4px;\n margin-right: 4px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:first-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: -18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-icon-button > div:last-child {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 18px;\n width: 24px;\n height: 24px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n.igv-navbar-text-button {\n cursor: pointer;\n position: relative;\n margin-left: 2px;\n margin-right: 2px;\n border: none;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.igv-navbar-text-button > div:nth-child(2) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 0;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n.igv-navbar-text-button > div:nth-child(3) {\n z-index: 512;\n position: absolute;\n top: 36px;\n left: 42px;\n width: 38px;\n height: 18px;\n border: none;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center;\n}\n\n#igv-text-button-label {\n text-anchor: middle;\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-inactive rect {\n stroke: #737373;\n fill: white;\n}\n.igv-navbar-text-button-svg-inactive text {\n fill: #737373;\n}\n.igv-navbar-text-button-svg-inactive tspan {\n dominant-baseline: middle;\n}\n\n.igv-navbar-text-button-svg-hover rect {\n stroke: #737373;\n fill: #737373;\n}\n.igv-navbar-text-button-svg-hover text {\n fill: white;\n}\n.igv-navbar-text-button-svg-hover tspan {\n dominant-baseline: middle;\n}\n\n#igv-save-svg-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-svg-group text {\n fill: #737373;\n}\n\n#igv-save-svg-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-svg-group:hover text {\n fill: white;\n}\n\n#igv-save-png-group rect {\n stroke: #737373;\n fill: white;\n}\n#igv-save-png-group text {\n fill: #737373;\n}\n\n#igv-save-png-group:hover rect {\n stroke: #737373;\n fill: #737373;\n}\n#igv-save-png-group:hover text {\n fill: white;\n}\n\n.igv-zoom-in-notice-container {\n z-index: 256;\n position: absolute;\n top: 8px;\n left: 50%;\n transform: translate(-50%, 0%);\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n background-color: white;\n}\n.igv-zoom-in-notice-container > div {\n padding-left: 4px;\n padding-right: 4px;\n padding-top: 2px;\n padding-bottom: 2px;\n width: 100%;\n height: 100%;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: #3f3f3f;\n}\n\n.igv-zoom-in-notice {\n position: absolute;\n top: 10px;\n left: 50%;\n}\n.igv-zoom-in-notice div {\n position: relative;\n left: -50%;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #3f3f3f;\n background-color: rgba(255, 255, 255, 0.51);\n z-index: 64;\n}\n\n.igv-container-spinner {\n position: absolute;\n top: 90%;\n left: 50%;\n transform: translate(-50%, -50%);\n z-index: 1024;\n width: 24px;\n height: 24px;\n pointer-events: none;\n color: #737373;\n}\n\n.igv-multi-locus-close-button {\n position: absolute;\n top: 2px;\n right: 0;\n padding-left: 2px;\n padding-right: 2px;\n width: 12px;\n height: 12px;\n color: #666666;\n background-color: white;\n z-index: 1000;\n}\n.igv-multi-locus-close-button > svg {\n vertical-align: top;\n}\n\n.igv-multi-locus-close-button:hover {\n cursor: pointer;\n color: #434343;\n}\n\n.igv-multi-locus-ruler-label {\n z-index: 64;\n position: absolute;\n top: 2px;\n left: 0;\n width: 100%;\n height: 12px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-multi-locus-ruler-label > div {\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n color: rgb(16, 16, 16);\n background-color: white;\n}\n.igv-multi-locus-ruler-label > div {\n cursor: pointer;\n}\n\n.igv-multi-locus-ruler-label-square-dot {\n z-index: 64;\n position: absolute;\n left: 50%;\n top: 5%;\n transform: translate(-50%, 0%);\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-multi-locus-ruler-label-square-dot > div:first-child {\n width: 14px;\n height: 14px;\n}\n.igv-multi-locus-ruler-label-square-dot > div:last-child {\n margin-left: 16px;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n color: rgb(16, 16, 16);\n}\n\n.igv-ruler-sweeper {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 26px;\n bottom: 0;\n left: 0;\n width: 0;\n z-index: 99999;\n background-color: rgba(68, 134, 247, 0.25);\n}\n\n.igv-ruler-tooltip {\n pointer-events: none;\n z-index: 128;\n position: absolute;\n top: 0;\n left: 0;\n width: 1px;\n height: 32px;\n background-color: transparent;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-ruler-tooltip > div {\n pointer-events: none;\n width: 128px;\n height: auto;\n padding: 1px;\n color: #373737;\n font-size: 10px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n background-color: white;\n border-style: solid;\n border-width: thin;\n border-color: #373737;\n}\n\n.igv-track-label {\n position: absolute;\n left: 8px;\n top: 8px;\n width: auto;\n height: auto;\n max-width: 50%;\n padding-left: 4px;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n text-align: center;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-color: #444;\n border-radius: 2px;\n border-style: solid;\n border-width: thin;\n background-color: white;\n z-index: 128;\n cursor: pointer;\n}\n\n.igv-track-label:hover,\n.igv-track-label:focus,\n.igv-track-label:active {\n background-color: #e8e8e8;\n}\n\n.igv-track-label-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-top: 4px;\n}\n\n.igv-center-line {\n display: none;\n pointer-events: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n z-index: 8;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n border-left-style: dashed;\n border-left-width: thin;\n border-right-style: dashed;\n border-right-width: thin;\n}\n\n.igv-center-line-wide {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(127, 127, 127, 0.51);\n}\n\n.igv-center-line-thin {\n background-color: rgba(0, 0, 0, 0);\n border-left-color: rgba(127, 127, 127, 0.51);\n border-right-color: rgba(0, 0, 0, 0);\n}\n\n.igv-cursor-guide-horizontal {\n display: none;\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n left: 0;\n right: 0;\n top: 50%;\n height: 1px;\n z-index: 32;\n margin-left: 50px;\n margin-right: 54px;\n border-top-style: dotted;\n border-top-width: thin;\n border-top-color: rgba(127, 127, 127, 0.76);\n}\n\n.igv-cursor-guide-vertical {\n pointer-events: none;\n user-select: none;\n -moz-user-select: none;\n -webkit-user-select: none;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 1px;\n z-index: 32;\n border-left-style: dotted;\n border-left-width: thin;\n border-left-color: rgba(127, 127, 127, 0.76);\n display: none;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-generic-dialog-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n width: 300px;\n height: 200px;\n border-color: #7F7F7F;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n z-index: 2048;\n background-color: white;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-generic-dialog-container .igv-generic-dialog-header div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-generic-dialog-container .igv-generic-dialog-one-liner {\n color: #373737;\n width: 95%;\n height: 24px;\n line-height: 24px;\n text-align: left;\n margin-top: 8px;\n padding-left: 8px;\n overflow-wrap: break-word;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input {\n margin-top: 8px;\n width: 95%;\n height: 24px;\n color: #373737;\n line-height: 24px;\n padding-left: 8px;\n background-color: white;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input div {\n width: 30%;\n height: 100%;\n font-size: 16px;\n text-align: right;\n padding-right: 8px;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-label-input input {\n width: 50%;\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input {\n margin-top: 8px;\n width: calc(100% - 16px);\n height: 24px;\n color: #373737;\n line-height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n display: block;\n height: 100%;\n width: 100%;\n padding-left: 4px;\n font-family: \"Open Sans\", sans-serif;\n font-weight: 400;\n color: #373737;\n text-align: left;\n outline: none;\n border-style: solid;\n border-width: thin;\n border-color: #7F7F7F;\n background-color: white;\n}\n.igv-generic-dialog-container .igv-generic-dialog-input input {\n font-size: 16px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel {\n width: 100%;\n height: 28px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div {\n margin-top: 32px;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: 14px;\n font-weight: 400;\n width: 75px;\n height: 28px;\n line-height: 28px;\n text-align: center;\n border-color: transparent;\n border-style: solid;\n border-width: thin;\n border-radius: 2px;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child {\n margin-left: 32px;\n margin-right: 0;\n background-color: #5ea4e0;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child {\n margin-left: 0;\n margin-right: 32px;\n background-color: #c4c4c4;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:first-child:hover {\n cursor: pointer;\n background-color: #3b5c7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok-cancel div:last-child:hover {\n cursor: pointer;\n background-color: #7f7f7f;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok {\n width: 100%;\n height: 36px;\n margin-top: 32px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div {\n width: 98px;\n height: 36px;\n line-height: 36px;\n text-align: center;\n color: white;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n border-color: white;\n border-style: solid;\n border-width: thin;\n border-radius: 4px;\n background-color: #2B81AF;\n}\n.igv-generic-dialog-container .igv-generic-dialog-ok div:hover {\n cursor: pointer;\n background-color: #25597f;\n}\n\n.igv-generic-container {\n position: fixed;\n top: 20%;\n left: 75%;\n transform: translateX(-50%);\n z-index: 2048;\n background-color: white;\n cursor: pointer;\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-generic-container > div:nth-child(1) {\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n height: 24px;\n width: 100%;\n background-color: #dddddd;\n}\n.igv-generic-container > div:nth-child(1) i {\n display: block;\n color: #5f5f5f;\n cursor: pointer;\n width: 14px;\n height: 14px;\n margin-right: 8px;\n margin-bottom: 4px;\n}\n\n.igv-menu-popup {\n position: absolute;\n top: 0;\n left: 0;\n width: max-content;\n max-width: 400px;\n z-index: 2048;\n cursor: pointer;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background: white;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-end;\n text-align: left;\n}\n.igv-menu-popup > div:not(:first-child) {\n width: 100%;\n}\n.igv-menu-popup > div:not(:first-child) > div {\n background: white;\n}\n.igv-menu-popup > div:not(:first-child) > div.context-menu {\n padding-left: 4px;\n padding-right: 4px;\n}\n.igv-menu-popup > div:not(:first-child) > div:last-child {\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-menu-popup > div:not(:first-child) > div:hover {\n background: #efefef;\n}\n\n.igv-menu-popup-shim {\n padding-left: 8px;\n padding-right: 8px;\n padding-bottom: 1px;\n padding-top: 1px;\n}\n\n.igv-menu-popup-header {\n position: relative;\n width: 100%;\n height: 24px;\n cursor: move;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-menu-popup-header div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-menu-popup-header div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-menu-popup-check-container {\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n width: 100%;\n height: 20px;\n margin-right: 4px;\n background-color: transparent;\n}\n.igv-menu-popup-check-container div {\n padding-top: 2px;\n padding-left: 8px;\n}\n.igv-menu-popup-check-container div:first-child {\n position: relative;\n width: 12px;\n height: 12px;\n}\n.igv-menu-popup-check-container div:first-child svg {\n position: absolute;\n width: 12px;\n height: 12px;\n}\n\n.igv-user-feedback {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 512px;\n height: 360px;\n z-index: 2048;\n background-color: white;\n border-color: #a2a2a2;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: medium;\n font-weight: 400;\n color: #444;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n}\n.igv-user-feedback div:first-child {\n position: relative;\n height: 24px;\n width: 100%;\n background-color: white;\n border-bottom-color: #a2a2a2;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-user-feedback div:first-child div {\n position: absolute;\n top: 2px;\n width: 16px;\n height: 16px;\n background-color: transparent;\n}\n.igv-user-feedback div:first-child div:first-child {\n left: 8px;\n}\n.igv-user-feedback div:first-child div:last-child {\n cursor: pointer;\n right: 8px;\n}\n.igv-user-feedback div:last-child {\n width: 100%;\n height: calc(100% - 24px);\n border-width: 0;\n}\n.igv-user-feedback div:last-child div {\n width: auto;\n height: auto;\n margin: 8px;\n}\n\n.igv-loading-spinner-container {\n z-index: 1024;\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 32px;\n height: 32px;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: center;\n align-items: center;\n}\n.igv-loading-spinner-container > div {\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n border: 4px solid rgba(128, 128, 128, 0.5);\n border-top-color: rgb(255, 255, 255);\n animation: spin 1s ease-in-out infinite;\n -webkit-animation: spin 1s ease-in-out infinite;\n}\n\n@keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n@-webkit-keyframes spin {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n.igv-roi-menu {\n position: absolute;\n z-index: 512;\n font-family: \"Open Sans\", sans-serif;\n font-size: small;\n font-weight: 400;\n color: #4b4b4b;\n background-color: white;\n width: 192px;\n border-radius: 4px;\n border-color: #7F7F7F;\n border-style: solid;\n border-width: thin;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-menu > div:first-child {\n height: 24px;\n border-top-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-color: #7F7F7F;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: flex-end;\n align-items: center;\n}\n.igv-roi-menu > div:first-child > div {\n margin-right: 4px;\n height: 12px;\n width: 12px;\n color: #7F7F7F;\n}\n.igv-roi-menu > div:first-child > div:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-menu > div:last-child {\n background-color: white;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-color: transparent;\n border-bottom-style: solid;\n border-bottom-width: 0;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n text-align: start;\n vertical-align: middle;\n}\n.igv-roi-menu > div:last-child > div {\n height: 24px;\n padding-left: 4px;\n border-bottom-style: solid;\n border-bottom-width: thin;\n border-bottom-color: #7f7f7f;\n}\n.igv-roi-menu > div:last-child > div:not(:first-child):hover {\n cursor: pointer;\n background-color: rgba(127, 127, 127, 0.1);\n}\n.igv-roi-menu > div:last-child div:first-child {\n font-style: italic;\n text-align: center;\n padding-right: 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.igv-roi-menu > div:last-child > div:last-child {\n border-bottom-width: 0;\n border-bottom-color: transparent;\n}\n\n.igv-roi-placeholder {\n font-style: normal;\n color: rgba(75, 75, 75, 0.6);\n}\n\n.igv-roi-table {\n position: absolute;\n z-index: 1024;\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n resize: both;\n overflow: hidden;\n width: min-content;\n max-width: 1600px;\n border-color: #7f7f7f;\n border-radius: 4px;\n border-style: solid;\n border-width: thin;\n font-family: \"Open Sans\", sans-serif;\n font-size: 12px;\n font-weight: 400;\n background-color: white;\n cursor: default;\n}\n.igv-roi-table > div {\n height: 24px;\n font-size: 14px;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n}\n.igv-roi-table > div:first-child {\n border-color: transparent;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-top-width: 0;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n background-color: #eee;\n cursor: move;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: center;\n}\n.igv-roi-table > div:first-child > div:first-child {\n text-align: center;\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n margin-left: 4px;\n margin-right: 4px;\n width: calc(100% - 4px - 12px);\n}\n.igv-roi-table > div:first-child > div:last-child {\n margin-right: 4px;\n margin-bottom: 2px;\n height: 12px;\n width: 12px;\n color: #7f7f7f;\n}\n.igv-roi-table > div:first-child > div:last-child > svg {\n display: block;\n}\n.igv-roi-table > div:first-child > div:last-child:hover {\n cursor: pointer;\n color: #444;\n}\n.igv-roi-table > .igv-roi-table-description {\n padding: 4px;\n margin-left: 4px;\n word-break: break-all;\n overflow-y: auto;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-goto-explainer {\n margin-top: 5px;\n margin-left: 4px;\n color: #7F7F7F;\n font-style: italic;\n height: 24px;\n border-top: solid lightgray;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-column-titles {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n padding-right: 16px;\n background-color: white;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: #7f7f7f;\n border-bottom-style: solid;\n border-bottom-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: #7f7f7f;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-column-titles > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container {\n display: flex;\n flex-flow: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n overflow: auto;\n height: 360px;\n flex: 1 1 auto;\n background-color: transparent;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row {\n height: 24px;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div {\n font-size: 14px;\n vertical-align: middle;\n line-height: 24px;\n text-align: left;\n margin-left: 4px;\n height: 24px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-right-color: transparent;\n border-right-style: solid;\n border-right-width: thin;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row > div:last-child {\n border-right: unset;\n}\n.igv-roi-table > .igv-roi-table-row-container > .igv-roi-table-row-hover {\n background-color: rgba(0, 0, 0, 0.04);\n}\n.igv-roi-table > div:last-child {\n min-height: 32px;\n height: 32px;\n line-height: 32px;\n border-top-color: #7f7f7f;\n border-top-style: solid;\n border-top-width: thin;\n border-bottom-color: transparent;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n border-bottom-width: 0;\n background-color: #eee;\n display: flex;\n flex-flow: row;\n flex-wrap: nowrap;\n justify-content: space-around;\n align-items: center;\n}\n\n.igv-roi-table-row-selected {\n background-color: rgba(0, 0, 0, 0.125);\n}\n\n.igv-roi-table-button {\n cursor: pointer;\n height: 20px;\n user-select: none;\n line-height: 20px;\n text-align: center;\n vertical-align: middle;\n font-family: \"Open Sans\", sans-serif;\n font-size: 13px;\n font-weight: 400;\n color: black;\n padding-left: 6px;\n padding-right: 6px;\n background-color: rgb(239, 239, 239);\n border-color: black;\n border-style: solid;\n border-width: thin;\n border-radius: 3px;\n}\n\n.igv-roi-table-button:hover {\n font-weight: 400;\n background-color: rgba(0, 0, 0, 0.13);\n}\n\n.igv-roi-region {\n z-index: 64;\n position: absolute;\n top: 0;\n bottom: 0;\n pointer-events: none;\n overflow: visible;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n}\n.igv-roi-region > div {\n position: relative;\n width: 100%;\n height: 8px;\n pointer-events: auto;\n}\n\n.igv-roi-menu-row {\n height: 24px;\n padding-left: 8px;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n line-height: 24px;\n background-color: white;\n}\n\n.igv-roi-menu-row-edit-description {\n width: -webkit-fill-available;\n font-size: small;\n text-align: start;\n vertical-align: middle;\n background-color: white;\n padding-left: 4px;\n padding-right: 4px;\n padding-bottom: 4px;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: stretch;\n align-items: stretch;\n}\n.igv-roi-menu-row-edit-description > label {\n margin-left: 2px;\n margin-bottom: 0;\n display: block;\n width: -webkit-fill-available;\n}\n.igv-roi-menu-row-edit-description > input {\n display: block;\n margin-left: 2px;\n margin-right: 2px;\n margin-bottom: 1px;\n width: -webkit-fill-available;\n}\n\n.picker_wrapper.no_alpha .picker_alpha {\n display: none;\n}\n\n.picker_wrapper.no_editor .picker_editor {\n position: absolute;\n z-index: -1;\n opacity: 0;\n}\n\n.picker_wrapper.no_cancel .picker_cancel {\n display: none;\n}\n\n.layout_default.picker_wrapper {\n display: flex;\n flex-flow: row wrap;\n justify-content: space-between;\n align-items: stretch;\n font-size: 10px;\n width: 25em;\n padding: 0.5em;\n}\n\n.layout_default.picker_wrapper input, .layout_default.picker_wrapper button {\n font-size: 1rem;\n}\n\n.layout_default.picker_wrapper > * {\n margin: 0.5em;\n}\n\n.layout_default.picker_wrapper::before {\n content: \"\";\n display: block;\n width: 100%;\n height: 0;\n order: 1;\n}\n\n.layout_default .picker_slider, .layout_default .picker_selector {\n padding: 1em;\n}\n\n.layout_default .picker_hue {\n width: 100%;\n}\n\n.layout_default .picker_sl {\n flex: 1 1 auto;\n}\n\n.layout_default .picker_sl::before {\n content: \"\";\n display: block;\n padding-bottom: 100%;\n}\n\n.layout_default .picker_editor {\n order: 1;\n width: 6.5rem;\n}\n\n.layout_default .picker_editor input {\n width: 100%;\n height: 100%;\n}\n\n.layout_default .picker_sample {\n order: 1;\n flex: 1 1 auto;\n}\n\n.layout_default .picker_done, .layout_default .picker_cancel {\n order: 1;\n}\n\n.picker_wrapper {\n box-sizing: border-box;\n background: #f2f2f2;\n box-shadow: 0 0 0 1px silver;\n cursor: default;\n font-family: sans-serif;\n color: #444;\n pointer-events: auto;\n}\n\n.picker_wrapper:focus {\n outline: none;\n}\n\n.picker_wrapper button, .picker_wrapper input {\n box-sizing: border-box;\n border: none;\n box-shadow: 0 0 0 1px silver;\n outline: none;\n}\n\n.picker_wrapper button:focus, .picker_wrapper button:active, .picker_wrapper input:focus, .picker_wrapper input:active {\n box-shadow: 0 0 2px 1px #1e90ff;\n}\n\n.picker_wrapper button {\n padding: 0.4em 0.6em;\n cursor: pointer;\n background-color: #f5f5f5;\n background-image: linear-gradient(0deg, gainsboro, transparent);\n}\n\n.picker_wrapper button:active {\n background-image: linear-gradient(0deg, transparent, gainsboro);\n}\n\n.picker_wrapper button:hover {\n background-color: #fff;\n}\n\n.picker_selector {\n position: absolute;\n z-index: 1;\n display: block;\n -webkit-transform: translate(-50%, -50%);\n transform: translate(-50%, -50%);\n border: 2px solid #fff;\n border-radius: 100%;\n box-shadow: 0 0 3px 1px #67b9ff;\n background: currentColor;\n cursor: pointer;\n}\n\n.picker_slider .picker_selector {\n border-radius: 2px;\n}\n\n.picker_hue {\n position: relative;\n background-image: linear-gradient(90deg, red, yellow, lime, cyan, blue, magenta, red);\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_sl {\n position: relative;\n box-shadow: 0 0 0 1px silver;\n background-image: linear-gradient(180deg, white, rgba(255, 255, 255, 0) 50%), linear-gradient(0deg, black, rgba(0, 0, 0, 0) 50%), linear-gradient(90deg, #808080, rgba(128, 128, 128, 0));\n}\n\n.picker_alpha, .picker_sample {\n position: relative;\n background: linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%) 0 0/2em 2em, linear-gradient(45deg, lightgrey 25%, white 25%, white 75%, lightgrey 75%) 1em 1em/2em 2em;\n box-shadow: 0 0 0 1px silver;\n}\n\n.picker_alpha .picker_selector, .picker_sample .picker_selector {\n background: none;\n}\n\n.picker_editor input {\n font-family: monospace;\n padding: 0.2em 0.4em;\n}\n\n.picker_sample::before {\n content: \"\";\n position: absolute;\n display: block;\n width: 100%;\n height: 100%;\n background: currentColor;\n}\n\n.picker_arrow {\n position: absolute;\n z-index: -1;\n}\n\n.picker_wrapper.popup {\n position: absolute;\n z-index: 2;\n margin: 1.5em;\n}\n\n.picker_wrapper.popup, .picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n background: #f2f2f2;\n box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.4);\n}\n\n.picker_wrapper.popup .picker_arrow {\n width: 3em;\n height: 3em;\n margin: 0;\n}\n\n.picker_wrapper.popup .picker_arrow::before, .picker_wrapper.popup .picker_arrow::after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n z-index: -99;\n}\n\n.picker_wrapper.popup .picker_arrow::before {\n width: 100%;\n height: 100%;\n -webkit-transform: skew(45deg);\n transform: skew(45deg);\n -webkit-transform-origin: 0 100%;\n transform-origin: 0 100%;\n}\n\n.picker_wrapper.popup .picker_arrow::after {\n width: 150%;\n height: 150%;\n box-shadow: none;\n}\n\n.popup.popup_top {\n bottom: 100%;\n left: 0;\n}\n\n.popup.popup_top .picker_arrow {\n bottom: 0;\n left: 0;\n -webkit-transform: rotate(-90deg);\n transform: rotate(-90deg);\n}\n\n.popup.popup_bottom {\n top: 100%;\n left: 0;\n}\n\n.popup.popup_bottom .picker_arrow {\n top: 0;\n left: 0;\n -webkit-transform: rotate(90deg) scale(1, -1);\n transform: rotate(90deg) scale(1, -1);\n}\n\n.popup.popup_left {\n top: 0;\n right: 100%;\n}\n\n.popup.popup_left .picker_arrow {\n top: 0;\n right: 0;\n -webkit-transform: scale(-1, 1);\n transform: scale(-1, 1);\n}\n\n.popup.popup_right {\n top: 0;\n left: 100%;\n}\n\n.popup.popup_right .picker_arrow {\n top: 0;\n left: 0;\n}\n\n.igv-container {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n padding-top: 4px;\n user-select: none;\n -webkit-user-select: none;\n -ms-user-select: none;\n min-height: 160px;\n}\n\n.igv-viewport {\n position: relative;\n margin-top: 5px;\n line-height: 1;\n overflow-x: hidden;\n overflow-y: hidden;\n}\n\n.igv-viewport-content {\n position: relative;\n width: 100%;\n}\n.igv-viewport-content > canvas {\n position: relative;\n display: block;\n}\n\n.igv-column-container {\n position: relative;\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: stretch;\n width: 100%;\n}\n\n.igv-column-shim {\n width: 1px;\n margin-left: 2px;\n margin-right: 2px;\n background-color: #545453;\n}\n\n.igv-axis-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 50px;\n}\n.igv-axis-column > div {\n position: relative;\n margin-top: 5px;\n width: 100%;\n}\n.igv-axis-column > div > div {\n z-index: 512;\n position: absolute;\n top: 8px;\n left: 8px;\n width: fit-content;\n height: fit-content;\n background-color: transparent;\n display: grid;\n align-items: start;\n justify-items: center;\n}\n.igv-axis-column > div > div > input {\n display: block;\n margin: unset;\n cursor: pointer;\n}\n\n.igv-column {\n position: relative;\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-info-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-sample-name-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n}\n\n.igv-scrollbar-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 14px;\n}\n.igv-scrollbar-column > div {\n position: relative;\n margin-top: 5px;\n width: 14px;\n}\n.igv-scrollbar-column > div > div {\n cursor: pointer;\n position: absolute;\n top: 0;\n left: 2px;\n width: 8px;\n border-width: 1px;\n border-style: solid;\n border-color: #c4c4c4;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.igv-scrollbar-column > div > div:hover {\n background-color: #c4c4c4;\n}\n\n.igv-track-drag-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 12px;\n background-color: white;\n}\n.igv-track-drag-column > .igv-track-drag-handle {\n z-index: 512;\n position: relative;\n cursor: pointer;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.igv-track-drag-column .igv-track-drag-handle-color {\n background-color: #c4c4c4;\n}\n.igv-track-drag-column .igv-track-drag-handle-hover-color {\n background-color: #787878;\n}\n.igv-track-drag-column .igv-track-drag-handle-selected-color {\n background-color: #0963fa;\n}\n.igv-track-drag-column > .igv-track-drag-shim {\n position: relative;\n margin-top: 5px;\n width: 100%;\n border-style: solid;\n border-width: 0;\n}\n\n.igv-gear-menu-column {\n position: relative;\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: flex-start;\n box-sizing: border-box;\n height: 100%;\n width: 28px;\n}\n.igv-gear-menu-column > div {\n display: flex;\n flex-direction: column;\n flex-wrap: nowrap;\n justify-content: flex-start;\n align-items: center;\n margin-top: 5px;\n width: 100%;\n background: white;\n}\n.igv-gear-menu-column > div > div {\n position: relative;\n margin-top: 4px;\n width: 16px;\n height: 16px;\n color: #7F7F7F;\n}\n.igv-gear-menu-column > div > div:hover {\n cursor: pointer;\n color: #444;\n}\n\n.igv-vertical-center {\n margin: 0 !important;\n top: 50% !important;\n -ms-transform: translateY(-50%) !important;\n transform: translateY(-50%) !important;\n}\n\n/*# sourceMappingURL=igv.css.map */\n';
72914
73625
 
72915
73626
  /**
72916
73627
  * Manages XQTL selections.
@@ -73444,7 +74155,7 @@ class Browser {
73444
74155
  this.dataRangeDialog = new DataRangeDialog(this, this.root);
73445
74156
  this.dataRangeDialog.container.id = `igv-data-range-dialog-${guid$2()}`;
73446
74157
 
73447
- this.genericColorPicker = new GenericColorPicker({ parent: this.root, width: 180 });
74158
+ this.genericColorPicker = new GenericColorPicker({parent: this.root, width: 180});
73448
74159
  this.genericColorPicker.container.id = `igv-track-color-picker-${guid$2()}`;
73449
74160
 
73450
74161
  this.sliderDialog = new SliderDialog(this.root);
@@ -73454,10 +74165,10 @@ class Browser {
73454
74165
 
73455
74166
  getSampleNameViewportWidth() {
73456
74167
 
73457
- if (undefined === this.sampleNameViewportWidth) {
74168
+ if (false === this.showSampleNames || undefined === this.sampleNameViewportWidth) {
73458
74169
  return 0
73459
74170
  } else {
73460
- return false === this.showSampleNames ? 0 : this.sampleNameViewportWidth
74171
+ return this.sampleNameViewportWidth
73461
74172
  }
73462
74173
 
73463
74174
  }
@@ -73619,7 +74330,8 @@ class Browser {
73619
74330
  } else {
73620
74331
  session = options;
73621
74332
  }
73622
- return this.loadSessionObject(session)
74333
+
74334
+ await this.loadSessionObject(session);
73623
74335
  }
73624
74336
 
73625
74337
  /**
@@ -73648,8 +74360,7 @@ class Browser {
73648
74360
  config = new XMLSession(string, knownGenomes);
73649
74361
 
73650
74362
  } else if (filename.endsWith("hub.txt")) {
73651
-
73652
- const hub = await Hub.loadHub(urlOrFile, options);
74363
+ const hub = await loadHub(urlOrFile);
73653
74364
  const genomeConfig = hub.getGenomeConfig();
73654
74365
  config = {
73655
74366
  reference: genomeConfig
@@ -73740,7 +74451,7 @@ class Browser {
73740
74451
  }
73741
74452
 
73742
74453
  // ROIs
73743
- if(session.showROIOverlays !== undefined) {
74454
+ if (session.showROIOverlays !== undefined) {
73744
74455
  this.roiManager.showOverlays = session.showROIOverlays;
73745
74456
  }
73746
74457
  this.roiManager.clearROIs();
@@ -73754,12 +74465,16 @@ class Browser {
73754
74465
  // Sample info
73755
74466
  const localSampleInfoFiles = [];
73756
74467
  if (session.sampleinfo) {
73757
- for (const config of session.sampleinfo) {
73758
-
73759
- if (config.file) {
73760
- localSampleInfoFiles.push(config.file);
74468
+ for (const sampleInfoConfig of session.sampleinfo) {
74469
+ // The "file" property is recorded in the session when a local file is referenced. It can't be used
74470
+ // on reloading, its only purpose is to present an alert to the user. This could also be used
74471
+ // to prompt the user to load the file manually, but we don't currently do that.
74472
+ if (sampleInfoConfig.file) {
74473
+ localSampleInfoFiles.push(sampleInfoConfig.file);
73761
74474
  } else {
73762
- this.loadSampleInfo(config);
74475
+ // this.loadSampleInfo(sampleInfoConfig)
74476
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74477
+
73763
74478
  }
73764
74479
 
73765
74480
  }
@@ -73800,23 +74515,14 @@ class Browser {
73800
74515
  }
73801
74516
  }
73802
74517
 
73803
- await this.loadTrackList(nonLocalTrackConfigurations);
73804
-
73805
- // The ruler and ideogram tracks are not explicitly loaded, but needs updated nonetheless.
73806
- for (let rtv of this.trackViews.filter((tv) => tv.track.type === 'ruler' || tv.track.type === 'ideogram')) {
73807
- await rtv.updateViews();
74518
+ // Load a hidden track -- used to populate searchable database without creating a track
74519
+ const configHidden = nonLocalTrackConfigurations.filter(config => true === config.hidden);
74520
+ for (const config of configHidden) {
74521
+ const featureSource = FeatureSource(config, this.genome);
74522
+ await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
73808
74523
  }
73809
74524
 
73810
- // If any tracks are selected show the selection buttons
73811
- if (this.trackViews.some(tv => tv.track.selected)) {
73812
- this.navbar.setEnableTrackSelection(true);
73813
- }
73814
-
73815
- this.updateUIWithReferenceFrameList();
73816
-
73817
- this.updateLocusSearchWidget();
73818
-
73819
- return trackConfigurations
74525
+ await this.loadTrackList(nonLocalTrackConfigurations);
73820
74526
 
73821
74527
  }
73822
74528
 
@@ -73877,13 +74583,8 @@ class Browser {
73877
74583
  }
73878
74584
 
73879
74585
  if (genomeChange) {
73880
- let trackConfigurations;
73881
- if (genomeConfig.hubURL) {
73882
- // TODO -- refactor this so "hub" is not loaded twice
73883
- const hub = await Hub.loadHub(genomeConfig.hubURL);
73884
- trackConfigurations = hub.getGroupedTrackConfigurations();
73885
- }
73886
- this.fireEvent('genomechange', [{genome, trackConfigurations}]);
74586
+
74587
+ this.fireEvent('genomechange', [{genome}]);
73887
74588
 
73888
74589
  if (this.circularView) {
73889
74590
  this.circularView.setAssembly({
@@ -73922,7 +74623,7 @@ class Browser {
73922
74623
  let genomeConfig;
73923
74624
  const isHubGenome = idOrConfig.hubURL || (idOrConfig.url && isString$2(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt"));
73924
74625
  if (isHubGenome) {
73925
- const hub = await Hub.loadHub(idOrConfig.hubURL || idOrConfig.url, idOrConfig);
74626
+ const hub = await loadHub(idOrConfig.hubURL || idOrConfig.url);
73926
74627
  genomeConfig = hub.getGenomeConfig();
73927
74628
  } else if (isString$2(idOrConfig) || !(idOrConfig.url || idOrConfig.fastaURL || idOrConfig.twoBitURL || idOrConfig.gbkURL)) {
73928
74629
  // Either an ID, a json string, or an object missing required properties.
@@ -73952,22 +74653,9 @@ class Browser {
73952
74653
 
73953
74654
  await this.loadTrackList(tracks);
73954
74655
 
73955
- await this.updateViews();
73956
-
73957
74656
  return this.genome
73958
74657
  }
73959
74658
 
73960
- /**
73961
- * Load a UCSC single-file genome assembly hub.
73962
- * @param options
73963
- * @returns {Promise<void>}
73964
- */
73965
- async loadTrackHub(options) {
73966
- const hub = await Hub.loadHub(options.url, options);
73967
- const genomeConfig = setDefaults(hub.getGenomeConfig());
73968
- return this.loadGenome(genomeConfig)
73969
- }
73970
-
73971
74659
  /**
73972
74660
  * Called after a session load, search, pan (horizontal drag), or resize
73973
74661
  *
@@ -74064,18 +74752,23 @@ class Browser {
74064
74752
  }
74065
74753
 
74066
74754
  const promises = [];
74067
- for (let config of configList) {
74068
- promises.push(this._loadTrack(config));
74755
+ for (const config of configList) {
74756
+ promises.push(this.#loadTrackHelper(config));
74069
74757
  }
74070
74758
 
74071
74759
  const loadedTracks = await Promise.all(promises);
74072
74760
 
74073
- const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
74074
- return trackView.track.autoscaleGroup
74075
- });
74076
- if (groupAutoscaleViews.length > 0) {
74077
- this.updateViews();
74761
+ // If any tracks are selected show the selection buttons
74762
+ if (this.trackViews.some(({track}) => track.selected)) {
74763
+ this.navbar.setEnableTrackSelection(true);
74078
74764
  }
74765
+
74766
+ this.reorderTracks();
74767
+
74768
+ await resize.call(this);
74769
+
74770
+ this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74771
+
74079
74772
  return loadedTracks
74080
74773
  }
74081
74774
 
@@ -74089,54 +74782,23 @@ class Browser {
74089
74782
  */
74090
74783
  async loadTrack(config) {
74091
74784
 
74092
- // Default configuration sync option to true. This is the expected behavior for public API calls
74093
- config.sync = (config.sync !== false);
74094
-
74095
- const newTrack = this._loadTrack(config);
74096
-
74097
- if (newTrack && config.autoscaleGroup) {
74098
- // Await newTrack load and update all views
74099
- await newTrack;
74785
+ const loadedTracks = await this.loadTrackList([config]);
74786
+ if (config.autoscaleGroup) {
74100
74787
  this.updateViews();
74101
74788
  }
74102
-
74103
- return newTrack
74789
+ return loadedTracks[0]
74104
74790
  }
74105
74791
 
74106
- /**
74107
- * Return a promise to load a track. Private function used by loadTrack() and loadTrackList()
74108
- *
74109
- * @param config
74110
- * @returns {*}
74111
- */
74112
-
74113
- async _loadTrack(config) {
74792
+ async #loadTrackHelper(config) {
74114
74793
 
74115
74794
  // config might be json
74116
74795
  if (isString$2(config)) {
74117
74796
  config = JSON.parse(config);
74118
74797
  }
74119
74798
 
74799
+ let track;
74120
74800
  try {
74121
-
74122
- // Load a hidden track -- used to populate searchable database without creating a track
74123
- if (config.hidden) {
74124
- const featureSource = FeatureSource(config, this.genome);
74125
- await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
74126
- return
74127
- }
74128
-
74129
- const newTrack = await this.createTrack(config);
74130
-
74131
- if ('sampleinfo' === config.type) {
74132
- this.layoutChange();
74133
- return
74134
- } else if (undefined === newTrack) {
74135
- return
74136
- }
74137
-
74138
- return this.addTrack(config, newTrack)
74139
-
74801
+ track = await this.createTrack(config);
74140
74802
  } catch (error) {
74141
74803
 
74142
74804
  let msg = error.message || error.error || error.toString();
@@ -74153,62 +74815,53 @@ class Browser {
74153
74815
  }
74154
74816
 
74155
74817
  msg = `${msg} : ${isFile(config.url) ? config.url.name : config.url}`;
74156
- // msg += (": " + FileUtils.isFile(config.url) ? config.url.name : config.url)
74157
74818
  const err = new Error(msg);
74158
74819
  console.error(err);
74159
74820
  throw err
74160
- // this.alert.present(new Error(msg), undefined)
74161
74821
  }
74162
- }
74163
74822
 
74164
- async addTrack(config, newTrack) {
74823
+ if (track) {
74824
+ return await this.addTrack(track)
74825
+ } else {
74826
+ return undefined
74827
+ }
74828
+
74829
+ }
74165
74830
 
74831
+ async addTrack(track) {
74166
74832
 
74167
74833
  // Set order field of track here, otherwise track order might get shuffled during asynchronous load
74168
- if (undefined === newTrack.order) {
74169
- newTrack.order = this.trackViews.length;
74834
+ if (undefined === track.order) {
74835
+ track.order = this.trackViews.length;
74170
74836
  }
74171
74837
 
74172
- const trackView = new TrackView(this, this.columnContainer, newTrack);
74838
+ const trackView = new TrackView(this, this.columnContainer, track);
74173
74839
  this.trackViews.push(trackView);
74174
74840
  toggleTrackLabels(this.trackViews, this.doShowTrackLabels);
74175
74841
 
74176
- if (typeof newTrack.postInit === 'function') {
74842
+ if (typeof track.postInit === 'function') {
74177
74843
  try {
74178
74844
  trackView.startSpinner();
74179
- await newTrack.postInit();
74845
+ await track.postInit();
74180
74846
  } finally {
74181
74847
  trackView.stopSpinner();
74182
74848
  }
74183
74849
  }
74184
74850
 
74185
- if (!newTrack.autoscaleGroup) {
74186
- // Group autoscale will get updated later (as a group)
74187
- if (config.sync) {
74188
- await trackView.updateViews();
74189
- } else {
74190
- trackView.updateViews();
74191
- }
74192
- }
74193
-
74194
- if (typeof newTrack.hasSamples === 'function' && newTrack.hasSamples()) {
74851
+ if (typeof track.hasSamples === 'function' && track.hasSamples()) {
74195
74852
 
74196
74853
  if (this.sampleInfo.hasAttributes()) {
74197
74854
  this.sampleInfoControl.setButtonVisibility(true);
74198
74855
  }
74199
74856
 
74200
74857
  if (this.config.showSampleNameButton !== false) {
74201
- this.sampleNameControl.show(); // If not explicitly set
74858
+ this.sampleNameControl.show();
74202
74859
  }
74203
74860
  }
74204
74861
 
74205
- // repositioned here to solve layout issue.
74206
- this.reorderTracks();
74207
- this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74862
+ track.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74208
74863
 
74209
- newTrack.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74210
-
74211
- return newTrack
74864
+ return track
74212
74865
 
74213
74866
  }
74214
74867
 
@@ -74353,7 +75006,6 @@ class Browser {
74353
75006
  }
74354
75007
  }
74355
75008
 
74356
-
74357
75009
  reorderTracks() {
74358
75010
 
74359
75011
  this.trackViews.sort(function (a, b) {
@@ -74590,19 +75242,19 @@ class Browser {
74590
75242
 
74591
75243
  this.updateLocusSearchWidget();
74592
75244
 
74593
- for (let frame of this.referenceFrameList) {
74594
- if (frame.bpPerPixel <= bppSequenceThreshold) {
74595
- await this.genome.getSequence(frame.chr, frame.start, frame.start + 1);
75245
+ for (const {bpPerPixel, chr, start} of this.referenceFrameList) {
75246
+ if (bpPerPixel <= bppSequenceThreshold) {
75247
+ await this.genome.getSequence(chr, start, start + 1);
74596
75248
  }
74597
75249
  }
74598
75250
 
74599
- for (let centerGuide of this.centerLineList) {
75251
+ for (const centerGuide of this.centerLineList) {
74600
75252
  centerGuide.repaint();
74601
75253
  }
74602
75254
 
74603
75255
  // Don't autoscale while dragging.
74604
75256
  if (this.dragObject) {
74605
- for (let trackView of trackViews) {
75257
+ for (const trackView of trackViews) {
74606
75258
  await trackView.updateViews();
74607
75259
  }
74608
75260
  } else {
@@ -74626,8 +75278,8 @@ class Browser {
74626
75278
  // Calculate group autoscale dataRange
74627
75279
  if (Object.entries(groupAutoscaleTrackViews).length > 0) {
74628
75280
  for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) {
74629
- const featureArray = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
74630
- const dataRange = doAutoscale(featureArray.flat());
75281
+ const inViewFeatures = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
75282
+ const dataRange = doAutoscale(inViewFeatures.flat());
74631
75283
  for (const trackView of trackViews) {
74632
75284
  trackView.track.dataRange = Object.assign({}, dataRange);
74633
75285
  trackView.track.autoscale = false;
@@ -74636,7 +75288,7 @@ class Browser {
74636
75288
  }
74637
75289
  }
74638
75290
 
74639
- await Promise.all(otherTrackViews.map(tv => tv.updateViews()));
75291
+ await Promise.all(otherTrackViews.map(trackView => trackView.updateViews()));
74640
75292
  }
74641
75293
 
74642
75294
  }
@@ -74659,10 +75311,10 @@ class Browser {
74659
75311
  referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width;
74660
75312
  }
74661
75313
 
74662
- const chrName = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '';
74663
-
74664
75314
  const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ');
74665
75315
 
75316
+ const chrName = referenceFrameList.length === 1 ? this.genome.getChromosomeDisplayName(this.referenceFrameList[0].chr) : '';
75317
+
74666
75318
  this.navbar.updateLocus(loc, chrName);
74667
75319
 
74668
75320
  this.fireEvent('locuschange', [this.referenceFrameList]);
@@ -74679,6 +75331,46 @@ class Browser {
74679
75331
  return Math.floor(width / columnCount)
74680
75332
  }
74681
75333
 
75334
+ /**
75335
+ * Update reference frames based on new viewport width
75336
+ * @param {number} viewportWidth - The calculated viewport width
75337
+ */
75338
+ updateReferenceFrames(viewportWidth) {
75339
+
75340
+ for (const referenceFrame of this.referenceFrameList) {
75341
+ referenceFrame.updateForViewportWidth(viewportWidth);
75342
+ }
75343
+ }
75344
+
75345
+ /**
75346
+ * Update DOM viewport elements with new width
75347
+ * @param {number} viewportWidth - The calculated viewport width
75348
+ */
75349
+ updateViewportElements(viewportWidth) {
75350
+
75351
+ for (let i = 0; i < this.referenceFrameList.length; i++) {
75352
+
75353
+ for (const {viewports} of this.trackViews) {
75354
+ viewports[i].setWidth(viewportWidth);
75355
+ }
75356
+
75357
+ for (const {sampleInfoViewport} of this.trackViews) {
75358
+ sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
75359
+ sampleInfoViewport.repaint();
75360
+ }
75361
+
75362
+ }
75363
+ }
75364
+
75365
+ /**
75366
+ * Synchronize UI state after viewport updates
75367
+ * @returns {Promise<void>}
75368
+ */
75369
+ async syncUIState() {
75370
+ this.updateUIWithReferenceFrameList();
75371
+ await this.updateViews(true);
75372
+ }
75373
+
74682
75374
  minimumBases() {
74683
75375
  return this.config.minimumBases
74684
75376
  }
@@ -74864,29 +75556,12 @@ class Browser {
74864
75556
  }
74865
75557
 
74866
75558
  /**
74867
- * @deprecated This is a deprecated method with no known usages. To be removed in a future release.
75559
+ * @deprecated This is a deprecated method with no known usages.
74868
75560
  */
74869
75561
  async goto(chr, start, end) {
74870
75562
  await this.search(chr + ":" + start + "-" + end);
74871
75563
  }
74872
75564
 
74873
- /**
74874
-
74875
- * Search for the locus string -- this function is called from various igv.js GUI elements, and is not part of the
74876
- * API. Wraps ```search``` and presents an error dialog if false.
74877
- *
74878
- * @param string
74879
- * @param init
74880
- * @returns {Promise<void>}
74881
- */
74882
- async doSearch(string, init) {
74883
- const success = await this.search(string, init);
74884
- if (!success) {
74885
- this.alert.present(new Error(`Unrecognized locus: <b> ${string} </b>`));
74886
- }
74887
- return success
74888
- }
74889
-
74890
75565
 
74891
75566
  /**
74892
75567
  * Search for the locus string
@@ -74899,6 +75574,10 @@ class Browser {
74899
75574
  async search(stringOrArray, init) {
74900
75575
 
74901
75576
  const loci = await search(this, stringOrArray);
75577
+ return this.updateLoci(loci, init)
75578
+ }
75579
+
75580
+ async updateLoci(loci, init) {
74902
75581
 
74903
75582
  if (loci && loci.length > 0) {
74904
75583
 
@@ -74935,9 +75614,9 @@ class Browser {
74935
75614
  }
74936
75615
  }
74937
75616
 
74938
- async loadSampleInfo(config) {
75617
+ async loadSampleInfo(sampleInfoConfig) {
74939
75618
 
74940
- await this.sampleInfo.loadSampleInfoFile(config.url);
75619
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74941
75620
 
74942
75621
  for (const {sampleInfoViewport} of this.trackViews) {
74943
75622
  sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
@@ -75075,9 +75754,9 @@ class Browser {
75075
75754
  json["locus"] = locus.length === 1 ? locus[0] : locus;
75076
75755
 
75077
75756
  const roiSets = this.roiManager.toJSON();
75078
- if(roiSets) {
75757
+ if (roiSets) {
75079
75758
  json["roi"] = roiSets;
75080
- if(!this.roiManager.showOverlays){
75759
+ if (!this.roiManager.showOverlays) {
75081
75760
  json["showROIOverlays"] = false; // true is the default
75082
75761
  }
75083
75762
  }
@@ -75420,8 +76099,6 @@ class Browser {
75420
76099
  }
75421
76100
  }
75422
76101
 
75423
-
75424
-
75425
76102
  // Navbar delegates
75426
76103
  get sampleInfoControl() {
75427
76104
  return this.navbar.sampleInfoControl
@@ -75443,6 +76120,9 @@ class Browser {
75443
76120
  return this.navbar.sampleNameControl
75444
76121
  }
75445
76122
 
76123
+ async blat(sequence) {
76124
+ return createBlatTrack({sequence, browser: this, name: 'Blat', title: 'Blat'})
76125
+ }
75446
76126
  }
75447
76127
 
75448
76128
  function getFileExtension(input) {
@@ -75468,7 +76148,7 @@ function getFileExtension(input) {
75468
76148
  }
75469
76149
 
75470
76150
  /**
75471
- * Function called win window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
76151
+ * Called when window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
75472
76152
  * than class method because it needs to be copied and bound to specific instances of browser to support listener
75473
76153
  * removal
75474
76154
  *
@@ -75476,40 +76156,14 @@ function getFileExtension(input) {
75476
76156
  */
75477
76157
  async function resize() {
75478
76158
 
75479
- if (!this.referenceFrameList) return
75480
-
75481
- const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
75482
-
75483
- for (let referenceFrame of this.referenceFrameList) {
75484
-
75485
- const index = this.referenceFrameList.indexOf(referenceFrame);
75486
-
75487
- const {chr, genome} = referenceFrame;
75488
-
75489
- const {bpLength} = genome.getChromosome(referenceFrame.chr);
75490
-
75491
- const viewportWidthBP = referenceFrame.toBP(viewportWidth);
75492
-
75493
- // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
75494
- if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
75495
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
75496
- referenceFrame.bpPerPixel = bpLength / viewportWidth;
75497
- } else {
75498
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
75499
- referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
75500
- }
75501
-
75502
- for (let {viewports} of this.trackViews) {
75503
- viewports[index].setWidth(viewportWidth);
75504
- }
75505
-
76159
+ if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
76160
+ return
75506
76161
  }
75507
76162
 
75508
- this.updateUIWithReferenceFrameList();
75509
-
75510
- //TODO -- update view only if needed. Reducing size never needed. Increasing size maybe
75511
-
75512
- await this.updateViews(true);
76163
+ const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
76164
+ this.updateReferenceFrames(viewportWidth);
76165
+ this.updateViewportElements(viewportWidth);
76166
+ await this.syncUIState();
75513
76167
  }
75514
76168
 
75515
76169
 
@@ -75978,7 +76632,8 @@ var index = {
75978
76632
  registerTrackClass,
75979
76633
  registerTrackCreatorFunction,
75980
76634
  registerFileFormats,
75981
- loadSessionFile: Browser.loadSessionFile
76635
+ loadSessionFile: Browser.loadSessionFile,
76636
+ loadHub
75982
76637
  };
75983
76638
 
75984
76639
  export { index as default };