igv 3.2.5 → 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
@@ -10946,7 +10946,7 @@ function createMenuElements$1(itemList, popover) {
10946
10946
  return list;
10947
10947
  }
10948
10948
 
10949
- /*! @license DOMPurify 3.2.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.4/LICENSE */
10949
+ /*! @license DOMPurify 3.2.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.5/LICENSE */
10950
10950
 
10951
10951
  const {
10952
10952
  entries,
@@ -11006,6 +11006,9 @@ const typeErrorCreate = unconstruct(TypeError);
11006
11006
  */
11007
11007
  function unapply(func) {
11008
11008
  return function (thisArg) {
11009
+ if (thisArg instanceof RegExp) {
11010
+ thisArg.lastIndex = 0;
11011
+ }
11009
11012
  for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
11010
11013
  args[_key - 1] = arguments[_key];
11011
11014
  }
@@ -11244,7 +11247,7 @@ const _createHooksMap = function _createHooksMap() {
11244
11247
  function createDOMPurify() {
11245
11248
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
11246
11249
  const DOMPurify = root => createDOMPurify(root);
11247
- DOMPurify.version = '3.2.4';
11250
+ DOMPurify.version = '3.2.5';
11248
11251
  DOMPurify.removed = [];
11249
11252
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
11250
11253
  // Not running in a browser, provide a factory function
@@ -11849,7 +11852,7 @@ function createDOMPurify() {
11849
11852
  allowedTags: ALLOWED_TAGS
11850
11853
  });
11851
11854
  /* Detect mXSS attempts abusing namespace confusion */
11852
- if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
11855
+ if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
11853
11856
  _forceRemove(currentNode);
11854
11857
  return true;
11855
11858
  }
@@ -12871,6 +12874,20 @@ class NonIndexedFasta {
12871
12874
 
12872
12875
  async loadAll() {
12873
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
+
12874
12891
  let data;
12875
12892
  if (isDataURL(this.fastaURL)) {
12876
12893
  let bytes = decodeDataURI$1(this.fastaURL);
@@ -12884,70 +12901,62 @@ class NonIndexedFasta {
12884
12901
 
12885
12902
  const chrNameSet = new Set();
12886
12903
  const lines = splitLines$2(data);
12887
- const len = lines.length;
12888
- let lineNo = 0;
12889
12904
  let order = 0;
12890
- let nextLine;
12891
12905
  let current = {};
12892
- while (lineNo < len) {
12893
- nextLine = lines[lineNo++].trim();
12906
+ for (let nextLine of lines) {
12894
12907
  if (nextLine.startsWith("#") || nextLine.length === 0) ; else if (nextLine.startsWith(">")) {
12895
12908
  // Start the next sequence
12896
- if (current && current.seq) {
12897
- pushChromosome.call(this, current, order++);
12909
+ if (current.seq && current.seq.length > 0) {
12910
+ pushChromosome(current, order++);
12898
12911
  }
12912
+ current.seq = "";
12899
12913
 
12900
12914
  const parts = nextLine.substr(1).split(/\s+/);
12901
12915
 
12902
- // Check for samtools style locus string. This is not perfect, and could fail on weird sequence names
12903
- const nameParts = parts[0].split(':');
12904
- current.chr = nameParts[0];
12905
- current.seq = "";
12906
- current.offset = 0;
12907
- if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12908
- const locusParts = nameParts[1].split('-');
12909
- if (locusParts.length === 2 &&
12910
- /^[0-9]+$/.test(locusParts[0]) &&
12911
- /^[0-9]+$/.test(locusParts[1])) ;
12912
- const from = Number.parseInt(locusParts[0]);
12913
- const to = Number.parseInt(locusParts[1]);
12914
- 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]);
12915
12929
  current.offset = from - 1;
12916
- }
12917
12930
 
12918
- // Check for chromosome length token
12919
- if (parts.length > 1 && parts[1].startsWith("@len=")) {
12920
- try {
12921
- current.length = parseInt(parts[1].trim().substring(5));
12922
- } 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 {
12923
12940
  current.length = undefined;
12924
- console.error(`Error parsing sequence length for ${nextLine}`);
12925
12941
  }
12926
- } else {
12927
- current.length = undefined;
12928
12942
  }
12943
+ } else {
12944
+ // No special tokens, a standard FASTA header
12945
+ current.chr = parts[0];
12946
+ current.offset = 0;
12929
12947
  }
12948
+
12930
12949
  } else {
12950
+ // Not a header or comment, so it must be sequence data
12931
12951
  current.seq += nextLine;
12932
12952
  }
12933
- // add last seq
12934
- if (current && current.seq) {
12935
- pushChromosome.call(this, current, order);
12936
- }
12937
12953
  }
12938
12954
 
12939
- function pushChromosome(current, order) {
12940
- const length = current.length || (current.offset + current.seq.length);
12941
- if (!chrNameSet.has(current.chr)) {
12942
- this.sequences.set(current.chr, []);
12943
- this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12944
- chrNameSet.add(current.chr);
12945
- } else {
12946
- const c = this.chromosomes.get(current.chr);
12947
- c.bpLength = Math.max(c.bpLength, length);
12948
- }
12949
- 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);
12950
12958
  }
12959
+
12951
12960
  }
12952
12961
 
12953
12962
  /**
@@ -13712,17 +13721,22 @@ class BPTree {
13712
13721
 
13713
13722
  static magic = 2026540177
13714
13723
  littleEndian = true
13724
+ type = 'BPTree' // Either BPTree or BPChromTree
13715
13725
  nodeCache = new Map()
13716
13726
 
13717
- static async loadBpTree(path, config, startOffset) {
13718
- 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);
13719
13729
  return bpTree.init()
13720
13730
  }
13721
13731
 
13722
- constructor(path, config, startOffset) {
13732
+ constructor(path, config, startOffset, type, loader) {
13723
13733
  this.path = path;
13724
13734
  this.config = config;
13725
13735
  this.startOffset = startOffset;
13736
+ if(type) {
13737
+ this.type = type;
13738
+ }
13739
+ this.loader = loader || igvxhr;
13726
13740
  }
13727
13741
 
13728
13742
  async init() {
@@ -13748,69 +13762,22 @@ class BPTree {
13748
13762
  return this
13749
13763
  }
13750
13764
 
13751
- async search(term) {
13752
-
13765
+ getItemCount() {
13753
13766
  if(!this.header) {
13754
- await this.init();
13767
+ throw Error("Header not initialized")
13755
13768
  }
13769
+ return this.header.itemCount
13770
+ }
13756
13771
 
13757
- const {keySize, valSize} = this.header;
13772
+ async search(term) {
13758
13773
 
13759
- if (!(valSize === 16 || valSize === 8)) {
13760
- throw Error(`Unexpected valSize ${valSize}`)
13774
+ if(!this.header) {
13775
+ await this.init();
13761
13776
  }
13762
13777
 
13763
- const readTreeNode = async (offset) => {
13764
-
13765
- if (this.nodeCache.has(offset)) {
13766
- return this.nodeCache.get(offset)
13767
- } else {
13768
-
13769
- let binaryParser = await this.#getParserFor(offset, 4);
13770
- const type = binaryParser.getByte();
13771
- binaryParser.getByte();
13772
- const count = binaryParser.getUShort();
13773
- const items = [];
13774
-
13775
- if (type === 1) {
13776
- // Leaf node
13777
- const size = count * (keySize + valSize);
13778
- binaryParser = await this.#getParserFor(offset + 4, size);
13779
- for (let i = 0; i < count; i++) {
13780
- const key = binaryParser.getFixedLengthString(keySize);
13781
- const offset = binaryParser.getLong();
13782
-
13783
- let value;
13784
- if (valSize === 16) {
13785
- const length = binaryParser.getInt();
13786
- binaryParser.getInt();
13787
- value = {offset, length};
13788
- } else {
13789
- value = {offset};
13790
- }
13791
- items.push({key, value});
13792
- }
13793
- } else {
13794
- // Non leaf node
13795
- const size = count * (keySize + 8);
13796
- binaryParser = await this.#getParserFor(offset + 4, size);
13797
-
13798
- for (let i = 0; i < count; i++) {
13799
- const key = binaryParser.getFixedLengthString(keySize);
13800
- const offset = binaryParser.getLong();
13801
- items.push({key, offset});
13802
- }
13803
- }
13804
-
13805
- const node = {type, count, items};
13806
- this.nodeCache.set(offset, node);
13807
- return node
13808
- }
13809
- };
13810
-
13811
13778
  const walkTreeNode = async (offset) => {
13812
13779
 
13813
- const node = await readTreeNode(offset);
13780
+ const node = await this.readTreeNode(offset);
13814
13781
 
13815
13782
  if (node.type === 1) {
13816
13783
  // Leaf node
@@ -13841,9 +13808,66 @@ class BPTree {
13841
13808
  return walkTreeNode(this.header.nodeOffset)
13842
13809
  }
13843
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
+
13844
13864
  async #getParserFor(start, size) {
13845
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13846
- 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
+ }
13847
13871
  }
13848
13872
 
13849
13873
  }
@@ -13869,6 +13893,7 @@ class TwobitSequence {
13869
13893
 
13870
13894
  littleEndian
13871
13895
  metaIndex = new Map()
13896
+ chromosomeNames
13872
13897
 
13873
13898
  constructor(config) {
13874
13899
  this.url = config.twoBitURL || config.fastaURL;
@@ -13961,9 +13986,16 @@ class TwobitSequence {
13961
13986
  return sequenceBases
13962
13987
  }
13963
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
+ */
13964
13995
  async _readIndex() {
13965
13996
 
13966
13997
  const index = new Map();
13998
+ this.chromosomeNames = [];
13967
13999
 
13968
14000
  const loadRange = {start: 0, size: 64};
13969
14001
  let arrayBuffer = await igvxhr.loadArrayBuffer(this.url, {range: loadRange});
@@ -14016,6 +14048,8 @@ class TwobitSequence {
14016
14048
  index.set(name, offset);
14017
14049
 
14018
14050
  estNameLength = Math.floor(estNameLength * (i / (i + 1)) + name.length / (i + 1));
14051
+
14052
+ this.chromosomeNames.push(name);
14019
14053
  }
14020
14054
  return index
14021
14055
  }
@@ -16343,7 +16377,10 @@ class FeatureParser {
16343
16377
  }
16344
16378
  } else {
16345
16379
  // All directives that could change the format, and thus decoder, should have been read by now.
16346
- this.setDecoder(header.format);
16380
+ // Set the decoder, unless it is explicitly set in the track configuration (not common)
16381
+ if(!this.config.decode) {
16382
+ this.setDecoder(header.format);
16383
+ }
16347
16384
 
16348
16385
  // If the line can be parsed as a feature assume we are beyond the header, if any
16349
16386
  const tokens = line.split(this.delimiter || "\t");
@@ -22158,84 +22195,6 @@ function zoomLevelForScale$1(chr, bpPerPixel, genome) {
22158
22195
  return Math.ceil(Math.log(Math.max(0, (chrSize / (bpPerPixel * 700)))) / log2)
22159
22196
  }
22160
22197
 
22161
- /**
22162
- * A ChromTree parses a UCSC bigbed/bigwig "chromosomeTree" section of the header to produce 2 maps,
22163
- * (1) ID -> chromosome names, and its
22164
- * (2) chromsome name -> ID
22165
- *
22166
- * Both maps are needed by IGV
22167
- *
22168
- * The chromosome tree is a B+ index, but is located continguously in memory in the header section of the file. This
22169
- * makes it feasible to parse the whole tree with data from a single fetch. In the end the tree is discarded
22170
- * leaving only the mapps.
22171
- */
22172
- class ChromTree {
22173
-
22174
- constructor(header, nameToID, valueToKey, sumLengths) {
22175
- this.header = header;
22176
- this.nameToId = nameToID;
22177
- this.idToName = valueToKey;
22178
- this.sumLengths = sumLengths;
22179
- }
22180
-
22181
- static parseTree(binaryParser, startOffset, genome = false) {
22182
- {
22183
- const magic = binaryParser.getInt();
22184
- const blockSize = binaryParser.getInt();
22185
- const keySize = binaryParser.getInt();
22186
- const valSize = binaryParser.getInt();
22187
- const itemCount = binaryParser.getLong();
22188
- const reserved = binaryParser.getLong();
22189
-
22190
- const header = {magic, blockSize, keySize, valSize, itemCount, reserved};
22191
- const nameToId = new Map();
22192
- const idToName = [];
22193
- let sumLengths = 0;
22194
- const readTreeNode = (offset) => {
22195
- if (offset >= 0) binaryParser.position = offset;
22196
- const type = binaryParser.getByte();
22197
- binaryParser.getByte();
22198
- const count = binaryParser.getUShort();
22199
-
22200
- if (type === 1) {
22201
- // Leaf node
22202
- for (let i = 0; i < count; i++) {
22203
- let key = binaryParser.getFixedLengthString(keySize);
22204
- let value;
22205
- if (valSize === 8) {
22206
- value = binaryParser.getInt();
22207
- const chromSize = binaryParser.getInt();
22208
- sumLengths += chromSize;
22209
- if (genome) key = genome.getChromosomeName(key); // Translate to canonical chr name
22210
- nameToId.set(key, value);
22211
- idToName[value] = key;
22212
-
22213
- } else {
22214
- throw Error(`Unexpected "valSize" value in chromosome tree. Expected 8, actual value is ${valSize}`)
22215
- }
22216
- }
22217
- } else {
22218
- // non-leaf
22219
- for (let i = 0; i < count; i++) {
22220
- binaryParser.getFixedLengthString(keySize);
22221
- const childOffset = binaryParser.getLong();
22222
- const bufferOffset = childOffset - startOffset;
22223
- const currOffset = binaryParser.position;
22224
- readTreeNode(bufferOffset);
22225
- binaryParser.position = currOffset;
22226
- }
22227
- }
22228
- };
22229
-
22230
- // Recursively walk tree to populate dictionary
22231
- readTreeNode( -1);
22232
-
22233
- return new ChromTree(header, nameToId, idToName, sumLengths)
22234
- }
22235
- }
22236
-
22237
- }
22238
-
22239
22198
  const RPTREE_HEADER_SIZE = 48;
22240
22199
  const RPTREE_NODE_LEAF_ITEM_SIZE = 32; // leaf item size
22241
22200
  const RPTREE_NODE_CHILD_ITEM_SIZE = 24; // child item size
@@ -22246,11 +22205,12 @@ class RPTree {
22246
22205
  littleEndian = true
22247
22206
  nodeCache = new Map()
22248
22207
 
22249
- constructor(path, config, startOffset) {
22208
+ constructor(path, config, startOffset, loader) {
22250
22209
 
22251
22210
  this.path = path;
22252
22211
  this.config = config;
22253
22212
  this.startOffset = startOffset;
22213
+ this.loader = loader || igvxhr;
22254
22214
  }
22255
22215
 
22256
22216
 
@@ -22294,7 +22254,7 @@ class RPTree {
22294
22254
  }
22295
22255
 
22296
22256
  async #getParserFor(start, size) {
22297
- 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}}));
22298
22258
  return new BinaryParser$1(new DataView(data), this.littleEndian)
