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.js CHANGED
@@ -10952,7 +10952,7 @@
10952
10952
  return list;
10953
10953
  }
10954
10954
 
10955
- /*! @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 */
10955
+ /*! @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 */
10956
10956
 
10957
10957
  const {
10958
10958
  entries,
@@ -11012,6 +11012,9 @@
11012
11012
  */
11013
11013
  function unapply(func) {
11014
11014
  return function (thisArg) {
11015
+ if (thisArg instanceof RegExp) {
11016
+ thisArg.lastIndex = 0;
11017
+ }
11015
11018
  for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
11016
11019
  args[_key - 1] = arguments[_key];
11017
11020
  }
@@ -11250,7 +11253,7 @@
11250
11253
  function createDOMPurify() {
11251
11254
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
11252
11255
  const DOMPurify = root => createDOMPurify(root);
11253
- DOMPurify.version = '3.2.4';
11256
+ DOMPurify.version = '3.2.5';
11254
11257
  DOMPurify.removed = [];
11255
11258
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
11256
11259
  // Not running in a browser, provide a factory function
@@ -11855,7 +11858,7 @@
11855
11858
  allowedTags: ALLOWED_TAGS
11856
11859
  });
11857
11860
  /* Detect mXSS attempts abusing namespace confusion */
11858
- if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) {
11861
+ if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
11859
11862
  _forceRemove(currentNode);
11860
11863
  return true;
11861
11864
  }
@@ -12877,6 +12880,20 @@
12877
12880
 
12878
12881
  async loadAll() {
12879
12882
 
12883
+
12884
+ const pushChromosome = (current, order) => {
12885
+ const length = current.length || (current.offset + current.seq.length);
12886
+ if (!chrNameSet.has(current.chr)) {
12887
+ this.sequences.set(current.chr, []);
12888
+ this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12889
+ chrNameSet.add(current.chr);
12890
+ } else {
12891
+ const c = this.chromosomes.get(current.chr);
12892
+ c.bpLength = Math.max(c.bpLength, length);
12893
+ }
12894
+ this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12895
+ };
12896
+
12880
12897
  let data;
12881
12898
  if (isDataURL(this.fastaURL)) {
12882
12899
  let bytes = decodeDataURI$1(this.fastaURL);
@@ -12890,70 +12907,62 @@
12890
12907
 
12891
12908
  const chrNameSet = new Set();
12892
12909
  const lines = splitLines$2(data);
12893
- const len = lines.length;
12894
- let lineNo = 0;
12895
12910
  let order = 0;
12896
- let nextLine;
12897
12911
  let current = {};
12898
- while (lineNo < len) {
12899
- nextLine = lines[lineNo++].trim();
12912
+ for (let nextLine of lines) {
12900
12913
  if (nextLine.startsWith("#") || nextLine.length === 0) ; else if (nextLine.startsWith(">")) {
12901
12914
  // Start the next sequence
12902
- if (current && current.seq) {
12903
- pushChromosome.call(this, current, order++);
12915
+ if (current.seq && current.seq.length > 0) {
12916
+ pushChromosome(current, order++);
12904
12917
  }
12918
+ current.seq = "";
12905
12919
 
12906
12920
  const parts = nextLine.substr(1).split(/\s+/);
12907
12921
 
12908
- // Check for samtools style locus string. This is not perfect, and could fail on weird sequence names
12909
- const nameParts = parts[0].split(':');
12910
- current.chr = nameParts[0];
12911
- current.seq = "";
12912
- current.offset = 0;
12913
- if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12914
- const locusParts = nameParts[1].split('-');
12915
- if (locusParts.length === 2 &&
12916
- /^[0-9]+$/.test(locusParts[0]) &&
12917
- /^[0-9]+$/.test(locusParts[1])) ;
12918
- const from = Number.parseInt(locusParts[0]);
12919
- const to = Number.parseInt(locusParts[1]);
12920
- if (to > from) { // TODO this should be an error
12922
+
12923
+ // Check for @len= token, which is a non-standard extension supporting igv-reports.
12924
+ if (nextLine.includes("@len=")) {
12925
+ const nameParts = parts[0].split(':');
12926
+ current.chr = nameParts[0];
12927
+ if (nameParts.length > 1 && nameParts[1].indexOf('-') > 0) {
12928
+
12929
+ const locusParts = nameParts[1].split('-');
12930
+ if (locusParts.length === 2 &&
12931
+ /^[0-9]+$/.test(locusParts[0]) &&
12932
+ /^[0-9]+$/.test(locusParts[1])) ;
12933
+ const from = Number.parseInt(locusParts[0]);
12934
+ Number.parseInt(locusParts[1]);
12921
12935
  current.offset = from - 1;
12922
- }
12923
12936
 
12924
- // Check for chromosome length token
12925
- if (parts.length > 1 && parts[1].startsWith("@len=")) {
12926
- try {
12927
- current.length = parseInt(parts[1].trim().substring(5));
12928
- } catch (e) {
12937
+ // Check for chromosome length token
12938
+ if (parts.length > 1 && parts[1].startsWith("@len=")) {
12939
+ try {
12940
+ current.length = parseInt(parts[1].trim().substring(5));
12941
+ } catch (e) {
12942
+ current.length = undefined;
12943
+ console.error(`Error parsing sequence length for ${nextLine}`);
12944
+ }
12945
+ } else {
12929
12946
  current.length = undefined;
12930
- console.error(`Error parsing sequence length for ${nextLine}`);
12931
12947
  }
12932
- } else {
12933
- current.length = undefined;
12934
12948
  }
12949
+ } else {
12950
+ // No special tokens, a standard FASTA header
12951
+ current.chr = parts[0];
12952
+ current.offset = 0;
12935
12953
  }
12954
+
12936
12955
  } else {
12956
+ // Not a header or comment, so it must be sequence data
12937
12957
  current.seq += nextLine;
12938
12958
  }
12939
- // add last seq
12940
- if (current && current.seq) {
12941
- pushChromosome.call(this, current, order);
12942
- }
12943
12959
  }
12944
12960
 
12945
- function pushChromosome(current, order) {
12946
- const length = current.length || (current.offset + current.seq.length);
12947
- if (!chrNameSet.has(current.chr)) {
12948
- this.sequences.set(current.chr, []);
12949
- this.chromosomes.set(current.chr, new Chromosome(current.chr, order, length));
12950
- chrNameSet.add(current.chr);
12951
- } else {
12952
- const c = this.chromosomes.get(current.chr);
12953
- c.bpLength = Math.max(c.bpLength, length);
12954
- }
12955
- this.sequences.get(current.chr).push(new SequenceSlice(current.offset, current.seq));
12961
+ // Handle the last sequence
12962
+ if (current.seq && current.seq.length > 0) {
12963
+ pushChromosome(current, order);
12956
12964
  }
12965
+
12957
12966
  }
12958
12967
 
12959
12968
  /**
@@ -13718,17 +13727,22 @@
13718
13727
 
13719
13728
  static magic = 2026540177
13720
13729
  littleEndian = true
13730
+ type = 'BPTree' // Either BPTree or BPChromTree
13721
13731
  nodeCache = new Map()
13722
13732
 
13723
- static async loadBpTree(path, config, startOffset) {
13724
- const bpTree = new BPTree(path, config, startOffset);
13733
+ static async loadBpTree(path, config, startOffset, type, loader) {
13734
+ const bpTree = new BPTree(path, config, startOffset, type, loader);
13725
13735
  return bpTree.init()
13726
13736
  }
13727
13737
 
13728
- constructor(path, config, startOffset) {
13738
+ constructor(path, config, startOffset, type, loader) {
13729
13739
  this.path = path;
13730
13740
  this.config = config;
13731
13741
  this.startOffset = startOffset;
13742
+ if(type) {
13743
+ this.type = type;
13744
+ }
13745
+ this.loader = loader || igvxhr;
13732
13746
  }
13733
13747
 
13734
13748
  async init() {
@@ -13754,69 +13768,22 @@
13754
13768
  return this
13755
13769
  }
13756
13770
 
13757
- async search(term) {
13758
-
13771
+ getItemCount() {
13759
13772
  if(!this.header) {
13760
- await this.init();
13773
+ throw Error("Header not initialized")
13761
13774
  }
13775
+ return this.header.itemCount
13776
+ }
13762
13777
 
13763
- const {keySize, valSize} = this.header;
13778
+ async search(term) {
13764
13779
 
13765
- if (!(valSize === 16 || valSize === 8)) {
13766
- throw Error(`Unexpected valSize ${valSize}`)
13780
+ if(!this.header) {
13781
+ await this.init();
13767
13782
  }
13768
13783
 
13769
- const readTreeNode = async (offset) => {
13770
-
13771
- if (this.nodeCache.has(offset)) {
13772
- return this.nodeCache.get(offset)
13773
- } else {
13774
-
13775
- let binaryParser = await this.#getParserFor(offset, 4);
13776
- const type = binaryParser.getByte();
13777
- binaryParser.getByte();
13778
- const count = binaryParser.getUShort();
13779
- const items = [];
13780
-
13781
- if (type === 1) {
13782
- // Leaf node
13783
- const size = count * (keySize + valSize);
13784
- binaryParser = await this.#getParserFor(offset + 4, size);
13785
- for (let i = 0; i < count; i++) {
13786
- const key = binaryParser.getFixedLengthString(keySize);
13787
- const offset = binaryParser.getLong();
13788
-
13789
- let value;
13790
- if (valSize === 16) {
13791
- const length = binaryParser.getInt();
13792
- binaryParser.getInt();
13793
- value = {offset, length};
13794
- } else {
13795
- value = {offset};
13796
- }
13797
- items.push({key, value});
13798
- }
13799
- } else {
13800
- // Non leaf node
13801
- const size = count * (keySize + 8);
13802
- binaryParser = await this.#getParserFor(offset + 4, size);
13803
-
13804
- for (let i = 0; i < count; i++) {
13805
- const key = binaryParser.getFixedLengthString(keySize);
13806
- const offset = binaryParser.getLong();
13807
- items.push({key, offset});
13808
- }
13809
- }
13810
-
13811
- const node = {type, count, items};
13812
- this.nodeCache.set(offset, node);
13813
- return node
13814
- }
13815
- };
13816
-
13817
13784
  const walkTreeNode = async (offset) => {
13818
13785
 
13819
- const node = await readTreeNode(offset);
13786
+ const node = await this.readTreeNode(offset);
13820
13787
 
13821
13788
  if (node.type === 1) {
13822
13789
  // Leaf node
@@ -13847,9 +13814,66 @@
13847
13814
  return walkTreeNode(this.header.nodeOffset)
13848
13815
  }
13849
13816
 
13817
+ async readTreeNode (offset) {
13818
+
13819
+ if (this.nodeCache.has(offset)) {
13820
+ return this.nodeCache.get(offset)
13821
+ } else {
13822
+ let binaryParser = await this.#getParserFor(offset, 4);
13823
+ const type = binaryParser.getByte();
13824
+ binaryParser.getByte();
13825
+ const count = binaryParser.getUShort();
13826
+ const items = [];
13827
+
13828
+ const {keySize, valSize} = this.header;
13829
+
13830
+ if (type === 1) {
13831
+ // Leaf node
13832
+ const size = count * (keySize + valSize);
13833
+ binaryParser = await this.#getParserFor(offset + 4, size);
13834
+ for (let i = 0; i < count; i++) {
13835
+ const key = binaryParser.getFixedLengthString(keySize);
13836
+ let value;
13837
+ if(this.type === 'BPChromTree') {
13838
+ const id = binaryParser.getInt();
13839
+ const size = binaryParser.getInt();
13840
+ value = {id, size};
13841
+ } else {
13842
+ const offset = binaryParser.getLong();
13843
+ if (valSize === 16) {
13844
+ const length = binaryParser.getLong();
13845
+ value = {offset, length};
13846
+ } else {
13847
+ value = {offset};
13848
+ }
13849
+ }
13850
+ items.push({key, value});
13851
+ }
13852
+ } else {
13853
+ // Non leaf node
13854
+ const size = count * (keySize + 8);
13855
+ binaryParser = await this.#getParserFor(offset + 4, size);
13856
+
13857
+ for (let i = 0; i < count; i++) {
13858
+ const key = binaryParser.getFixedLengthString(keySize);
13859
+ const offset = binaryParser.getLong();
13860
+ items.push({key, offset});
13861
+ }
13862
+ }
13863
+
13864
+ const node = {type, count, items};
13865
+ this.nodeCache.set(offset, node);
13866
+ return node
13867
+ }
13868
+ }
13869
+
13850
13870
  async #getParserFor(start, size) {
13851
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13852
- return new BinaryParser$1(new DataView(data), this.littleEndian)
13871
+ try {
13872
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
13873
+ return new BinaryParser$1(new DataView(data), this.littleEndian)
13874
+ } catch (e) {
13875
+ console.error(e);
13876
+ }
13853
13877
  }
13854
13878
 
13855
13879
  }
@@ -13875,6 +13899,7 @@
13875
13899
 
13876
13900
  littleEndian
13877
13901
  metaIndex = new Map()
13902
+ chromosomeNames
13878
13903
 
13879
13904
  constructor(config) {
13880
13905
  this.url = config.twoBitURL || config.fastaURL;
@@ -13967,9 +13992,16 @@
13967
13992
  return sequenceBases
13968
13993
  }
13969
13994
 
13995
+ /**
13996
+ * Read the internal index of the 2bit file. This is a list of sequence names and their offsets in the file.
13997
+ *
13998
+ * @returns {Promise<Map<any, any>>}
13999
+ * @private
14000
+ */
13970
14001
  async _readIndex() {
13971
14002
 
13972
14003
  const index = new Map();
14004
+ this.chromosomeNames = [];
13973
14005
 
13974
14006
  const loadRange = {start: 0, size: 64};
13975
14007
  let arrayBuffer = await igvxhr.loadArrayBuffer(this.url, {range: loadRange});
@@ -14022,6 +14054,8 @@
14022
14054
  index.set(name, offset);
14023
14055
 
14024
14056
  estNameLength = Math.floor(estNameLength * (i / (i + 1)) + name.length / (i + 1));
14057
+
14058
+ this.chromosomeNames.push(name);
14025
14059
  }
14026
14060
  return index
14027
14061
  }
@@ -16349,7 +16383,10 @@
16349
16383
  }
16350
16384
  } else {
16351
16385
  // All directives that could change the format, and thus decoder, should have been read by now.
16352
- this.setDecoder(header.format);
16386
+ // Set the decoder, unless it is explicitly set in the track configuration (not common)
16387
+ if(!this.config.decode) {
16388
+ this.setDecoder(header.format);
16389
+ }
16353
16390
 
16354
16391
  // If the line can be parsed as a feature assume we are beyond the header, if any
16355
16392
  const tokens = line.split(this.delimiter || "\t");
@@ -22164,84 +22201,6 @@
22164
22201
  return Math.ceil(Math.log(Math.max(0, (chrSize / (bpPerPixel * 700)))) / log2)
22165
22202
  }
22166
22203
 
22167
- /**
22168
- * A ChromTree parses a UCSC bigbed/bigwig "chromosomeTree" section of the header to produce 2 maps,
22169
- * (1) ID -> chromosome names, and its
22170
- * (2) chromsome name -> ID
22171
- *
22172
- * Both maps are needed by IGV
22173
- *
22174
- * The chromosome tree is a B+ index, but is located continguously in memory in the header section of the file. This
22175
- * makes it feasible to parse the whole tree with data from a single fetch. In the end the tree is discarded
22176
- * leaving only the mapps.
22177
- */
22178
- class ChromTree {
22179
-
22180
- constructor(header, nameToID, valueToKey, sumLengths) {
22181
- this.header = header;
22182
- this.nameToId = nameToID;
22183
- this.idToName = valueToKey;
22184
- this.sumLengths = sumLengths;
22185
- }
22186
-
22187
- static parseTree(binaryParser, startOffset, genome = false) {
22188
- {
22189
- const magic = binaryParser.getInt();
22190
- const blockSize = binaryParser.getInt();
22191
- const keySize = binaryParser.getInt();
22192
- const valSize = binaryParser.getInt();
22193
- const itemCount = binaryParser.getLong();
22194
- const reserved = binaryParser.getLong();
22195
-
22196
- const header = {magic, blockSize, keySize, valSize, itemCount, reserved};
22197
- const nameToId = new Map();
22198
- const idToName = [];
22199
- let sumLengths = 0;
22200
- const readTreeNode = (offset) => {
22201
- if (offset >= 0) binaryParser.position = offset;
22202
- const type = binaryParser.getByte();
22203
- binaryParser.getByte();
22204
- const count = binaryParser.getUShort();
22205
-
22206
- if (type === 1) {
22207
- // Leaf node
22208
- for (let i = 0; i < count; i++) {
22209
- let key = binaryParser.getFixedLengthString(keySize);
22210
- let value;
22211
- if (valSize === 8) {
22212
- value = binaryParser.getInt();
22213
- const chromSize = binaryParser.getInt();
22214
- sumLengths += chromSize;
22215
- if (genome) key = genome.getChromosomeName(key); // Translate to canonical chr name
22216
- nameToId.set(key, value);
22217
- idToName[value] = key;
22218
-
22219
- } else {
22220
- throw Error(`Unexpected "valSize" value in chromosome tree. Expected 8, actual value is ${valSize}`)
22221
- }
22222
- }
22223
- } else {
22224
- // non-leaf
22225
- for (let i = 0; i < count; i++) {
22226
- binaryParser.getFixedLengthString(keySize);
22227
- const childOffset = binaryParser.getLong();
22228
- const bufferOffset = childOffset - startOffset;
22229
- const currOffset = binaryParser.position;
22230
- readTreeNode(bufferOffset);
22231
- binaryParser.position = currOffset;
22232
- }
22233
- }
22234
- };
22235
-
22236
- // Recursively walk tree to populate dictionary
22237
- readTreeNode( -1);
22238
-
22239
- return new ChromTree(header, nameToId, idToName, sumLengths)
22240
- }
22241
- }
22242
-
22243
- }
22244
-
22245
22204
  const RPTREE_HEADER_SIZE = 48;
22246
22205
  const RPTREE_NODE_LEAF_ITEM_SIZE = 32; // leaf item size
22247
22206
  const RPTREE_NODE_CHILD_ITEM_SIZE = 24; // child item size
@@ -22252,11 +22211,12 @@
22252
22211
  littleEndian = true
22253
22212
  nodeCache = new Map()
22254
22213
 
22255
- constructor(path, config, startOffset) {
22214
+ constructor(path, config, startOffset, loader) {
22256
22215
 
22257
22216
  this.path = path;
22258
22217
  this.config = config;
22259
22218
  this.startOffset = startOffset;
22219
+ this.loader = loader || igvxhr;
22260
22220
  }
22261
22221
 
22262
22222
 
@@ -22300,7 +22260,7 @@
22300
22260
  }
22301
22261
 
22302
22262
  async #getParserFor(start, size) {
22303
- const data = await igvxhr.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22263
+ const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {range: {start, size}}));
22304
22264
  return new BinaryParser$1(new DataView(data), this.littleEndian)
22305
22265
  }
22306
22266
 
@@ -22657,6 +22617,165 @@
22657
22617
  }
22658
22618
  }
22659
22619
 
22620
+ class ChromTree {
22621
+
22622
+ nameToId = new Map()
22623
+ idToName = new Map()
22624
+
22625
+ constructor(path, config, startOffset, loader) {
22626
+ this.path = path;
22627
+ this.config = config;
22628
+ this.startOffset = startOffset;
22629
+
22630
+ this.bpTree = new BPTree(path, config, startOffset, 'BPChromTree', loader);
22631
+ }
22632
+
22633
+ async init() {
22634
+ return this.bpTree.init()
22635
+ }
22636
+
22637
+ getItemCount() {
22638
+ return this.bpTree.getItemCount()
22639
+ }
22640
+
22641
+ /**
22642
+ * Return the chromosome ID for the given name. This is the internal chromosome ID for the parent BB file only.
22643
+ * @param {string} chr - The chromosome name.
22644
+ * @returns {number|null} - The chromosome ID or null if not found.
22645
+ */
22646
+ async getIdForName(chr) {
22647
+ if (this.nameToId.has(chr)) {
22648
+ return this.nameToId.get(chr)
22649
+ } else {
22650
+ try {
22651
+ const result = await this.bpTree.search(chr);
22652
+ if (result) {
22653
+ const id = result.id;
22654
+ this.nameToId.set(chr, id);
22655
+ return id
22656
+ } else {
22657
+ return
22658
+ }
22659
+ } catch (error) {
22660
+ throw new Error(error)
22661
+ }
22662
+ }
22663
+ }
22664
+
22665
+ /**
22666
+ * Return the chromosome name for the given ID. This is a potentially expensive operation as it involves
22667
+ * walking the tree until the leaf item for the given name is found. Currently it is used in only 2
22668
+ * situations:
22669
+ * (1) decoding features from a bigbed search-by-name query
22670
+ * (2) decoding bigwig data from the whole genome view
22671
+ * @param {number} id
22672
+ * @return {string|null}
22673
+ */
22674
+ async getNameForId(id) {
22675
+ if (this.idToName.has(id)) {
22676
+ return this.idToName.get(id)
22677
+ } else {
22678
+ const name = await this.searchForName(id);
22679
+ if (name !== null) {
22680
+ this.idToName.set(id, name);
22681
+ return name
22682
+ }
22683
+ }
22684
+ return null
22685
+ }
22686
+
22687
+ /**
22688
+ * Perform a reverse search by traversing the tree starting at the given offset. This is potentially expensive
22689
+ * as it traverses the tree to find the name corresponding to the given ID. It shoud not be used for large trees.
22690
+ *
22691
+ * @param {number} id - The ID to search for.
22692
+ * @returns {string|null} - The name corresponding to the ID, or null if not found.
22693
+ */
22694
+ async searchForName(id) {
22695
+
22696
+ const reverseSearch = async (offset, id) => {
22697
+
22698
+ const node = await this.bpTree.readTreeNode(offset);
22699
+
22700
+ let found = null;
22701
+
22702
+ if (node.type === 1) {
22703
+ // Leaf node
22704
+ for (const item of node.items) {
22705
+ const key = item.key;
22706
+ const itemId = item.value.id;
22707
+ if (itemId === id) {
22708
+ found = key;
22709
+ }
22710
+ // Cache the name and ID for future lookups
22711
+ this.nameToId.set(key, itemId);
22712
+ this.idToName.set(id, itemId);
22713
+ }
22714
+ return found
22715
+ } else {
22716
+ // Non-leaf node
22717
+ for (const item of node.items) {
22718
+ found = await reverseSearch.call(this, item.offset, id);
22719
+ if (found !== null) {
22720
+ break
22721
+ }
22722
+ }
22723
+ }
22724
+ return found
22725
+ };
22726
+
22727
+ try {
22728
+ return reverseSearch.call(this, this.startOffset + 32, id)
22729
+ } catch (error) {
22730
+ throw new Error(error)
22731
+ }
22732
+ }
22733
+
22734
+ /**
22735
+ * Return an estimated length of the genome, which might be the actual length if the number of contigs is small.
22736
+ * This is only used for calculating a default feature visibility window.
22737
+ *
22738
+ * @return {number}
22739
+ */
22740
+ async estimateGenomeSize() {
22741
+ try {
22742
+ const runningTotal = {total: 0, count: 0};
22743
+ await this.accumulateSize(this.startOffset + 32, runningTotal, 10000);
22744
+ const itemCount = this.getItemCount();
22745
+ return (itemCount / runningTotal.count) * runningTotal.total
22746
+
22747
+ } catch (error) {
22748
+ console.error("Error estimating genome size", error);
22749
+ return -1
22750
+ }
22751
+ }
22752
+
22753
+ async accumulateSize(offset, runningTotal, maxCount) {
22754
+
22755
+ const node = await this.bpTree.readTreeNode(offset);
22756
+
22757
+ if (node.type === 1) {
22758
+ // Leaf node
22759
+ for (const item of node.items) {
22760
+ const value = item.value;
22761
+ runningTotal.total += value.size;
22762
+ runningTotal.count += 1;
22763
+ }
22764
+ } else {
22765
+ // Non-leaf node. Items are visited in random order to avoid biasing the estimate
22766
+ const shuffledItems = node.items.slice().sort(() => Math.random() - 0.5);
22767
+ for (const item of shuffledItems) {
22768
+ await this.accumulateSize(item.offset, runningTotal, maxCount);
22769
+ if (runningTotal.count > maxCount) {
22770
+ break
22771
+ }
22772
+ }
22773
+ }
22774
+ return runningTotal
22775
+ }
22776
+
22777
+ }
22778
+
22660
22779
  /*
22661
22780
  * The MIT License (MIT)
22662
22781
  *
@@ -22720,14 +22839,40 @@
22720
22839
  async preload() {
22721
22840
  const data = await igvxhr.loadArrayBuffer(this.path);
22722
22841
  this.loader = new DataBuffer(data);
22842
+ for (let rpTree of this.rpTreeCache.values()) {
22843
+ rpTree.loader = this.loader;
22844
+ }
22845
+ if (this._searchTrees) {
22846
+ for (let bpTree of this._searchTrees) {
22847
+ bpTree.loader = this.loader;
22848
+ }
22849
+ }
22723
22850
  }
22724
22851
 
22725
- async readWGFeatures(bpPerPixel, windowFunction) {
22852
+ async readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction) {
22853
+
22726
22854
  await this.loadHeader();
22727
- const chrIdx1 = 0;
22728
- const chrIdx2 = this.chromTree.idToName.length - 1;
22729
- const chr1 = this.chromTree.idToName[chrIdx1];
22730
- const chr2 = this.chromTree.idToName[chrIdx2];
22855
+ // Convert the logic to JavaScript
22856
+ let minID = Number.MAX_SAFE_INTEGER;
22857
+ let maxID = -1;
22858
+ let chr1;
22859
+ let chr2;
22860
+
22861
+ for (const chr of wgChromosomeNames) {
22862
+ const id = await this.getIdForChr(chr);
22863
+ if (id === null || id === undefined) {
22864
+ continue
22865
+ }
22866
+ if (id < minID) {
22867
+ minID = id;
22868
+ chr1 = chr;
22869
+ }
22870
+ if (id > maxID) {
22871
+ maxID = id;
22872
+ chr2 = chr;
22873
+ }
22874
+ }
22875
+
22731
22876
  return this.readFeatures(chr1, 0, chr2, Number.MAX_VALUE, bpPerPixel, windowFunction)
22732
22877
  }
22733
22878
 
@@ -22738,8 +22883,8 @@
22738
22883
 
22739
22884
  await this.loadHeader();
22740
22885
 
22741
- let chrIdx1 = await this.#getIdForChr(chr1);
22742
- let chrIdx2 = await this.#getIdForChr(chr2);
22886
+ const chrIdx1 = await this.getIdForChr(chr1);
22887
+ const chrIdx2 = await this.getIdForChr(chr2);
22743
22888
 
22744
22889
  if (chrIdx1 === undefined || chrIdx2 === undefined) {
22745
22890
  return []
@@ -22798,7 +22943,7 @@
22798
22943
  } else {
22799
22944
  plain = uint8Array;
22800
22945
  }
22801
- decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, this.chromTree.idToName, windowFunction, this.littleEndian);
22946
+ await decodeFunction.call(this, new DataView(plain.buffer), chrIdx1, bpStart, chrIdx2, bpEnd, features, windowFunction);
22802
22947
  }
22803
22948
 
22804
22949
  features.sort(function (a, b) {
@@ -22815,29 +22960,30 @@
22815
22960
  * @param chr
22816
22961
  * @returns {Promise<*>}
22817
22962
  */
22818
- async #getIdForChr(chr) {
22963
+ async getIdForChr(chr) {
22819
22964
 
22820
22965
  if (this.chrAliasTable.has(chr)) {
22821
22966
  chr = this.chrAliasTable.get(chr);
22822
- if (chr === undefined) {
22967
+ if (!chr) {
22823
22968
  return undefined
22824
22969
  }
22825
22970
  }
22826
22971
 
22827
- let chrIdx = this.chromTree.nameToId.get(chr);
22972
+ let chrIdx = await this.chromTree.getIdForName(chr);
22828
22973
 
22829
22974
  // Try alias
22830
22975
  if (chrIdx === undefined && this.genome) {
22831
22976
  const aliasRecord = await this.genome.getAliasRecord(chr);
22832
22977
  let alias;
22833
22978
  if (aliasRecord) {
22834
- const aliases = Object.keys(aliasRecord)
22835
- .filter(k => k !== "start" && k !== "end")
22836
- .map(k => aliasRecord[k])
22837
- .filter(a => this.chromTree.nameToId.has(a));
22838
- if (aliases.length > 0) {
22839
- alias = aliases[0];
22840
- chrIdx = this.chromTree.nameToId.get(aliases[0]);
22979
+ for (let k of Object.keys(aliasRecord)) {
22980
+ if (k === "start" || k === "end") continue
22981
+ alias = aliasRecord[k];
22982
+ if (alias === chr) continue // Already tried this
22983
+ chrIdx = await this.chromTree.getIdForName(alias);
22984
+ if (chrIdx !== undefined) {
22985
+ break
22986
+ }
22841
22987
  }
22842
22988
  }
22843
22989
  this.chrAliasTable.set(chr, alias); // alias may be undefined => no alias exists. Setting prevents repeated attempts
@@ -22924,7 +23070,8 @@
22924
23070
  this.header.extraIndexOffsets.length > 0) {
22925
23071
  this._searchTrees = [];
22926
23072
  for (let offset of this.header.extraIndexOffsets) {
22927
- const bpTree = await BPTree.loadBpTree(this.path, this.config, offset);
23073
+ const type = undefined;
23074
+ const bpTree = await BPTree.loadBpTree(this.path, this.config, offset, type, this.loader);
22928
23075
  this._searchTrees.push(bpTree);
22929
23076
  }
22930
23077
  }
@@ -23041,15 +23188,13 @@
23041
23188
  this.totalSummary = new BWTotalSummary(extHeaderParser);
23042
23189
  }
23043
23190
 
23044
- // Chrom data index. The start is known, size is not, but we can estimate it
23045
- const bufferSize = Math.min(200000, Math.max(10000, header.fullDataOffset - header.chromTreeOffset));
23046
- this.chromTree = await this.#readChromTree(header.chromTreeOffset, bufferSize);
23047
- this.chrNames = new Set(this.chromTree.idToName);
23191
+ this.chromTree = new ChromTree(this.path, this.config, header.chromTreeOffset, this.loader);
23192
+ await this.chromTree.init();
23048
23193
 
23049
23194
  // Estimate feature density from dataCount (bigbed only)
23050
- if("bigbed" === this.type) {
23195
+ if ("bigbed" === this.type) {
23051
23196
  const dataCount = await this.#readDataCount(header.fullDataOffset);
23052
- this.featureDensity = dataCount / this.chromTree.sumLengths;
23197
+ this.featureDensity = dataCount / await this.chromTree.estimateGenomeSize();
23053
23198
  }
23054
23199
 
23055
23200
  this.header = header;
@@ -23073,38 +23218,6 @@
23073
23218
  return binaryParser.getInt()
23074
23219
  }
23075
23220
 
23076
- /**
23077
- * Used when the chromTreeOffset is > fullDataOffset, that is when the chrom tree is not in the initial chunk
23078
- * read for parsing the header. We know the start position, but not the total size of the chrom tree
23079
- *
23080
- * @returns {Promise<void>}
23081
- */
23082
- async #readChromTree(chromTreeOffset, bufferSize) {
23083
-
23084
- let size = bufferSize;
23085
- const load = async () => {
23086
- const data = await this.loader.loadArrayBuffer(this.path, buildOptions(this.config, {
23087
- range: {
23088
- start: chromTreeOffset,
23089
- size: size
23090
- }
23091
- }));
23092
- const binaryParser = new BinaryParser$1(new DataView(data), this.littleEndian);
23093
- return ChromTree.parseTree(binaryParser, chromTreeOffset, this.genome)
23094
- };
23095
-
23096
- let error;
23097
- while (size < 1000000) {
23098
- try {
23099
- const chromTree = await load();
23100
- return chromTree
23101
- } catch (e) {
23102
- error = e;
23103
- size *= 2;
23104
- }
23105
- }
23106
- throw (error)
23107
- }
23108
23221
 
23109
23222
  async loadExtendedHeader(offset) {
23110
23223
 
@@ -23160,7 +23273,7 @@
23160
23273
  if (rpTree) {
23161
23274
  return rpTree
23162
23275
  } else {
23163
- rpTree = new RPTree(this.path, this.config, offset);
23276
+ rpTree = new RPTree(this.path, this.config, offset, this.loader);
23164
23277
  await rpTree.init();
23165
23278
  this.rpTreeCache.set(offset, rpTree);
23166
23279
  return rpTree
@@ -23200,9 +23313,7 @@
23200
23313
  const plain = (this.header.uncompressBuffSize > 0) ? inflate_1$3(uint8Array) : uint8Array;
23201
23314
  const decodeFunction = getBedDataDecoder.call(this);
23202
23315
  const features = [];
23203
- decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
23204
- features, this.chromTree.idToName);
23205
-
23316
+ await decodeFunction.call(this, new DataView(plain.buffer), 0, 0, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, features);
23206
23317
  return features
23207
23318
 
23208
23319
  }
@@ -23270,7 +23381,7 @@
23270
23381
  }
23271
23382
 
23272
23383
 
23273
- function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23384
+ async function decodeWigData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23274
23385
 
23275
23386
  const binaryParser = new BinaryParser$1(data, littleEndian);
23276
23387
  const chromId = binaryParser.getInt();
@@ -23311,7 +23422,7 @@
23311
23422
  else if (chromId > chrIdx2 || (chromId === chrIdx2 && chromStart >= bpEnd)) break
23312
23423
 
23313
23424
  if (Number.isFinite(value)) {
23314
- const chr = chrDict[chromId];
23425
+ const chr = await this.chromTree.getNameForId(chromId);
23315
23426
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23316
23427
  }
23317
23428
  }
@@ -23322,12 +23433,13 @@
23322
23433
 
23323
23434
  const minSize = 3 * 4 + 1; // Minimum # of bytes required for a bed record
23324
23435
  const decoder = getDecoder(this.header.definedFieldCount, this.header.fieldCount, this.autoSql, this.format);
23325
- return function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23326
- const binaryParser = new BinaryParser$1(data, littleEndian);
23436
+ return async function (data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray) {
23437
+
23438
+ const binaryParser = new BinaryParser$1(data, this.littleEndian);
23327
23439
  while (binaryParser.remLength() >= minSize) {
23328
23440
 
23329
23441
  const chromId = binaryParser.getInt();
23330
- const chr = chrDict[chromId];
23442
+ const chr = await this.chromTree.getNameForId(chromId);
23331
23443
  const chromStart = binaryParser.getInt();
23332
23444
  const chromEnd = binaryParser.getInt();
23333
23445
  const rest = binaryParser.getString();
@@ -23344,8 +23456,7 @@
23344
23456
  }
23345
23457
  }
23346
23458
 