22299
22259
  }
22300
22260
 
@@ -22651,6 +22611,165 @@ class Trix {
22651
22611
  }
22652
22612
  }
22653
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
+
22654
22773
  /*
22655
22774
  * The MIT License (MIT)
22656
22775
  *
@@ -22714,14 +22833,40 @@ class BWReader {
22714
22833
  async preload() {
22715
22834
  const data = await igvxhr.loadArrayBuffer(this.path);
22716
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
+ }
22717
22844
  }
22718
22845
 
22719
- async readWGFeatures(bpPerPixel, windowFunction) {
22846
+ async readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction) {
22847
+
22720
22848
  await this.loadHeader();
22721
- const chrIdx1 = 0;
22722
- const chrIdx2 = this.chromTree.idToName.length - 1;
22723
- const chr1 = this.chromTree.idToName[chrIdx1];
22724
- 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
+
22725
22870
  return this.readFeatures(chr1, 0, chr2, Number.MAX_VALUE, bpPerPixel, windowFunction)
22726
22871
  }
22727
22872
 
@@ -22732,8 +22877,8 @@ class BWReader {
22732
22877
 
22733
22878
  await this.loadHeader();
22734
22879
 
22735
- let chrIdx1 = await this.#getIdForChr(chr1);
22736
- let chrIdx2 = await this.#getIdForChr(chr2);
22880
+ const chrIdx1 = await this.getIdForChr(chr1);
22881
+ const chrIdx2 = await this.getIdForChr(chr2);
22737
22882
 
22738
22883
  if (chrIdx1 === undefined || chrIdx2 === undefined) {
22739
22884
  return []
@@ -22792,7 +22937,7 @@ class BWReader {
22792
22937
  } else {
22793
22938
  plain = uint8Array;
22794
22939
  }
22795
- 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);
22796
22941
  }
22797
22942
 
22798
22943
  features.sort(function (a, b) {
@@ -22809,29 +22954,30 @@ class BWReader {
22809
22954
  * @param chr
22810
22955
  * @returns {Promise<*>}
22811
22956
  */
22812
- async #getIdForChr(chr) {
22957
+ async getIdForChr(chr) {
22813
22958
 
22814
22959
  if (this.chrAliasTable.has(chr)) {
22815
22960
  chr = this.chrAliasTable.get(chr);
22816
- if (chr === undefined) {
22961
+ if (!chr) {
22817
22962
  return undefined
22818
22963
  }
22819
22964
  }
22820
22965
 
22821
- let chrIdx = this.chromTree.nameToId.get(chr);
22966
+ let chrIdx = await this.chromTree.getIdForName(chr);
22822
22967
 
22823
22968
  // Try alias
22824
22969
  if (chrIdx === undefined && this.genome) {
22825
22970
  const aliasRecord = await this.genome.getAliasRecord(chr);
22826
22971
  let alias;
22827
22972
  if (aliasRecord) {
22828
- const aliases = Object.keys(aliasRecord)
22829
- .filter(k => k !== "start" && k !== "end")
22830
- .map(k => aliasRecord[k])
22831
- .filter(a => this.chromTree.nameToId.has(a));
22832
- if (aliases.length > 0) {
22833
- alias = aliases[0];
22834
- 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
+ }
22835
22981
  }
22836
22982
  }
22837
22983
  this.chrAliasTable.set(chr, alias); // alias may be undefined => no alias exists. Setting prevents repeated attempts
@@ -22918,7 +23064,8 @@ class BWReader {
22918
23064
  this.header.extraIndexOffsets.length > 0) {
22919
23065
  this._searchTrees = [];
22920
23066
  for (let offset of this.header.extraIndexOffsets) {
22921
- 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);
22922
23069
  this._searchTrees.push(bpTree);
22923
23070
  }
22924
23071
  }
@@ -23035,15 +23182,13 @@ class BWReader {
23035
23182
  this.totalSummary = new BWTotalSummary(extHeaderParser);
23036
23183
  }
23037
23184
 
23038
- // Chrom data index. The start is known, size is not, but we can estimate it
23039
- const bufferSize = Math.min(200000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset));
23040
- this.chromTree = await this.#readChromTree(header.chromTreeOffset, bufferSize);
23041
- 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();
23042
23187
 
23043
23188
  // Estimate feature density from dataCount (bigbed only)
23044
- if("bigbed" === this.type) {
23189
+ if ("bigbed" === this.type) {
23045
23190
  const dataCount = await this.#readDataCount(header.fullDataOffset);
23046
- this.featureDensity = dataCount / this.chromTree.sumLengths;
23191
+ this.featureDensity = dataCount / await this.chromTree.estimateGenomeSize();
23047
23192
  }
23048
23193
 
23049
23194
  this.header = header;
@@ -23067,38 +23212,6 @@ class BWReader {
23067
23212
  return binaryParser.getInt()
23068
23213
  }
23069
23214
 
23070
- /**
23071
- * Used when the chromTreeOffset is > fullDataOffset, that is when the chrom tree is not in the initial chunk
23072
- * read for parsing the header. We know the start position, but not the total size of the chrom tree
23073
- *
23074
- * @returns {Promise<void>}
23075
- */
23076
- async #readChromTree(chromTreeOffset, bufferSize) {
23077
-
23078
- let size = bufferSize;
23079
- const load = async () => {
23080
- const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
23081
- range: {
23082
- start: chromTreeOffset,
23083
- size: size
23084
- }
23085
- }));
23086
- const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
23087
- return ChromTree.parseTree(binaryParser, chromTreeOffset, this.genome)
23088
- };
23089
-
23090
- let error;
23091
- while (size < 1000000) {
23092
- try {
23093
- const chromTree = await load();
23094
- return chromTree
23095
- } catch (e) {
23096
- error = e;
23097
- size *= 2;
23098
- }
23099
- }
23100
- throw (error)
23101
- }
23102
23215
 
23103
23216
  async loadExtendedHeader(offset) {
23104
23217
 
@@ -23154,7 +23267,7 @@ class BWReader {
23154
23267
  if (rpTree) {
23155
23268
  return rpTree
23156
23269
  } else {
23157
- rpTree = new RPTree(this.path, this.config, offset);
23270
+ rpTree = new RPTree(this.path, this.config, offset, this.loader);
23158
23271
  await rpTree.init();
23159
23272
  this.rpTreeCache.set(offset, rpTree);
23160
23273
  return rpTree
@@ -23194,9 +23307,7 @@ class BWReader {
23194
23307
  const plain = (this.header.uncompressBuffSize > 0) ? inflate_1$3(uint8Array) : uint8Array;
23195
23308
  const decodeFunction = getBedDataDecoder.call(this);
23196
23309
  const features = [];
23197
- decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
23198
- features, this.chromTree.idToName);
23199
-
23310
+ await decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, features);
23200
23311
  return features
23201
23312
 
23202
23313
  }
@@ -23264,7 +23375,7 @@ function zoomLevelForScale(bpPerPixel, zoomLevelHeaders) {
23264
23375
  }
23265
23376
 
23266
23377
 
23267
- function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23378
+ async function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23268
23379
 
23269
23380
  const binaryParser = new BinaryParser$1(data, littleEndian);
23270
23381
  const chromId = binaryParser.getInt();
@@ -23305,7 +23416,7 @@ function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chr
23305
23416
  else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break
23306
23417
 
23307
23418
  if (Number.isFinite(value)) {
23308
- const chr = chrDict[chromId];
23419
+ const chr = await this.chromTree.getNameForId(chromId);
23309
23420
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23310
23421
  }
23311
23422
  }
@@ -23316,12 +23427,13 @@ function getBedDataDecoder() {
23316
23427
 
23317
23428
  const minSize = 3 * 4 + 1; // Minimum # of bytes required for a bed record
23318
23429
  const decoder = getDecoder(this.header.definedFieldCount, this.header.fieldCount, this.autoSql, this.format);
23319
- return function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23320
- 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);
23321
23433
  while (binaryParser.remLength() >= minSize) {
23322
23434
 
23323
23435
  const chromId = binaryParser.getInt();
23324
- const chr = chrDict[chromId];
23436
+ const chr = await this.chromTree.getNameForId(chromId);
23325
23437
  const chromStart = binaryParser.getInt();
23326
23438
  const chromEnd = binaryParser.getInt();
23327
23439
  const rest = binaryParser.getString();
@@ -23338,8 +23450,7 @@ function getBedDataDecoder() {
23338
23450
  }
23339
23451
  }
23340
23452
 
23341
-
23342
- function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23453
+ async function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23343
23454
 
23344
23455
  const binaryParser = new BinaryParser$1(data, littleEndian);
23345
23456
  const minSize = 8 * 4; // Minimum # of bytes required for a zoom record
@@ -23371,7 +23482,7 @@ function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, ch
23371
23482
 
23372
23483
 
23373
23484
  if (Number.isFinite(value)) {
23374
- const chr = chrDict[chromId];
23485
+ const chr = await this.chromTree.getNameForId(chromId);
23375
23486
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23376
23487
 
23377
23488
 
@@ -23456,7 +23567,8 @@ class BWSource extends BaseFeatureSource {
23456
23567
 
23457
23568
  let features;
23458
23569
  if ("all" === chr.toLowerCase()) {
23459
- features = isBigWig ? await this.getWGValues(windowFunction, bpPerPixel) : [];
23570
+ const wgChromosomeNames = this.genome.wgChromosomeNames;
23571
+ features = isBigWig && wgChromosomeNames? await this.getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) : [];
23460
23572
  } else {
23461
23573
  features = await this.reader.readFeatures(chr, start, chr, end, bpPerPixel, windowFunction);
23462
23574
  }
@@ -23480,15 +23592,14 @@ class BWSource extends BaseFeatureSource {
23480
23592
 
23481
23593
  }
23482
23594
 
23483
- async getWGValues(windowFunction, bpPerPixel) {
23595
+ async getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) {
23484
23596
 
23485
23597
  const genome = this.genome;
23486
23598
  const cached = this.#wgValues[windowFunction];
23487
23599
  if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
23488
23600
  return cached.values
23489
23601
  } else {
23490
-
23491
- const features = await this.reader.readWGFeatures(bpPerPixel, windowFunction);
23602
+ const features = await this.reader.readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction);
23492
23603
  let wgValues = [];
23493
23604
  for (let f of features) {
23494
23605
  const chr = f.chr;
@@ -28283,7 +28394,7 @@ async function createBlatTrack({sequence, browser, name, title}) {
28283
28394
  features
28284
28395
  };
28285
28396
 
28286
- const track = await browser.loadTrack(trackConfig);
28397
+ const track = (await browser.loadTrackList([trackConfig]))[0];
28287
28398
  track.openTableView();
28288
28399
 
28289
28400
  } catch (e) {
@@ -30335,109 +30446,578 @@ function convertToHubURL(accension) {
30335
30446
  }
30336
30447
  }
30337
30448
 
30338
- /*
30339
- https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30340
- https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30341
- https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30342
- https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30343
- */
30449
+ const parentOverrideProperties = new Set(["visibility", "priority", "group"]);
30344
30450
 
30451
+ const nonInheritableProperties = new Set([
30452
+ "track", "type", "shortLabel", "longLabel", "bigDataUrl",
30453
+ "parent", "superTrack", "priority", "view", "compositeContainer", "compositeTrack"
30454
+ ]);
30345
30455
 
30346
- class Hub {
30347
30456
 
30348
- static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30349
- static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30350
- "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
+ }
30487
+
30488
+ hasOwnProperty(key) {
30489
+ return this.properties.has(key)
30490
+ }
30491
+
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"
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
+ }
30537
+
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);
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")
30616
+ }
30617
+ }
30618
+ }
30619
+
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
+ }
30351
30632
 