23347
-
23348
- function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, chrDict, windowFunction, littleEndian) {
23459
+ async function decodeZoomData(data, chrIdx1, bpStart, chrIdx2, bpEnd, featureArray, windowFunction, littleEndian) {
23349
23460
 
23350
23461
  const binaryParser = new BinaryParser$1(data, littleEndian);
23351
23462
  const minSize = 8 * 4; // Minimum # of bytes required for a zoom record
@@ -23377,7 +23488,7 @@
23377
23488
 
23378
23489
 
23379
23490
  if (Number.isFinite(value)) {
23380
- const chr = chrDict[chromId];
23491
+ const chr = await this.chromTree.getNameForId(chromId);
23381
23492
  featureArray.push({chr: chr, start: chromStart, end: chromEnd, value: value});
23382
23493
 
23383
23494
 
@@ -23462,7 +23573,8 @@
23462
23573
 
23463
23574
  let features;
23464
23575
  if ("all" === chr.toLowerCase()) {
23465
- features = isBigWig ? await this.getWGValues(windowFunction, bpPerPixel) : [];
23576
+ const wgChromosomeNames = this.genome.wgChromosomeNames;
23577
+ features = isBigWig && wgChromosomeNames? await this.getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) : [];
23466
23578
  } else {
23467
23579
  features = await this.reader.readFeatures(chr, start, chr, end, bpPerPixel, windowFunction);
23468
23580
  }
@@ -23486,15 +23598,14 @@
23486
23598
 
23487
23599
  }
23488
23600
 
23489
- async getWGValues(windowFunction, bpPerPixel) {
23601
+ async getWGValues(wgChromosomeNames, windowFunction, bpPerPixel) {
23490
23602
 
23491
23603
  const genome = this.genome;
23492
23604
  const cached = this.#wgValues[windowFunction];
23493
23605
  if (cached && cached.bpPerPixel > 0.8 * bpPerPixel && cached.bpPerPixel < 1.2 * bpPerPixel) {
23494
23606
  return cached.values
23495
23607
  } else {
23496
-
23497
- const features = await this.reader.readWGFeatures(bpPerPixel, windowFunction);
23608
+ const features = await this.reader.readWGFeatures(wgChromosomeNames, bpPerPixel, windowFunction);
23498
23609
  let wgValues = [];
23499
23610
  for (let f of features) {
23500
23611
  const chr = f.chr;
@@ -28289,7 +28400,7 @@
28289
28400
  features
28290
28401
  };
28291
28402
 
28292
- const track = await browser.loadTrack(trackConfig);
28403
+ const track = (await browser.loadTrackList([trackConfig]))[0];
28293
28404
  track.openTableView();
28294
28405
 
28295
28406
  } catch (e) {
@@ -30341,109 +30452,578 @@
30341
30452
  }
30342
30453
  }
30343
30454
 
30344
- /*
30345
- https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30346
- https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30347
- https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30348
- https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30349
- */
30455
+ const parentOverrideProperties = new Set(["visibility", "priority", "group"]);
30350
30456
 
30457
+ const nonInheritableProperties = new Set([
30458
+ "track", "type", "shortLabel", "longLabel", "bigDataUrl",
30459
+ "parent", "superTrack", "priority", "view", "compositeContainer", "compositeTrack"
30460
+ ]);
30351
30461
 
30352
- class Hub {
30353
30462
 
30354
- static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30355
- static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30356
- "cpgIslandExtUnmasked", "windowMasker"])
30463
+ class Stanza {
30464
+
30465
+ properties = new Map()
30466
+
30467
+ constructor(type, name) {
30468
+ this.type = type;
30469
+ this.name = name;
30470
+ }
30471
+
30472
+ setProperty(key, value) {
30473
+ this.properties.set(key, value);
30474
+ }
30475
+
30476
+ getProperty(key) {
30477
+ if (this.properties.has("noInherit")) {
30478
+ return this.properties.get(key)
30479
+ } else if (this.parent && parentOverrideProperties.has(key) && this.parent.hasProperty(key)) {
30480
+ return this.parent.getProperty(key)
30481
+ } else if (this.properties.has(key)) {
30482
+ return this.properties.get(key)
30483
+ } else if (this.parent && !nonInheritableProperties.has(key)) {
30484
+ return this.parent.getProperty(key)
30485
+ } else {
30486
+ return undefined
30487
+ }
30488
+ }
30489
+
30490
+ hasProperty(key) {
30491
+ return this.getProperty(key) !== null && this.getProperty(key) !== undefined
30492
+ }
30493
+
30494
+ hasOwnProperty(key) {
30495
+ return this.properties.has(key)
30496
+ }
30497
+
30498
+ getOwnProperty(key) {
30499
+ return this.properties.get(key)
30500
+ }
30501
+
30502
+ removeProperty(key) {
30503
+ this.properties.delete(key);
30504
+ }
30505
+
30506
+ get format() {
30507
+ const type = this.getProperty("type");
30508
+ if (type) {
30509
+ // Trim extra bed qualifiers (e.g. bigBed + 4)
30510
+ return firstWord$1(type)
30511
+ }
30512
+ return undefined // unknown type
30513
+ }
30514
+
30515
+ /**
30516
+ * IGV display mode
30517
+ */
30518
+ get displayMode() {
30519
+ let viz = this.getProperty("visibility");
30520
+ if (!viz) {
30521
+ return "COLLAPSED"
30522
+ } else {
30523
+ viz = viz.toLowerCase();
30524
+ switch (viz) {
30525
+ case "dense":
30526
+ return "COLLAPSED"
30527
+ case "pack":
30528
+ return "EXPANDED"
30529
+ case "squish":
30530
+ return "SQUISHED"
30531
+ default:
30532
+ return "COLLAPSED"
30533
+ }
30534
+ }
30535
+ }
30536
+ }
30537
+
30538
+
30539
+ function firstWord$1(str) {
30540
+ const idx = str.indexOf(' ');
30541
+ return idx > 0 ? str.substring(0, idx) : str
30542
+ }
30543
+
30544
+ class TrackConfigContainer {
30545
+ constructor(name, label, priority, defaultOpen) {
30546
+ this.name = name;
30547
+ this.priority = priority;
30548
+ this.label = label;
30549
+ this.defaultOpen = defaultOpen;
30550
+ this.tracks = [];
30551
+ this.children = [];
30552
+ }
30553
+
30554
+ isEmpty() {
30555
+ return this.tracks.length === 0 &&
30556
+ (!this.children || this.children.length === 0 || this.children.every(child => child.isEmpty()));
30557
+ }
30558
+
30559
+ map(callback) {
30560
+ this.tracks.forEach(callback);
30561
+ this.children.forEach(child => child.map(callback));
30562
+ }
30563
+
30564
+ findTracks(filter) {
30565
+ const found = [];
30566
+ this._find(found, filter);
30567
+ return found;
30568
+ }
30569
+
30570
+ _find(found, filter) {
30571
+ this.tracks.forEach(track => {
30572
+ if (filter(track)) {
30573
+ found.push(track);
30574
+ }
30575
+ });
30576
+ this.children.forEach(child => child._find(found, filter));
30577
+ }
30578
+
30579
+ countTracks() {
30580
+ return this.tracks.length + this.children.reduce((count, child) => count + child.countTracks(), 0);
30581
+ }
30582
+
30583
+ countSelectedTracks() {
30584
+ const selectedCount = this.tracks.filter(track => track.visible).length;
30585
+ return selectedCount + this.children.reduce((count, child) => count + child.countSelectedTracks(), 0);
30586
+ }
30587
+
30588
+ trim() {
30589
+ this.children = this.children.filter(child => !child.isEmpty());
30590
+ this.children.forEach(child => child.trim());
30591
+ }
30592
+
30593
+ setTrackVisibility(loadedTrackPaths) {
30594
+ this.tracks.forEach(track => {
30595
+ track.visible = loadedTrackPaths.has(track.url);
30596
+ });
30597
+ this.children.forEach(child => child.setTrackVisibility(loadedTrackPaths));
30598
+ }
30599
+ }
30600
+
30601
+ const supportedTypes = new Set([
30602
+ "bigbed", "bigwig", "biggenepred", "vcftabix", "refgene",
30603
+ "bam", "sampleinfo", "vcf.list", "ucscsnp", "bed", "tdf", "gff", "gff3", "gtf", "vcf", "vcfphasedtrio",
30604
+ "bigdbsnp", "rmask", "genepred", "wig", "bedgraph", "interact", "broadpeak", "narrowpeak", "gappedpeak",
30605
+ "gistic", "seg", "mut, bigrmsk"
30606
+ ]);
30607
+
30608
+ const filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30609
+ "cpgIslandExtUnmasked", "windowMasker"]);
30610
+
30611
+ class TrackDbHub {
30612
+
30613
+ constructor(trackStanzas, groupStanzas) {
30614
+ this.groupStanzas = groupStanzas;
30615
+ this.trackStanzas = trackStanzas;
30616
+ }
30617
+
30618
+ findCytobandURL() {
30619
+ for (const t of this.trackStanzas) {
30620
+ if (t.name === "cytoBandIdeo" && t.hasProperty("bigDataUrl")) {
30621
+ return t.getProperty("bigDataUrl")
30622
+ }
30623
+ }
30624
+ }
30625
+
30626
+ getSupportedTrackCount() {
30627
+ let count = 0;
30628
+ for (const t of this.trackStanzas) {
30629
+ if (!filterTracks.has(t.name) &&
30630
+ t.hasProperty("bigDataUrl") &&
30631
+ t.format &&
30632
+ supportedTypes.has(t.format.toLowerCase())) {
30633
+ count++;
30634
+ }
30635
+ }
30636
+ return count
30637
+ }
30357
30638
 
30358
- static async loadHub(url) {
30639
+ getGroupedTrackConfigurations() {
30359
30640
 
30360
- const idx = url.lastIndexOf("/");
30361
- const baseURL = url.substring(0, idx + 1);
30362
- const stanzas = await loadStanzas(url);
30363
- let groups;
30364
- if ("genome" === stanzas[1].type) {
30365
- const genome = stanzas[1];
30366
- if (genome.hasProperty("groups")) {
30367
- const groupsTxtURL = baseURL + genome.getProperty("groups");
30368
- groups = await loadStanzas(groupsTxtURL);
30641
+ if (!this.groupTrackConfigs) {
30642
+ this.groupTrackConfigs = [];
30643
+ const trackContainers = new Map();
30644
+
30645
+ // create a container for tracks with no parent
30646
+ const nullContainer = new TrackConfigContainer('', '', 0, true);
30647
+ this.groupTrackConfigs.push(nullContainer);
30648
+
30649
+ const hasGroups = this.groupStanzas && this.groupStanzas.length > 0;
30650
+ if (hasGroups) {
30651
+ for (const groupStanza of this.groupStanzas) {
30652
+ const name = groupStanza.getProperty("name");
30653
+ const defaultOpen = groupStanza.getProperty("defaultIsClosed") === "0";
30654
+ const priority = groupStanza.hasProperty("priority") ? getPriority(groupStanza.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30655
+ const container = new TrackConfigContainer(name, groupStanza.getProperty("label"), priority, defaultOpen);
30656
+ trackContainers.set(name, container);
30657
+ this.groupTrackConfigs.push(container);
30658
+ }
30369
30659
  }
30370
30660
 
30371
- // If the genome has a chromSizes file, and it is not too large, set the chromSizesURL property. This will
30372
- // enable whole genome view and the chromosome pulldown
30373
- if (genome.hasProperty("chromSizes")) {
30374
- const chromSizesURL = baseURL + genome.getProperty("chromSizes");
30375
- const l = await getContentLength(chromSizesURL);
30376
- if (l !== null && Number.parseInt(l) < 1000000) {
30377
- genome.setProperty("chromSizesURL", chromSizesURL);
30661
+ for (let s of this.trackStanzas) {
30662
+
30663
+ const isContainer = (s.hasOwnProperty("superTrack") && !s.hasOwnProperty("bigDataUrl")) ||
30664
+ s.hasOwnProperty("compositeTrack") || s.hasOwnProperty("view") ||
30665
+ (s.hasOwnProperty("container") && s.getOwnProperty("container").equals("multiWig"));
30666
+
30667
+ // Find parent, if any. "group" containers can be implicit, all other types should be explicitly
30668
+ // defined before their children
30669
+ let parent;
30670
+
30671
+ if (s.hasOwnProperty("parent")) {
30672
+ parent = trackContainers.get(s.getOwnProperty("parent"));
30673
+ }
30674
+
30675
+ if (!parent && hasGroups && s.hasProperty("group")) {
30676
+ const groupName = s.getProperty("group");
30677
+ if (trackContainers.has(groupName)) {
30678
+ parent = trackContainers.get(groupName);
30679
+ } else {
30680
+ const container = new TrackConfigContainer(groupName, groupName, 1000, true);
30681
+ trackContainers.set(groupName, container);
30682
+ this.groupTrackConfigs.push(container);
30683
+ parent = container;
30684
+ }
30685
+ }
30686
+
30687
+ if (isContainer) {
30688
+
30689
+ const name = s.getProperty("track");
30690
+ const priority = s.hasProperty("priority") ? getPriority(s.getProperty("priority")) : Number.MAX_SAFE_INTEGER - 1;
30691
+ const defaultOpen = s.getProperty("defaultIsClosed") === "0";
30692
+ const longLabel = s.getOwnProperty("longLabel");
30693
+ const label = longLabel && longLabel.length < 50 ? longLabel : s.getOwnProperty("shortLabel");
30694
+ const container = new TrackConfigContainer(name, label, priority, defaultOpen);
30695
+
30696
+ if (trackContainers.has(name)) {
30697
+ throw new Error(`Duplicate track container: ${name}`)
30698
+ }
30699
+ trackContainers.set(name, container);
30700
+
30701
+ if (parent) {
30702
+ parent.children.push(container);
30703
+ } else {
30704
+ // No parent or a superTrack => promote to top level
30705
+ this.groupTrackConfigs.push(container);
30706
+ }
30707
+ } else if (!filterTracks.has(s.name) &&
30708
+ s.hasProperty("bigDataUrl") &&
30709
+ s.format &&
30710
+ supportedTypes.has(s.format.toLowerCase())) {
30711
+
30712
+ const trackConfig = this.#getTrackConfig(s);
30713
+ if (parent) {
30714
+ parent.tracks.push(trackConfig);
30715
+ } else {
30716
+ nullContainer.tracks.push(trackConfig);
30717
+ }
30378
30718
  }
30379
30719
  }
30720
+
30380
30721
  }
30381
30722
 
30382
- // TODO -- categorize extra "user" supplied and other tracks in some distinctive way before including them
30383
- // load includes. Nested includes are not supported
30384
- // for (let s of stanzas.slice()) {
30385
- // if ("include" === s.type) {
30386
- // const includeStanzas = await loadStanzas(baseURL + s.getProperty("include"))
30387
- // for (s of includeStanzas) {
30388
- // s.setProperty("visibility", "hide")
30389
- // stanzas.push(s)
30390
- // }
30391
- // }
30392
- // }
30723
+ // Filter empty groups and sort
30724
+ this.groupTrackConfigs.forEach(c => c.trim());
30725
+ this.groupTrackConfigs = this.groupTrackConfigs.filter(t => !t.isEmpty());
30393
30726
 
30394
- return new Hub(url, stanzas, groups)
30727
+ this.groupTrackConfigs.sort((a, b) => a.priority - b.priority);
30728
+ return this.groupTrackConfigs
30395
30729
  }
30396
30730
 
30397
- constructor(url, stanzas, groupStanzas) {
30731
+ /**
30732
+ * Return an array of igv track config objects that satisfy the filter
30733
+ */
30734
+ #getTracksConfigs(filter) {
30735
+ return this.trackStanzas.filter(t => {
30736
+ return supportedTypes.has(t.format) && t.hasProperty("bigDataUrl") && (!filter || filter(t))
30737
+ })
30738
+ .map(t => this.#getTrackConfig(t))
30739
+ }
30398
30740
 
30399
- this.url = url;
30400
30741
 
30401
- const idx = url.lastIndexOf("/");
30402
- this.baseURL = url.substring(0, idx + 1);
30742
+ /** example
30743
+ * track gc5Base
30744
+ * shortLabel GC Percent
30745
+ * longLabel GC Percent in 5-Base Windows
30746
+ * group map
30747
+ * visibility full
30748
+ * autoScale Off
30749
+ * maxHeightPixels 128:36:16
30750
+ * graphTypeDefault Bar
30751
+ * gridDefault OFF
30752
+ * windowingFunction Mean
30753
+ * color 0,0,0
30754
+ * altColor 128,128,128
30755
+ * viewLimits 30:70
30756
+ * type bigWig 0 100
30757
+ * bigDataUrl bbi/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base.bw
30758
+ * html html/GCA_011100615.1_Macaca_fascicularis_6.0.gc5Base
30759
+ * @param t
30760
+ */
30761
+ #getTrackConfig(t) {
30762
+
30763
+ const format = t.format;
30403
30764
 
30404
- // The first stanza must be type = hub
30405
- if ("hub" === stanzas[0].type) {
30406
- this.hubStanza = stanzas[0];
30407
- } else {
30408
- throw Error("Unexpected hub.txt file -- does the first line start with 'hub'?")
30765
+ const config = {
30766
+ "id": t.getProperty("track"),
30767
+ "name": t.getProperty("shortLabel"),
30768
+ "format": format,
30769
+ "url": t.getProperty("bigDataUrl"),
30770
+ "displayMode": t.displayMode,
30771
+ };
30772
+
30773
+ if ("vcfTabix" === format) {
30774
+ config.indexURL = config.url + ".tbi";
30409
30775
  }
30410
- if ("on" !== this.hubStanza.getProperty("useOneFile")) {
30411
- throw Error("Only 'useOneFile' hubs are currently supported")
30776
+
30777
+ if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30778
+ if (config.description) config.description += "<br/>";
30779
+ config.description =
30780
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30781
+ } else if (t.hasProperty("longLabel")) {
30782
+ config.description = t.getProperty("longLabel");
30412
30783
  }
30413
- if (stanzas.length < 2) {
30414
- throw Error("Expected at least 2 stanzas, hub and genome")
30784
+
30785
+ if (t.hasProperty("autoScale")) {
30786
+ config.autoscale = t.getProperty("autoScale").toLowerCase() === "on";
30415
30787
  }
30788
+ if (t.hasProperty("maxHeightPixels")) {
30789
+ const tokens = t.getProperty("maxHeightPixels").split(":");
30790
+ config.maxHeight = Number.parseInt(tokens[0]);
30791
+ config.height = Number.parseInt(tokens[1]);
30792
+ config.minHeight = Number.parseInt(tokens[2]);
30793
+ }
30794
+ // TODO -- graphTypeDefault
30795
+ // TODO -- windowingFunction
30796
+ if (t.hasProperty("color")) {
30797
+ const c = t.getProperty("color");
30798
+ config.color = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30799
+ }
30800
+ if (t.hasProperty("altColor")) {
30801
+ const c = t.getProperty("altColor");
30802
+ config.altColor = c.indexOf(",") > 0 ? `rgb(${c})` : c;
30803
+ }
30804
+ if (t.hasProperty("viewLimits")) {
30805
+ const tokens = t.getProperty("viewLimits").split(":");
30806
+ let min, max;
30807
+ if (tokens.length > 1) {
30808
+ min = Number.parseInt(tokens[0]);
30809
+ max = Number.parseInt(tokens[1]);
30810
+ }
30811
+ if (Number.isNaN(max) || Number.isNaN(min)) {
30812
+ console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
30813
+ } else {
30814
+ config.min = min;
30815
+ config.max = max;
30816
+ }
30416
30817
 
30417
- // The second stanza should be a genome
30418
- if ("genome" === stanzas[1].type) {
30419
- this.genomeStanza = stanzas[1];
30420
- } else {
30421
- throw Error(`Unexpected hub file -- expected "genome" stanza but found "${stanzas[1].type}"`)
30818
+ }
30819
+ if (t.hasProperty("itemRgb")) ;
30820
+ if ("hide" === t.getProperty("visibility")) {
30821
+ // TODO -- this not supported yet
30822
+ config.visible = false;
30823
+ }
30824
+ if (t.hasProperty("url")) {
30825
+ config.infoURL = t.getProperty("url");
30826
+ }
30827
+ if (t.hasProperty("searchIndex")) {
30828
+ config.searchIndex = t.getProperty("searchIndex");
30829
+ }
30830
+ if (t.hasProperty("searchTrix")) {
30831
+ config.trixURL = t.getProperty("searchTrix");
30832
+ }
30833
+ if (t.hasProperty("html")) {
30834
+ config.html = t.getProperty("html");
30835
+ }
30836
+
30837
+ if (t.hasProperty("group")) {
30838
+ config._group = t.getProperty("group");
30839
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
30840
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30841
+ config.order = nextPriority;
30842
+ this.groupPriorityMap.set(config._group, nextPriority);
30843
+ }
30844
+ }
30845
+
30846
+ if (t.hasProperty("metadata")) {
30847
+ config.attributes = parseMetadata(t.getProperty("metadata"));
30848
+ }
30849
+
30850
+ if (t.hasProperty("maxWindowToDraw")) {
30851
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowToDraw"), 10);
30852
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30853
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30854
+ }
30855
+ config.visibilityWindow = maxWindowToDraw;
30422
30856
  }
30423
30857
 
30424
- // Remaining stanzas should be tracks
30425
- this.trackStanzas = [];
30426
- for (let i = 2; i < stanzas.length; i++) {
30427
- if ("track" === stanzas[i].type) {
30428
- this.trackStanzas.push(stanzas[i]);
30858
+ // IGV does not support "maxWindowCoverage" in the same way as UCSC. Use to limit visibility window
30859
+ if (t.hasProperty("maxWindowCoverage")) {
30860
+ let maxWindowToDraw = parseInt(t.getProperty("maxWindowCoverage"), 10);
30861
+ if (maxWindowToDraw > Number.MAX_SAFE_INTEGER) {
30862
+ maxWindowToDraw = Number.MAX_SAFE_INTEGER;
30429
30863
  }
30864
+ config.visibilityWindow = maxWindowToDraw;
30430
30865
  }
30431
30866
 
30432
- if (groupStanzas) {
30433
- this.groupStanzas = groupStanzas;
30434
- this.groupPriorityMap = new Map();
30435
- for (let g of groupStanzas) {
30436
- if (g.hasProperty("priority")) {
30437
- this.groupPriorityMap.set(g.getProperty("name"), Number.parseInt(g.getProperty("priority")) * 10);
30867
+ return config
30868
+ }
30869
+ }
30870
+
30871
+ function htmlText(html) {
30872
+ // Assumes a pattern like <span style="color:#C58DAA">Digestive</span>
30873
+ const idx1 = html.indexOf('>');
30874
+ const idx2 = html.indexOf('<', idx1);
30875
+ if (idx1 > 0 && idx2 > idx1) {
30876
+ return html.substring(idx1 + 1, idx2)
30877
+ } else {
30878
+ return html
30879
+ }
30880
+ }
30881
+
30882
+ /**
30883
+ * Return the priority for the group. The priority format is uncertain, but extends to at least 2 levels (e.g. 3.4).
30884
+ * Ignore levels > 3
30885
+ *
30886
+ * @param {string} priorityString Priority as a string (e.g. 3.4)
30887
+ * @return {number} A priority as an integer
30888
+ */
30889
+ function getPriority(priorityString) {
30890
+ try {
30891
+ const tokens = priorityString.trim().split(".");
30892
+ let p = parseInt(tokens[0], 10) * 100;
30893
+ if (tokens.length > 1) {
30894
+ p += parseInt(tokens[1], 10) * 10;
30895
+ }
30896
+ if (tokens.length > 2) {
30897
+ p += parseInt(tokens[2], 10);
30898
+ }
30899
+ return p
30900
+ } catch (e) {
30901
+ console.error(`Error parsing priority string: ${priorityString}`, e);
30902
+ return Number.MAX_SAFE_INTEGER
30903
+ }
30904
+ }
30905
+
30906
+ function parseMetadata(metadata) {
30907
+ const attrs = new Map();
30908
+ let lastMetdataLengh = -1;
30909
+ while (metadata && metadata.length > 0) {
30910
+ try {
30911
+ if (metadata.length === lastMetdataLengh) {
30912
+ break
30913
+ }
30914
+ lastMetdataLengh = metadata.length;
30915
+ let idx = metadata.indexOf("=");
30916
+ if (idx === -1 || idx === metadata.length - 1) {
30917
+ break
30918
+ }
30919
+ let idx2;
30920
+ const key = capitalize(stripQuotes$2(metadata.substring(0, idx)));
30921
+ let value;
30922
+
30923
+ if (metadata.charAt(idx + 1) === '"') {
30924
+ idx++;
30925
+ idx2 = metadata.indexOf('" ', idx + 1);
30926
+ value = idx2 > 0 ? metadata.substring(idx + 1, idx2) : metadata.substring(idx + 1);
30927
+ idx2++;
30928
+ } else {
30929
+ idx2 = metadata.indexOf(" ", idx + 1);
30930
+ if (idx2 === -1) {
30931
+ idx2 = metadata.length;
30438
30932
  }
30933
+ value = metadata.substring(idx + 1, idx2);
30934
+ }
30935
+ value = stripQuotes$2(value);
30936
+ if (value.endsWith('"')) {
30937
+ value = value.substring(0, value.length - 1);
30938
+ }
30939
+ if (value.startsWith("<") && value.endsWith(">")) {
30940
+ value = htmlText(value);
30439
30941
  }
30942
+ attrs.set(key, value);
30943
+ if (idx2 === metadata.length) {
30944
+ break
30945
+ }
30946
+ metadata = idx2 > 0 ? metadata.substring(idx2 + 1).trim() : "";
30947
+ } catch (e) {
30948
+ // We don't want to fail parsing the hub due to a failure parsing metadata. Also, we don't want to
30949
+ // overwhelm the log. Metadata is of marginal importance in IGV.
30950
+ }
30951
+ }
30952
+ return attrs
30953
+ }
30954
+
30955
+ /*
30956
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
30957
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
30958
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
30959
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
30960
+ */
30961
+
30962
+ const idMappings = new Map([
30963
+ ["hg38", "GCF_000001405.40"],
30964
+ ["mm39", "GCF_000001635.27"],
30965
+ ["mm10", "GCF_000001635.26"],
30966
+ ["bosTau9", "GCF_002263795.1"],
30967
+ ["canFam4", "GCF_011100685.1"],
30968
+ ["canFam6", "GCF_000002285.5"],
30969
+ ["ce11", "GCF_000002985.6"],
30970
+ ["dm6", "GCF_000001215.4"],
30971
+ ["galGal6", "GCF_000002315.6"],
30972
+ ["gorGor6", "GCF_008122165.1"],
30973
+ ["macFas5", "GCA_000364345.1"],
30974
+ ["panTro6", "GCA_002880755.3"],
30975
+ ["rn6", "GCF_000001895.5"],
30976
+ ["rn7", "GCF_015227675.2"],
30977
+ ["sacCer3", "GCF_000146045.2"],
30978
+ ["sacCer2", "GCF_000146045.2"],
30979
+ ["susScr11", "GCF_000003025.6"],
30980
+ ["taeGut1", "GCF_000002275.3"],
30981
+ ["tetNig2", "GCF_000002275.3"],
30982
+ ["xenTro10", "GCF_000002035.6"],
30983
+ ["xenTro9", "GCF_000002035.6"],
30984
+ ["tair10", "GCF_000001735.4"],
30985
+ ]);
30986
+
30987
+ class Hub {
30988
+
30989
+ static supportedTypes = new Set(["bigBed", "bigWig", "bigGenePred", "vcfTabix"])
30990
+ static filterTracks = new Set(["cytoBandIdeo", "assembly", "gap", "gapOverlap", "allGaps",
30991
+ "cpgIslandExtUnmasked", "windowMasker"])
30992
+
30993
+ constructor(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas) {
30994
+
30995
+ this.url = url;
30996
+ this.hubStanza = hubStanza;
30997
+ this.genomeStanzas = genomeStanzas;
30998
+ this.trackStanzas = trackStanzas;
30999
+ this.groupStanzas = groupStanzas;
31000
+ this.trackHubMap = new Map();
31001
+
31002
+ // trackStanzas will not be null if this is a "onefile" hub
31003
+ if (trackStanzas) {
31004
+ const genomeId = genomeStanzas[0].getProperty("genome"); // Assumption here this is a single genome hub
31005
+ this.trackHubMap.set(genomeId, new TrackDbHub(trackStanzas, groupStanzas));
30440
31006
  }
30441
31007
  }
30442
31008
 
30443
- getDefaultPosition() {
30444
- return this.genomeStanza.getProperty("defaultPos")
31009
+
31010
+ getName() {
31011
+ return this.hubStanza.getProperty("hub")
30445
31012
  }
30446
31013
 
31014
+ getShortLabel() {
31015
+ return this.hubStanza.getProperty("shortLabel")
31016
+ }
31017
+
31018
+ getLongLabel() {
31019
+ return this.hubStanza.getProperty("longLabel")
31020
+ }
31021
+
31022
+ getDescriptionUrl() {
31023
+ return this.hubStanza.getProperty("descriptionUrl")
31024
+ }
31025
+
31026
+
30447
31027
  /* Example genome stanza
30448
31028
  genome GCF_000186305.1
30449
31029
  taxId 176946
@@ -30462,133 +31042,126 @@
30462
31042
  isPcr dynablat-01.soe.ucsc.edu 4040 dynamic GCF/000/186/305/GCF_000186305.1
30463
31043
  */
30464
31044
 
30465
- getGenomeConfig(options = {}) {
30466
- // TODO -- add blat? htmlPath?
31045
+ getGenomeConfig(genomeId) {
31046
+
31047
+ const genomeStanza = genomeId ? this.genomeStanzas.find(s => s.getProperty("genome") === genomeId) : this.genomeStanzas[0];
31048
+ if (!genomeStanza) {
31049
+ throw new Error(`Genome not found in hub: ${genomeId}`)
31050
+ }
31051
+ return this.#getGenomeConfig(genomeStanza)
31052
+ }
31053
+
31054
+ #getGenomeConfig(genomeStanza) {
30467
31055
 
30468
- const id = this.genomeStanza.getProperty("genome");
31056
+ const id = genomeStanza.getProperty("genome");
30469
31057
  const gsName =
30470
31058
  this.hubStanza.getProperty("shortLabel") ||
30471
- this.genomeStanza.getProperty("scientificName") ||
30472
- this.genomeStanza.getProperty("organism") ||
30473
- this.genomeStanza.getProperty("description");
31059
+ genomeStanza.getProperty("scientificName") ||
31060
+ genomeStanza.getProperty("organism") ||
31061
+ genomeStanza.getProperty("description");
30474
31062
  const name = gsName + (gsName ? ` (${id})` : ` ${id}`);
30475
31063
 
30476
31064
  const config = {
30477
- hubURL: this.url,
31065
+
30478
31066
  id: id,
30479
31067
  name: name,
30480
- twoBitURL: this.baseURL + this.genomeStanza.getProperty("twoBitPath"),
31068
+ twoBitURL: genomeStanza.getProperty("twoBitPath"),
30481
31069
  nameSet: "ucsc",
31070
+ hubs: [this.url]
30482
31071
  };
30483
31072
 
30484
- if (this.genomeStanza.hasProperty("chromSizesURL")) {
30485
- config.chromSizesURL = this.genomeStanza.getProperty("chromSizesURL");
31073
+ if (genomeStanza.hasProperty("chromSizes")) {
31074
+ config.chromSizesURL = genomeStanza.getProperty("chromSizes");
30486
31075
  } else {
30487
31076
  config.wholeGenomeView = false;
30488
31077
  config.showChromosomeWidget = false;
30489
31078
  }
30490
31079
 
30491
- if (this.genomeStanza.hasProperty("defaultPos")) {
30492
- const hubLocus = this.genomeStanza.getProperty("defaultPos");
31080
+ if (genomeStanza.hasProperty("defaultPos")) {
31081
+ const hubLocus = genomeStanza.getProperty("defaultPos");
30493
31082
  // Strip out coordinates => whole chromosome view
30494
- if (hubLocus) {
30495
- const idx = hubLocus.lastIndexOf(":");
30496
- config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus;
30497
- }
31083
+ // if (hubLocus) {
31084
+ // const idx = hubLocus.lastIndexOf(":")
31085
+ // config.locus = idx > 0 ? hubLocus.substring(0, idx) : hubLocus
31086
+ // }
31087
+ config.locus = hubLocus;
30498
31088
  }
30499
31089
 
30500
- if (this.genomeStanza.hasProperty("blat")) {
30501
- config.blat = this.baseURL + this.genomeStanza.getProperty("blat");
30502
- }
30503
- if (this.genomeStanza.hasProperty("chromAliasBb")) {
30504
- config.chromAliasBbURL = this.baseURL + this.genomeStanza.getProperty("chromAliasBb");
31090
+ if (genomeStanza.hasProperty("blat")) {
31091
+ config.blat = genomeStanza.getProperty("blat");
30505
31092
  }
30506
- if (this.genomeStanza.hasProperty("chromAlias")) {
30507
- config.aliasURL = this.baseURL + this.genomeStanza.getProperty("chromAlias");
31093
+ if (genomeStanza.hasProperty("chromAliasBb")) {
31094
+ config.chromAliasBbURL = genomeStanza.getProperty("chromAliasBb");
30508
31095
  }
30509
- if (this.genomeStanza.hasProperty("twoBitBptURL")) {
30510
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptURL");
31096
+ if (genomeStanza.hasProperty("chromAlias")) {
31097
+ config.aliasURL = genomeStanza.getProperty("chromAlias");
30511
31098
  }
30512
-
30513
- if (this.genomeStanza.hasProperty("twoBitBptUrl")) {
30514
- config.twoBitBptURL = this.baseURL + this.genomeStanza.getProperty("twoBitBptUrl");
31099
+ if (genomeStanza.hasProperty("twoBitBptURL")) {
31100
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptURL");
30515
31101
  }
30516
31102
 
30517
- // chromSizes can take a very long time to load, and is not useful with the default WGV = off
30518
- if (options.includeChromSizes && this.genomeStanza.hasProperty("chromSizes")) {
30519
- config.chromSizesURL = this.baseURL + this.genomeStanza.getProperty("chromSizes");
31103
+ if (genomeStanza.hasProperty("twoBitBptUrl")) {
31104
+ config.twoBitBptURL = genomeStanza.getProperty("twoBitBptUrl");
30520
31105
  }
30521
31106
 
30522
31107
  if (this.hubStanza.hasProperty("longLabel")) {
30523
31108
  config.description = this.hubStanza.getProperty("longLabel").replace("/", "\n");
30524
31109
  } else {
30525
31110
  config.description = config.id;
30526
- if (this.genomeStanza.hasProperty("description")) {
30527
- config.description += `\n${this.genomeStanza.getProperty("description")}`;
31111
+ if (genomeStanza.hasProperty("description")) {
31112
+ config.description += `\n${genomeStanza.getProperty("description")}`;
30528
31113
  }
30529
- if (this.genomeStanza.hasProperty("organism")) {
30530
- config.description += `\n${this.genomeStanza.getProperty("organism")}`;
31114
+ if (genomeStanza.hasProperty("organism")) {
31115
+ config.description += `\n${genomeStanza.getProperty("organism")}`;
30531
31116
  }
30532
- if (this.genomeStanza.hasProperty("scientificName")) {
30533
- config.description += `\n${this.genomeStanza.getProperty("scientificName")}`;
31117
+ if (genomeStanza.hasProperty("scientificName")) {
31118
+ config.description += `\n${genomeStanza.getProperty("scientificName")}`;
30534
31119
  }
30535
31120
 
30536
- if (this.genomeStanza.hasProperty("htmlPath")) {
30537
- config.infoURL = this.baseURL + this.genomeStanza.getProperty("htmlPath");
31121
+ if (genomeStanza.hasProperty("htmlPath")) {
31122
+ config.infoURL = genomeStanza.getProperty("htmlPath");
30538
31123
  }
30539
31124
  }
30540
31125
 
30541
- // Search for cytoband
30542
- /*
30543
- track cytoBandIdeo
30544
- shortLabel Chromosome Band (Ideogram)
30545
- longLabel Ideogram for Orientation
30546
- group map
30547
- visibility dense
30548
- type bigBed 4 +
30549
- bigDataUrl bbi/GCA_004027145.1_DauMad_v1_BIUU.cytoBand.bb
30550
- */
30551
- const cytoStanza = this.trackStanzas.filter(t => "cytoBandIdeo" === t.name && t.hasProperty("bigDataUrl"));
30552
- if (cytoStanza.length > 0) {
30553
- config.cytobandBbURL = this.baseURL + cytoStanza[0].getProperty("bigDataUrl");
30554
- }
30555
-
30556
31126
  // Tracks.
30557
31127
  const filter = (t) => !Hub.filterTracks.has(t.name) && "hide" !== t.getProperty("visibility");
30558
31128
  config.tracks = this.#getTracksConfigs(filter);
30559
31129
 
30560
-
30561
31130
  return config
30562
31131
  }
30563
31132
 
30564
- getGroupedTrackConfigurations() {
30565
-
30566
- // Organize track configs by group
30567
- const trackConfigMap = new Map();
30568
- for (let c of this.#getTracksConfigs()) {
30569
- if (c.name === "cytoBandIdeo") continue
30570
- const groupName = c.group || "other";
30571
- if (trackConfigMap.has(groupName)) {
30572
- trackConfigMap.get(groupName).push(c);
30573
- } else {
30574
- trackConfigMap.set(groupName, [c]);
30575
- }
31133
+ async getGroupedTrackConfigurations(genomeId) {
31134
+ let trackHub = await this.#getTrackDbHub(genomeId);
31135
+ if (!trackHub && idMappings.has(genomeId)) {
31136
+ trackHub = await this.#getTrackDbHub(idMappings.get(genomeId));
30576
31137
  }
31138
+ if (!trackHub) {
31139
+ console.log(`Warning: no trackDB found for genomeId ${genomeId}.`);
31140
+ }
31141
+ return trackHub ? trackHub.getGroupedTrackConfigurations() : []
31142
+ }
30577
31143
 
30578
- // Build group structure
30579
- const groupStanazMap = this.groupStanzas ?
30580
- new Map(this.groupStanzas.map(groupStanza => [groupStanza.getProperty("name"), groupStanza])) :
30581
- new Map();
30582
-
30583
- return Array.from(trackConfigMap.keys()).map(groupName => {
30584
- return {
30585
- label: groupStanazMap.has(groupName) ? groupStanazMap.get(groupName).getProperty("label") : groupName,
30586
- tracks: trackConfigMap.get(groupName)
31144
+ async #getTrackDbHub(genomeId) {
31145
+ let trackHub = this.trackHubMap.get(genomeId);
31146
+ if (!trackHub) {
31147
+ for (let stanza of this.genomeStanzas) {
31148
+ if (genomeId === stanza.getProperty("genome")) {
31149
+ try {
31150
+ const trackDbURL = stanza.getProperty("trackDb");
31151
+ const trackStanzas = await loadStanzas(trackDbURL);
31152
+ trackHub = new TrackDbHub(trackStanzas, this.groupStanzas);
31153
+ this.trackHubMap.set(genomeId, trackHub);
31154
+ } catch (error) {
31155
+ console.error(`Error loading trackDb file: ${stanza.getProperty("trackDb")}`, error);
31156
+ }
31157
+ break
31158
+ }
30587
31159
  }
30588
- })
30589
-
31160
+ }
31161
+ return trackHub
30590
31162
  }
30591
31163
 
31164
+
30592
31165
  /**
30593
31166
  * Return an array of igv track config objects that satisfy the filter
30594
31167
  */
@@ -30626,7 +31199,7 @@
30626
31199
  "id": t.getProperty("track"),
30627
31200
  "name": t.getProperty("shortLabel"),
30628
31201
  "format": format,
30629
- "url": this.baseURL + t.getProperty("bigDataUrl"),
31202
+ "url": t.getProperty("bigDataUrl"),
30630
31203
  "displayMode": t.displayMode,
30631
31204
  };
30632
31205
 
@@ -30637,7 +31210,7 @@
30637
31210
  if (t.hasProperty("longLabel") && t.hasProperty("html")) {
30638
31211
  if (config.description) config.description += "<br/>";
30639
31212
  config.description =
30640
- `<a target="_blank" href="${this.baseURL + t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
31213
+ `<a target="_blank" href="${t.getProperty("html")}">${t.getProperty("longLabel")}</a>`;
30641
31214
  } else if (t.hasProperty("longLabel")) {
30642
31215
  config.description = t.getProperty("longLabel");
30643
31216
  }
@@ -30669,7 +31242,7 @@
30669
31242
  max = Number.parseInt(tokens[1]);
30670
31243
  }
30671
31244
  if (Number.isNaN(max) || Number.isNaN(min)) {
30672
- console.warn(`Unexpected viewLimits value in track line: ${properties["viewLimits"]}`);
31245
+ console.warn(`Unexpected viewLimits value in track line: ${t.getProperty("viewLimits")}`);
30673
31246
  } else {
30674
31247
  config.min = min;
30675
31248
  config.max = max;
@@ -30688,15 +31261,15 @@
30688
31261
  config.searchIndex = t.getProperty("searchIndex");
30689
31262
  }
30690
31263
  if (t.hasProperty("searchTrix")) {
30691
- config.trixURL = this.baseURL + t.getProperty("searchTrix");
31264
+ config.trixURL = t.getProperty("searchTrix");
30692
31265
  }
30693
31266
 
30694
31267
  if (t.hasProperty("group")) {
30695
- config.group = t.getProperty("group");
30696
- if (this.groupPriorityMap && this.groupPriorityMap.has(config.group)) {
30697
- const nextPriority = this.groupPriorityMap.get(config.group) + 1;
31268
+ config._group = t.getProperty("group");
31269
+ if (this.groupPriorityMap && this.groupPriorityMap.has(config._group)) {
31270
+ const nextPriority = this.groupPriorityMap.get(config._group) + 1;
30698
31271
  config.order = nextPriority;
30699
- this.groupPriorityMap.set(config.group, nextPriority);
31272
+ this.groupPriorityMap.set(config._group, nextPriority);
30700
31273
  }
30701
31274
  }
30702
31275
 
@@ -30705,96 +31278,92 @@
30705
31278
 
30706
31279
  }
30707
31280
 
30708
- function firstWord(str) {
30709
- const idx = str.indexOf(' ');
30710
- return idx > 0 ? str.substring(0, idx) : str
30711
- }
31281
+ /*
31282
+ https://genomewiki.ucsc.edu/index.php/Assembly_Hubs
31283
+ https://genome.ucsc.edu/goldenpath/help/hgTrackHubHelp.html
31284
+ https://genome.ucsc.edu/goldenPath/help/hgTrackHubHelp
31285
+ https://genome.ucsc.edu/goldenpath/help/trackDb/trackDbHub.html
31286
+ */
30712
31287
 
30713
- class Stanza {
31288
+ const urlProperties = new Set(["descriptionUrl", "desriptionUrl",
31289
+ "twoBitPath", "blat", "chromAliasBb", "twoBitBptURL", "twoBitBptUrl", "htmlPath", "bigDataUrl",
31290
+ "genomesFile", "trackDb", "groups", "include", "html", "searchTrix", "groups",
31291
+ "chromSizes"]);
30714
31292
 
30715
- properties = new Map()
30716
31293
 
30717
- constructor(type, name) {
30718
- this.type = type;
30719
- this.name = name;
30720
- }
31294
+ const hubCache = new Map();
30721
31295
 
30722
- setProperty(key, value) {
30723
- this.properties.set(key, value);
31296
+ async function loadHub(url) {
31297
+ if (hubCache.has(url)) {
31298
+ return hubCache.get(url)
30724
31299
  }
30725
31300
 
30726
- getProperty(key) {
30727
- if (this.properties.has(key)) {
30728
- return this.properties.get(key)
30729
- } else if (this.parent) {
30730
- return this.parent.getProperty(key)
30731
- } else {
30732
- return undefined
30733
- }
31301
+ const stanzas = await loadStanzas(url);
31302
+ if (stanzas.length < 1) {
31303
+ throw new Error("Empty hub file")
30734
31304
  }
30735
31305
 
30736
- hasProperty(key) {
30737
- if (this.properties.has(key)) {
30738
- return true
30739
- } else if (this.parent) {
30740
- return this.parent.hasProperty(key)
30741
- } else {
30742
- return false
30743
- }
31306
+ const hubStanza = stanzas[0];
31307
+ if (hubStanza.type !== "hub") {
31308
+ throw new Error("First stanza must be a hub stanza")
30744
31309
  }
30745
31310
 
30746
- get format() {
30747
- const type = this.getProperty("type");
30748
- if (type) {
30749
- // Trim extra bed qualifiers (e.g. bigBed + 4)
30750
- return firstWord(type)
31311
+ let genomeStanzas;
31312
+ let trackStanzas;
31313
+ if (hubStanza.getProperty("useOneFile") === "on") {
31314
+ // This is a "onefile" hub, all stanzas are in the same file
31315
+ if (stanzas[1].type !== "genome") {
31316
+ throw new Error("Unexpected hub file -- expected 'genome' stanza but found " + stanzas[1].type)
30751
31317
  }
30752
- return undefined // unknown type
30753
- }
31318
+ const genomeStanza = stanzas[1];
31319
+ genomeStanzas = [genomeStanza];
31320
+ trackStanzas = stanzas.slice(2);
30754
31321
 
30755
- /**
30756
- * IGV display mode
30757
- */
30758
- get displayMode() {
30759
- let viz = this.getProperty("visibility");
30760
- if (!viz) {
30761
- return "COLLAPSED"
30762
- } else {
30763
- viz = viz.toLowerCase();
30764
- switch (viz) {
30765
- case "dense":
30766
- return "COLLAPSED"
30767
- case "pack":
30768
- return "EXPANDED"
30769
- case "squish":
30770
- return "SQUISHED"
30771
- default:
30772
- return "COLLAPSED"
31322
+ // If this is an assembly check chromSizes. This file can be very large, and not needed if whole genome view
31323
+ // is not enabled. Remove it if > 100 kb
31324
+ if (genomeStanza.hasOwnProperty("chromSizes")) {
31325
+ const chromSizes = genomeStanza.getProperty("chromSizes");
31326
+ try {
31327
+ const contentLength = await igvxhr.getContentLength(chromSizes);
31328
+ if (contentLength > 100000) {
31329
+ genomeStanza.removeProperty("chromSizes");
31330
+ }
31331
+ } catch (e) {
31332
+ console.error(`Error getting content length for chromSizes ${chromSizes}`, e);
30773
31333
  }
30774
- }
30775
- }
30776
- }
30777
31334
 
31335
+ }
30778
31336
 
30779
- /**
30780
- * Return the content length of the resource. If the content length cannot be determined return null;
30781
- * @param url
30782
- * @returns {Promise<number|string>}
30783
- */
30784
- async function getContentLength(url) {
30785
- try {
30786
- const response = await fetch(url, {method: 'HEAD'});
30787
- const headers = response.headers;
30788
- if (headers.has("content-length")) {
30789
- return headers.get("content-length")
30790
- } else {
30791
- return null
31337
+ } else {
31338
+ if (!hubStanza.hasProperty("genomesFile")) {
31339
+ throw new Error("hub.txt must specify 'genomesFile'")
30792
31340
  }
30793
- } catch (e) {
30794
- return null
31341
+ genomeStanzas = await loadStanzas(hubStanza.getProperty("genomesFile"));
30795
31342
  }
31343
+
31344
+ // Load group files for all genomes, if any.
31345
+ const uniqGroupURLs = new Set();
31346
+ genomeStanzas.forEach(s => {
31347
+ const groupURL = s.getProperty("groups");
31348
+ if (groupURL) uniqGroupURLs.add(groupURL);
31349
+
31350
+ });
31351
+ const groupStanzas = [];
31352
+ const groupPromises = Array.from(uniqGroupURLs).map(async url => {
31353
+ const stanza = await loadStanzas(url);
31354
+ return stanza
31355
+ });
31356
+ const groupResults = await Promise.all(groupPromises);
31357
+ groupResults.forEach(stanza => groupStanzas.push(...stanza));
31358
+
31359
+ const hub = new Hub(url, hubStanza, genomeStanzas, trackStanzas, groupStanzas);
31360
+
31361
+ hubCache.set(url, hub);
31362
+
31363
+ return hub
30796
31364
  }
30797
31365
 
31366
+
30798
31367
  /**
30799
31368
  * Parse a UCSC file
30800
31369
  * @param url
@@ -30802,38 +31371,92 @@
30802
31371
  */
30803
31372
  async function loadStanzas(url) {
30804
31373
 
30805
- const response = await fetch(url);
30806
- const data = await response.text();
31374
+ const idx = url.lastIndexOf("/");
31375
+ const baseURL = url.substring(0, idx + 1);
31376
+ const host = getHost(url);
31377
+
31378
+ //const response = await fetch(url)
31379
+ const data = await igvxhr.loadString(url, {}); //await response.text()
30807
31380
  const lines = data.split(/\n|\r\n|\r/g);
30808
31381
 
30809
31382
  const nodes = [];
30810
31383
  let currentNode;
30811
31384
  let startNewNode = true;
30812
- for (let line of lines) {
30813
- const indent = indentLevel(line);
30814
- const i = line.indexOf(' ', indent);
30815
- if (i < 0) {
31385
+ for (let i = 0; i < lines.length; i++) {
31386
+
31387
+ let line = lines[i].trim();
31388
+
31389
+ if (line.length == 0) {
30816
31390
  // Break - start a new node
30817
31391
  startNewNode = true;
30818
31392
  } else {
30819
- const key = line.substring(indent, i).trim();
30820
- if (key.startsWith("#")) continue
30821
- const value = line.substring(i + 1).trim();
31393
+ if (line.startsWith("#")) {
31394
+ continue
31395
+ }
31396
+
31397
+ while (line.endsWith('\\')) {
31398
+ i++;
31399
+ if (i >= lines.length) {
31400
+ break
31401
+ }
31402
+ line = line.substring(0, line.length - 1) + lines[i].trim();
31403
+ }
31404
+
31405
+ if (line.startsWith("include")) {
31406
+ const relativeURL = line.substring(8).trim();
31407
+ const includeURL = getDataURL(relativeURL, host, baseURL);
31408
+ const includeStanzas = await loadStanzas(includeURL);
31409
+ for (let s of includeStanzas) {
31410
+ nodes.push(s);
31411
+ }
31412
+ }
31413
+
31414
+
31415
+ const i = line.indexOf(' ');
31416
+ const key = line.substring(0, i).trim();
31417
+ let value = line.substring(i + 1).trim();
31418
+
31419
+ if (key === "type") {
31420
+ // The "type" property contains format and sometimes other information. For example, data range
31421
+ // on a bigwig "type bigWig 0 .5"
31422
+ const tokens = value.split(/\s+/);
31423
+ value = tokens[0];
31424
+ if (value === "bigWig" && tokens.length === 3) {
31425
+ // This is a bigWig with a range
31426
+ const min = tokens[1];
31427
+ const max = tokens[2];
31428
+ if (currentNode) {
31429
+ currentNode.setProperty("min", min);
31430
+ currentNode.setProperty("max", max);
31431
+ }
31432
+ }
31433
+
31434
+ } else if (!["shortLabel", "longLabel", "metadata", "label"].includes(key)) {
31435
+ const tokens = value.split(/\s+/);
31436
+ value = tokens[0];
31437
+ }
31438
+
31439
+ if (urlProperties.has(key) || value.endsWith("URL") || value.endsWith("Url")) {
31440
+ value = getDataURL(value, host, baseURL);
31441
+ }
31442
+
30822
31443
  if (startNewNode) {
30823
- // Start a new node -- indent is currently ignored as igv.js does not support sub-tracks,
30824
- // so track stanzas are flattened
30825
- const newNode = new Stanza(key, value);
30826
- nodes.push(newNode);
30827
- currentNode = newNode;
31444
+ currentNode = new Stanza(key, value);
31445
+ nodes.push(currentNode);
30828
31446
  startNewNode = false;
30829
31447
  }
31448
+
30830
31449
  currentNode.setProperty(key, value);
30831
31450
  }
30832
31451
  }
30833
-
30834
31452
  return resolveParents(nodes)
30835
31453
  }
30836
31454
 
31455
+ function firstWord(str) {
31456
+ const idx = str.indexOf(' ');
31457
+ return idx > 0 ? str.substring(0, idx) : str
31458
+ }
31459
+
30837
31460
  function resolveParents(nodes) {
30838
31461
  const nodeMap = new Map();
30839
31462
  for (let n of nodes) {
@@ -30848,17 +31471,34 @@
30848
31471
  return nodes
30849
31472
  }
30850
31473
 
30851
- function indentLevel(str) {
30852
- let level = 0;
30853
- for (level = 0; level < str.length; level++) {
30854
- const c = str.charAt(level);
30855
- if (c !== ' ' && c !== '\t') break
31474
+ function getDataURL(url, host, baseURL) {
31475
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("gs://") || url.startsWith("s3://")) {
31476
+ return url
31477
+ } else if (url.startsWith("/")) {
31478
+ return host + url
31479
+ } else {
31480
+ return baseURL + url
30856
31481
  }
30857
- return level
31482
+ }
31483
+
31484
+ function getHost(url) {
31485
+ let host;
31486
+ if (url.startsWith("https://") || url.startsWith("http://")) {
31487
+ try {
31488
+ const tmp = new URL(url);
31489
+ host = `${tmp.protocol}//${tmp.host}`;
31490
+ } catch (e) {
31491
+ console.error("Error parsing base URL host", e);
31492
+ throw e
31493
+ }
31494
+ } else {
31495
+ host = '';
31496
+ }
31497
+ return host
30858
31498
  }
30859
31499
 
30860
31500
  const DEFAULT_GENOMES_URL = "https://igv.org/genomes/genomes.json";
30861
- const BACKUP_GENOMES_URL = "https://raw.githubusercontent.com/igvteam/igv.js/main/packages/igv/src/genomes/genomes.json";
31501
+ const BACKUP_GENOMES_URL = "https://raw.githubusercontent.com/igvteam/igv-genomes/refs/heads/main/dist/genomes.json";
30862
31502
 
30863
31503
  const GenomeUtils = {
30864
31504
 
@@ -30866,7 +31506,7 @@
30866
31506
 
30867
31507
  if (!GenomeUtils.KNOWN_GENOMES) {
30868
31508
 
30869
- GenomeUtils.KNOWN_GENOMES = {};
31509
+ let table = {};
30870
31510
 
30871
31511
  const processJson = (jsonArray, table) => {
30872
31512
  jsonArray.forEach(function (json) {
@@ -30879,12 +31519,12 @@
30879
31519
  if (config.loadDefaultGenomes !== false) {
30880
31520
  try {
30881
31521
  const jsonArray = await igvxhr.loadJson(DEFAULT_GENOMES_URL, {timeout: 2000});
30882
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31522
+ processJson(jsonArray, table);
30883
31523
  } catch (error) {
30884
31524
  try {
30885
31525
  console.error("Error initializing default genomes:", error);
30886
31526
  const jsonArray = await igvxhr.loadJson(BACKUP_GENOMES_URL, {timeout: 2000});
30887
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31527
+ processJson(jsonArray, table);
30888
31528
  } catch (e) {
30889
31529
  console.error("Error initializing backup genomes:", error);
30890
31530
  }
@@ -30896,11 +31536,12 @@
30896
31536
  if (genomeList) {
30897
31537
  if (typeof genomeList === 'string') {
30898
31538
  const jsonArray = await igvxhr.loadJson(genomeList, {});
30899
- processJson(jsonArray, GenomeUtils.KNOWN_GENOMES);
31539
+ processJson(jsonArray, table);
30900
31540
  } else {
30901
- processJson(genomeList, GenomeUtils.KNOWN_GENOMES);
31541
+ processJson(genomeList, table);
30902
31542
  }
30903
31543
  }
31544
+ GenomeUtils.KNOWN_GENOMES = table;
30904
31545
  }
30905
31546
  },
30906
31547
 
@@ -30937,8 +31578,8 @@
30937
31578
  if ((genomeID.startsWith("GCA_") || genomeID.startsWith("GCF_")) && genomeID.length >= 13) {
30938
31579
  try {
30939
31580
  const hubURL = convertToHubURL(genomeID);
30940
- const hub = await Hub.loadHub(hubURL);
30941
- reference = hub.getGenomeConfig();
31581
+ const hub = await loadHub(hubURL);
31582
+ reference = hub.getGenomeConfig(genomeID);
30942
31583
  } catch (e) {
30943
31584
  console.error(e);
30944
31585
  }
@@ -32900,27 +33541,18 @@
32900
33541
  return [ r, g, b ]
32901
33542
  });
32902
33543
 
32903
- const colorForNA = appleCrayonRGB('magnesium');
32904
- const sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors'];
32905
-
32906
33544
  class SampleInfo {
32907
33545
 
32908
33546
  static emptySpaceReplacement = '|'
32909
-
32910
- sampleDictionary = {}
32911
- attributeNames = []
32912
- sampleMappingDictionary = {}
32913
- colorDictionary = {}
32914
- attributeRangeLUT = {}
33547
+ static colorForNA = appleCrayonRGB('magnesium')
33548
+ static sampleInfoFileHeaders = ['#sampleTable', '#sampleMapping', '#colors']
32915
33549
 
32916
33550
  constructor(browser) {
32917
-
32918
33551
  const found = browser.tracks.some(t => typeof t.getSamples === 'function');
32919
33552
  if (found.length > 0) {
32920
33553
  browser.sampleInfoControl.setButtonVisibility(true);
32921
33554
  }
32922
33555
  this.initialize();
32923
-
32924
33556
  }
32925
33557
 
32926
33558
  initialize() {
@@ -32947,47 +33579,60 @@
32947
33579
 
32948
33580
  getAttributes(sampleName) {
32949
33581
 
32950
- const key = 0 === Object.keys(this.sampleMappingDictionary) ? sampleName : (this.sampleMappingDictionary[sampleName] || sampleName);
33582
+ const key = this.sampleMappingDictionary[sampleName] || sampleName;
32951
33583
  return this.sampleDictionary[key]
32952
33584
  }
32953
33585
 
32954
- async loadSampleInfoFile(path) {
32955
- try {
32956
- const string = await igvxhr.loadString(path);
32957
- this.#processSampleInfoFileAsString(string);
32958
- this.sampleInfoFiles.push(path);
32959
- } catch (e) {
32960
- console.error(e.message);
33586
+ async loadSampleInfo(config) {
33587
+
33588
+ if (config.url) {
33589
+ await this.loadSampleInfoFile(config.url);
33590
+ } else {
33591
+
33592
+ const samples = { ...config };
33593
+ for (const [key, record] of Object.entries(samples)) {
33594
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33595
+ }
33596
+
33597
+ const [ value ] = Object.values(samples);
33598
+ const attributes = Object.keys(value);
33599
+
33600
+ this.loadSampleInfoHelper(attributes, samples);
33601
+
32961
33602
  }
32962
- }
32963
33603
 
32964
- #processSampleInfoFileAsString(string) {
33604
+ this.initialized = true;
33605
+ }
32965
33606
 
32966
- const sectionDictionary = createSectionDictionary(string);
33607
+ loadSampleInfoHelper(attributes, samples){
32967
33608
 
32968
- for (const [header, value] of Object.entries(sectionDictionary)) {
32969
- switch (header) {
32970
- case '#sampleTable':
32971
- this.#accumulateSampleTableDictionary(value);
32972
- break
32973
- case '#sampleMapping':
32974
- this.#accumulateSampleMappingDictionary(value);
32975
- break
32976
- case '#colors':
32977
- this.#accumulateColorScheme(value);
32978
- break
33609
+ // Establish the range of values for each attribute
33610
+ const lut = createAttributeRangeLUT(attributes, samples);
33611
+ accumulateDictionary(this.attributeRangeLUT, lut);
32979
33612
 
33613
+ // Ensure unique attribute names list
33614
+ const currentAttributeNameSet = new Set(this.attributeNames);
33615
+ for (const name of attributes) {
33616
+ if (!currentAttributeNameSet.has(name)) {
33617
+ this.attributeNames.push(name);
32980
33618
  }
32981
33619
  }
32982
33620
 
32983
- this.initialized = true;
33621
+ accumulateDictionary(this.sampleDictionary, samples);
33622
+
33623
+ }
32984
33624
 
33625
+ async loadSampleInfoFile(path) {
33626
+ try {
33627
+ const string = await igvxhr.loadString(path);
33628
+ this.#processSampleInfoFileAsString(string);
33629
+ this.sampleInfoFiles.push(path);
33630
+ } catch (e) {
33631
+ console.error(e.message);
33632
+ }
32985
33633
  }
32986
33634
 
32987
33635
  getAttributeColor(attribute, value) {
32988
- // if (value === 'NA') {
32989
- // console.log(`${ attribute } : ${ value }`)
32990
- // }
32991
33636
 
32992
33637
  let color;
32993
33638
 
@@ -33005,7 +33650,7 @@
33005
33650
 
33006
33651
  } else if (typeof value === "string") {
33007
33652
 
33008
- color = 'NA' === value ? colorForNA : stringToRGBString(value);
33653
+ color = 'NA' === value ? SampleInfo.colorForNA : SampleInfo.stringToRGBString(value);
33009
33654
 
33010
33655
  } else {
33011
33656
 
@@ -33073,6 +33718,27 @@
33073
33718
  return json
33074
33719
  }
33075
33720
 
33721
+ #processSampleInfoFileAsString(string) {
33722
+
33723
+ const sectionDictionary = createSectionDictionary(string);
33724
+
33725
+ for (const [header, value] of Object.entries(sectionDictionary)) {
33726
+ switch (header) {
33727
+ case '#sampleTable':
33728
+ this.#accumulateSampleTableDictionary(value);
33729
+ break
33730
+ case '#sampleMapping':
33731
+ this.#accumulateSampleMappingDictionary(value);
33732
+ break
33733
+ case '#colors':
33734
+ this.#accumulateColorScheme(value);
33735
+ break
33736
+
33737
+ }
33738
+ }
33739
+
33740
+ }
33741
+
33076
33742
  #accumulateSampleTableDictionary(lines) {
33077
33743
 
33078
33744
  // shift array with first item that is 'sample' or 'Linking_id'. Remaining items are attribute names
@@ -33112,22 +33778,11 @@
33112
33778
  } // for (lines)
33113
33779
 
33114
33780
  for (const [key, record] of Object.entries(samples)) {
33115
- samples[key] = toNumericalRepresentation(record);
33781
+ samples[key] = SampleInfo.toNumericalRepresentation(record);
33116
33782
  }
33117
33783
 
33118
- // Establish the range of values for each attribute
33119
- const lut = createAttributeRangeLUT(attributes, samples);
33120
- accumulateDictionary(this.attributeRangeLUT, lut);
33121
-
33122
- // Ensure unique attribute names list
33123
- const currentAttributeNameSet = new Set(this.attributeNames);
33124
- for (const name of attributes) {
33125
- if (!currentAttributeNameSet.has(name)) {
33126
- this.attributeNames.push(name);
33127
- }
33128
- }
33784
+ this.loadSampleInfoHelper(attributes, samples);
33129
33785
 
33130
- accumulateDictionary(this.sampleDictionary, samples);
33131
33786
  }
33132
33787
 
33133
33788
  #accumulateSampleMappingDictionary(lines) {
@@ -33228,7 +33883,7 @@
33228
33883
  this.colorDictionary[attribute] = attributeValue => {
33229
33884
 
33230
33885
  if ('NA' === attributeValue) {
33231
- return colorForNA
33886
+ return SampleInfo.colorForNA
33232
33887
  } else {
33233
33888
  const [min, max] = this.attributeRangeLUT[attribute];
33234
33889
  const interpolant = (attributeValue - min) / (max - min);
@@ -33248,8 +33903,34 @@
33248
33903
 
33249
33904
  }
33250
33905
 
33251
- }
33906
+ static toNumericalRepresentation(obj) {
33907
+ const result = Object.assign({}, obj);
33908
+
33909
+ for (const [key, value] of Object.entries(result)) {
33910
+ if (typeof value === 'string' && !isNaN(value)) {
33911
+ result[key] = Number(value);
33912
+ }
33913
+ }
33914
+
33915
+ return result
33916
+ }
33917
+
33918
+ static stringToRGBString(str) {
33919
+ let hash = 0;
33920
+ for (let i = 0; i < str.length; i++) {
33921
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
33922
+ }
33252
33923
 
33924
+ let color = [];
33925
+ for (let i = 0; i < 3; i++) {
33926
+ const value = (hash >> (i * 8)) & 0xff;
33927
+ color.push(value);
33928
+ }
33929
+
33930
+ return `rgb(${color.join(', ')})`
33931
+ }
33932
+
33933
+ }
33253
33934
 
33254
33935
  function createSectionDictionary(string) {
33255
33936
 
@@ -33260,14 +33941,14 @@
33260
33941
  let currentHeader;
33261
33942
 
33262
33943
  // If the first line does not start with a section header an initial #sampleTable is implied
33263
- if (!sampleInfoFileHeaders.includes(lines[0])) {
33944
+ if (!SampleInfo.sampleInfoFileHeaders.includes(lines[0])) {
33264
33945
  currentHeader = '#sampleTable';
33265
33946
  dictionary[currentHeader] = [];
33266
33947
  }
33267
33948
 
33268
33949
  for (const line of lines) {
33269
33950
 
33270
- if (sampleInfoFileHeaders.includes(line)) {
33951
+ if (SampleInfo.sampleInfoFileHeaders.includes(line)) {
33271
33952
  currentHeader = line;
33272
33953
  dictionary[currentHeader] = [];
33273
33954
  } else if (currentHeader && false === line.startsWith('#')) {
@@ -33286,7 +33967,6 @@
33286
33967
  }
33287
33968
  }
33288
33969
 
33289
-
33290
33970
  function createAttributeRangeLUT(names, dictionary) {
33291
33971
 
33292
33972
  const lut = {};
@@ -33332,35 +34012,6 @@
33332
34012
  return lut
33333
34013
  }
33334
34014
 
33335
- function toNumericalRepresentation(obj) {
33336
-
33337
- const result = Object.assign({}, obj);
33338
-
33339
- for (const [key, value] of Object.entries(result)) {
33340
- if (typeof value === 'string' && !isNaN(value)) {
33341
- result[key] = Number(value);
33342
- }
33343
- }
33344
-
33345
- return result
33346
- }
33347
-
33348
- function stringToRGBString(str) {
33349
-
33350
- let hash = 0;
33351
- for (let i = 0; i < str.length; i++) {
33352
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
33353
- }
33354
-
33355
- let color = [];
33356
- for (let i = 0; i < 3; i++) {
33357
- const value = (hash >> (i * 8)) & 0xff;
33358
- color.push(value);
33359
- }
33360
-
33361
- return `rgb(${color.join(', ')})`
33362
- }
33363
-
33364
34015
  const sampleInfoTileXShim = 8;
33365
34016
  const sampleInfoTileWidth = 16;
33366
34017
 
@@ -33969,7 +34620,11 @@
33969
34620
 
33970
34621
  checkCanvas() {
33971
34622
 
33972
- const width = this.browser.sampleNameViewportWidth || 0;
34623
+ let width = 0;
34624
+ if (true === this.browser.showSampleNames) {
34625
+ width = undefined === this.browser.sampleNameViewportWidth ? 0 : this.browser.sampleNameViewportWidth;
34626
+ }
34627
+
33973
34628
  this.ctx.canvas.width = width * window.devicePixelRatio;
33974
34629
  this.ctx.canvas.style.width = `${width}px`;
33975
34630
 
@@ -35081,7 +35736,7 @@
35081
35736
  let element = document.createElement('div');
35082
35737
  element.textContent = 'Separate tracks';
35083
35738
 
35084
- function click(e) {
35739
+ async function click(e) {
35085
35740
 
35086
35741
  // Capture state which will be nulled when track is removed
35087
35742
  const groupAutoscale = this.autoscale;
@@ -35097,9 +35752,10 @@
35097
35752
  track.autoscaleGroup = name;
35098
35753
  }
35099
35754
  track.isMergedTrack = false;
35100
- browser.addTrack(track.config, track);
35755
+ browser.addTrack(track);
35101
35756
  }
35102
- browser.updateViews();
35757
+ await browser.updateViews();
35758
+ browser.reorderTracks();
35103
35759
  }
35104
35760
 
35105
35761
  return {element, click}
@@ -35194,7 +35850,7 @@
35194
35850
  }
35195
35851
  }
35196
35852
 
35197
- function trackOverlayClickHandler(e) {
35853
+ async function trackOverlayClickHandler(e) {
35198
35854
 
35199
35855
  if (true === isOverlayTrackCriteriaMet(this.browser)) {
35200
35856
 
@@ -35231,9 +35887,9 @@
35231
35887
  track.trackView.dispose();
35232
35888
  }
35233
35889
 
35234
- this.browser.addTrack(config, mergedTrack);
35235
- mergedTrack.trackView.updateViews();
35236
-
35890
+ this.browser.addTrack(mergedTrack);
35891
+ await mergedTrack.trackView.updateViews();
35892
+ this.browser.reorderTracks();
35237
35893
  }
35238
35894
 
35239
35895
  }
@@ -36840,14 +37496,45 @@
36840
37496
 
36841
37497
 
36842
37498
  present(options, e) {
36843
-
36844
37499
  this.label.textContent = options.label;
36845
37500
  this._input.value = options.value;
36846
37501
  this.callback = options.callback || options.click;
36847
37502
 
36848
- const { top} = e.currentTarget.parentElement.getBoundingClientRect();
36849
- this.container.style.top = `${ top }px`;
36850
- this.container.style.display = 'flex';
37503
+ this.container.style.display = '';
37504
+
37505
+ // Get click coordinates
37506
+ const clickX = e.clientX;
37507
+ const clickY = e.clientY;
37508
+
37509
+ // Get dialog dimensions
37510
+ const dialogWidth = this.container.offsetWidth;
37511
+ const dialogHeight = this.container.offsetHeight;
37512
+
37513
+ // Calculate available space
37514
+ const windowWidth = window.innerWidth;
37515
+ const windowHeight = window.innerHeight;
37516
+
37517
+ // Calculate position to keep dialog on screen
37518
+ let left = clickX;
37519
+ let top = clickY;
37520
+
37521
+ // Adjust horizontal position if dialog would go off screen
37522
+ if (left + dialogWidth > windowWidth) {
37523
+ left = windowWidth - dialogWidth - 10; // 10px padding from edge
37524
+ }
37525
+
37526
+ // Adjust vertical position if dialog would go off screen
37527
+ if (top + dialogHeight > windowHeight) {
37528
+ top = windowHeight - dialogHeight - 10; // 10px padding from edge
37529
+ }
37530
+
37531
+ // Ensure minimum distance from edges
37532
+ left = Math.max(10, left);
37533
+ top = Math.max(10, top);
37534
+
37535
+ // Apply positions
37536
+ this.container.style.left = `${left}px`;
37537
+ this.container.style.top = `${top}px`;
36851
37538
  }
36852
37539
  }
36853
37540
 
@@ -47627,7 +48314,7 @@
47627
48314
  if (!this.colorBy) {
47628
48315
  this.colorBy = this.hasPairs ? "unexpectedPair" : "none";
47629
48316
  }
47630
-
48317
+
47631
48318
  let pixelTop = options.pixelTop - BAMTrack.coverageTrackHeight;
47632
48319
  if (this.top) {
47633
48320
  ctx.translate(0, this.top);
@@ -48334,7 +49021,12 @@
48334
49021
  this.trackView.repaintViews();
48335
49022
  }
48336
49023
 
48337
- return {name: undefined, element: createCheckbox(menuItem.label, showCheck), click: clickHandler, init: undefined}
49024
+ return {
49025
+ name: undefined,
49026
+ element: createCheckbox(menuItem.label, showCheck),
49027
+ click: clickHandler,
49028
+ init: undefined
49029
+ }
48338
49030
  }
48339
49031
 
48340
49032
 
@@ -48381,7 +49073,12 @@
48381
49073
  }
48382
49074
  }
48383
49075
 
48384
- return {name: undefined, element: createCheckbox(menuItem.label, showCheck), dialog: clickHandler, init: undefined}
49076
+ return {
49077
+ name: undefined,
49078
+ element: createCheckbox(menuItem.label, showCheck),
49079
+ dialog: clickHandler,
49080
+ init: undefined
49081
+ }
48385
49082
 
48386
49083
  }
48387
49084
 
@@ -48503,38 +49200,88 @@
48503
49200
  });
48504
49201
  }
48505
49202
 
49203
+ list.push('<hr/>');
49204
+ const softClips = clickedAlignment.softClippedBlocks();
48506
49205
  list.push({
48507
49206
  label: 'View read sequence',
48508
49207
  click: () => {
48509
- const seqstring = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
48510
- if (!seqstring || "*" === seqstring) {
48511
- this.browser.alert.present("Read sequence: *");
48512
- } else {
48513
- this.browser.alert.present(seqstring);
48514
- }
49208
+ const seqstring = clickedAlignment.seq;
49209
+ this.browser.alert.present(seqstring && seqstring !== "*" ? seqstring : "Read sequence: *");
48515
49210
  }
48516
49211
  });
48517
49212
 
49213
+ if (softClips.left && softClips.left.len > 0) {
49214
+ list.push({
49215
+ label: 'View left soft-clipped sequence',
49216
+ click: () => {
49217
+ const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
49218
+ this.browser.alert.present(clippedSequence);
49219
+ }
49220
+ });
49221
+ }
49222
+
49223
+ if (softClips.right && softClips.right.len > 0) {
49224
+ list.push({
49225
+ label: 'View right soft-clipped sequence',
49226
+ click: () => {
49227
+ const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
49228
+ this.browser.alert.present(clippedSequence);
49229
+ }
49230
+ });
49231
+ }
49232
+
49233
+ list.push('<hr/>');
49234
+
48518
49235
  if (isSecureContext()) {
48519
49236
  list.push({
48520
49237
  label: 'Copy read sequence',
48521
49238
  click: async () => {
48522
- const seq = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
48523
49239
  try {
48524
- await navigator.clipboard.writeText(seq);
49240
+ await navigator.clipboard.writeText(clickedAlignment.seq);
48525
49241
  } catch (e) {
48526
49242
  console.error(e);
48527
49243
  this.browser.alert.present(`error copying sequence to clipboard ${e}`);
48528
49244
  }
48529
-
48530
49245
  }
48531
49246
  });
49247
+
49248
+ if (softClips.left && softClips.left.len > 0) {
49249
+ list.push({
49250
+ label: 'Copy left soft-clipped sequence',
49251
+ click: async () => {
49252
+ try {
49253
+ const clippedSequence = clickedAlignment.seq.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
49254
+ await navigator.clipboard.writeText(clippedSequence);
49255
+ } catch (e) {
49256
+ console.error(e);
49257
+ this.browser.alert.present(`error copying sequence to clipboard ${e}`);
49258
+ }
49259
+ }
49260
+ });
49261
+ }
49262
+
49263
+ if (softClips.right && softClips.right.len > 0) {
49264
+ list.push({
49265
+ label: 'Copy right soft-clipped sequence',
49266
+ click: async () => {
49267
+ try {
49268
+ const clippedSequence = clickedAlignment.seq.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
49269
+ await navigator.clipboard.writeText(clippedSequence);
49270
+ } catch (e) {
49271
+ console.error(e);
49272
+ this.browser.alert.present(`error copying sequence to clipboard ${e}`);
49273
+ }
49274
+ }
49275
+ });
49276
+ }
48532
49277
  }
48533
49278
 
48534
- // TODO if genome supports blat
49279
+ // TODO test if genome supports blat
48535
49280
  const seqstring = clickedAlignment.seq;
48536
49281
  if (seqstring && "*" !== seqstring) {
48537
49282
 
49283
+ list.push('<hr/>');
49284
+
48538
49285
  if (seqstring.length < maxSequenceSize$1) {
48539
49286
  list.push({
48540
49287
  label: 'BLAT read sequence',
@@ -48552,7 +49299,7 @@
48552
49299
  list.push({
48553
49300
  label: 'BLAT left soft-clipped sequence',
48554
49301
  click: () => {
48555
- const clippedSequence = seqstring.substr(softClips.left.seqOffset, softClips.left.len);
49302
+ const clippedSequence = seqstring.substring(softClips.left.seqOffset, softClips.left.seqOffset + softClips.left.len);
48556
49303
  const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
48557
49304
  const name = `${clickedAlignment.readName} - blat left clip`;
48558
49305
  const title = `${this.name} - ${name}`;
@@ -48564,7 +49311,7 @@
48564
49311
  list.push({
48565
49312
  label: 'BLAT right soft-clipped sequence',
48566
49313
  click: () => {
48567
- const clippedSequence = seqstring.substr(softClips.right.seqOffset, softClips.right.len);
49314
+ const clippedSequence = seqstring.substring(softClips.right.seqOffset, softClips.right.seqOffset + softClips.right.len);
48568
49315
  const sequence = clickedAlignment.isNegativeStrand() ? reverseComplementSequence(clippedSequence) : clippedSequence;
48569
49316
  const name = `${clickedAlignment.readName} - blat right clip`;
48570
49317
  const title = `${this.name} - ${name}`;
@@ -48613,7 +49360,7 @@
48613
49360
  const offsetY = y - this.top;
48614
49361
  const genomicLocation = clickState.genomicLocation;
48615
49362
 
48616
- if(features.packedGroups) {
49363
+ if (features.packedGroups) {
48617
49364
  let minGroupY = Number.MAX_VALUE;
48618
49365
  for (let group of features.packedGroups.values()) {
48619
49366
  minGroupY = Math.min(minGroupY, group.pixelTop);
@@ -50435,7 +51182,7 @@
50435
51182
 
50436
51183
  let url = this.url.slice(); // slice => copy
50437
51184
  if (this.config.oauthToken) {
50438
- const token = resolveToken(this.config.oauthToken);
51185
+ const token = await resolveToken(this.config.oauthToken);
50439
51186
  headers['Authorization'] = `Bearer ${token}`;
50440
51187
  }
50441
51188
 
@@ -55200,7 +55947,7 @@
55200
55947
  async function openH5File(options) {
55201
55948
 
55202
55949
  // Some clients (notably igv-webapp) pass a File reference in the url field. Fix this
55203
- if(options.url && isBlobLike(options.url)) {
55950
+ if (options.url && isBlobLike(options.url)) {
55204
55951
  options.file = options.url;
55205
55952
  options.url = undefined;
55206
55953
  }
@@ -55230,26 +55977,30 @@
55230
55977
 
55231
55978
  async function readExternalIndex(options) {
55232
55979
 
55233
- let indexReader;
55234
- if(options.indexReader) {
55235
- indexReader = options.indexReader;
55236
- }
55237
- else if(options.index) {
55980
+ if (options.index) {
55238
55981
  return options.index
55239
- } else if (options.indexURL) {
55240
- indexReader = new RemoteFile({url: options.indexURL});
55241
- } else if (options.indexPath) {
55242
- indexReader = new NodeLocalFile({path: options.indexPath});
55243
- } else if (options.indexFile) {
55244
- indexReader = new BlobFile({file: options.indexFile});
55245
- }
55246
- if (indexReader) {
55982
+ } else {
55983
+ let indexReader;
55984
+ if (options.indexReader) {
55985
+ indexReader = options.indexReader;
55986
+ } else {
55987
+ let indexOptions;
55988
+ if (options.indexURL) {
55989
+ indexOptions = Object.assign({url: options.indexURL}, options);
55990
+ } else if (options.indexPath) {
55991
+ indexOptions = Object.assign({path: options.indexPath}, options);
55992
+ } else if (options.indexFile) {
55993
+ indexOptions = Object.assign({file: options.indexFile}, options);
55994
+ } else {
55995
+ return undefined
55996
+ }
55997
+ indexReader = getReaderFor(indexOptions);
55998
+ }
55247
55999
  const indexFileContents = await indexReader.read();
55248
56000
  const indexFileJson = new TextDecoder().decode(indexFileContents);
55249
56001
  return JSON.parse(indexFileJson)
55250
- } else {
55251
- return undefined
55252
56002
  }
56003
+
55253
56004
  }
55254
56005
 
55255
56006
 
@@ -55327,9 +56078,9 @@
55327
56078
  * @param {string} h5_file - path for the pytor file
55328
56079
  * @param {integer} bin_size - bin size
55329
56080
  */
55330
- constructor(h5_file, bin_size=100000){
56081
+ constructor(config, bin_size=100000){
55331
56082
 
55332
- this.h5_file = h5_file;
56083
+ this.config = config;
55333
56084
  this.bin_size = bin_size;
55334
56085
  this.h5_obj = undefined;
55335
56086
  this.pytorKeys = [];
@@ -55339,11 +56090,8 @@
55339
56090
  async fetch(){
55340
56091
 
55341
56092
  if(!this.h5_obj) {
55342
- this.h5_obj = await openH5File({
55343
- url: this.h5_file,
55344
- fetchSize: 1000000,
55345
- maxSize: 200000000
55346
- });
56093
+ const options = Object.assign(this.config, {fetchSize: 1000000, maxSize: 200000000});
56094
+ this.h5_obj = await openH5File(options);
55347
56095
  }
55348
56096
  return this.h5_obj
55349
56097
  }
@@ -64379,7 +65127,7 @@ ${indent}columns: ${matrix.columns}
64379
65127
  this.set_available_callers();
64380
65128
 
64381
65129
  } else {
64382
- this.cnvpytor_obj = new HDF5Reader(this.config.url, this.bin_size);
65130
+ this.cnvpytor_obj = new HDF5Reader(this.config, this.bin_size);
64383
65131
  // get chrom list that currently user viewing
64384
65132
  let chroms = [ ...new Set(this.browser.referenceFrameList.map(val => val.chr))];
64385
65133
 
@@ -69367,6 +70115,23 @@ ${indent}columns: ${matrix.columns}
69367
70115
  return this.genome.getChromosome(this.chr)
69368
70116
  }
69369
70117
 
70118
+ /**
70119
+ * Update reference frame based on new viewport width
70120
+ * @param {number} viewportWidth - The calculated viewport width
70121
+ */
70122
+ updateForViewportWidth(viewportWidth) {
70123
+ const {chr} = this;
70124
+ const {bpLength} = this.getChromosome();
70125
+ const viewportWidthBP = this.toBP(viewportWidth);
70126
+
70127
+ // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
70128
+ if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
70129
+ this.bpPerPixel = bpLength / viewportWidth;
70130
+ } else {
70131
+ this.end = this.start + this.toBP(viewportWidth);
70132
+ }
70133
+ }
70134
+
69370
70135
  getMultiLocusLabelBPLengthOnly(pixels) {
69371
70136
  const margin = '&nbsp';
69372
70137
  const ss = Math.floor(this.start) + 1;
@@ -69452,7 +70217,7 @@ ${indent}columns: ${matrix.columns}
69452
70217
  })
69453
70218
  }
69454
70219
 
69455
- const _version = "3.2.5";
70220
+ const _version = "3.3.0";
69456
70221
  function version() {
69457
70222
  return _version
69458
70223
  }
@@ -69496,10 +70261,24 @@ ${indent}columns: ${matrix.columns}
69496
70261
  this.select.setAttribute('name', 'chromosome-select-widget');
69497
70262
  this.container.appendChild(this.select);
69498
70263
 
69499
- this.select.addEventListener('change', () => {
70264
+ this.select.addEventListener('change', async () => {
69500
70265
  this.select.blur();
69501
70266
  if (this.select.value !== '' && maximumSequenceCountExceeded !== this.select.value) {
69502
- browser.search(this.select.value);
70267
+
70268
+ if (this.select.value.trim().toLowerCase() === "all" || this.select.value === "*") {
70269
+ if (browser.genome.wholeGenomeView) {
70270
+ const wgChr = browser.genome.getChromosome("all");
70271
+ return {chr: "all", start: 0, end: wgChr.bpLength}
70272
+ }
70273
+ } else {
70274
+ const chromosome = await browser.genome.loadChromosome(this.select.value);
70275
+ const locusObject = {chr: chromosome.name};
70276
+ if (locusObject.start === undefined && locusObject.end === undefined) {
70277
+ locusObject.start = 0;
70278
+ locusObject.end = chromosome.bpLength;
70279
+ }
70280
+ browser.updateLoci([locusObject]);
70281
+ }
69503
70282
  }
69504
70283
  });
69505
70284
 
@@ -69524,7 +70303,9 @@ ${indent}columns: ${matrix.columns}
69524
70303
  this.genome = genome;
69525
70304
 
69526
70305
  // Start with explicit chromosome name list
69527
- const list = genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) || [];
70306
+ const list = (genome.wgChromosomeNames) ?
70307
+ genome.wgChromosomeNames.map(nm => genome.getChromosomeDisplayName(nm)) :
70308
+ [];
69528
70309
 
69529
70310
  if (this.showAllChromosomes && genome.chromosomeNames.length > 1) {
69530
70311
  const exclude = new Set(list);
@@ -70750,7 +71531,7 @@ ${indent}columns: ${matrix.columns}
70750
71531
  });
70751
71532
 
70752
71533
  this.searchInput.addEventListener('change', () => {
70753
- browser.doSearch(this.searchInput.value);
71534
+ this.doSearch(this.searchInput.value);
70754
71535
  });
70755
71536
 
70756
71537
  const searchIconContainer = document.createElement('div');
@@ -70761,7 +71542,7 @@ ${indent}columns: ${matrix.columns}
70761
71542
  searchIconContainer.appendChild(searchIcon);
70762
71543
 
70763
71544
  searchIconContainer.addEventListener('click', () => {
70764
- browser.doSearch(this.searchInput.value);
71545
+ this.doSearch(this.searchInput.value);
70765
71546
  });
70766
71547
 
70767
71548
  this.windowSizePanel = new WindowSizePanel(locusSizeGroup, browser);
@@ -70909,6 +71690,22 @@ ${indent}columns: ${matrix.columns}
70909
71690
  this.navigation.style.display = 'flex';
70910
71691
  }
70911
71692
 
71693
+ /**
71694
+
71695
+ * Search for the locus string -- this function is called from the navbar search box, and is not part of the API.
71696
+ * Wraps ```search``` and presents an error dialog if false.
71697
+ *
71698
+ * @param locus
71699
+ * @param init
71700
+ * @returns {Promise<void>}
71701
+ */
71702
+ async doSearch(locus) {
71703
+ const success = await this.browser.search(locus);
71704
+ if (!success) {
71705
+ this.browser.alert.present(new Error(`Unrecognized locus: <b> ${locus} </b>`));
71706
+ }
71707
+ }
71708
+
70912
71709
  }
70913
71710
 
70914
71711
  function logo() {
@@ -72160,6 +72957,9 @@ ${indent}columns: ${matrix.columns}
72160
72957
  aliasRecord["_cap"] = cap;
72161
72958
  }
72162
72959
 
72960
+ const chrPrefixAlias = aliasRecord.chr.startsWith("chr") ? aliasRecord.chr.substring(3) : "chr" + aliasRecord.chr;
72961
+ aliasRecord["_chrprefix_"] = chrPrefixAlias;
72962
+
72163
72963
  }
72164
72964
 
72165
72965
  }
@@ -72184,7 +72984,7 @@ ${indent}columns: ${matrix.columns}
72184
72984
  }
72185
72985
 
72186
72986
  async preload(chrNames) {
72187
- await this.reader.preload();
72987
+ await this.reader.preload();
72188
72988
  for(let nm of chrNames) {
72189
72989
  await this.search(nm);
72190
72990
  }
@@ -72236,12 +73036,6 @@ ${indent}columns: ${matrix.columns}
72236
73036
  }
72237
73037
  return this.aliasRecordCache.get(alias)
72238
73038
  }
72239
-
72240
- async getChromosomeNames() {
72241
- await this.reader.loadHeader();
72242
- return Array.from(this.reader.chrNames)
72243
- }
72244
-
72245
73039
  }
72246
73040
 
72247
73041
  /**
@@ -72266,10 +73060,9 @@ ${indent}columns: ${matrix.columns}
72266
73060
  this.genome = genome;
72267
73061
  }
72268
73062
 
72269
- async preload() {
72270
- return this.loadAliases();
73063
+ async preload(chrNames) {
73064
+ // A no-op, this is a text file, no need to preload
72271
73065
  }
72272
-
72273
73066
  /**
72274
73067
  * Return the canonical chromosome name for the alias. If none found return the alias
72275
73068
  *
@@ -72435,7 +73228,7 @@ ${indent}columns: ${matrix.columns}
72435
73228
  for (let line of lines) {
72436
73229
 
72437
73230
  const tokens = line.split("\t");
72438
- const chrName = tokens[0]; //genome.getChromosomeName(tokens[0]) // Note allowance for alias name, not sure why this is needed here
73231
+ const chrName = tokens[0];
72439
73232
  if (!lastChr) lastChr = chrName;
72440
73233
 
72441
73234
  if (chrName !== lastChr) {
@@ -72453,37 +73246,20 @@ ${indent}columns: ${matrix.columns}
72453
73246
  bands.push(new Cytoband(start, end, name, stain));
72454
73247
  }
72455
73248
  }
72456
-
72457
- }
72458
-
72459
- /**
72460
- * Infer genome chromosome names from cytoband data. This should only be used as a last resort
72461
- */
72462
- async getChromosomeNames() {
72463
- if(this.cytobands.size === 0) {
72464
- await this.#loadCytobands();
72465
- }
72466
- return Array.from(this.cytobands.keys())
72467
- }
72468
-
72469
- /**
72470
- * Infer chromosome objects from cytoband data. This should only be used as last resort.
72471
- */
72472
- async getChromosomes() {
72473
- if(this.cytobands.size === 0) {
72474
- await this.#loadCytobands();
73249
+ if(bands.length > 0) {
73250
+ this.cytobands.set(lastChr, bands);
72475
73251
  }
72476
73252
 
72477
- const chromosomes = [];
72478
- let order = 0;
72479
- for(let [chrName, cytoList] of this.cytobands.entries()) {
72480
- chromosomes.push(new Chromosome(chrName, order++, cytoList[cytoList.length - 1].end));
72481
- }
72482
- return chromosomes
72483
73253
  }
72484
73254
 
72485
73255
  }
72486
73256
 
73257
+ const ucsdIDMap = new Map([
73258
+ ["1kg_ref", "hg18"],
73259
+ ["1kg_v37", "hg19"],
73260
+ ["b37", "hg19"]
73261
+ ]);
73262
+
72487
73263
  /**
72488
73264
  * The Genome class represents an assembly and consists of the following elements
72489
73265
  * sequence - Object representing the DNA sequence
@@ -72508,8 +73284,10 @@ ${indent}columns: ${matrix.columns}
72508
73284
  this.config = config;
72509
73285
  this.browser = browser;
72510
73286
  this.id = config.id || generateGenomeID(config);
73287
+ this.ucscID = config.ucscID || ucsdIDMap.get(this.id) || this.id;
73288
+ this.blatDB = config.blatDB || this.ucscID;
72511
73289
  this.name = config.name;
72512
- this.nameSet = config.nameSet;
73290
+ this.nameSet = config.nameSet || 'ucsc';
72513
73291
  }
72514
73292
 
72515
73293
 
@@ -72517,49 +73295,45 @@ ${indent}columns: ${matrix.columns}
72517
73295
 
72518
73296
  const config = this.config;
72519
73297
 
73298
+ // Load sequence
72520
73299
  this.sequence = await loadSequence(config, this.browser);
72521
73300
 
72522
- if (config.chromSizesURL) {
72523
- // a chromSizes file is neccessary for 2bit sequences for whole-genome view
73301
+
73302
+ // Load cytobands. This is optional but required to support the ideogram. Only needed for whole genome view
73303
+ if(false !== config.showIdeogram && false !== config.wholeGenomeView) {
73304
+ if (config.cytobandURL) {
73305
+ this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
73306
+ } else if (config.cytobandBbURL) {
73307
+ this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
73308
+ }
73309
+ }
73310
+
73311
+ // Search for chromosomes, that is an array of chromosome objects containing name and length. This is
73312
+ // optional but required to support whole genome view.
73313
+ if (this.sequence.chromosomes) {
73314
+ this.chromosomes = this.sequence.chromosomes;
73315
+ } else if (config.chromSizesURL) {
72524
73316
  this.chromosomes = await loadChromSizes(config.chromSizesURL);
72525
73317
  } else {
72526
- // if the sequence defines chromosomes use them (fasta does, 2bit does not)
72527
- this.chromosomes = this.sequence.chromosomes || new Map();
73318
+ this.chromosomes = new Map(); // Cache, chromosome are added as they are loaded
72528
73319
  }
72529
73320
 
72530
- if (this.chromosomes.size > 0) {
73321
+ // Search for chromosome names. This is optional but required to support the chromosome pulldown
73322
+ if (this.sequence.chromosomeNames) {
73323
+ this.chromosomeNames = this.sequence.chromosomeNames; // Twobit files can supply chromosome names unless they use an external index
73324
+ } else if (this.chromosomes.size > 0) {
72531
73325
  this.chromosomeNames = Array.from(this.chromosomes.keys());
72532
73326
  }
72533
73327
 
73328
+ // Chromosome alias
72534
73329
  if (config.chromAliasBbURL) {
72535
73330
  this.chromAlias = new ChromAliasBB(config.chromAliasBbURL, Object.assign({}, config), this);
72536
- if (!this.chromosomeNames) {
72537
- this.chromosomeNames = await this.chromAlias.getChromosomeNames();
72538
- }
72539
73331
  } else if (config.aliasURL) {
72540
73332
  this.chromAlias = new ChromAliasFile(config.aliasURL, Object.assign({}, config), this);
72541
73333
  } else if (this.chromosomeNames) {
72542
73334
  this.chromAlias = new ChromAliasDefaults(this.id, this.chromosomeNames);
72543
73335
  }
72544
73336
 
72545
- if (config.cytobandBbURL) {
72546
- this.cytobandSource = new CytobandFileBB(config.cytobandBbURL, Object.assign({}, config), this);
72547
- } else if (config.cytobandURL) {
72548
- this.cytobandSource = new CytobandFile(config.cytobandURL, Object.assign({}, config));
72549
- }
72550
-
72551
- // Last resort for chromosome information -- retrieve it from the cytoband source if supported
72552
- if (!this.chromosomeNames && typeof this.cytobandSource.getChromosomeNames === 'function') {
72553
- this.chromosomeNames = await this.cytobandSource.getChromosomeNames();
72554
- }
72555
- if (this.chromosomes.size === 0 && typeof this.cytobandSource.getChromosomes === 'function') {
72556
- const c = await this.cytobandSource.getChromosomes();
72557
- for (let chromosome of c) {
72558
- this.chromosomes.set(c.name, c);
72559
- }
72560
- }
72561
-
72562
-
72563
73337
  if (false !== config.wholeGenomeView && this.chromosomes.size > 0) {
72564
73338
  // Set chromosome order for WG view and chromosome pulldown. If chromosome order is not specified sort
72565
73339
  if (config.chromosomeOrder) {
@@ -72611,9 +73385,9 @@ ${indent}columns: ${matrix.columns}
72611
73385
  getHomeChromosomeName() {
72612
73386
  if (this.showWholeGenomeView() && this.chromosomes.has("all")) {
72613
73387
  return "all"
72614
- } else {
73388
+ } else if (this.chromosomeNames) {
72615
73389
  return this.chromosomeNames[0]
72616
- }
73390
+ } else ;
72617
73391
  }
72618
73392
 
72619
73393
  getChromosomeName(chr) {
@@ -72664,18 +73438,18 @@ ${indent}columns: ${matrix.columns}
72664
73438
  if (!aliasRecord && chr !== chr.toLowerCase()) {
72665
73439
  aliasRecord = await this.chromAlias.search(chr.toLowerCase());
72666
73440
  }
72667
- if(aliasRecord) {
73441
+ if (aliasRecord) {
72668
73442
  // Add some aliases for case insensitivy
72669
73443
  const upper = aliasRecord.chr.toUpperCase();
72670
73444
  const lower = aliasRecord.chr.toLowerCase();
72671
73445
  const cap = aliasRecord.chr.charAt(0).toUpperCase() + aliasRecord.chr.slice(1);
72672
- if(aliasRecord.chr !== upper) {
73446
+ if (aliasRecord.chr !== upper) {
72673
73447
  aliasRecord["_uppercase"] = upper;
72674
73448
  }
72675
- if(aliasRecord.chr !== lower) {
73449
+ if (aliasRecord.chr !== lower) {
72676
73450
  aliasRecord["_lowercase"] = lower;
72677
73451
  }
72678
- if(aliasRecord.chr !== cap) {
73452
+ if (aliasRecord.chr !== cap) {
72679
73453
  aliasRecord["_cap"] = cap;
72680
73454
  }
72681
73455
  }
@@ -72807,6 +73581,10 @@ ${indent}columns: ${matrix.columns}
72807
73581
  return undefined
72808
73582
  }
72809
73583
  }
73584
+
73585
+ getHubURLs() {
73586
+ return this.config.hubs
73587
+ }
72810
73588
  }
72811
73589
 
72812
73590
  /**
@@ -72849,7 +73627,7 @@ ${indent}columns: ${matrix.columns}
72849
73627
  }
72850
73628
  }
72851
73629
 
72852
- 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';
73630
+ 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';
72853
73631
 
72854
73632
  /**
72855
73633
  * Manages XQTL selections.
@@ -73383,7 +74161,7 @@ ${indent}columns: ${matrix.columns}
73383
74161
  this.dataRangeDialog = new DataRangeDialog(this, this.root);
73384
74162
  this.dataRangeDialog.container.id = `igv-data-range-dialog-${guid$2()}`;
73385
74163
 
73386
- this.genericColorPicker = new GenericColorPicker({ parent: this.root, width: 180 });
74164
+ this.genericColorPicker = new GenericColorPicker({parent: this.root, width: 180});
73387
74165
  this.genericColorPicker.container.id = `igv-track-color-picker-${guid$2()}`;
73388
74166
 
73389
74167
  this.sliderDialog = new SliderDialog(this.root);
@@ -73393,10 +74171,10 @@ ${indent}columns: ${matrix.columns}
73393
74171
 
73394
74172
  getSampleNameViewportWidth() {
73395
74173
 
73396
- if (undefined === this.sampleNameViewportWidth) {
74174
+ if (false === this.showSampleNames || undefined === this.sampleNameViewportWidth) {
73397
74175
  return 0
73398
74176
  } else {
73399
- return false === this.showSampleNames ? 0 : this.sampleNameViewportWidth
74177
+ return this.sampleNameViewportWidth
73400
74178
  }
73401
74179
 
73402
74180
  }
@@ -73558,7 +74336,8 @@ ${indent}columns: ${matrix.columns}
73558
74336
  } else {
73559
74337
  session = options;
73560
74338
  }
73561
- return this.loadSessionObject(session)
74339
+
74340
+ await this.loadSessionObject(session);
73562
74341
  }
73563
74342
 
73564
74343
  /**
@@ -73587,8 +74366,7 @@ ${indent}columns: ${matrix.columns}
73587
74366
  config = new XMLSession(string, knownGenomes);
73588
74367
 
73589
74368
  } else if (filename.endsWith("hub.txt")) {
73590
-
73591
- const hub = await Hub.loadHub(urlOrFile, options);
74369
+ const hub = await loadHub(urlOrFile);
73592
74370
  const genomeConfig = hub.getGenomeConfig();
73593
74371
  config = {
73594
74372
  reference: genomeConfig
@@ -73679,7 +74457,7 @@ ${indent}columns: ${matrix.columns}
73679
74457
  }
73680
74458
 
73681
74459
  // ROIs
73682
- if(session.showROIOverlays !== undefined) {
74460
+ if (session.showROIOverlays !== undefined) {
73683
74461
  this.roiManager.showOverlays = session.showROIOverlays;
73684
74462
  }
73685
74463
  this.roiManager.clearROIs();
@@ -73693,12 +74471,16 @@ ${indent}columns: ${matrix.columns}
73693
74471
  // Sample info
73694
74472
  const localSampleInfoFiles = [];
73695
74473
  if (session.sampleinfo) {
73696
- for (const config of session.sampleinfo) {
73697
-
73698
- if (config.file) {
73699
- localSampleInfoFiles.push(config.file);
74474
+ for (const sampleInfoConfig of session.sampleinfo) {
74475
+ // The "file" property is recorded in the session when a local file is referenced. It can't be used
74476
+ // on reloading, its only purpose is to present an alert to the user. This could also be used
74477
+ // to prompt the user to load the file manually, but we don't currently do that.
74478
+ if (sampleInfoConfig.file) {
74479
+ localSampleInfoFiles.push(sampleInfoConfig.file);
73700
74480
  } else {
73701
- this.loadSampleInfo(config);
74481
+ // this.loadSampleInfo(sampleInfoConfig)
74482
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74483
+
73702
74484
  }
73703
74485
 
73704
74486
  }
@@ -73739,23 +74521,14 @@ ${indent}columns: ${matrix.columns}
73739
74521
  }
73740
74522
  }
73741
74523
 
73742
- await this.loadTrackList(nonLocalTrackConfigurations);
73743
-
73744
- // The ruler and ideogram tracks are not explicitly loaded, but needs updated nonetheless.
73745
- for (let rtv of this.trackViews.filter((tv) => tv.track.type === 'ruler' || tv.track.type === 'ideogram')) {
73746
- await rtv.updateViews();
74524
+ // Load a hidden track -- used to populate searchable database without creating a track
74525
+ const configHidden = nonLocalTrackConfigurations.filter(config => true === config.hidden);
74526
+ for (const config of configHidden) {
74527
+ const featureSource = FeatureSource(config, this.genome);
74528
+ await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
73747
74529
  }
73748
74530
 
73749
- // If any tracks are selected show the selection buttons
73750
- if (this.trackViews.some(tv => tv.track.selected)) {
73751
- this.navbar.setEnableTrackSelection(true);
73752
- }
73753
-
73754
- this.updateUIWithReferenceFrameList();
73755
-
73756
- this.updateLocusSearchWidget();
73757
-
73758
- return trackConfigurations
74531
+ await this.loadTrackList(nonLocalTrackConfigurations);
73759
74532
 
73760
74533
  }
73761
74534
 
@@ -73816,13 +74589,8 @@ ${indent}columns: ${matrix.columns}
73816
74589
  }
73817
74590
 
73818
74591
  if (genomeChange) {
73819
- let trackConfigurations;
73820
- if (genomeConfig.hubURL) {
73821
- // TODO -- refactor this so "hub" is not loaded twice
73822
- const hub = await Hub.loadHub(genomeConfig.hubURL);
73823
- trackConfigurations = hub.getGroupedTrackConfigurations();
73824
- }
73825
- this.fireEvent('genomechange', [{genome, trackConfigurations}]);
74592
+
74593
+ this.fireEvent('genomechange', [{genome}]);
73826
74594
 
73827
74595
  if (this.circularView) {
73828
74596
  this.circularView.setAssembly({
@@ -73861,7 +74629,7 @@ ${indent}columns: ${matrix.columns}
73861
74629
  let genomeConfig;
73862
74630
  const isHubGenome = idOrConfig.hubURL || (idOrConfig.url && isString$2(idOrConfig.url) && idOrConfig.url.endsWith("/hub.txt"));
73863
74631
  if (isHubGenome) {
73864
- const hub = await Hub.loadHub(idOrConfig.hubURL || idOrConfig.url, idOrConfig);
74632
+ const hub = await loadHub(idOrConfig.hubURL || idOrConfig.url);
73865
74633
  genomeConfig = hub.getGenomeConfig();
73866
74634
  } else if (isString$2(idOrConfig) || !(idOrConfig.url || idOrConfig.fastaURL || idOrConfig.twoBitURL || idOrConfig.gbkURL)) {
73867
74635
  // Either an ID, a json string, or an object missing required properties.
@@ -73891,22 +74659,9 @@ ${indent}columns: ${matrix.columns}
73891
74659
 
73892
74660
  await this.loadTrackList(tracks);
73893
74661
 
73894
- await this.updateViews();
73895
-
73896
74662
  return this.genome
73897
74663
  }
73898
74664
 
73899
- /**
73900
- * Load a UCSC single-file genome assembly hub.
73901
- * @param options
73902
- * @returns {Promise<void>}
73903
- */
73904
- async loadTrackHub(options) {
73905
- const hub = await Hub.loadHub(options.url, options);
73906
- const genomeConfig = setDefaults(hub.getGenomeConfig());
73907
- return this.loadGenome(genomeConfig)
73908
- }
73909
-
73910
74665
  /**
73911
74666
  * Called after a session load, search, pan (horizontal drag), or resize
73912
74667
  *
@@ -74003,18 +74758,23 @@ ${indent}columns: ${matrix.columns}
74003
74758
  }
74004
74759
 
74005
74760
  const promises = [];
74006
- for (let config of configList) {
74007
- promises.push(this._loadTrack(config));
74761
+ for (const config of configList) {
74762
+ promises.push(this.#loadTrackHelper(config));
74008
74763
  }
74009
74764
 
74010
74765
  const loadedTracks = await Promise.all(promises);
74011
74766
 
74012
- const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
74013
- return trackView.track.autoscaleGroup
74014
- });
74015
- if (groupAutoscaleViews.length > 0) {
74016
- this.updateViews();
74767
+ // If any tracks are selected show the selection buttons
74768
+ if (this.trackViews.some(({track}) => track.selected)) {
74769
+ this.navbar.setEnableTrackSelection(true);
74017
74770
  }
74771
+
74772
+ this.reorderTracks();
74773
+
74774
+ await resize.call(this);
74775
+
74776
+ this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74777
+
74018
74778
  return loadedTracks
74019
74779
  }
74020
74780
 
@@ -74028,54 +74788,23 @@ ${indent}columns: ${matrix.columns}
74028
74788
  */
74029
74789
  async loadTrack(config) {
74030
74790
 
74031
- // Default configuration sync option to true. This is the expected behavior for public API calls
74032
- config.sync = (config.sync !== false);
74033
-
74034
- const newTrack = this._loadTrack(config);
74035
-
74036
- if (newTrack && config.autoscaleGroup) {
74037
- // Await newTrack load and update all views
74038
- await newTrack;
74791
+ const loadedTracks = await this.loadTrackList([config]);
74792
+ if (config.autoscaleGroup) {
74039
74793
  this.updateViews();
74040
74794
  }
74041
-
74042
- return newTrack
74795
+ return loadedTracks[0]
74043
74796
  }
74044
74797
 
74045
- /**
74046
- * Return a promise to load a track. Private function used by loadTrack() and loadTrackList()
74047
- *
74048
- * @param config
74049
- * @returns {*}
74050
- */
74051
-
74052
- async _loadTrack(config) {
74798
+ async #loadTrackHelper(config) {
74053
74799
 
74054
74800
  // config might be json
74055
74801
  if (isString$2(config)) {
74056
74802
  config = JSON.parse(config);
74057
74803
  }
74058
74804
 
74805
+ let track;
74059
74806
  try {
74060
-
74061
- // Load a hidden track -- used to populate searchable database without creating a track
74062
- if (config.hidden) {
74063
- const featureSource = FeatureSource(config, this.genome);
74064
- await featureSource.getFeatures({chr: "1", start: 0, end: Number.MAX_SAFE_INTEGER});
74065
- return
74066
- }
74067
-
74068
- const newTrack = await this.createTrack(config);
74069
-
74070
- if ('sampleinfo' === config.type) {
74071
- this.layoutChange();
74072
- return
74073
- } else if (undefined === newTrack) {
74074
- return
74075
- }
74076
-
74077
- return this.addTrack(config, newTrack)
74078
-
74807
+ track = await this.createTrack(config);
74079
74808
  } catch (error) {
74080
74809
 
74081
74810
  let msg = error.message || error.error || error.toString();
@@ -74092,62 +74821,53 @@ ${indent}columns: ${matrix.columns}
74092
74821
  }
74093
74822
 
74094
74823
  msg = `${msg} : ${isFile(config.url) ? config.url.name : config.url}`;
74095
- // msg += (": " + FileUtils.isFile(config.url) ? config.url.name : config.url)
74096
74824
  const err = new Error(msg);
74097
74825
  console.error(err);
74098
74826
  throw err
74099
- // this.alert.present(new Error(msg), undefined)
74100
74827
  }
74101
- }
74102
74828
 
74103
- async addTrack(config, newTrack) {
74829
+ if (track) {
74830
+ return await this.addTrack(track)
74831
+ } else {
74832
+ return undefined
74833
+ }
74104
74834
 
74835
+ }
74836
+
74837
+ async addTrack(track) {
74105
74838
 
74106
74839
  // Set order field of track here, otherwise track order might get shuffled during asynchronous load
74107
- if (undefined === newTrack.order) {
74108
- newTrack.order = this.trackViews.length;
74840
+ if (undefined === track.order) {
74841
+ track.order = this.trackViews.length;
74109
74842
  }
74110
74843
 
74111
- const trackView = new TrackView(this, this.columnContainer, newTrack);
74844
+ const trackView = new TrackView(this, this.columnContainer, track);
74112
74845
  this.trackViews.push(trackView);
74113
74846
  toggleTrackLabels(this.trackViews, this.doShowTrackLabels);
74114
74847
 
74115
- if (typeof newTrack.postInit === 'function') {
74848
+ if (typeof track.postInit === 'function') {
74116
74849
  try {
74117
74850
  trackView.startSpinner();
74118
- await newTrack.postInit();
74851
+ await track.postInit();
74119
74852
  } finally {
74120
74853
  trackView.stopSpinner();
74121
74854
  }
74122
74855
  }
74123
74856
 
74124
- if (!newTrack.autoscaleGroup) {
74125
- // Group autoscale will get updated later (as a group)
74126
- if (config.sync) {
74127
- await trackView.updateViews();
74128
- } else {
74129
- trackView.updateViews();
74130
- }
74131
- }
74132
-
74133
- if (typeof newTrack.hasSamples === 'function' && newTrack.hasSamples()) {
74857
+ if (typeof track.hasSamples === 'function' && track.hasSamples()) {
74134
74858
 
74135
74859
  if (this.sampleInfo.hasAttributes()) {
74136
74860
  this.sampleInfoControl.setButtonVisibility(true);
74137
74861
  }
74138
74862
 
74139
74863
  if (this.config.showSampleNameButton !== false) {
74140
- this.sampleNameControl.show(); // If not explicitly set
74864
+ this.sampleNameControl.show();
74141
74865
  }
74142
74866
  }
74143
74867
 
74144
- // repositioned here to solve layout issue.
74145
- this.reorderTracks();
74146
- this.fireEvent('trackorderchanged', [this.getTrackOrder()]);
74147
-
74148
- newTrack.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74868
+ track.trackView.enableTrackSelection(this.navbar.getEnableTrackSelection());
74149
74869
 
74150
- return newTrack
74870
+ return track
74151
74871
 
74152
74872
  }
74153
74873
 
@@ -74292,7 +75012,6 @@ ${indent}columns: ${matrix.columns}
74292
75012
  }
74293
75013
  }
74294
75014
 
74295
-
74296
75015
  reorderTracks() {
74297
75016
 
74298
75017
  this.trackViews.sort(function (a, b) {
@@ -74529,19 +75248,19 @@ ${indent}columns: ${matrix.columns}
74529
75248
 
74530
75249
  this.updateLocusSearchWidget();
74531
75250
 
74532
- for (let frame of this.referenceFrameList) {
74533
- if (frame.bpPerPixel <= bppSequenceThreshold) {
74534
- await this.genome.getSequence(frame.chr, frame.start, frame.start + 1);
75251
+ for (const {bpPerPixel, chr, start} of this.referenceFrameList) {
75252
+ if (bpPerPixel <= bppSequenceThreshold) {
75253
+ await this.genome.getSequence(chr, start, start + 1);
74535
75254
  }
74536
75255
  }
74537
75256
 
74538
- for (let centerGuide of this.centerLineList) {
75257
+ for (const centerGuide of this.centerLineList) {
74539
75258
  centerGuide.repaint();
74540
75259
  }
74541
75260
 
74542
75261
  // Don't autoscale while dragging.
74543
75262
  if (this.dragObject) {
74544
- for (let trackView of trackViews) {
75263
+ for (const trackView of trackViews) {
74545
75264
  await trackView.updateViews();
74546
75265
  }
74547
75266
  } else {
@@ -74565,8 +75284,8 @@ ${indent}columns: ${matrix.columns}
74565
75284
  // Calculate group autoscale dataRange
74566
75285
  if (Object.entries(groupAutoscaleTrackViews).length > 0) {
74567
75286
  for (const [group, trackViews] of Object.entries(groupAutoscaleTrackViews)) {
74568
- const featureArray = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
74569
- const dataRange = doAutoscale(featureArray.flat());
75287
+ const inViewFeatures = await Promise.all(trackViews.map(trackView => trackView.getInViewFeatures()));
75288
+ const dataRange = doAutoscale(inViewFeatures.flat());
74570
75289
  for (const trackView of trackViews) {
74571
75290
  trackView.track.dataRange = Object.assign({}, dataRange);
74572
75291
  trackView.track.autoscale = false;
@@ -74575,7 +75294,7 @@ ${indent}columns: ${matrix.columns}
74575
75294
  }
74576
75295
  }
74577
75296
 
74578
- await Promise.all(otherTrackViews.map(tv => tv.updateViews()));
75297
+ await Promise.all(otherTrackViews.map(trackView => trackView.updateViews()));
74579
75298
  }
74580
75299
 
74581
75300
  }
@@ -74598,10 +75317,10 @@ ${indent}columns: ${matrix.columns}
74598
75317
  referenceFrame.end = referenceFrame.start + referenceFrame.bpPerPixel * width;
74599
75318
  }
74600
75319
 
74601
- const chrName = referenceFrameList.length === 1 ? this.referenceFrameList[0].chr : '';
74602
-
74603
75320
  const loc = this.referenceFrameList.map(rf => rf.getLocusString()).join(' ');
74604
75321
 
75322
+ const chrName = referenceFrameList.length === 1 ? this.genome.getChromosomeDisplayName(this.referenceFrameList[0].chr) : '';
75323
+
74605
75324
  this.navbar.updateLocus(loc, chrName);
74606
75325
 
74607
75326
  this.fireEvent('locuschange', [this.referenceFrameList]);
@@ -74618,6 +75337,46 @@ ${indent}columns: ${matrix.columns}
74618
75337
  return Math.floor(width / columnCount)
74619
75338
  }
74620
75339
 
75340
+ /**
75341
+ * Update reference frames based on new viewport width
75342
+ * @param {number} viewportWidth - The calculated viewport width
75343
+ */
75344
+ updateReferenceFrames(viewportWidth) {
75345
+
75346
+ for (const referenceFrame of this.referenceFrameList) {
75347
+ referenceFrame.updateForViewportWidth(viewportWidth);
75348
+ }
75349
+ }
75350
+
75351
+ /**
75352
+ * Update DOM viewport elements with new width
75353
+ * @param {number} viewportWidth - The calculated viewport width
75354
+ */
75355
+ updateViewportElements(viewportWidth) {
75356
+
75357
+ for (let i = 0; i < this.referenceFrameList.length; i++) {
75358
+
75359
+ for (const {viewports} of this.trackViews) {
75360
+ viewports[i].setWidth(viewportWidth);
75361
+ }
75362
+
75363
+ for (const {sampleInfoViewport} of this.trackViews) {
75364
+ sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
75365
+ sampleInfoViewport.repaint();
75366
+ }
75367
+
75368
+ }
75369
+ }
75370
+
75371
+ /**
75372
+ * Synchronize UI state after viewport updates
75373
+ * @returns {Promise<void>}
75374
+ */
75375
+ async syncUIState() {
75376
+ this.updateUIWithReferenceFrameList();
75377
+ await this.updateViews(true);
75378
+ }
75379
+
74621
75380
  minimumBases() {
74622
75381
  return this.config.minimumBases
74623
75382
  }
@@ -74803,29 +75562,12 @@ ${indent}columns: ${matrix.columns}
74803
75562
  }
74804
75563
 
74805
75564
  /**
74806
- * @deprecated This is a deprecated method with no known usages. To be removed in a future release.
75565
+ * @deprecated This is a deprecated method with no known usages.
74807
75566
  */
74808
75567
  async goto(chr, start, end) {
74809
75568
  await this.search(chr + ":" + start + "-" + end);
74810
75569
  }
74811
75570
 
74812
- /**
74813
-
74814
- * Search for the locus string -- this function is called from various igv.js GUI elements, and is not part of the
74815
- * API. Wraps ```search``` and presents an error dialog if false.
74816
- *
74817
- * @param string
74818
- * @param init
74819
- * @returns {Promise<void>}
74820
- */
74821
- async doSearch(string, init) {
74822
- const success = await this.search(string, init);
74823
- if (!success) {
74824
- this.alert.present(new Error(`Unrecognized locus: <b> ${string} </b>`));
74825
- }
74826
- return success
74827
- }
74828
-
74829
75571
 
74830
75572
  /**
74831
75573
  * Search for the locus string
@@ -74838,6 +75580,10 @@ ${indent}columns: ${matrix.columns}
74838
75580
  async search(stringOrArray, init) {
74839
75581
 
74840
75582
  const loci = await search(this, stringOrArray);
75583
+ return this.updateLoci(loci, init)
75584
+ }
75585
+
75586
+ async updateLoci(loci, init) {
74841
75587
 
74842
75588
  if (loci && loci.length > 0) {
74843
75589
 
@@ -74874,9 +75620,9 @@ ${indent}columns: ${matrix.columns}
74874
75620
  }
74875
75621
  }
74876
75622
 
74877
- async loadSampleInfo(config) {
75623
+ async loadSampleInfo(sampleInfoConfig) {
74878
75624
 
74879
- await this.sampleInfo.loadSampleInfoFile(config.url);
75625
+ await this.sampleInfo.loadSampleInfo(sampleInfoConfig);
74880
75626
 
74881
75627
  for (const {sampleInfoViewport} of this.trackViews) {
74882
75628
  sampleInfoViewport.setWidth(this.getSampleInfoColumnWidth());
@@ -75014,9 +75760,9 @@ ${indent}columns: ${matrix.columns}
75014
75760
  json["locus"] = locus.length === 1 ? locus[0] : locus;
75015
75761
 
75016
75762
  const roiSets = this.roiManager.toJSON();
75017
- if(roiSets) {
75763
+ if (roiSets) {
75018
75764
  json["roi"] = roiSets;
75019
- if(!this.roiManager.showOverlays){
75765
+ if (!this.roiManager.showOverlays) {
75020
75766
  json["showROIOverlays"] = false; // true is the default
75021
75767
  }
75022
75768
  }
@@ -75359,8 +76105,6 @@ ${indent}columns: ${matrix.columns}
75359
76105
  }
75360
76106
  }
75361
76107
 
75362
-
75363
-
75364
76108
  // Navbar delegates
75365
76109
  get sampleInfoControl() {
75366
76110
  return this.navbar.sampleInfoControl
@@ -75382,6 +76126,9 @@ ${indent}columns: ${matrix.columns}
75382
76126
  return this.navbar.sampleNameControl
75383
76127
  }
75384
76128
 
76129
+ async blat(sequence) {
76130
+ return createBlatTrack({sequence, browser: this, name: 'Blat', title: 'Blat'})
76131
+ }
75385
76132
  }
75386
76133
 
75387
76134
  function getFileExtension(input) {
@@ -75407,7 +76154,7 @@ ${indent}columns: ${matrix.columns}
75407
76154
  }
75408
76155
 
75409
76156
  /**
75410
- * Function called win window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
76157
+ * Called when window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
75411
76158
  * than class method because it needs to be copied and bound to specific instances of browser to support listener
75412
76159
  * removal
75413
76160
  *
@@ -75415,40 +76162,14 @@ ${indent}columns: ${matrix.columns}
75415
76162
  */
75416
76163
  async function resize() {
75417
76164
 
75418
- if (!this.referenceFrameList) return
75419
-
75420
- const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
75421
-
75422
- for (let referenceFrame of this.referenceFrameList) {
75423
-
75424
- const index = this.referenceFrameList.indexOf(referenceFrame);
75425
-
75426
- const {chr, genome} = referenceFrame;
75427
-
75428
- const {bpLength} = genome.getChromosome(referenceFrame.chr);
75429
-
75430
- const viewportWidthBP = referenceFrame.toBP(viewportWidth);
75431
-
75432
- // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
75433
- if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
75434
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
75435
- referenceFrame.bpPerPixel = bpLength / viewportWidth;
75436
- } else {
75437
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
75438
- referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
75439
- }
75440
-
75441
- for (let {viewports} of this.trackViews) {
75442
- viewports[index].setWidth(viewportWidth);
75443
- }
75444
-
76165
+ if (undefined === this.referenceFrameList || 0 === this.referenceFrameList.length) {
76166
+ return
75445
76167
  }
75446
76168
 
75447
- this.updateUIWithReferenceFrameList();
75448
-
75449
- //TODO -- update view only if needed. Reducing size never needed. Increasing size maybe
75450
-
75451
- await this.updateViews(true);
76169
+ const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
76170
+ this.updateReferenceFrames(viewportWidth);
76171
+ this.updateViewportElements(viewportWidth);
76172
+ await this.syncUIState();
75452
76173
  }
75453
76174
 
75454
76175
 
@@ -75917,7 +76638,8 @@ ${indent}columns: ${matrix.columns}
75917
76638
  registerTrackClass,
75918
76639
  registerTrackCreatorFunction,
75919
76640
  registerFileFormats,
75920
- loadSessionFile: Browser.loadSessionFile
76641
+ loadSessionFile: Browser.loadSessionFile,
76642
+ loadHub
75921
76643
  };
75922
76644
 
75923
76645
  return index;