30352
- static async loadHub(url) {
30633
+ getGroupedTrackConfigurations() {
30353
30634
 
30354
- const idx = url.lastIndexOf("/");
30355
- const baseURL = url.substring(0, idx + 1);
30356
- const stanzas = await loadStanzas(url);
30357
- let groups;
30358
- if ("genome" === stanzas[1].type) {
30359
- const genome = stanzas[1];
30360
- if (genome.hasProperty("groups")) {
30361
- const groupsTxtURL = baseURL + genome.getProperty("groups");
30362
- groups = await loadStanzas(groupsTxtURL);
30635
+ if (!this.groupTrackConfigs) {
30636
+ this.groupTrackConfigs = [];
30637
+ const trackContainers = new Map();
30638
+
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
+ }
30363
30653
  }
30364
30654
 
30365
- // If the genome has a chromSizes file, and it is not too large, set the chromSizesURL property. This will
30366
- // enable whole genome view and the chromosome pulldown
30367
- if (genome.hasProperty("chromSizes")) {
30368
- const chromSizesURL = baseURL + genome.getProperty("chromSizes");
30369
- const l = await getContentLength(chromSizesURL);
30370
- if (l !== null && Number.parseInt(l) < 1000000) {
30371
- genome.setProperty("chromSizesURL", chromSizesURL);
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
+ }
30372
30712
  }
30373
30713
  }
30714
+
30374
30715
  }
30375
30716
 
30376
- // TODO -- categorize extra "user" supplied and other tracks in some distinctive way before including them
30377
- // load includes. Nested includes are not supported
30378
- // for (let s of stanzas.slice()) {
30379
- // if ("include" === s.type) {
30380
- // const includeStanzas = await loadStanzas(baseURL + s.getProperty("include"))
30381
- // for (s of includeStanzas) {
30382
- // s.setProperty("visibility", "hide")
30383
- // stanzas.push(s)
30384
- // }
30385
- // }
30386
- // }
30717
+ // Filter empty groups and sort
30718
+ this.groupTrackConfigs.forEach(c => c.trim());
30719
+ this.groupTrackConfigs = this.groupTrackConfigs.filter(t => !t.isEmpty());
30387
30720
 
30388
- return new Hub(url, stanzas, groups)
30721
+ this.groupTrackConfigs.sort((a, b) => a.priority - b.priority);
30722
+ return this.groupTrackConfigs
30389
30723
  }
30390
30724
 
30391
- 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
+ }
30392
30734
 
30393
- this.url = url;
30394
30735
 
30395
- const idx = url.lastIndexOf("/");
30396
- 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) {
30756
+
30757
+ const format = t.format;
30397
30758
 
30398
- // The first stanza must be type = hub
30399
- if ("hub" === stanzas[0].type) {
30400
- this.hubStanza = stanzas[0];
30401
- } else {
30402
- throw Error("Unexpected hub.txt file -- does the first line start with 'hub'?")
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";
30403
30769
  }
30404
- if ("on" !== this.hubStanza.getProperty("useOneFile")) {
30405
- throw Error("Only 'useOneFile' hubs are currently supported")
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");
30406
30777
  }
30407
- if (stanzas.length < 2) {
30408
- throw Error("Expected at least 2 stanzas, hub and genome")
30778
+
30779
+ if (t.hasProperty("autoScale")) {
30780
+ config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
30409
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
+ }
30410
30811
 
30411
- // The second stanza should be a genome
30412
- if ("genome" === stanzas[1].type) {
30413
- this.genomeStanza = stanzas[1];
30414
- } else {
30415
- throw Error(`Unexpected hub file -- expected "genome" stanza but found "${stanzas[1].type}"`)
30812
+ }
30813
+ if (t.hasProperty("itemRgb")) ;
30814
+ if ("hide" === t.getProperty("visibility")) {
30815
+ // TODO -- this not supported yet
30816
+ config.visible = false;
30817
+ }
30818
+ if (t.hasProperty("url")) {
30819
+ config.infoURL = t.getProperty("url");
30820
+ }
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");
30829
+ }
30830
+
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;
30416
30850
  }
30417
30851
 
30418
- // Remaining stanzas should be tracks
30419
- this.trackStanzas = [];
30420
- for (let i = 2; i < stanzas.length; i++) {
30421
- if ("track" === stanzas[i].type) {
30422
- 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;
30423
30857
  }
30858
+ config.visibilityWindow = maxWindowToDraw;
30424
30859
  }
30425
30860
 
30426
- if (groupStanzas) {
30427
- this.groupStanzas = groupStanzas;
30428
- this.groupPriorityMap = new Map();
30429
- for (let g of groupStanzas) {
30430
- if (g.hasProperty("priority")) {
30431
- 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;
30432
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);
30433
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.
30944
+ }
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
+ ]);
30980
+
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));
30434
31000
  }
30435
31001
  }
30436
31002
 
30437
- getDefaultPosition() {
30438
- return this.genomeStanza.getProperty("defaultPos")
31003
+
31004
+ getName() {
31005
+ return this.hubStanza.getProperty("hub")
30439
31006
  }
30440
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
+
30441
31021
  /* Example genome stanza
30442
31022
  genome GCF_000186305.1
30443
31023
  taxId 176946
@@ -30456,133 +31036,126 @@ transBlat dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30456
31036
  isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30457
31037
  */
30458
31038
 
30459
- getGenomeConfig(options = {}) {
30460
- // 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) {
30461
31049
 
30462
- const id = this.genomeStanza.getProperty("genome");
31050
+ const id = genomeStanza.getProperty("genome");
30463
31051
  const gsName =
30464
31052
  this.hubStanza.getProperty("shortLabel") ||
30465
- this.genomeStanza.getProperty("scientificName") ||
30466
- this.genomeStanza.getProperty("organism") ||
30467
- this.genomeStanza.getProperty("description");
31053
+ genomeStanza.getProperty("scientificName") ||
31054
+ genomeStanza.getProperty("organism") ||
31055
+ genomeStanza.getProperty("description");
30468
31056
  const name = gsName + (gsName ? ` (${id})` : ` ${id}`);
30469
31057
 
30470
31058
  const config = {
30471
- hubURL: this.url,
31059
+
30472
31060
  id: id,
30473
31061
  name: name,
30474
- twoBitURL: this.baseURL + this.genomeStanza.getProperty("twoBitPath"),
31062
+ twoBitURL: genomeStanza.getProperty("twoBitPath"),
30475
31063
  nameSet: "ucsc",
31064
+ hubs: [this.url]
30476
31065
  };
30477
31066
 
30478
- if (this.genomeStanza.hasProperty("chromSizesURL")) {
30479
- config.chromSizesURL = this.genomeStanza.getProperty("chromSizesURL");
31067
+ if (genomeStanza.hasProperty("chromSizes")) {
31068
+ config.chromSizesURL = genomeStanza.getProperty("chromSizes");
30480
31069
  } else {
30481
31070
  config.wholeGenomeView = false;
30482
31071
  config.showChromosomeWidget = false;
30483
31072
  }
30484
31073
 
30485
- if (this.genomeStanza.hasProperty("defaultPos")) {
30486
- const hubLocus = this.genomeStanza.getProperty("defaultPos");
31074
+ if (genomeStanza.hasProperty("defaultPos")) {
31075
+ const hubLocus = genomeStanza.getProperty("defaultPos");
30487
31076
  // Strip out coordinates => whole chromosome view
30488
- if (hubLocus) {
30489
- const idx = hubLocus.lastIndexOf(":");
30490
- config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus;
30491
- }
31077
+ // if (hubLocus) {
31078
+ // const idx = hubLocus.lastIndexOf(":")
31079
+ // config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus
31080
+ // }
31081
+ config.locus = hubLocus;
30492
31082
  }
30493
31083
 
30494
- if (this.genomeStanza.hasProperty("blat")) {
30495
- config.blat = this.baseURL + this.genomeStanza.getProperty("blat");
30496
- }
30497
- if (this.genomeStanza.hasProperty("chromAliasBb")) {
30498
- config.chromAliasBbURL = this.baseURL + this.genomeStanza.getProperty("chromAliasBb");
31084
+ if (genomeStanza.hasProperty("blat")) {
31085
+ config.blat = genomeStanza.getProperty("blat");
30499
31086
  }
30500
- if (this.genomeStanza.hasProperty("chromAlias")) {
30501
- config.aliasURL = this.baseURL + this.genomeStanza.getProperty("chromAlias");
31087
+ if (genomeStanza.hasProperty("chromAliasBb")) {
31088
+ config.chromAliasBbURL = genomeStanza.getProperty("chromAliasBb");
30502
31089
  }
30503
- if (this.genomeStanza.hasProperty("twoBitBptURL")) {
30504
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptURL");
31090
+ if (genomeStanza.hasProperty("chromAlias")) {
31091
+ config.aliasURL = genomeStanza.getProperty("chromAlias");
30505
31092
  }
30506
-
30507
- if (this.genomeStanza.hasProperty("twoBitBptUrl")) {
30508
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl");
31093
+ if (genomeStanza.hasProperty("twoBitBptURL")) {
31094
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptURL");
30509
31095
  }
30510
31096
 
30511
- // chromSizes can take a very long time to load, and is not useful with the default WGV = off
30512
- if (options.includeChromSizes && this.genomeStanza.hasProperty("chromSizes")) {
30513
- config.chromSizesURL = this.baseURL + this.genomeStanza.getProperty("chromSizes");
31097
+ if (genomeStanza.hasProperty("twoBitBptUrl")) {
31098
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptUrl");
30514
31099
  }
30515
31100
 
30516
31101
  if (this.hubStanza.hasProperty("longLabel")) {
30517
31102
  config.description = this.hubStanza.getProperty("longLabel").replace("/", "\n");
30518
31103
  } else {
30519
31104
  config.description = config.id;
30520
- if (this.genomeStanza.hasProperty("description")) {
30521
- config.description += `\n${this.genomeStanza.getProperty("description")}`;
31105
+ if (genomeStanza.hasProperty("description")) {
31106
+ config.description += `\n${genomeStanza.getProperty("description")}`;
30522
31107
  }
30523
- if (this.genomeStanza.hasProperty("organism")) {
30524
- config.description += `\n${this.genomeStanza.getProperty("organism")}`;
31108
+ if (genomeStanza.hasProperty("organism")) {
31109
+ config.description += `\n${genomeStanza.getProperty("organism")}`;
30525
31110
  }
30526
- if (this.genomeStanza.hasProperty("scientificName")) {
30527
- config.description += `\n${this.genomeStanza.getProperty("scientificName")}`;
31111
+ if (genomeStanza.hasProperty("scientificName")) {
31112
+ config.description += `\n${genomeStanza.getProperty("scientificName")}`;
30528
31113
  }
30529
31114
 
30530
- if (this.genomeStanza.hasProperty("htmlPath")) {
30531
- config.infoURL = this.baseURL + this.genomeStanza.getProperty("htmlPath");
31115
+ if (genomeStanza.hasProperty("htmlPath")) {
31116
+ config.infoURL = genomeStanza.getProperty("htmlPath");
30532
31117
  }
30533
31118
  }
30534
31119
 
30535
- // Search for cytoband
30536
- /*
30537
- track cytoBandIdeo
30538
- shortLabel Chromosome Band (Ideogram)
30539
- longLabel Ideogram for Orientation
30540
- group map
30541
- visibility dense
30542
- type bigBed 4 +
30543
- bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb
30544
- */
30545
- const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.hasProperty("bigDataUrl"));
30546
- if (cytoStanza.length > 0) {
30547
- config.cytobandBbURL = this.baseURL + cytoStanza[0].getProperty("bigDataUrl");
30548
- }
30549
-
30550
31120
  // Tracks.
30551
31121
  const filter = (t) => !Hub.filterTracks.has(t.name) && "hide" !== t.getProperty("visibility");
30552
31122
  config.tracks = this.#getTracksConfigs(filter);
30553
31123
 
30554
-
30555
31124
  return config
30556
31125
  }
30557
31126
 
30558
- getGroupedTrackConfigurations() {
30559
-
30560
- // Organize track configs by group
30561
- const trackConfigMap = new Map();
30562
- for (let c of this.#getTracksConfigs()) {
30563
- if (c.name === "cytoBandIdeo") continue
30564
- const groupName = c.group || "other";
30565
- if (trackConfigMap.has(groupName)) {
30566
- trackConfigMap.get(groupName).push(c);
30567
- } else {
30568
- trackConfigMap.set(groupName, [c]);
30569
- }
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));
30570
31131
  }
31132
+ if (!trackHub) {
31133
+ console.log(`Warning: no trackDB found for genomeId ${genomeId}.`);
31134
+ }
31135
+ return trackHub ? trackHub.getGroupedTrackConfigurations() : []
31136
+ }
30571
31137
 
30572
- // Build group structure
30573
- const groupStanazMap = this.groupStanzas ?
30574
- new Map(this.groupStanzas.map(groupStanza => [groupStanza.getProperty("name"), groupStanza])) :
30575
- new Map();
30576
-
30577
- return Array.from(trackConfigMap.keys()).map(groupName => {
30578
- return {
30579
- label: groupStanazMap.has(groupName) ? groupStanazMap.get(groupName).getProperty("label") : groupName,
30580
- 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
+ }
30581
31153
  }
30582
- })
30583
-
31154
+ }
31155
+ return trackHub
30584
31156
  }
30585
31157
 
31158
+
30586
31159
  /**
30587
31160
  * Return an array of igv track config objects that satisfy the filter
30588
31161
  */
@@ -30620,7 +31193,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30620
31193
  "id": t.getProperty("track"),
30621
31194
  "name": t.getProperty("shortLabel"),
30622
31195
  "format": format,
30623
- "url": this.baseURL + t.getProperty("bigDataUrl"),
31196
+ "url": t.getProperty("bigDataUrl"),
30624
31197
  "displayMode": t.displayMode,
30625
31198
  };
30626
31199
 
@@ -30631,7 +31204,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30631
31204
  if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30632
31205
  if (config.description) config.description += "<br/>";
30633
31206
  config.description =
30634
- `<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>`;
30635
31208
  } else if (t.hasProperty("longLabel")) {
30636
31209
  config.description = t.getProperty("longLabel");
30637
31210
  }
@@ -30663,7 +31236,7 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30663
31236
  max = Number.parseInt(tokens[1]);
30664
31237
  }
30665
31238
  if (Number.isNaN(max) || Number.isNaN(min)) {
30666
- console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
31239
+ console.warn(`Unexpected viewLimits value in track line: ${t.getProperty("viewLimits")}`);
30667
31240
  } else {
30668
31241
  config.min = min;
30669
31242
  config.max = max;
@@ -30682,15 +31255,15 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30682
31255
  config.searchIndex = t.getProperty("searchIndex");
30683
31256
  }
30684
31257
  if (t.hasProperty("searchTrix")) {
30685
- config.trixURL = this.baseURL + t.getProperty("searchTrix");
31258
+ config.trixURL = t.getProperty("searchTrix");
30686
31259
  }
30687
31260
 
30688
31261
  if (t.hasProperty("group")) {
30689
- config.group = t.getProperty("group");
30690
- if (this.groupPriorityMap && this.groupPriorityMap.has(config.group)) {
30691
- 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;
30692
31265
  config.order = nextPriority;
30693
- this.groupPriorityMap.set(config.group, nextPriority);
31266
+ this.groupPriorityMap.set(config._group, nextPriority);
30694
31267
  }
30695
31268
  }
30696
31269
 
@@ -30699,96 +31272,92 @@ isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30699
31272
 
30700
31273
  }
30701
31274
 
30702
- function firstWord(str) {
30703
- const idx = str.indexOf(' ');
30704
- return idx > 0 ? str.substring(0, idx) : str
30705
- }
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
+ */
30706
31281
 
30707
- 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"]);
30708
31286
 
30709
- properties = new Map()
30710
31287
 
30711
- constructor(type, name) {
30712
- this.type = type;
30713
- this.name = name;
30714
- }
31288
+ const hubCache = new Map();
30715
31289
 
30716
- setProperty(key, value) {
30717
- this.properties.set(key, value);
31290
+ async function loadHub(url) {
31291
+ if (hubCache.has(url)) {
31292
+ return hubCache.get(url)
30718
31293
  }
30719
31294
 
30720
- getProperty(key) {
30721
- if (this.properties.has(key)) {
30722
- return this.properties.get(key)
30723
- } else if (this.parent) {
30724
- return this.parent.getProperty(key)
30725
- } else {
30726
- return undefined
30727
- }
31295
+ const stanzas = await loadStanzas(url);
31296
+ if (stanzas.length < 1) {
31297
+ throw new Error("Empty hub file")
30728
31298
  }
30729
31299
 
30730
- hasProperty(key) {
30731
- if (this.properties.has(key)) {
30732
- return true
30733
- } else if (this.parent) {
30734
- return this.parent.hasProperty(key)
30735
- } else {
30736
- return false
30737
- }
31300
+ const hubStanza = stanzas[0];
31301
+ if (hubStanza.type !== "hub") {
31302
+ throw new Error("First stanza must be a hub stanza")
30738
31303
  }
30739
31304
 
30740
- get format() {
30741
- const type = this.getProperty("type");
30742
- if (type) {
30743
- // Trim extra bed qualifiers (e.g. bigBed + 4)
30744
- 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)
30745
31311
  }
30746
- return undefined // unknown type
30747
- }
31312
+ const genomeStanza = stanzas[1];
31313
+ genomeStanzas = [genomeStanza];
31314
+ trackStanzas = stanzas.slice(2);
30748
31315
 
30749
- /**
30750
- * IGV display mode
30751
- */
30752
- get displayMode() {
30753
- let viz = this.getProperty("visibility");
30754
- if (!viz) {
30755
- return "COLLAPSED"
30756
- } else {
30757
- viz = viz.toLowerCase();
30758
- switch (viz) {
30759
- case "dense":
30760
- return "COLLAPSED"
30761
- case "pack":
30762
- return "EXPANDED"
30763
- case "squish":
30764
- return "SQUISHED"
30765
- default:
30766
- 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);
30767
31327
  }
30768
- }
30769
- }
30770
- }
30771
31328
 
31329
+ }
30772
31330
 
30773
- /**
30774
- * Return the content length of the resource. If the content length cannot be determined return null;
30775
- * @param url
30776
- * @returns {Promise<number|string>}
30777
- */
30778
- async function getContentLength(url) {
30779
- try {
30780
- const response = await fetch(url, {method: 'HEAD'});
30781
- const headers = response.headers;
30782
- if (headers.has("content-length")) {
30783
- return headers.get("content-length")
30784
- } else {
30785
- return null
31331
+ } else {
31332
+ if (!hubStanza.hasProperty("genomesFile")) {
31333
+ throw new Error("hub.txt must specify 'genomesFile'")
30786
31334
  }
30787
- } catch (e) {
30788
- return null
31335
+ genomeStanzas = await loadStanzas(hubStanza.getProperty("genomesFile"));
30789
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
30790
31358
  }
30791
31359
 
31360
+
30792
31361
  /**
30793
31362
  * Parse a UCSC file
30794
31363
  * @param url
@@ -30796,38 +31365,92 @@ async function getContentLength(url) {
30796
31365
  */
30797
31366
  async function loadStanzas(url) {
30798
31367
 
30799
- const response = await fetch(url);
30800
- 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()
30801
31374
  const lines = data.split(/\n|\r\n|\r/g);
30802
31375
 
30803
31376
  const nodes = [];
30804
31377
  let currentNode;
30805
31378
  let startNewNode = true;
30806
- for (let line of lines) {
30807
- const indent = indentLevel(line);
30808
- const i = line.indexOf(' ', indent);
30809
- 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) {
30810
31384
  // Break - start a new node
30811
31385
  startNewNode = true;
30812
31386
  } else {
30813
- const key = line.substring(indent, i).trim();
30814
- if (key.startsWith("#")) continue
30815
- 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
+
30816
31437
  if (startNewNode) {
30817
- // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks,
30818
- // so track stanzas are flattened
30819
- const newNode = new Stanza(key, value);
30820
- nodes.push(newNode);
30821
- currentNode = newNode;
31438
+ currentNode = new Stanza(key, value);
31439
+ nodes.push(currentNode);
30822
31440
  startNewNode = false;
30823
31441
  }
31442
+
30824
31443
  currentNode.setProperty(key, value);
30825
31444
  }
30826
31445
  }
30827
-
30828
31446
  return resolveParents(nodes)
30829
31447
  }
30830
31448
 
31449
+ function firstWord(str) {
31450
+ const idx = str.indexOf(' ');
31451
+ return idx > 0 ? str.substring(0, idx) : str
31452
+ }
31453
+
30831
31454
  function resolveParents(nodes) {
30832
31455
  const nodeMap = new Map();
30833
31456
  for (let n of nodes) {
@@ -30842,17 +31465,34 @@ function resolveParents(nodes) {
30842
31465
  return nodes
30843
31466
  }
30844
31467
 
30845
- function indentLevel(str) {
30846
- let level = 0;
30847
- for (level = 0; level < str.length; level++) {
30848
- const c = str.charAt(level);
30849
- 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
30850
31475
  }
30851
- 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
30852
31492
  }
30853
31493
 
30854
31494
  const DEFAULT_GENOMES_URL = "https://igv.org/genomes/genomes.json";
30855
- const BACKUP_GENOMES_URL = "https://raw.githubusercontent.com/igvteam/igv.js/main/packages/igv/src/genomes/genomes.json";
31495
+ const BACKUP_GENOMES_URL = "https://raw.githubusercontent.com/igvteam/igv-genomes/refs/heads/main/dist/genomes.json";
30856
31496
 
30857
31497
  const GenomeUtils = {
30858
31498
 
@@ -30860,7 +31500,7 @@ const GenomeUtils = {
30860
31500
 
30861
31501
  if (!GenomeUtils.KNOWN_GENOMES) {
30862
31502
 
30863
- GenomeUtils.KNOWN_GENOMES = {};
31503
+ let table = {};
30864
31504
 
30865
31505
  const processJson = (jsonArray, table) => {
30866
31506
  jsonArray.forEach(function (json) {
@@ -30873,12 +31513,12 @@ const GenomeUtils = {
30873
31513
  if (config.loadDefaultGenomes !== false) {
30874
31514
  try {
30875
31515
  const jsonArray = await igvxhr.loadJson(DEFAULT_GENOMES_URL, {timeout: 2000});
30876
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31516
+ processJson(jsonArray, table);
30877
31517
  } catch (error) {
30878
31518
  try {
30879
31519
  console.error("Error initializing default genomes:", error);
30880
31520
  const jsonArray = await igvxhr.loadJson(BACKUP_GENOMES_URL, {timeout: 2000});
30881
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31521
+ processJson(jsonArray, table);
30882
31522
  } catch (e) {
30883
31523
  console.error("Error initializing backup genomes:", error);
30884
31524
  }
@@ -30890,11 +31530,12 @@ const GenomeUtils = {
30890
31530
  if (genomeList) {
30891
31531
  if (typeof genomeList === 'string') {
30892
31532
  const jsonArray = await igvxhr.loadJson(genomeList, {});
30893
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31533
+ processJson(jsonArray, table);
30894
31534
  } else {
30895
- processJson(genomeList, GenomeUtils.KNOWN_GENOMES);
31535
+ processJson(genomeList, table);
30896
31536
  }
30897
31537
  }
31538
+ GenomeUtils.KNOWN_GENOMES = table;
30898
31539
  }
30899
31540
  },
30900
31541
 
@@ -30931,8 +31572,8 @@ const GenomeUtils = {
30931
31572
  if ((genomeID.startsWith("GCA_") || genomeID.startsWith("GCF_")) && genomeID.length >= 13) {
30932
31573
  try {
30933
31574
  const hubURL = convertToHubURL(genomeID);
30934
- const hub = await Hub.loadHub(hubURL);
30935
- reference = hub.getGenomeConfig();
31575
+ const hub = await loadHub(hubURL);
31576
+ reference = hub.getGenomeConfig(genomeID);
30936
31577
  } catch (e) {
30937
31578
  console.error(e);
30938
31579
  }
@@ -32894,27 +33535,18 @@ const distinctColorsPalette = _18_DistinctColorsViaChatGPT4.map(str => {
32894
33535
  return [ r, g, b ]
32895
33536
  });
32896
33537
 
32897
- const colorForNA = appleCrayonRGB('magnesium');
32898
- const sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors'];
32899
-
32900
33538
  class SampleInfo {
32901
33539
 
32902
33540
  static emptySpaceReplacement = '|'
32903
-
32904
- sampleDictionary = {}
32905
- attributeNames = []
32906
- sampleMappingDictionary = {}
32907
- colorDictionary = {}
32908
- attributeRangeLUT = {}
33541
+ static colorForNA = appleCrayonRGB('magnesium')
33542
+ static sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors']
32909
33543
 
32910
33544
  constructor(browser) {
32911
-
32912
33545
  const found = browser.tracks.some(t => typeof t.getSamples === 'function');
32913
33546
  if (found.length > 0) {
32914
33547
  browser.sampleInfoControl.setButtonVisibility(true);
32915
33548
  }
32916
33549
  this.initialize();
32917
-
32918
33550
  }
32919
33551
 
32920
33552
  initialize() {
@@ -32941,47 +33573,60 @@ class SampleInfo {
32941
33573
 
32942
33574
  getAttributes(sampleName) {
32943
33575
 
32944
- const key = 0 === Object.keys(this.sampleMappingDictionary) ? sampleName : (this.sampleMappingDictionary[sampleName] || sampleName);
33576
+ const key = this.sampleMappingDictionary[sampleName] || sampleName;
32945
33577
  return this.sampleDictionary[key]
32946
33578
  }
32947
33579
 
32948
- async loadSampleInfoFile(path) {
32949
- try {
32950
- const string = await igvxhr.loadString(path);
32951
- this.#processSampleInfoFileAsString(string);
32952
- this.sampleInfoFiles.push(path);
32953
- } catch (e) {
32954
- 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
+
32955
33596
  }
32956
- }
32957
33597
 
32958
- #processSampleInfoFileAsString(string) {
33598
+ this.initialized = true;
33599
+ }
32959
33600
 
32960
- const sectionDictionary = createSectionDictionary(string);
33601
+ loadSampleInfoHelper(attributes, samples){
32961
33602
 
32962
- for (const [header, value] of Object.entries(sectionDictionary)) {
32963
- switch (header) {
32964
- case '#sampleTable':
32965
- this.#accumulateSampleTableDictionary(value);
32966
- break
32967
- case '#sampleMapping':
32968
- this.#accumulateSampleMappingDictionary(value);
32969
- break
32970
- case '#colors':
32971
- this.#accumulateColorScheme(value);
32972
- break
33603
+ // Establish the range of values for each attribute
33604
+ const lut = createAttributeRangeLUT(attributes, samples);
33605
+ accumulateDictionary(this.attributeRangeLUT, lut);
32973
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);
32974
33612
  }
32975
33613
  }
32976
33614
 
32977
- this.initialized = true;
33615
+ accumulateDictionary(this.sampleDictionary, samples);
33616
+
33617
+ }
32978
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
+ }
32979
33627
  }
32980
33628
 
32981
33629
  getAttributeColor(attribute, value) {
32982
- // if (value === 'NA') {
32983
- // console.log(`${ attribute } : ${ value }`)
32984
- // }
32985
33630
 
32986
33631
  let color;
32987
33632
 
@@ -32999,7 +33644,7 @@ class SampleInfo {
32999
33644
 
33000
33645
  } else if (typeof value === "string") {
33001
33646
 
33002
- color = 'NA' === value ? colorForNA : stringToRGBString(value);
33647
+ color = 'NA' === value ? SampleInfo.colorForNA : SampleInfo.stringToRGBString(value);
33003
33648
 
33004
33649
  } else {
33005
33650
 
@@ -33067,6 +33712,27 @@ class SampleInfo {
33067
33712
  return json
33068
33713
  }
33069
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
+
33070
33736
  #accumulateSampleTableDictionary(lines) {
33071
33737
 
33072
33738
  // shift array with first item that is 'sample' or 'Linking_id'. Remaining items are attribute names
@@ -33106,22 +33772,11 @@ class SampleInfo {
33106
33772
  } // for (lines)
33107
33773
 
33108
33774
  for (const [key, record] of Object.entries(samples)) {
33109
- samples[key] = toNumericalRepresentation(record);
33775
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33110
33776
  }
33111
33777
 
33112
- // Establish the range of values for each attribute
33113
- const lut = createAttributeRangeLUT(attributes, samples);
33114
- accumulateDictionary(this.attributeRangeLUT, lut);
33115
-
33116
- // Ensure unique attribute names list
33117
- const currentAttributeNameSet = new Set(this.attributeNames);
33118
- for (const name of attributes) {
33119
- if (!currentAttributeNameSet.has(name)) {
33120
- this.attributeNames.push(name);
33121
- }
33122
- }
33778
+ this.loadSampleInfoHelper(attributes, samples);
33123
33779
 
33124
- accumulateDictionary(this.sampleDictionary, samples);
33125
33780
  }
33126
33781
 
33127
33782
  #accumulateSampleMappingDictionary(lines) {
@@ -33222,7 +33877,7 @@ class SampleInfo {
33222
33877
  this.colorDictionary[attribute] = attributeValue => {
33223
33878
 
33224
33879
  if ('NA' === attributeValue) {
33225
- return colorForNA
33880
+ return SampleInfo.colorForNA
33226
33881
  } else {
33227
33882
  const [min, max] = this.attributeRangeLUT[attribute];
33228
33883
  const interpolant = (attributeValue - min) / (max - min);
@@ -33242,8 +33897,34 @@ class SampleInfo {
33242
33897
 
33243
33898
  }
33244
33899
 
33245
- }
33900
+ static toNumericalRepresentation(obj) {
33901
+ const result = Object.assign({}, obj);
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
+ }
33246
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
+ }
33247
33928
 
33248
33929
  function createSectionDictionary(string) {
33249
33930
 
@@ -33254,14 +33935,14 @@ function createSectionDictionary(string) {
33254
33935
  let currentHeader;
33255
33936
 
33256
33937
  // If the first line does not start with a section header an initial #sampleTable is implied
33257
- if (!sampleInfoFileHeaders.includes(lines[0])) {
33938
+ if (!SampleInfo.sampleInfoFileHeaders.includes(lines[0])) {
33258
33939
  currentHeader = '#sampleTable';
33259
33940
  dictionary[currentHeader] = [];
33260
33941
  }
33261
33942
 
33262
33943
  for (const line of lines) {
33263
33944
 
33264
- if (sampleInfoFileHeaders.includes(line)) {
33945
+ if (SampleInfo.sampleInfoFileHeaders.includes(line)) {
33265
33946
  currentHeader = line;
33266
33947
  dictionary[currentHeader] = [];
33267
33948
  } else if (currentHeader && false === line.startsWith('#')) {
@@ -33280,7 +33961,6 @@ function accumulateDictionary(accumulator, dictionary) {
33280
33961
  }
33281
33962
  }
33282
33963
 
33283
-
33284
33964
  function createAttributeRangeLUT(names, dictionary) {
33285
33965
 
33286
33966
  const lut = {};
@@ -33326,35 +34006,6 @@ function createAttributeRangeLUT(names, dictionary) {
33326
34006
  return lut
33327
34007
  }
33328
34008
 
33329
- function toNumericalRepresentation(obj) {
33330
-
33331
- const result = Object.assign({}, obj);
33332
-
33333
- for (const [key, value] of Object.entries(result)) {
33334
- if (typeof value === 'string' && !isNaN(value)) {
33335
- result[key] = Number(value);
33336
- }
33337
- }
33338
-
33339
- return result
33340
- }
33341
-
33342
- function stringToRGBString(str) {
33343
-
33344
- let hash = 0;
33345
- for (let i = 0; i < str.length; i++) {
33346
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
33347
- }
33348
-
33349
- let color = [];
33350
- for (let i = 0; i < 3; i++) {
33351
- const value = (hash >> (i * 8)) & 0xff;
33352
- color.push(value);
33353
- }
33354
-
33355
- return `rgb(${color.join(', ')})`
33356
- }
33357
-
33358
34009
  const sampleInfoTileXShim = 8;
33359
34010
  const sampleInfoTileWidth = 16;
33360
34011
 
@@ -33963,7 +34614,11 @@ class SampleNameViewport {
33963
34614
 
33964
34615
  checkCanvas() {
33965
34616
 
33966
- 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
+
33967
34622
  this.ctx.canvas.width = width * window.devicePixelRatio;
33968
34623
  this.ctx.canvas.style.width = `${width}px`;
33969
34624
 
@@ -35075,7 +35730,7 @@ class MergedTrack extends TrackBase {
35075
35730
  let element = document.createElement('div');
35076
35731
  element.textContent = 'Separate tracks';
35077
35732
 
35078
- function click(e) {
35733
+ async function click(e) {
35079
35734
 
35080
35735
  // Capture state which will be nulled when track is removed
35081
35736
  const groupAutoscale = this.autoscale;
@@ -35091,9 +35746,10 @@ class MergedTrack extends TrackBase {
35091
35746
  track.autoscaleGroup = name;
35092
35747
  }
35093
35748
  track.isMergedTrack = false;
35094
- browser.addTrack(track.config, track);
35749
+ browser.addTrack(track);
35095
35750
  }
35096
- browser.updateViews();
35751
+ await browser.updateViews();
35752
+ browser.reorderTracks();
35097
35753
  }
35098
35754
 
35099
35755
  return {element, click}
@@ -35188,7 +35844,7 @@ class OverlayTrackButton extends NavbarButton {
35188
35844
  }
35189
35845
  }
35190
35846
 
35191
- function trackOverlayClickHandler(e) {
35847
+ async function trackOverlayClickHandler(e) {
35192
35848
 
35193
35849
  if (true === isOverlayTrackCriteriaMet(this.browser)) {
35194
35850
 
@@ -35225,9 +35881,9 @@ function trackOverlayClickHandler(e) {
35225
35881
  track.trackView.dispose();
35226
35882
  }
35227
35883
 
35228
- this.browser.addTrack(config, mergedTrack);
35229
- mergedTrack.trackView.updateViews();
35230
-
35884
+ this.browser.addTrack(mergedTrack);
35885
+ await mergedTrack.trackView.updateViews();
35886
+ this.browser.reorderTracks();
35231
35887
  }
35232
35888
 
35233
35889
  }
@@ -36834,14 +37490,45 @@ class InputDialog {
36834
37490
 
36835
37491
 
36836
37492
  present(options, e) {
36837
-
36838
37493
  this.label.textContent = options.label;
36839
37494
  this._input.value = options.value;
36840
37495
  this.callback = options.callback || options.click;
36841
37496
 
36842
- const { top} = e.currentTarget.parentElement.getBoundingClientRect();
36843
- this.container.style.top = `${ top }px`;
36844
- 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`;
36845
37532
  }
36846
37533
  }
36847
37534
 
@@ -47621,7 +48308,7 @@ class AlignmentTrack extends TrackBase {
47621
48308
  if (!this.colorBy) {
47622
48309
  this.colorBy = this.hasPairs ? "unexpectedPair" : "none";
47623
48310
  }
47624
-
48311
+
47625
48312
  let pixelTop = options.pixelTop - BAMTrack.coverageTrackHeight;
47626
48313
  if (this.top) {
47627
48314
  ctx.translate(0, this.top);
@@ -48328,7 +49015,12 @@ class AlignmentTrack extends TrackBase {
48328
49015
  this.trackView.repaintViews();
48329
49016
  }
48330
49017
 
48331
- return {name: undefined, element: createCheckbox(menuItem.label, showCheck), click: clickHandler, init: undefined}
49018
+ return {
49019
+ name: undefined,
49020
+ element: createCheckbox(menuItem.label, showCheck),
49021
+ click: clickHandler,
49022
+ init: undefined
49023
+ }
48332
49024
  }
48333
49025
 
48334
49026
 
@@ -48375,7 +49067,12 @@ class AlignmentTrack extends TrackBase {
48375
49067
  }
48376
49068
  }
48377
49069
 
48378
- return {name: undefined, element: createCheckbox(menuItem.label, showCheck), dialog: clickHandler, init: undefined}
49070
+ return {
49071
+ name: undefined,
49072
+ element: createCheckbox(menuItem.label, showCheck),
49073
+ dialog: clickHandler,
49074
+ init: undefined
49075
+ }
48379
49076
 
48380
49077
  }
48381
49078
 
@@ -48497,38 +49194,88 @@ class AlignmentTrack extends TrackBase {
48497
49194
  });
48498
49195
  }
48499
49196
 
49197
+ list.push('<hr/>');
49198
+ const softClips = clickedAlignment.softClippedBlocks();
48500
49199
  list.push({
48501
49200
  label: 'View read sequence',
48502
49201
  click: () => {
48503
- const seqstring = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
48504
- if (!seqstring || "*" === seqstring) {
48505
- this.browser.alert.present("Read sequence: *");
48506
- } else {
48507
- this.browser.alert.present(seqstring);
48508
- }
49202
+ const seqstring = clickedAlignment.seq;
49203
+ this.browser.alert.present(seqstring && seqstring !== "*" ? seqstring : "Read sequence: *");
48509
49204
  }
48510
49205
  });
48511
49206
 
49207
+ if (softClips.left && softClips.left.len > 0) {
49208
+ list.push({
49209
+ label: 'View left soft-clipped sequence',
49210
+ click: () => {
49211
+ const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
49212
+ this.browser.alert.present(clippedSequence);
49213
+ }
49214
+ });
49215
+ }
49216
+
49217
+ if (softClips.right && softClips.right.len > 0) {
49218
+ list.push({
49219
+ label: 'View right soft-clipped sequence',
49220
+ click: () => {
49221
+ const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
49222
+ this.browser.alert.present(clippedSequence);
49223
+ }
49224
+ });
49225
+ }
49226
+
49227
+ list.push('<hr/>');
49228
+
48512
49229
  if (isSecureContext()) {
48513
49230
  list.push({
48514
49231
  label: 'Copy read sequence',
48515
49232
  click: async () => {
48516
- const seq = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
48517
49233
  try {
48518
- await navigator.clipboard.writeText(seq);
49234
+ await navigator.clipboard.writeText(clickedAlignment.seq);
48519
49235
  } catch (e) {
48520
49236
  console.error(e);
48521
49237
  this.browser.alert.present(`error copying sequence to clipboard ${e}`);
48522
49238
  }
48523
-
48524
49239
  }
48525
49240
  });
49241
+
49242
+ if (softClips.left && softClips.left.len > 0) {
49243
+ list.push({
49244
+ label: 'Copy left soft-clipped sequence',
49245
+ click: async () => {
49246
+ try {
49247
+ const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
49248
+ await navigator.clipboard.writeText(clippedSequence);
49249
+ } catch (e) {
49250
+ console.error(e);
49251
+ this.browser.alert.present(`error copying sequence to clipboard ${e}`);
49252
+ }
49253
+ }
49254
+ });
49255
+ }
49256
+
49257
+ if (softClips.right && softClips.right.len > 0) {
49258
+ list.push({
49259
+ label: 'Copy right soft-clipped sequence',
49260
+ click: async () => {
49261
+ try {
49262
+ const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
49263
+ await navigator.clipboard.writeText(clippedSequence);
49264
+ } catch (e) {
49265
+ console.error(e);
49266
+ this.browser.alert.present(`error copying sequence to clipboard ${e}`);
49267
+ }
49268
+ }
49269
+ });
49270
+ }
48526
49271
  }
48527
49272
 
48528
- // TODO if genome supports blat
49273
+ // TODO test if genome supports blat
48529
49274
  const seqstring = clickedAlignment.seq;
48530
49275
  if (seqstring && "*" !== seqstring) {
48531
49276
 
49277
+ list.push('<hr/>');
49278
+
48532
49279
  if (seqstring.length < maxSequenceSize$1) {
48533
49280
  list.push({
48534
49281
  label: 'BLAT read sequence',
@@ -48546,7 +49293,7 @@ class AlignmentTrack extends TrackBase {
48546
49293
  list.push({
48547
49294
  label: 'BLAT left soft-clipped sequence',
48548
49295
  click: () => {
48549
- const clippedSequence = seqstring.substr(softClips.left.seqOffset, softClips.left.len);
49296
+ const clippedSequence = seqstring.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
48550
49297
  const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
48551
49298
  const name = `${clickedAlignment.readName} - blat left clip`;
48552
49299
  const title = `${this.name} - ${name}`;
@@ -48558,7 +49305,7 @@ class AlignmentTrack extends TrackBase {
48558
49305
  list.push({
48559
49306
  label: 'BLAT right soft-clipped sequence',
48560
49307
  click: () => {
48561
- const clippedSequence = seqstring.substr(softClips.right.seqOffset, softClips.right.len);
49308
+ const clippedSequence = seqstring.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
48562
49309
  const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
48563
49310
  const name = `${clickedAlignment.readName} - blat right clip`;
48564
49311
  const title = `${this.name} - ${name}`;
@@ -48607,7 +49354,7 @@ class AlignmentTrack extends TrackBase {
48607
49354
  const offsetY = y - this.top;
48608
49355
  const genomicLocation = clickState.genomicLocation;
48609
49356
 
48610
- if(features.packedGroups) {
49357
+ if (features.packedGroups) {
48611
49358
  let minGroupY = Number.MAX_VALUE;
48612
49359
  for (let group of features.packedGroups.values()) {
48613
49360
  minGroupY = Math.min(minGroupY, group.pixelTop);
@@ -50429,7 +51176,7 @@ class RemoteFile {
50429
51176
 
50430
51177
  let url = this.url.slice(); // slice => copy
50431
51178
  if (this.config.oauthToken) {
50432
- const token = resolveToken(this.config.oauthToken);
51179
+ const token = await resolveToken(this.config.oauthToken);
50433
51180
  headers['Authorization'] = `Bearer ${token}`;
50434
51181
  }
50435
51182
 
@@ -55194,7 +55941,7 @@ function create_nested_array(value, shape) {
55194
55941
  async function openH5File(options) {
55195
55942
 
55196
55943
  // Some clients (notably igv-webapp) pass a File reference in the url field. Fix this
55197
- if(options.url && isBlobLike(options.url)) {
55944
+ if (options.url && isBlobLike(options.url)) {
55198
55945
  options.file = options.url;
55199
55946
  options.url = undefined;
55200
55947
  }
@@ -55224,26 +55971,30 @@ async function openH5File(options) {
55224
55971
 
55225
55972
  async function readExternalIndex(options) {
55226
55973
 
55227
- let indexReader;
55228
- if(options.indexReader) {
55229
- indexReader = options.indexReader;
55230
- }
55231
- else if(options.index) {
55974
+ if (options.index) {
55232
55975
  return options.index
55233
- } else if (options.indexURL) {
55234
- indexReader = new RemoteFile({url: options.indexURL});
55235
- } else if (options.indexPath) {
55236
- indexReader = new NodeLocalFile({path: options.indexPath});
55237
- } else if (options.indexFile) {
55238
- indexReader = new BlobFile({file: options.indexFile});
55239
- }
55240
- 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
+ }
55241
55993
  const indexFileContents = await indexReader.read();
55242
55994
  const indexFileJson = new TextDecoder().decode(indexFileContents);
55243
55995
  return JSON.parse(indexFileJson)
55244
- } else {
55245
- return undefined
55246
55996
  }
55997
+
55247
55998
  }
55248
55999
 
55249
56000
 
@@ -55321,9 +56072,9 @@ class HDF5Reader {
55321
56072
  * @param {string} h5_file - path for the pytor file
55322
56073
  * @param {integer} bin_size - bin size
55323
56074
  */
55324
- constructor(h5_file, bin_size=100000){
56075
+ constructor(config, bin_size=100000){
55325
56076
 
55326
- this.h5_file = h5_file;
56077
+ this.config = config;
55327
56078
  this.bin_size = bin_size;
55328
56079
  this.h5_obj = undefined;
55329
56080
  this.pytorKeys = [];
@@ -55333,11 +56084,8 @@ class HDF5Reader {
55333
56084
  async fetch(){
55334
56085
 
55335
56086
  if(!this.h5_obj) {
55336
- this.h5_obj = await openH5File({
55337
- url: this.h5_file,
55338
- fetchSize: 1000000,
55339
- maxSize: 200000000
55340
- });
56087
+ const options = Object.assign(this.config, {fetchSize: 1000000, maxSize: 200000000});
56088
+ this.h5_obj = await openH5File(options);
55341
56089
  }
55342
56090
  return this.h5_obj
55343
56091
  }
@@ -64373,7 +65121,7 @@ class CNVPytorTrack extends TrackBase {
64373
65121
  this.set_available_callers();
64374
65122
 
64375
65123
  } else {
64376
- this.cnvpytor_obj = new HDF5Reader(this.config.url, this.bin_size);
65124
+ this.cnvpytor_obj = new HDF5Reader(this.config, this.bin_size);
64377
65125
  // get chrom list that currently user viewing
64378
65126
  let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
64379
65127
 
@@ -69361,6 +70109,23 @@ class ReferenceFrame {
69361
70109
  return this.genome.getChromosome(this.chr)
69362
70110
  }
69363
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
+
69364
70129
  getMultiLocusLabelBPLengthOnly(pixels) {
69365
70130
  const margin = '&nbsp';
69366
70131
  const ss = Math.floor(this.start) + 1;
@@ -69446,7 +70211,7 @@ function createReferenceFrameList(loci, genome, browserFlanking, minimumBases, v
69446
70211
  })
69447
70212
  }
69448
70213
 
69449
- const _version = "3.2.5";
70214
+ const _version = "3.3.0";
69450
70215
  function version() {
69451
70216
  return _version
69452
70217
  }
@@ -69490,10 +70255,24 @@ class ChromosomeSelectWidget {
69490
70255
  this.select.setAttribute('name', 'chromosome-select-widget');
69491
70256
  this.container.appendChild(this.select);
69492
70257
 
69493
- this.select.addEventListener('change', () => {
70258
+ this.select.addEventListener('change', async () => {
69494
70259
  this.select.blur();
69495
70260
  if (this.select.value !== '' && maximumSequenceCountExceeded !== this.select.value) {
69496
- 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
+ }
69497
70276
  }
69498
70277
  });
69499
70278
 
@@ -69518,7 +70297,9 @@ class ChromosomeSelectWidget {
69518
70297
  this.genome = genome;
69519
70298
 
69520
70299
  // Start with explicit chromosome name list
69521
- const list = genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) || [];
70300
+ const list = (genome.wgChromosomeNames) ?
70301
+ genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) :
70302
+ [];
69522
70303
 
69523
70304
  if (this.showAllChromosomes && genome.chromosomeNames.length > 1) {
69524
70305
  const exclude = new Set(list);
@@ -70744,7 +71525,7 @@ class ResponsiveNavbar {
70744
71525
  });
70745
71526
 
70746
71527
  this.searchInput.addEventListener('change', () => {
70747
- browser.doSearch(this.searchInput.value);
71528
+ this.doSearch(this.searchInput.value);
70748
71529
  });
70749
71530
 
70750
71531
  const searchIconContainer = document.createElement('div');
@@ -70755,7 +71536,7 @@ class ResponsiveNavbar {
70755
71536
  searchIconContainer.appendChild(searchIcon);
70756
71537
 
70757
71538
  searchIconContainer.addEventListener('click', () => {
70758
- browser.doSearch(this.searchInput.value);
71539
+ this.doSearch(this.searchInput.value);
70759
71540
  });
70760
71541
 
70761
71542
  this.windowSizePanel = new WindowSizePanel(locusSizeGroup, browser);
@@ -70903,6 +71684,22 @@ class ResponsiveNavbar {
70903
71684
  this.navigation.style.display = 'flex';
70904
71685
  }
70905
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
+
70906
71703
  }
70907
71704
 
70908
71705
  function logo() {
@@ -72154,6 +72951,9 @@ class ChromAliasDefaults {
72154
72951
  aliasRecord["_cap"] = cap;
72155
72952
  }
72156
72953
 
72954
+ const chrPrefixAlias = aliasRecord.chr.startsWith("chr") ? aliasRecord.chr.substring(3) : "chr" + aliasRecord.chr;
72955
+ aliasRecord["_chrprefix_"] = chrPrefixAlias;
72956
+
72157
72957
  }
72158
72958
 
72159
72959
  }
@@ -72178,7 +72978,7 @@ class ChromAliasBB {
72178
72978
  }
72179
72979
 
72180
72980
  async preload(chrNames) {
72181
- await this.reader.preload();
72981
+ await this.reader.preload();
72182
72982
  for(let nm of chrNames) {
72183
72983
  await this.search(nm);
72184
72984
  }
@@ -72230,12 +73030,6 @@ class ChromAliasBB {
72230
73030
  }
72231
73031
  return this.aliasRecordCache.get(alias)
72232
73032
  }
72233
-
72234
- async getChromosomeNames() {
72235
- await this.reader.loadHeader();
72236
- return Array.from(this.reader.chrNames)
72237
- }
72238
-
72239
73033
  }
72240
73034
 
72241
73035
  /**
@@ -72260,10 +73054,9 @@ class ChromAliasFile {
72260
73054
  this.genome = genome;
72261
73055
  }
72262
73056
 
72263
- async preload() {
72264
- return this.loadAliases();
73057
+ async preload(chrNames) {
73058
+ // A no-op, this is a text file, no need to preload
72265
73059
  }
72266
-
72267
73060
  /**
72268
73061
  * Return the canonical chromosome name for the alias. If none found return the alias
72269
73062
  *
@@ -72429,7 +73222,7 @@ class CytobandFile {
72429
73222
  for (let line of lines) {
72430
73223
 
72431
73224
  const tokens = line.split("\t");
72432
- 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];
72433
73226
  if (!lastChr) lastChr = chrName;
72434
73227
 
72435
73228
  if (chrName !== lastChr) {
@@ -72447,37 +73240,20 @@ class CytobandFile {
72447
73240
  bands.push(new Cytoband(start, end, name, stain));
72448
73241
  }
72449
73242
  }
72450
-
72451
- }
72452
-
72453
- /**
72454
- * Infer genome chromosome names from cytoband data. This should only be used as a last resort
72455
- */
72456
- async getChromosomeNames() {
72457
- if(this.cytobands.size === 0) {
72458
- await this.#loadCytobands();
72459
- }
72460
- return Array.from(this.cytobands.keys())
72461
- }
72462
-
72463
- /**
72464
- * Infer chromosome objects from cytoband data. This should only be used as last resort.
72465
- */
72466
- async getChromosomes() {
72467
- if(this.cytobands.size === 0) {
72468
- await this.#loadCytobands();
73243
+ if(bands.length > 0) {
73244
+ this.cytobands.set(lastChr, bands);
72469
73245
  }
72470
73246
 
72471
- const chromosomes = [];
72472
- let order = 0;
72473
- for(let [chrName, cytoList] of this.cytobands.entries()) {
72474
- chromosomes.push(new Chromosome(chrName, order++, cytoList[cytoList.length - 1].end));
72475
- }
72476
- return chromosomes
72477
73247
  }
72478
73248
 
72479
73249
  }
72480
73250
 
73251
+ const ucsdIDMap = new Map([
73252
+ ["1kg_ref", "hg18"],
73253
+ ["1kg_v37", "hg19"],
73254
+ ["b37", "hg19"]
73255
+ ]);
73256
+
72481
73257
  /**
72482
73258
  * The Genome class represents an assembly and consists of the following elements
72483
73259
  * sequence - Object representing the DNA sequence
@@ -72502,8 +73278,10 @@ class Genome {
72502
73278
  this.config = config;
72503
73279
  this.browser = browser;
72504
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;
72505
73283
  this.name = config.name;
72506
- this.nameSet = config.nameSet;
73284
+ this.nameSet = config.nameSet || 'ucsc';
72507
73285
  }
72508
73286
 
72509
73287
 
@@ -72511,49 +73289,45 @@ class Genome {
72511
73289
 
72512
73290
  const config = this.config;
72513
73291
 
73292
+ // Load sequence
72514
73293
  this.sequence = await loadSequence(config, this.browser);
72515
73294
 
72516
- if (config.chromSizesURL) {
72517
- // 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) {
72518
73310
  this.chromosomes = await loadChromSizes(config.chromSizesURL);
72519
73311
  } else {
72520
- // if the sequence defines chromosomes use them (fasta does, 2bit does not)
72521
- this.chromosomes = this.sequence.chromosomes || new Map();
73312
+ this.chromosomes = new Map(); // Cache, chromosome are added as they are loaded
72522
73313
  }
72523
73314
 
72524
- 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) {
72525
73319
  this.chromosomeNames = Array.from(this.chromosomes.keys());
72526
73320
  }
72527
73321
 
73322
+ // Chromosome alias
72528
73323
  if (config.chromAliasBbURL) {
72529
73324
  this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this);
72530
- if (!this.chromosomeNames) {
72531
- this.chromosomeNames = await this.chromAlias.getChromosomeNames();
72532
- }
72533
73325
  } else if (config.aliasURL) {
72534
73326
  this.chromAlias = new ChromAliasFile(config.aliasURL, Object.assign({}, config), this);
72535
73327
  } else if (this.chromosomeNames) {
72536
73328
  this.chromAlias = new ChromAliasDefaults(this.id, this.chromosomeNames);
72537
73329
  }
72538
73330
 
72539
- if (config.cytobandBbURL) {
72540
- this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
72541
- } else if (config.cytobandURL) {
72542
- this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
72543
- }
72544
-
72545
- // Last resort for chromosome information -- retrieve it from the cytoband source if supported
72546
- if (!this.chromosomeNames && typeof this.cytobandSource.getChromosomeNames === 'function') {
72547
- this.chromosomeNames = await this.cytobandSource.getChromosomeNames();
72548
- }
72549
- if (this.chromosomes.size === 0 && typeof this.cytobandSource.getChromosomes === 'function') {
72550
- const c = await this.cytobandSource.getChromosomes();
72551
- for (let chromosome of c) {
72552
- this.chromosomes.set(c.name, c);
72553
- }
72554
- }
72555
-
72556
-
72557
73331
  if (false !== config.wholeGenomeView && this.chromosomes.size > 0) {
72558
73332
  // Set chromosome order for WG view and chromosome pulldown. If chromosome order is not specified sort
72559
73333
  if (config.chromosomeOrder) {
@@ -72605,9 +73379,9 @@ class Genome {
72605
73379
  getHomeChromosomeName() {
72606
73380
  if (this.showWholeGenomeView() && this.chromosomes.has("all")) {
72607
73381
  return "all"
72608
- } else {
73382
+ } else if (this.chromosomeNames) {
72609
73383
  return this.chromosomeNames[0]
72610
- }
73384
+ } else ;
72611
73385
  }
72612
73386
 
72613
73387
  getChromosomeName(chr) {
@@ -72658,18 +73432,18 @@ class Genome {
72658
73432
  if (!aliasRecord && chr !== chr.toLowerCase()) {
72659
73433
  aliasRecord = await this.chromAlias.search(chr.toLowerCase());
72660
73434
  }
72661
- if(aliasRecord) {
73435
+ if (aliasRecord) {
72662
73436
  // Add some aliases for case insensitivy
72663
73437
  const upper = aliasRecord.chr.toUpperCase();
72664
73438
  const lower = aliasRecord.chr.toLowerCase();
72665
73439
  const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
72666
- if(aliasRecord.chr !== upper) {
73440
+ if (aliasRecord.chr !== upper) {
72667
73441
  aliasRecord["_uppercase"] = upper;
72668
73442
  }
72669
- if(aliasRecord.chr !== lower) {
73443
+ if (aliasRecord.chr !== lower) {
72670
73444
  aliasRecord["_lowercase"] = lower;
72671
73445
  }
72672
- if(aliasRecord.chr !== cap) {
73446
+ if (aliasRecord.chr !== cap) {
72673
73447
  aliasRecord["_cap"] = cap;
72674
73448
  }
72675
73449
  }
@@ -72801,6 +73575,10 @@ class Genome {
72801
73575
  return undefined
72802
73576
  }
72803
73577
  }
73578
+
73579
+ getHubURLs() {
73580
+ return this.config.hubs
73581
+ }
72804
73582
  }
72805
73583
 
72806
73584
  /**
@@ -72843,7 +73621,7 @@ function generateGenomeID(config) {
72843
73621
  }
72844
73622
  }
72845
73623
 
72846
- 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';
72847
73625
 
72848
73626
  /**
72849
73627
  * Manages XQTL selections.
@@ -73377,7 +74155,7 @@ class Browser {
73377
74155
  this.dataRangeDialog = new DataRangeDialog(this, this.root);
73378
74156
  this.dataRangeDialog.container.id = `igv-data-range-dialog-${guid$2()}`;
73379
74157
 
73380
- this.genericColorPicker = new GenericColorPicker({ parent: this.root, width: 180 });
74158
+ this.genericColorPicker = new GenericColorPicker({parent: this.root, width: 180});
73381
74159
  this.genericColorPicker.container.id = `igv-track-color-picker-${guid$2()}`;
73382
74160
 
73383
74161
  this.sliderDialog = new SliderDialog(this.root);
@@ -73387,10 +74165,10 @@ class Browser {
73387
74165
 
73388
74166
  getSampleNameViewportWidth() {
73389
74167
 
73390
- if (undefined === this.sampleNameViewportWidth) {
74168
+ if (false === this.showSampleNames || undefined === this.sampleNameViewportWidth) {
73391
74169
  return 0
73392
74170
  } else {
73393
- return false === this.showSampleNames ? 0 : this.sampleNameViewportWidth
74171
+ return this.sampleNameViewportWidth
73394
74172
  }
73395
74173
 
73396
74174
  }
@@ -73552,7 +74330,8 @@ class Browser {
73552
74330
  } else {
73553
74331
  session = options;
73554
74332
  }
73555
- return this.loadSessionObject(session)
74333
+
74334
+ await this.loadSessionObject(session);
73556
74335
  }
73557
74336
 
73558
74337
  /**
@@ -73581,8 +74360,7 @@ class Browser {
73581
74360
  config = new XMLSession(string, knownGenomes);
73582
74361
 
73583
74362
  } else if (filename.endsWith("hub.txt")) {
73584
-
73585
- const hub = await Hub.loadHub(urlOrFile, options);
74363
+ const hub = await loadHub(urlOrFile);
73586
74364
  const genomeConfig = hub.getGenomeConfig();
73587
74365
  config = {
73588
74366
  reference: genomeConfig
@@ -73673,7 +74451,7 @@ class Browser {
73673
74451
  }
73674
74452
 
73675
74453
  // ROIs
73676
- if(session.showROIOverlays !== undefined) {
74454
+ if (session.showROIOverlays !== undefined) {
73677
74455
  this.roiManager.showOverlays = session.showROIOverlays;
73678
74456
  }
73679
74457
  this.roiManager.clearROIs();
@@ -73687,12 +74465,16 @@ class Browser {
73687
74465
  // Sample info
73688
74466
  const localSampleInfoFiles = [];
73689
74467
  if (session.sampleinfo) {
73690
- for (const config of session.sampleinfo) {
73691
-
73692
- if (config.file) {
73693
- 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);
73694
74474
  } else {
73695
- this.loadSampleInfo(config);
74475
+ // this.loadSampleInfo(sampleInfoConfig)
74476
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74477
+
73696
74478
  }
73697
74479
 
73698
74480
  }
@@ -73733,23 +74515,14 @@ class Browser {
73733
74515
  }
73734
74516
  }
73735
74517
 
73736
- await this.loadTrackList(nonLocalTrackConfigurations);
73737
-
73738
- // The ruler and ideogram tracks are not explicitly loaded, but needs updated nonetheless.
73739
- for (let rtv of this.trackViews.filter((tv) => tv.track.type === 'ruler' || tv.track.type === 'ideogram')) {
73740
- 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});
73741
74523
  }
73742
74524
 
73743
- // If any tracks are selected show the selection buttons
73744
- if (this.trackViews.some(tv => tv.track.selected)) {
73745
- this.navbar.setEnableTrackSelection(true);
73746
- }
73747
-
73748
- this.updateUIWithReferenceFrameList();
73749
-
73750
- this.updateLocusSearchWidget();
73751
-
73752
- return trackConfigurations
74525
+ await this.loadTrackList(nonLocalTrackConfigurations);
73753
74526
 
73754
74527
  }
73755
74528
 
@@ -73810,13 +74583,8 @@ class Browser {
73810
74583
  }
73811
74584
 
73812
74585
  if (genomeChange) {
73813
- let trackConfigurations;
73814
- if (genomeConfig.hubURL) {
73815
- // TODO -- refactor this so "hub" is not loaded twice
73816
- const hub = await Hub.loadHub(genomeConfig.hubURL);
73817
- trackConfigurations = hub.getGroupedTrackConfigurations();
73818
- }
73819
- this.fireEvent('genomechange', [{genome, trackConfigurations}]);
74586
+
74587
+ this.fireEvent('genomechange', [{genome}]);
73820
74588
 
73821
74589
  if (this.circularView) {
73822
74590
  this.circularView.setAssembly({
@@ -73855,7 +74623,7 @@ class Browser {
73855
74623
  let genomeConfig;
73856
74624
  const isHubGenome = idOrConfig.hubURL || (idOrConfig.url && isString$2(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt"));
73857
74625
  if (isHubGenome) {
73858
- const hub = await Hub.loadHub(idOrConfig.hubURL || idOrConfig.url, idOrConfig);
74626
+ const hub = await loadHub(idOrConfig.hubURL || idOrConfig.url);
73859
74627
  genomeConfig = hub.getGenomeConfig();
73860
74628
  } else if (isString$2(idOrConfig) || !(idOrConfig.url || idOrConfig.fastaURL || idOrConfig.twoBitURL || idOrConfig.gbkURL)) {
73861
74629
  // Either an ID, a json string, or an object missing required properties.
@@ -73885,22 +74653,9 @@ class Browser {
73885
74653
 
73886
74654
  await this.loadTrackList(tracks);
73887
74655
 
73888
- await this.updateViews();
73889
-
73890
74656
  return this.genome
73891
74657
  }
73892
74658
 
73893
- /**
73894
- * Load a UCSC single-file genome assembly hub.
73895
- * @param options
73896
- * @returns {Promise<void>}
73897
- */
73898
- async loadTrackHub(options) {
73899
- const hub = await Hub.loadHub(options.url, options);
73900
- const genomeConfig = setDefaults(hub.getGenomeConfig());
73901
- return this.loadGenome(genomeConfig)
73902
- }
73903
-
73904
74659
  /**
73905
74660
  * Called after a session load, search, pan (horizontal drag), or resize
73906
74661
  *
@@ -73997,18 +74752,23 @@ class Browser {
73997
74752
  }
73998
74753
 
73999
74754
  const promises = [];
74000
- for (let config of configList) {
74001
- promises.push(this._loadTrack(config));
74755
+ for (const config of configList) {
74756
+ promises.push(this.#loadTrackHelper(config));
74002
74757
  }
74003
74758
 
74004
74759
  const loadedTracks = await Promise.all(promises);
74005
74760
 
74006
- const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
74007
- return trackView.track.autoscaleGroup
74008
- });
74009
- if (groupAutoscaleViews.length > 0) {
74010
- 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);
74011
74764
  }
74765
+
74766
+ this.reorderTracks();
74767
+
74768
+ await resize.call(this);
74769
+
74770
+ this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74771
+
74012
74772
  return loadedTracks
74013
74773
  }
74014
74774
 
@@ -74022,54 +74782,23 @@ class Browser {
74022
74782
  */
74023
74783
  async loadTrack(config) {
74024
74784
 
74025
- // Default configuration sync option to true. This is the expected behavior for public API calls
74026
- config.sync = (config.sync !== false);
74027
-
74028
- const newTrack = this._loadTrack(config);
74029
-
74030
- if (newTrack && config.autoscaleGroup) {
74031
- // Await newTrack load and update all views
74032
- await newTrack;
74785
+ const loadedTracks = await this.loadTrackList([config]);
74786
+ if (config.autoscaleGroup) {
74033
74787
  this.updateViews();
74034
74788
  }
74035
-
74036
- return newTrack
74789
+ return loadedTracks[0]
74037
74790
  }
74038
74791
 
74039
- /**
74040
- * Return a promise to load a track. Private function used by loadTrack() and loadTrackList()
74041
- *
74042
- * @param config
74043
- * @returns {*}
74044
- */
74045
-
74046
- async _loadTrack(config) {
74792
+ async #loadTrackHelper(config) {
74047
74793
 
74048
74794
  // config might be json
74049
74795
  if (isString$2(config)) {
74050
74796
  config = JSON.parse(config);
74051
74797
  }
74052
74798
 
74799
+ let track;
74053
74800
  try {
74054
-
74055
- // Load a hidden track -- used to populate searchable database without creating a track
74056
- if (config.hidden) {
74057
- const featureSource = FeatureSource(config, this.genome);
74058
- await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
74059
- return
74060
- }
74061
-
74062
- const newTrack = await this.createTrack(config);
74063
-
74064
- if ('sampleinfo' === config.type) {
74065
- this.layoutChange();
74066
- return
74067
- } else if (undefined === newTrack) {
74068
- return
74069
- }
74070
-
74071
- return this.addTrack(config, newTrack)
74072
-
74801
+ track = await this.createTrack(config);
74073
74802
  } catch (error) {
74074
74803
 
74075
74804
  let msg = error.message || error.error || error.toString();
@@ -74086,62 +74815,53 @@ class Browser {
74086
74815
  }
74087
74816
 
74088
74817
  msg = `${msg} : ${isFile(config.url) ? config.url.name : config.url}`;
74089
- // msg += (": " + FileUtils.isFile(config.url) ? config.url.name : config.url)
74090
74818
  const err = new Error(msg);
74091
74819
  console.error(err);
74092
74820
  throw err
74093
- // this.alert.present(new Error(msg), undefined)
74094
74821
  }
74095
- }
74096
74822
 
74097
- async addTrack(config, newTrack) {
74823
+ if (track) {
74824
+ return await this.addTrack(track)
74825
+ } else {
74826
+ return undefined
74827
+ }
74098
74828
 
74829
+ }
74830
+
74831
+ async addTrack(track) {
74099
74832
 
74100
74833
  // Set order field of track here, otherwise track order might get shuffled during asynchronous load
74101
- if (undefined === newTrack.order) {
74102
- newTrack.order = this.trackViews.length;
74834
+ if (undefined === track.order) {
74835
+ track.order = this.trackViews.length;
74103
74836
  }
74104
74837
 
74105
- const trackView = new TrackView(this, this.columnContainer, newTrack);
74838
+ const trackView = new TrackView(this, this.columnContainer, track);
74106
74839
  this.trackViews.push(trackView);
74107
74840
  toggleTrackLabels(this.trackViews, this.doShowTrackLabels);
74108
74841
 
74109
- if (typeof newTrack.postInit === 'function') {
74842
+ if (typeof track.postInit === 'function') {
74110
74843
  try {
74111
74844
  trackView.startSpinner();
74112
- await newTrack.postInit();
74845
+ await track.postInit();
74113
74846
  } finally {
74114
74847
  trackView.stopSpinner();
74115
74848
  }
74116
74849
  }
74117
74850
 
74118
- if (!newTrack.autoscaleGroup) {
74119
- // Group autoscale will get updated later (as a group)
74120
- if (config.sync) {
74121
- await trackView.updateViews();
74122
- } else {
74123
- trackView.updateViews();
74124
- }
74125
- }
74126
-
74127
- if (typeof newTrack.hasSamples === 'function' && newTrack.hasSamples()) {
74851
+ if (typeof track.hasSamples === 'function' && track.hasSamples()) {
74128
74852
 
74129
74853
  if (this.sampleInfo.hasAttributes()) {
74130
74854
  this.sampleInfoControl.setButtonVisibility(true);
74131
74855
  }
74132
74856
 
74133
74857
  if (this.config.showSampleNameButton !== false) {
74134
- this.sampleNameControl.show(); // If not explicitly set
74858
+ this.sampleNameControl.show();
74135
74859
  }
74136
74860
  }
74137
74861
 
74138
- // repositioned here to solve layout issue.
74139
- this.reorderTracks();
74140
- this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74141
-
74142
- newTrack.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74862
+ track.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74143
74863
 
74144
- return newTrack
74864
+ return track
74145
74865
 
74146
74866
  }
74147
74867
 
@@ -74286,7 +75006,6 @@ class Browser {
74286
75006
  }
74287
75007
  }
74288
75008
 
74289
-
74290
75009
  reorderTracks() {
74291
75010
 
74292
75011
  this.trackViews.sort(function (a, b) {
@@ -74523,19 +75242,19 @@ class Browser {
74523
75242
 
74524
75243
  this.updateLocusSearchWidget();
74525
75244
 
74526
- for (let frame of this.referenceFrameList) {
74527
- if (frame.bpPerPixel <= bppSequenceThreshold) {
74528
- 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);
74529
75248
  }
74530
75249
  }
74531
75250
 
74532
- for (let centerGuide of this.centerLineList) {
75251
+ for (const centerGuide of this.centerLineList) {
74533
75252
  centerGuide.repaint();
74534
75253
  }
74535
75254
 
74536
75255
  // Don't autoscale while dragging.
74537
75256
  if (this.dragObject) {
74538
- for (let trackView of trackViews) {
75257
+ for (const trackView of trackViews) {
74539
75258
  await trackView.updateViews();
74540
75259
  }
74541
75260
  } else {
@@ -74559,8 +75278,8 @@ class Browser {
74559
75278
  // Calculate group autoscale dataRange
74560
75279
  if (Object.entries(groupAutoscaleTrackViews).length > 0) {
74561
75280
  for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) {
74562
- const featureArray = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
74563
- const dataRange = doAutoscale(featureArray.flat());
75281
+ const inViewFeatures = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
75282
+ const dataRange = doAutoscale(inViewFeatures.flat());
74564
75283
  for (const trackView of trackViews) {
74565
75284
  trackView.track.dataRange = Object.assign({}, dataRange);
74566
75285
  trackView.track.autoscale = false;
@@ -74569,7 +75288,7 @@ class Browser {
74569
75288
  }
74570
75289
  }
74571
75290
 
74572
- await Promise.all(otherTrackViews.map(tv => tv.updateViews()));
75291
+ await Promise.all(otherTrackViews.map(trackView => trackView.updateViews()));
74573
75292
  }
74574
75293
 
74575
75294
  }
@@ -74592,10 +75311,10 @@ class Browser {
74592
75311
  referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width;
74593
75312
  }
74594
75313
 
74595
- const chrName = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '';
74596
-
74597
75314
  const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ');
74598
75315
 
75316
+ const chrName = referenceFrameList.length === 1 ? this.genome.getChromosomeDisplayName(this.referenceFrameList[0].chr) : '';
75317
+
74599
75318
  this.navbar.updateLocus(loc, chrName);
74600
75319
 
74601
75320
  this.fireEvent('locuschange', [this.referenceFrameList]);
@@ -74612,6 +75331,46 @@ class Browser {
74612
75331
  return Math.floor(width / columnCount)
74613
75332
  }
74614
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
+
74615
75374
  minimumBases() {
74616
75375
  return this.config.minimumBases
74617
75376
  }
@@ -74797,29 +75556,12 @@ class Browser {
74797
75556
  }
74798
75557
 
74799
75558
  /**
74800
- * @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.
74801
75560
  */
74802
75561
  async goto(chr, start, end) {
74803
75562
  await this.search(chr + ":" + start + "-" + end);
74804
75563
  }
74805
75564
 
74806
- /**
74807
-
74808
- * Search for the locus string -- this function is called from various igv.js GUI elements, and is not part of the
74809
- * API. Wraps ```search``` and presents an error dialog if false.
74810
- *
74811
- * @param string
74812
- * @param init
74813
- * @returns {Promise<void>}
74814
- */
74815
- async doSearch(string, init) {
74816
- const success = await this.search(string, init);
74817
- if (!success) {
74818
- this.alert.present(new Error(`Unrecognized locus: <b> ${string} </b>`));
74819
- }
74820
- return success
74821
- }
74822
-
74823
75565
 
74824
75566
  /**
74825
75567
  * Search for the locus string
@@ -74832,6 +75574,10 @@ class Browser {
74832
75574
  async search(stringOrArray, init) {
74833
75575
 
74834
75576
  const loci = await search(this, stringOrArray);
75577
+ return this.updateLoci(loci, init)
75578
+ }
75579
+
75580
+ async updateLoci(loci, init) {
74835
75581
 
74836
75582
  if (loci && loci.length > 0) {
74837
75583
 
@@ -74868,9 +75614,9 @@ class Browser {
74868
75614
  }
74869
75615
  }
74870
75616
 
74871
- async loadSampleInfo(config) {
75617
+ async loadSampleInfo(sampleInfoConfig) {
74872
75618
 
74873
- await this.sampleInfo.loadSampleInfoFile(config.url);
75619
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74874
75620
 
74875
75621
  for (const {sampleInfoViewport} of this.trackViews) {
74876
75622
  sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
@@ -75008,9 +75754,9 @@ class Browser {
75008
75754
  json["locus"] = locus.length === 1 ? locus[0] : locus;
75009
75755
 
75010
75756
  const roiSets = this.roiManager.toJSON();
75011
- if(roiSets) {
75757
+ if (roiSets) {
75012
75758
  json["roi"] = roiSets;
75013
- if(!this.roiManager.showOverlays){
75759
+ if (!this.roiManager.showOverlays) {
75014
75760
  json["showROIOverlays"] = false; // true is the default
75015
75761
  }
75016
75762
  }
@@ -75353,8 +76099,6 @@ class Browser {
75353
76099
  }
75354
76100
  }
75355
76101
 
75356
-
75357
-
75358
76102
  // Navbar delegates
75359
76103
  get sampleInfoControl() {
75360
76104
  return this.navbar.sampleInfoControl
@@ -75376,6 +76120,9 @@ class Browser {
75376
76120
  return this.navbar.sampleNameControl
75377
76121
  }
75378
76122
 
76123
+ async blat(sequence) {
76124
+ return createBlatTrack({sequence, browser: this, name: 'Blat', title: 'Blat'})
76125
+ }
75379
76126
  }
75380
76127
 
75381
76128
  function getFileExtension(input) {
@@ -75401,7 +76148,7 @@ function getFileExtension(input) {
75401
76148
  }
75402
76149
 
75403
76150
  /**
75404
- * 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
75405
76152
  * than class method because it needs to be copied and bound to specific instances of browser to support listener
75406
76153
  * removal
75407
76154
  *
@@ -75409,40 +76156,14 @@ function getFileExtension(input) {
75409
76156
  */
75410
76157
  async function resize() {
75411
76158
 
75412
- if (!this.referenceFrameList) return
75413
-
75414
- const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
75415
-
75416
- for (let referenceFrame of this.referenceFrameList) {
75417
-
75418
- const index = this.referenceFrameList.indexOf(referenceFrame);
75419
-
75420
- const {chr, genome} = referenceFrame;
75421
-
75422
- const {bpLength} = genome.getChromosome(referenceFrame.chr);
75423
-
75424
- const viewportWidthBP = referenceFrame.toBP(viewportWidth);
75425
-
75426
- // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
75427
- if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
75428
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
75429
- referenceFrame.bpPerPixel = bpLength / viewportWidth;
75430
- } else {
75431
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
75432
- referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
75433
- }
75434
-
75435
- for (let {viewports} of this.trackViews) {
75436
- viewports[index].setWidth(viewportWidth);
75437
- }
75438
-
76159
+ if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
76160
+ return
75439
76161
  }
75440
76162
 
75441
- this.updateUIWithReferenceFrameList();
75442
-
75443
- //TODO -- update view only if needed. Reducing size never needed. Increasing size maybe
75444
-
75445
- await this.updateViews(true);
76163
+ const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
76164
+ this.updateReferenceFrames(viewportWidth);
76165
+ this.updateViewportElements(viewportWidth);
76166
+ await this.syncUIState();
75446
76167
  }
75447
76168
 
75448
76169
 
@@ -75911,7 +76632,8 @@ var index = {
75911
76632
  registerTrackClass,
75912
76633
  registerTrackCreatorFunction,
75913
76634
  registerFileFormats,
75914
- loadSessionFile: Browser.loadSessionFile
76635
+ loadSessionFile: Browser.loadSessionFile,
76636
+ loadHub
75915
76637
  };
75916
76638
 
75917
76639
  export { index as default };