igv 2.11.2 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/igv.esm.js CHANGED
@@ -17624,7 +17624,7 @@ function Node(interval) {
17624
17624
  * @constructor
17625
17625
  */
17626
17626
 
17627
- class FeatureCache {
17627
+ class FeatureCache$1 {
17628
17628
 
17629
17629
  constructor(featureList, genome, range) {
17630
17630
 
@@ -19854,6 +19854,7 @@ function inferTrackType(config) {
19854
19854
  return "alignment"
19855
19855
  case "bedpe":
19856
19856
  case "bedpe-loop":
19857
+ case "biginteract":
19857
19858
  return "interact"
19858
19859
  case "bp":
19859
19860
  return "arc"
@@ -19862,7 +19863,6 @@ function inferTrackType(config) {
19862
19863
  case "bed":
19863
19864
  case "bigbed":
19864
19865
  case "bb":
19865
- case "biginteract":
19866
19866
  return "bedtype"
19867
19867
  default:
19868
19868
  return "annotation"
@@ -20113,6 +20113,38 @@ function isSecureContext() {
20113
20113
  return window.location.protocol === "https:" || window.location.hostname === "localhost"
20114
20114
  }
20115
20115
 
20116
+ const pairs =
20117
+ [
20118
+ ['A', 'T'],
20119
+ ['G', 'C'],
20120
+ ['Y', 'R'],
20121
+ ['W', 'S'],
20122
+ ['K', 'M'],
20123
+ ['D', 'H'],
20124
+ ['B', 'V']
20125
+ ];
20126
+
20127
+ const complements = new Map();
20128
+ for (let p of pairs) {
20129
+ const p1 = p[0];
20130
+ const p2 = p[1];
20131
+ complements.set(p1, p2);
20132
+ complements.set(p2, p1);
20133
+ complements.set(p1.toLowerCase(), p2.toLowerCase());
20134
+ complements.set(p2.toLowerCase(), p1.toLowerCase());
20135
+ }
20136
+
20137
+ function reverseComplementSequence(sequence) {
20138
+
20139
+ let comp = '';
20140
+ let idx = sequence.length;
20141
+ while (idx-- > 0) {
20142
+ const base = sequence[idx];
20143
+ comp += complements.has(base) ? complements.get(base) : base;
20144
+ }
20145
+ return comp
20146
+ }
20147
+
20116
20148
  /*
20117
20149
  * The MIT License (MIT)
20118
20150
  *
@@ -20237,7 +20269,6 @@ class SequenceTrack {
20237
20269
  }
20238
20270
 
20239
20271
  menuItemList() {
20240
-
20241
20272
  return [
20242
20273
  {
20243
20274
  name: this.reversed ? "Forward" : "Reverse",
@@ -20272,17 +20303,20 @@ class SequenceTrack {
20272
20303
  contextMenuItemList(clickState) {
20273
20304
  const viewport = clickState.viewport;
20274
20305
  if (viewport.referenceFrame.bpPerPixel <= 1) {
20306
+ const pixelWidth = viewport.getWidth();
20307
+ const bpWindow = pixelWidth * viewport.referenceFrame.bpPerPixel;
20308
+ const chr = viewport.referenceFrame.chr;
20309
+ const start = Math.floor(viewport.referenceFrame.start);
20310
+ const end = Math.ceil(start + bpWindow);
20275
20311
  const items = [
20276
20312
  {
20277
- label: 'View visible sequence...',
20313
+ label: this.reversed ? 'View visible sequence (reversed)...' : 'View visible sequence...',
20278
20314
  click: async () => {
20279
- const pixelWidth = viewport.getWidth();
20280
- const bpWindow = pixelWidth * viewport.referenceFrame.bpPerPixel;
20281
- const chr = viewport.referenceFrame.chr;
20282
- const start = viewport.referenceFrame.start;
20283
- const end = start + bpWindow;
20284
- const sequence = await this.browser.genome.sequence.getSequence(chr, start, end);
20285
- Alert.presentAlert(sequence);
20315
+ let seq = await this.browser.genome.sequence.getSequence(chr, start, end);
20316
+ if (this.reversed) {
20317
+ seq = reverseComplementSequence(seq);
20318
+ }
20319
+ Alert.presentAlert(seq);
20286
20320
  }
20287
20321
  }
20288
20322
  ];
@@ -20290,14 +20324,18 @@ class SequenceTrack {
20290
20324
  items.push({
20291
20325
  label: 'Copy visible sequence',
20292
20326
  click: async () => {
20293
- const pixelWidth = viewport.getWidth();
20294
- const bpWindow = pixelWidth * viewport.referenceFrame.bpPerPixel;
20295
- const chr = viewport.referenceFrame.chr;
20296
- const start = viewport.referenceFrame.start;
20297
- const end = start + bpWindow;
20298
- const sequence = await this.browser.genome.sequence.getSequence(chr, start, end);
20299
- navigator.clipboard.writeText(sequence);
20327
+ let seq = await this.browser.genome.sequence.getSequence(chr, start, end);
20328
+ if (this.reversed) {
20329
+ seq = reverseComplementSequence(seq);
20330
+ }
20331
+ try {
20332
+ await navigator.clipboard.writeText(seq);
20333
+ } catch (e) {
20334
+ console.error(e);
20335
+ Alert.presentAlert(`error copying sequence to clipboard ${e}`);
20336
+ }
20300
20337
  }
20338
+
20301
20339
  });
20302
20340
  }
20303
20341
  items.push('<hr/>');
@@ -20441,7 +20479,7 @@ class SequenceTrack {
20441
20479
  }
20442
20480
  }
20443
20481
 
20444
- supportsWholeGenome() {
20482
+ get supportsWholeGenome() {
20445
20483
  return false
20446
20484
  }
20447
20485
 
@@ -20515,13 +20553,13 @@ class Viewport {
20515
20553
  this.$content.height(this.$viewport.height());
20516
20554
  this.contentDiv = this.$content.get(0);
20517
20555
 
20518
- this.$canvas = $$1('<canvas>');
20519
- this.$content.append(this.$canvas);
20520
-
20521
- this.canvas = this.$canvas.get(0);
20522
- this.ctx = this.canvas.getContext("2d");
20556
+ // this.$canvas = $('<canvas>')
20557
+ // this.$content.append(this.$canvas)
20558
+ //
20559
+ // this.canvas = this.$canvas.get(0)
20560
+ // this.ctx = this.canvas.getContext("2d")
20523
20561
 
20524
- this.setWidth(width);
20562
+ this.$viewport.width(width);
20525
20563
 
20526
20564
  this.initializationHelper();
20527
20565
 
@@ -20588,14 +20626,13 @@ class Viewport {
20588
20626
  console.log('Viewport - draw(drawConfiguration, features, roiFeatures)');
20589
20627
  }
20590
20628
 
20591
- checkContentHeight() {
20629
+ checkContentHeight(features) {
20592
20630
 
20593
20631
  let track = this.trackView.track;
20594
-
20632
+ features = features || this.cachedFeatures;
20595
20633
  if ("FILL" === track.displayMode) {
20596
20634
  this.setContentHeight(this.$viewport.height());
20597
20635
  } else if (typeof track.computePixelHeight === 'function') {
20598
- let features = this.cachedFeatures;
20599
20636
  if (features && features.length > 0) {
20600
20637
  let requiredContentHeight = track.computePixelHeight(features);
20601
20638
  let currentContentHeight = this.$content.height();
@@ -20611,12 +20648,10 @@ class Viewport {
20611
20648
  }
20612
20649
 
20613
20650
  setContentHeight(contentHeight) {
20651
+
20614
20652
  // Maximum height of a canvas is ~32,000 pixels on Chrome, possibly smaller on other platforms
20615
20653
  contentHeight = Math.min(contentHeight, 32000);
20616
-
20617
20654
  this.$content.height(contentHeight);
20618
-
20619
- if (this.tile) this.tile.invalidate = true;
20620
20655
  }
20621
20656
 
20622
20657
  isLoading() {
@@ -20633,8 +20668,6 @@ class Viewport {
20633
20668
 
20634
20669
  setWidth(width) {
20635
20670
  this.$viewport.width(width);
20636
- this.canvas.style.width = (`${width}px`);
20637
- this.canvas.setAttribute('width', width);
20638
20671
  }
20639
20672
 
20640
20673
  getWidth() {
@@ -20664,8 +20697,6 @@ class Viewport {
20664
20697
  this.popover.dispose();
20665
20698
  }
20666
20699
 
20667
- this.removeMouseHandlers();
20668
-
20669
20700
  this.$viewport.get(0).remove();
20670
20701
 
20671
20702
  // Null out all properties -- this should not be neccessary, but just in case there is a
@@ -22761,7 +22792,7 @@ const Cytoband = function (start, end, name, typestain) {
22761
22792
  }
22762
22793
  };
22763
22794
 
22764
- const _version = "2.11.2";
22795
+ const _version = "2.12.0";
22765
22796
  function version() {
22766
22797
  return _version
22767
22798
  }
@@ -23115,6 +23146,7 @@ class Genome {
23115
23146
  }
23116
23147
 
23117
23148
  async getSequence(chr, start, end) {
23149
+ chr = this.getChromosomeName(chr);
23118
23150
  return this.sequence.getSequence(chr, start, end)
23119
23151
  }
23120
23152
  }
@@ -23250,28 +23282,27 @@ class TrackViewport extends Viewport {
23250
23282
  this.$viewport.append(this.$spinner);
23251
23283
  this.$spinner.append($$1('<div>'));
23252
23284
 
23253
- const {track} = this.trackView;
23254
-
23285
+ const track = this.trackView.track;
23255
23286
  if ('sequence' !== track.type) {
23256
23287
  this.$zoomInNotice = this.createZoomInNotice(this.$content);
23257
23288
  }
23258
23289
 
23259
- if (track.name && "sequence" !== track.config.type) {
23260
-
23290
+ if (track.name && "sequence" !== track.id) {
23261
23291
  this.$trackLabel = $$1('<div class="igv-track-label">');
23262
23292
  this.$viewport.append(this.$trackLabel);
23263
23293
  this.setTrackLabel(track.name);
23264
-
23265
23294
  if (false === this.browser.trackLabelsVisible) {
23266
23295
  this.$trackLabel.hide();
23267
23296
  }
23268
-
23269
23297
  }
23270
23298
 
23271
23299
  this.stopSpinner();
23272
-
23273
23300
  this.addMouseHandlers();
23301
+ }
23274
23302
 
23303
+ setContentHeight(contentHeight) {
23304
+ super.setContentHeight(contentHeight);
23305
+ if (this.featureCache) this.featureCache.redraw = true;
23275
23306
  }
23276
23307
 
23277
23308
  setTrackLabel(label) {
@@ -23293,61 +23324,74 @@ class TrackViewport extends Viewport {
23293
23324
  }
23294
23325
  }
23295
23326
 
23327
+ /**
23328
+ * Test to determine if we are zoomed in far enough to see features. Applicable to tracks with visibility windows.
23329
+ *
23330
+ * As a side effect the viewports canvas is removed if zoomed out.
23331
+ *
23332
+ * @returns {boolean} true if we are zoomed in past visibility window, false otherwise
23333
+ */
23296
23334
  checkZoomIn() {
23297
23335
 
23298
- const showZoomInNotice = () => {
23299
- const referenceFrame = this.referenceFrame;
23300
- if (this.referenceFrame.chr.toLowerCase() === "all" && !this.trackView.track.supportsWholeGenome()) {
23336
+ const zoomedOutOfWindow = () => {
23337
+ if (this.referenceFrame.chr.toLowerCase() === "all" && !this.trackView.track.supportsWholeGenome) {
23301
23338
  return true
23302
23339
  } else {
23303
23340
  const visibilityWindow = this.trackView.track.visibilityWindow;
23304
23341
  return (
23305
23342
  visibilityWindow !== undefined && visibilityWindow > 0 &&
23306
- (referenceFrame.bpPerPixel * this.$viewport.width() > visibilityWindow))
23343
+ (this.referenceFrame.bpPerPixel * this.$viewport.width() > visibilityWindow))
23307
23344
  }
23308
23345
  };
23309
23346
 
23347
+ if (this.trackView.track && "sequence" === this.trackView.track.type && this.referenceFrame.bpPerPixel > 1) {
23348
+ $$1(this.canvas).remove();
23349
+ this.canvas = undefined;
23350
+ //this.featureCache = undefined
23351
+ return false
23352
+ }
23353
+
23310
23354
  if (!(this.viewIsReady())) {
23311
23355
  return false
23312
23356
  }
23313
23357
 
23314
- if (this.$zoomInNotice) {
23315
- if (showZoomInNotice()) {
23316
- // Out of visibility window
23317
- if (this.canvas) {
23318
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
23319
- this.tile = undefined;
23320
- }
23321
- this.$zoomInNotice.show();
23322
23358
 
23323
- if (this.trackView.track.autoHeight) {
23324
- const minHeight = this.trackView.minHeight || 0;
23325
- this.setContentHeight(minHeight);
23326
- }
23359
+ if (zoomedOutOfWindow()) {
23327
23360
 
23328
- return false
23329
- } else {
23361
+ // Out of visibility window
23362
+ if (this.canvas) {
23363
+ $$1(this.canvas).remove();
23364
+ this.canvas = undefined;
23365
+ //this.featureCache = undefined
23366
+ }
23367
+ if (this.trackView.track.autoHeight) {
23368
+ const minHeight = this.trackView.minHeight || 0;
23369
+ this.setContentHeight(minHeight);
23370
+ }
23371
+ if (this.$zoomInNotice) {
23372
+ this.$zoomInNotice.show();
23373
+ }
23374
+ return false
23375
+ } else {
23376
+ if (this.$zoomInNotice) {
23330
23377
  this.$zoomInNotice.hide();
23331
- return true
23332
23378
  }
23379
+ return true
23333
23380
  }
23334
23381
 
23335
- return true
23336
-
23337
-
23338
23382
  }
23339
23383
 
23384
+ /**
23385
+ * Adjust the canvas to the current genomic state.
23386
+ */
23340
23387
  shift() {
23341
- const self = this;
23342
- const referenceFrame = self.referenceFrame;
23343
-
23344
- if (self.canvas &&
23345
- self.tile &&
23346
- self.tile.chr === self.referenceFrame.chr &&
23347
- self.tile.bpPerPixel === referenceFrame.bpPerPixel) {
23348
-
23349
- const pixelOffset = Math.round((self.tile.startBP - referenceFrame.start) / referenceFrame.bpPerPixel);
23350
- self.canvas.style.left = pixelOffset + "px";
23388
+ const referenceFrame = this.referenceFrame;
23389
+ if (this.canvas &&
23390
+ this.canvas._data &&
23391
+ this.canvas._data.chr === this.referenceFrame.chr &&
23392
+ this.canvas._data.bpPerPixel === referenceFrame.bpPerPixel) {
23393
+ const pixelOffset = Math.round((this.canvas._data.startBP - referenceFrame.start) / referenceFrame.bpPerPixel);
23394
+ this.canvas.style.left = pixelOffset + "px";
23351
23395
  }
23352
23396
  }
23353
23397
 
@@ -23370,22 +23414,23 @@ class TrackViewport extends Viewport {
23370
23414
  this.startSpinner();
23371
23415
 
23372
23416
  try {
23373
- const features = await this.getFeatures(this.trackView.track, chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
23417
+ const track = this.trackView.track;
23418
+ const features = await this.getFeatures(track, chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
23374
23419
  let roiFeatures = [];
23375
- const roi = mergeArrays(this.browser.roi, this.trackView.track.roi);
23420
+ const roi = mergeArrays(this.browser.roi, track.roi);
23376
23421
  if (roi) {
23377
23422
  for (let r of roi) {
23378
- const f = await
23379
- r.getFeatures(chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
23423
+ const f = await r.getFeatures(chr, bpStart, bpEnd, referenceFrame.bpPerPixel);
23380
23424
  roiFeatures.push({track: r, features: f});
23381
23425
  }
23382
23426
  }
23383
23427
 
23384
- this.tile = new Tile(chr, bpStart, bpEnd, referenceFrame.bpPerPixel, features, roiFeatures);
23428
+ const mr = track && ("wig" === track.type || "merged" === track.type); // wig tracks are potentially multiresolution (e.g. bigwig)
23429
+ this.featureCache = new FeatureCache(chr, bpStart, bpEnd, referenceFrame.bpPerPixel, features, roiFeatures, mr);
23385
23430
  this.loading = false;
23386
23431
  this.hideMessage();
23387
23432
  this.stopSpinner();
23388
- return this.tile
23433
+ return this.featureCache
23389
23434
  } catch (error) {
23390
23435
  // Track might have been removed during load
23391
23436
  if (this.trackView && this.trackView.disposed !== true) {
@@ -23399,32 +23444,29 @@ class TrackViewport extends Viewport {
23399
23444
  }
23400
23445
  }
23401
23446
 
23402
- async repaint() {
23447
+ /**
23448
+ * Repaint the canvas using the cached features
23449
+ *
23450
+ */
23451
+ repaint() {
23403
23452
 
23404
- if (undefined === this.tile) {
23453
+ if (undefined === this.featureCache) {
23405
23454
  return
23406
23455
  }
23407
23456
 
23408
- let {features, roiFeatures, bpPerPixel, startBP, endBP} = this.tile;
23457
+ let {features, roiFeatures} = this.featureCache;
23458
+ //this.tile.bpPerPixel = this.referenceFrame.bpPerPixel
23409
23459
 
23410
23460
  // const isWGV = GenomeUtils.isWholeGenomeView(this.browser.referenceFrameList[0].chr)
23411
23461
  const isWGV = GenomeUtils.isWholeGenomeView(this.referenceFrame.chr);
23412
- let pixelWidth;
23413
-
23414
- if (isWGV) {
23415
- bpPerPixel = this.referenceFrame.end / this.$viewport.width();
23416
- startBP = 0;
23417
- endBP = this.referenceFrame.end;
23418
- pixelWidth = this.$viewport.width();
23419
- } else {
23420
- pixelWidth = Math.ceil((endBP - startBP) / bpPerPixel);
23421
- }
23422
23462
 
23463
+ // Canvas dimensions. There is no left-right panning for WGV so canvas width is viewport width.
23423
23464
  // For deep tracks we paint a canvas == 3*viewportHeight centered on the current vertical scroll position
23465
+ const pixelWidth = isWGV ? this.$viewport.width() : 3 * this.$viewport.width();
23424
23466
  const viewportHeight = this.$viewport.height();
23425
23467
  const contentHeight = this.getContentHeight();
23426
23468
  const minHeight = roiFeatures ? Math.max(contentHeight, viewportHeight) : contentHeight; // Need to fill viewport for ROIs.
23427
- let pixelHeight = Math.min(minHeight, 3 * viewportHeight);
23469
+ const pixelHeight = Math.min(minHeight, 3 * viewportHeight);
23428
23470
  if (0 === pixelWidth || 0 === pixelHeight) {
23429
23471
  if (this.canvas) {
23430
23472
  $$1(this.canvas).remove();
@@ -23433,30 +23475,25 @@ class TrackViewport extends Viewport {
23433
23475
  }
23434
23476
  const canvasTop = Math.max(0, -(this.$content.position().top) - viewportHeight);
23435
23477
 
23436
- // Always use high DPI if in compressed display mode, otherwise use preference setting;
23437
- let devicePixelRatio;
23438
- if ("FILL" === this.trackView.track.displayMode) {
23439
- devicePixelRatio = window.devicePixelRatio;
23440
- } else {
23441
- devicePixelRatio = (this.trackView.track.supportHiDPI === false) ? 1 : window.devicePixelRatio;
23442
- }
23443
-
23444
- const pixelXOffset = Math.round((startBP - this.referenceFrame.start) / this.referenceFrame.bpPerPixel);
23478
+ const bpPerPixel = this.referenceFrame.bpPerPixel;
23479
+ const startBP = this.referenceFrame.start - (isWGV ? 0 : pixelWidth / 3 * bpPerPixel);
23480
+ const endBP = this.referenceFrame.end + (isWGV ? 0 : pixelWidth / 3 * bpPerPixel);
23481
+ const pixelXOffset = Math.round((startBP - this.referenceFrame.start) / bpPerPixel);
23445
23482
 
23446
23483
  const newCanvas = $$1('<canvas class="igv-canvas">').get(0);
23447
- const ctx = newCanvas.getContext("2d");
23448
-
23449
23484
  newCanvas.style.width = pixelWidth + "px";
23450
23485
  newCanvas.style.height = pixelHeight + "px";
23486
+ newCanvas.style.left = pixelXOffset + "px";
23487
+ newCanvas.style.top = canvasTop + "px";
23451
23488
 
23489
+ // Always use high DPI if in "FILL" display mode, otherwise use track setting;
23490
+ const devicePixelRatio = ("FILL" === this.trackView.track.displayMode || this.trackView.track.supportHiDPI !== false) ?
23491
+ window.devicePixelRatio : 1;
23452
23492
  newCanvas.width = devicePixelRatio * pixelWidth;
23453
23493
  newCanvas.height = devicePixelRatio * pixelHeight;
23454
23494
 
23495
+ const ctx = newCanvas.getContext("2d");
23455
23496
  ctx.scale(devicePixelRatio, devicePixelRatio);
23456
-
23457
- newCanvas.style.left = pixelXOffset + "px";
23458
- newCanvas.style.top = canvasTop + "px";
23459
-
23460
23497
  ctx.translate(0, -canvasTop);
23461
23498
 
23462
23499
  const drawConfiguration =
@@ -23477,17 +23514,27 @@ class TrackViewport extends Viewport {
23477
23514
 
23478
23515
  this.draw(drawConfiguration, features, roiFeatures);
23479
23516
 
23480
- this.canvasVerticalRange = {top: canvasTop, bottom: canvasTop + pixelHeight};
23517
+ this.featureCache.canvasTop = canvasTop;
23518
+ this.featureCache.height = pixelHeight;
23481
23519
 
23482
- if (this.$canvas) {
23483
- this.$canvas.remove();
23520
+ if (this.canvas) {
23521
+ $$1(this.canvas).remove();
23484
23522
  }
23485
- this.$canvas = $$1(newCanvas);
23486
- this.$content.append(this.$canvas);
23523
+ newCanvas._data = {
23524
+ chr: this.featureCache.chr, bpPerPixel, startBP, endBP, pixelHeight, pixelTop: canvasTop
23525
+ };
23487
23526
  this.canvas = newCanvas;
23488
- this.ctx = ctx;
23527
+ this.$content.append($$1(newCanvas));
23528
+
23489
23529
  }
23490
23530
 
23531
+ /**
23532
+ * Draw the associated track.
23533
+ *
23534
+ * @param drawConfiguration
23535
+ * @param features
23536
+ * @param roiFeatures
23537
+ */
23491
23538
  draw(drawConfiguration, features, roiFeatures) {
23492
23539
 
23493
23540
  // console.log(`${ Date.now() } viewport draw(). track ${ this.trackView.track.type }. content-css-top ${ this.$content.css('top') }. canvas-top ${ drawConfiguration.pixelTop }.`)
@@ -23504,60 +23551,6 @@ class TrackViewport extends Viewport {
23504
23551
  }
23505
23552
  }
23506
23553
 
23507
- // TODO: Nolonger used. Will discard
23508
- async toSVG(tile) {
23509
-
23510
- // Nothing to do if zoomInNotice is active
23511
- if (this.$zoomInNotice && this.$zoomInNotice.is(":visible")) {
23512
- return
23513
- }
23514
-
23515
- const referenceFrame = this.referenceFrame;
23516
- const bpPerPixel = tile.bpPerPixel;
23517
- const features = tile.features;
23518
- const roiFeatures = tile.roiFeatures;
23519
- const pixelWidth = this.$viewport.width();
23520
- const pixelHeight = this.$viewport.height();
23521
- const bpStart = referenceFrame.start;
23522
- const bpEnd = referenceFrame.start + pixelWidth * referenceFrame.bpPerPixel;
23523
-
23524
- const ctx$1 = new ctx(
23525
- {
23526
- // svg
23527
- width: pixelWidth,
23528
- height: pixelHeight,
23529
- viewbox:
23530
- {
23531
- x: 0,
23532
- y: -this.$content.position().top,
23533
- width: pixelWidth,
23534
- height: pixelHeight
23535
- }
23536
-
23537
- });
23538
-
23539
- const drawConfiguration =
23540
- {
23541
- viewport: this,
23542
- context: ctx$1,
23543
- top: -this.$content.position().top,
23544
- pixelTop: 0, // for compatibility with canvas draw
23545
- pixelWidth,
23546
- pixelHeight,
23547
- bpStart,
23548
- bpEnd,
23549
- bpPerPixel,
23550
- referenceFrame: this.referenceFrame,
23551
- selection: this.selection,
23552
- viewportWidth: pixelWidth,
23553
- };
23554
-
23555
- this.draw(drawConfiguration, features, roiFeatures);
23556
-
23557
- return ctx$1.getSerializedSvg(true)
23558
-
23559
- }
23560
-
23561
23554
  containsPosition(chr, position) {
23562
23555
  if (this.referenceFrame.chr === chr && position >= this.referenceFrame.start) {
23563
23556
  return position <= this.referenceFrame.calculateEnd(this.getWidth())
@@ -23570,18 +23563,20 @@ class TrackViewport extends Viewport {
23570
23563
  return this.loading
23571
23564
  }
23572
23565
 
23573
- saveImage() {
23566
+ savePNG() {
23574
23567
 
23575
- if (!this.ctx) return
23568
+ if (!this.canvas) return
23576
23569
 
23577
- const canvasTop = this.canvasVerticalRange ? this.canvasVerticalRange.top : 0;
23570
+ const canvasMetadata = this.featureCache;
23571
+ const canvasTop = canvasMetadata ? canvasMetadata.canvasTop : 0;
23578
23572
  const devicePixelRatio = window.devicePixelRatio;
23579
23573
  const w = this.$viewport.width() * devicePixelRatio;
23580
23574
  const h = this.$viewport.height() * devicePixelRatio;
23581
23575
  const x = -$$1(this.canvas).position().left * devicePixelRatio;
23582
23576
  const y = (-this.$content.position().top - canvasTop) * devicePixelRatio;
23583
23577
 
23584
- const imageData = this.ctx.getImageData(x, y, w, h);
23578
+ const ctx = this.canvas.getContext("2d");
23579
+ const imageData = ctx.getImageData(x, y, w, h);
23585
23580
  const exportCanvas = document.createElement('canvas');
23586
23581
  const exportCtx = exportCanvas.getContext('2d');
23587
23582
  exportCanvas.width = imageData.width;
@@ -23704,32 +23699,53 @@ class TrackViewport extends Viewport {
23704
23699
  selection: this.selection
23705
23700
  };
23706
23701
 
23707
- const features = this.tile ? this.tile.features : [];
23708
- const roiFeatures = this.tile ? this.tile.roiFeatures : undefined;
23702
+ const features = this.featureCache ? this.featureCache.features : [];
23703
+ const roiFeatures = this.featureCache ? this.featureCache.roiFeatures : undefined;
23709
23704
  this.draw(config, features, roiFeatures);
23710
23705
 
23711
23706
  context.restore();
23712
23707
 
23713
23708
  }
23714
23709
 
23715
- getCachedFeatures() {
23716
- return this.tile ? this.tile.features : []
23710
+ get cachedFeatures() {
23711
+ return this.featureCache ? this.featureCache.features : []
23717
23712
  }
23718
23713
 
23719
23714
  async getFeatures(track, chr, start, end, bpPerPixel) {
23720
23715
 
23721
- if (this.tile && this.tile.containsRange(chr, start, end, bpPerPixel)) {
23722
- return this.tile.features
23716
+ if (this.featureCache && this.featureCache.containsRange(chr, start, end, bpPerPixel)) {
23717
+ return this.featureCache.features
23723
23718
  } else if (typeof track.getFeatures === "function") {
23724
23719
  const features = await track.getFeatures(chr, start, end, bpPerPixel, this);
23725
- this.cachedFeatures = features;
23726
- this.checkContentHeight();
23720
+ this.checkContentHeight(features);
23727
23721
  return features
23728
23722
  } else {
23729
23723
  return undefined
23730
23724
  }
23731
23725
  }
23732
23726
 
23727
+ needsRepaint() {
23728
+
23729
+ if (!this.canvas) return true
23730
+
23731
+ const data = this.canvas._data;
23732
+ return !data ||
23733
+ this.referenceFrame.start < data.startBP ||
23734
+ this.referenceFrame.end > data.endBP ||
23735
+ this.referenceFrame.chr !== data.chr ||
23736
+ this.referenceFrame.bpPerPixel != data.bpPerPixel
23737
+ }
23738
+
23739
+ needsReload() {
23740
+ if (!this.featureCache) return true
23741
+ const referenceFrame = this.referenceFrame;
23742
+ const chr = this.referenceFrame.chr;
23743
+ const start = referenceFrame.start;
23744
+ const end = start + referenceFrame.toBP($$1(this.contentDiv).width());
23745
+ const bpPerPixel = referenceFrame.bpPerPixel;
23746
+ return (!this.featureCache.containsRange(chr, start, end, bpPerPixel))
23747
+ }
23748
+
23733
23749
  createZoomInNotice($parent) {
23734
23750
 
23735
23751
  const $container = $$1('<div>', {class: 'igv-zoom-in-notice-container'});
@@ -23751,15 +23767,33 @@ class TrackViewport extends Viewport {
23751
23767
 
23752
23768
  addMouseHandlers() {
23753
23769
 
23754
- this.addViewportContextMenuHandler(this.$viewport.get(0));
23755
-
23756
- this.addViewportMouseDownHandler(this.$viewport.get(0));
23770
+ const viewport = this.$viewport.get(0);
23757
23771
 
23758
- this.addViewportTouchStartHandler(this.$viewport.get(0));
23772
+ this.addViewportContextMenuHandler(viewport);
23759
23773
 
23760
- this.addViewportMouseUpHandler(this.$viewport.get(0));
23774
+ const md = (event) => {
23775
+ this.enableClick = true;
23776
+ this.browser.mouseDownOnViewport(event, this);
23777
+ pageCoordinates$1(event);
23778
+ };
23779
+ viewport.addEventListener('mousedown', md);
23780
+ viewport.addEventListener('touchstart', md);
23781
+
23782
+ const mu = (event) => {
23783
+ // Any mouse up cancels drag and scrolling
23784
+ if (this.browser.dragObject || this.browser.isScrolling) {
23785
+ this.browser.cancelTrackPan();
23786
+ // event.preventDefault();
23787
+ // event.stopPropagation();
23788
+ this.enableClick = false; // Until next mouse down
23789
+ } else {
23790
+ this.browser.cancelTrackPan();
23791
+ this.browser.endTrackDrag();
23792
+ }
23793
+ };
23761
23794
 
23762
- this.addViewportTouchEndHandler(this.$viewport.get(0));
23795
+ viewport.addEventListener('mouseup', mu);
23796
+ viewport.addEventListener('touchend', mu);
23763
23797
 
23764
23798
  this.addViewportClickHandler(this.$viewport.get(0));
23765
23799
 
@@ -23769,32 +23803,9 @@ class TrackViewport extends Viewport {
23769
23803
 
23770
23804
  }
23771
23805
 
23772
- removeMouseHandlers() {
23773
-
23774
- this.removeViewportContextMenuHandler(this.$viewport.get(0));
23775
-
23776
- this.removeViewportMouseDownHandler(this.$viewport.get(0));
23777
-
23778
- this.removeViewportTouchStartHandler(this.$viewport.get(0));
23779
-
23780
- this.removeViewportMouseUpHandler(this.$viewport.get(0));
23781
-
23782
- this.removeViewportTouchEndHandler(this.$viewport.get(0));
23783
-
23784
- this.removeViewportClickHandler(this.$viewport.get(0));
23785
-
23786
- if (this.trackView.track.name && "sequence" !== this.trackView.track.config.type) {
23787
- this.removeTrackLabelClickHandler(this.$trackLabel.get(0));
23788
- }
23789
-
23790
- }
23791
-
23792
23806
  addViewportContextMenuHandler(viewport) {
23793
23807
 
23794
- this.boundContextMenuHandler = contextMenuHandler.bind(this);
23795
- viewport.addEventListener('contextmenu', this.boundContextMenuHandler);
23796
-
23797
- function contextMenuHandler(event) {
23808
+ viewport.addEventListener('contextmenu', (event) => {
23798
23809
 
23799
23810
  // Ignore if we are doing a drag. This can happen with touch events.
23800
23811
  if (this.browser.dragObject) {
@@ -23827,56 +23838,14 @@ class TrackViewport extends Viewport {
23827
23838
  menuItems.push({label: 'Save Image (SVG)', click: () => this.saveSVG()});
23828
23839
 
23829
23840
  this.browser.menuPopup.presentTrackContextMenu(event, menuItems);
23830
- }
23831
-
23832
- }
23833
-
23834
- removeViewportContextMenuHandler(viewport) {
23835
- viewport.removeEventListener('contextmenu', this.boundContextMenuHandler);
23836
- }
23837
-
23838
- addViewportMouseDownHandler(viewport) {
23839
- this.boundMouseDownHandler = mouseDownHandler.bind(this);
23840
- viewport.addEventListener('mousedown', this.boundMouseDownHandler);
23841
- }
23842
-
23843
- removeViewportMouseDownHandler(viewport) {
23844
- viewport.removeEventListener('mousedown', this.boundMouseDownHandler);
23845
- }
23846
-
23847
- addViewportTouchStartHandler(viewport) {
23848
- this.boundTouchStartHandler = mouseDownHandler.bind(this);
23849
- viewport.addEventListener('touchstart', this.boundTouchStartHandler);
23850
- }
23851
-
23852
- removeViewportTouchStartHandler(viewport) {
23853
- viewport.removeEventListener('touchstart', this.boundTouchStartHandler);
23854
- }
23855
-
23856
- addViewportMouseUpHandler(viewport) {
23857
- this.boundMouseUpHandler = mouseUpHandler.bind(this);
23858
- viewport.addEventListener('mouseup', this.boundMouseUpHandler);
23859
- }
23860
-
23861
- removeViewportMouseUpHandler(viewport) {
23862
- viewport.removeEventListener('mouseup', this.boundMouseUpHandler);
23863
- }
23841
+ });
23864
23842
 
23865
- addViewportTouchEndHandler(viewport) {
23866
- this.boundTouchEndHandler = mouseUpHandler.bind(this);
23867
- viewport.addEventListener('touchend', this.boundTouchEndHandler);
23868
23843
  }
23869
23844
 
23870
- removeViewportTouchEndHandler(viewport) {
23871
- viewport.removeEventListener('touchend', this.boundTouchEndHandler);
23872
- }
23873
23845
 
23874
23846
  addViewportClickHandler(viewport) {
23875
23847
 
23876
- this.boundClickHandler = clickHandler.bind(this);
23877
- viewport.addEventListener('click', this.boundClickHandler);
23878
-
23879
- function clickHandler(event) {
23848
+ viewport.addEventListener('click', (event) => {
23880
23849
 
23881
23850
  if (this.enableClick) {
23882
23851
  if (3 === event.which || event.ctrlKey) {
@@ -23961,21 +23930,12 @@ class TrackViewport extends Viewport {
23961
23930
  lastClickTime = time;
23962
23931
 
23963
23932
  }
23964
-
23965
- }
23966
-
23967
- }
23968
-
23969
- removeViewportClickHandler(viewport) {
23970
- viewport.removeEventListener('click', this.boundClickHandler);
23933
+ });
23971
23934
  }
23972
23935
 
23973
23936
  addTrackLabelClickHandler(trackLabel) {
23974
23937
 
23975
- this.boundTrackLabelClickHandler = clickHandler.bind(this);
23976
- trackLabel.addEventListener('click', this.boundTrackLabelClickHandler);
23977
-
23978
- function clickHandler(event) {
23938
+ trackLabel.addEventListener('click', (event) => {
23979
23939
 
23980
23940
  event.stopPropagation();
23981
23941
 
@@ -23995,48 +23955,18 @@ class TrackViewport extends Viewport {
23995
23955
  this.popover = new Popover(this.browser.columnContainer, (track.name || ''));
23996
23956
  this.popover.presentContentWithEvent(event, str);
23997
23957
  }
23998
- }
23999
- }
24000
-
24001
- removeTrackLabelClickHandler(trackLabel) {
24002
- trackLabel.removeEventListener('click', this.boundTrackLabelClickHandler);
23958
+ });
24003
23959
  }
24004
23960
 
24005
23961
  }
24006
23962
 
24007
- function mouseDownHandler(event) {
24008
- this.enableClick = true;
24009
- this.browser.mouseDownOnViewport(event, this);
24010
- pageCoordinates$1(event);
24011
- }
24012
-
24013
- function mouseUpHandler(event) {
24014
-
24015
- // Any mouse up cancels drag and scrolling
24016
- if (this.browser.dragObject || this.browser.isScrolling) {
24017
- this.browser.cancelTrackPan();
24018
- // event.preventDefault();
24019
- // event.stopPropagation();
24020
- this.enableClick = false; // Until next mouse down
24021
- } else {
24022
- this.browser.cancelTrackPan();
24023
- this.browser.endTrackDrag();
24024
- }
24025
- }
24026
-
24027
23963
  function createClickState(event, viewport) {
24028
23964
 
24029
23965
  const referenceFrame = viewport.referenceFrame;
24030
-
24031
23966
  const viewportCoords = translateMouseCoordinates$1(event, viewport.contentDiv);
24032
23967
  const canvasCoords = translateMouseCoordinates$1(event, viewport.canvas);
24033
-
24034
23968
  const genomicLocation = ((referenceFrame.start) + referenceFrame.toBP(viewportCoords.x));
24035
23969
 
24036
- if (undefined === genomicLocation || null === viewport.tile) {
24037
- return undefined
24038
- }
24039
-
24040
23970
  return {
24041
23971
  event,
24042
23972
  viewport,
@@ -24097,22 +24027,30 @@ function formatPopoverText(nameValues) {
24097
24027
  return rows.join('')
24098
24028
  }
24099
24029
 
24100
- var Tile = function (chr, tileStart, tileEnd, bpPerPixel, features, roiFeatures) {
24101
- this.chr = chr;
24102
- this.startBP = tileStart;
24103
- this.endBP = tileEnd;
24104
- this.bpPerPixel = bpPerPixel;
24105
- this.features = features;
24106
- this.roiFeatures = roiFeatures;
24107
- };
24030
+ class FeatureCache {
24108
24031
 
24109
- Tile.prototype.containsRange = function (chr, start, end, bpPerPixel) {
24110
- return this.bpPerPixel === bpPerPixel && start >= this.startBP && end <= this.endBP && chr === this.chr
24111
- };
24032
+ constructor(chr, tileStart, tileEnd, bpPerPixel, features, roiFeatures, multiresolution) {
24033
+ this.chr = chr;
24034
+ this.startBP = tileStart;
24035
+ this.endBP = tileEnd;
24036
+ this.bpPerPixel = bpPerPixel;
24037
+ this.features = features;
24038
+ this.roiFeatures = roiFeatures;
24039
+ this.multiresolution = multiresolution;
24040
+ }
24112
24041
 
24113
- Tile.prototype.overlapsRange = function (chr, start, end) {
24114
- return this.chr === chr && end >= this.startBP && start <= this.endBP
24115
- };
24042
+ containsRange(chr, start, end, bpPerPixel) {
24043
+
24044
+ // For multi-resolution tracks allow for a 2X change in bpPerPixel
24045
+ const r = this.multiresolution ? this.bpPerPixel / bpPerPixel : 1;
24046
+
24047
+ return start >= this.startBP && end <= this.endBP && chr === this.chr && r > 0.5 && r < 2
24048
+ }
24049
+
24050
+ overlapsRange(chr, start, end) {
24051
+ return this.chr === chr && end >= this.startBP && start <= this.endBP
24052
+ }
24053
+ }
24116
24054
 
24117
24055
 
24118
24056
  /**
@@ -24277,11 +24215,13 @@ class RulerSweeper {
24277
24215
 
24278
24216
  validateLocusExtent(this.rulerViewport.browser.genome.getChromosome(this.rulerViewport.referenceFrame.chr).bpLength, extent, this.rulerViewport.browser.minimumBases());
24279
24217
 
24280
- this.rulerViewport.referenceFrame.bpPerPixel = (Math.round(extent.end) - Math.round(extent.start)) / this.rulerViewport.contentDiv.clientWidth;
24281
- this.rulerViewport.referenceFrame.start = Math.round(extent.start);
24282
- this.rulerViewport.referenceFrame.end = Math.round(extent.end);
24218
+ const newStart = Math.round(extent.start);
24219
+ const newEnd = Math.round(extent.end);
24220
+ this.rulerViewport.referenceFrame.bpPerPixel = (newEnd - newStart) / this.rulerViewport.contentDiv.clientWidth;
24221
+ this.rulerViewport.referenceFrame.start = newStart;
24222
+ this.rulerViewport.referenceFrame.end = newEnd;
24283
24223
 
24284
- this.rulerViewport.browser.updateViews(this.rulerViewport.referenceFrame);
24224
+ this.rulerViewport.browser.updateViews();
24285
24225
  }
24286
24226
 
24287
24227
  }
@@ -24419,6 +24359,18 @@ class PairedAlignment {
24419
24359
  return true // By definition
24420
24360
  }
24421
24361
 
24362
+ isMateMapped() {
24363
+ return true // By definition
24364
+ }
24365
+
24366
+ isProperPair() {
24367
+ return this.firstAlignment.isProperPair()
24368
+ }
24369
+
24370
+ get fragmentLength() {
24371
+ return Math.abs(this.firstAlignment.fragmentLength)
24372
+ }
24373
+
24422
24374
  firstOfPairStrand() {
24423
24375
 
24424
24376
  if (this.firstAlignment.isFirstOfPair()) {
@@ -24760,7 +24712,10 @@ function packAlignmentRows(alignments, start, end, showSoftClips) {
24760
24712
 
24761
24713
 
24762
24714
  class AlignmentContainer {
24763
- constructor(chr, start, end, samplingWindowSize, samplingDepth, pairsSupported, alleleFreqThreshold) {
24715
+
24716
+ // this.config.samplingWindowSize, this.config.samplingDepth,
24717
+ // this.config.pairsSupported, this.config.alleleFreqThreshold)
24718
+ constructor(chr, start, end, {samplingWindowSize, samplingDepth, pairsSupported, alleleFreqThreshold}) {
24764
24719
 
24765
24720
  this.chr = chr;
24766
24721
  this.start = Math.floor(start);
@@ -24788,17 +24743,12 @@ class AlignmentContainer {
24788
24743
  return alignment.isMapped() && !alignment.isFailsVendorQualityCheck()
24789
24744
  };
24790
24745
 
24791
- this.pairedEndStats = new PairedEndStats();
24792
24746
  }
24793
24747
 
24794
24748
  push(alignment) {
24795
24749
 
24796
24750
  if (this.filter(alignment) === false) return
24797
24751
 
24798
- if (alignment.isPaired()) {
24799
- this.pairedEndStats.push(alignment);
24800
- }
24801
-
24802
24752
  this.coverageMap.incCounts(alignment); // Count coverage before any downsampling
24803
24753
 
24804
24754
  if (this.pairsSupported && this.downsampledReads.has(alignment.readName)) {
@@ -24830,8 +24780,6 @@ class AlignmentContainer {
24830
24780
 
24831
24781
  this.pairsCache = undefined;
24832
24782
  this.downsampledReads = undefined;
24833
-
24834
- this.pairedEndStats.compute();
24835
24783
  }
24836
24784
 
24837
24785
  contains(chr, start, end) {
@@ -25142,69 +25090,6 @@ class DownsampledInterval {
25142
25090
  }
25143
25091
  }
25144
25092
 
25145
- class PairedEndStats {
25146
-
25147
- constructor(lowerPercentile, upperPercentile) {
25148
- this.totalCount = 0;
25149
- this.frCount = 0;
25150
- this.rfCount = 0;
25151
- this.ffCount = 0;
25152
- this.sumF = 0;
25153
- this.sumF2 = 0;
25154
- //this.lp = lowerPercentile === undefined ? 0.005 : lowerPercentile;
25155
- //this.up = upperPercentile === undefined ? 0.995 : upperPercentile;
25156
- //this.digest = new Digest();
25157
- }
25158
-
25159
- push(alignment) {
25160
-
25161
- if (alignment.isProperPair()) {
25162
-
25163
- var fragmentLength = Math.abs(alignment.fragmentLength);
25164
- //this.digest.push(fragmentLength);
25165
- this.sumF += fragmentLength;
25166
- this.sumF2 += fragmentLength * fragmentLength;
25167
-
25168
- var po = alignment.pairOrientation;
25169
-
25170
- if (typeof po === "string" && po.length === 4) {
25171
- var tmp = '' + po.charAt(0) + po.charAt(2);
25172
- switch (tmp) {
25173
- case 'FF':
25174
- case 'RR':
25175
- this.ffCount++;
25176
- break
25177
- case "FR":
25178
- this.frCount++;
25179
- break
25180
- case"RF":
25181
- this.rfCount++;
25182
- }
25183
- }
25184
- this.totalCount++;
25185
- }
25186
- }
25187
-
25188
- compute() {
25189
-
25190
- if (this.totalCount > 100) {
25191
- if (this.ffCount / this.totalCount > 0.9) this.orienation = "ff";
25192
- else if (this.frCount / this.totalCount > 0.9) this.orienation = "fr";
25193
- else if (this.rfCount / this.totalCount > 0.9) this.orienation = "rf";
25194
-
25195
-
25196
- var fMean = this.sumF / this.totalCount;
25197
- var stdDev = Math.sqrt((this.totalCount * this.sumF2 - this.sumF * this.sumF) / (this.totalCount * this.totalCount));
25198
- this.lowerFragmentLength = fMean - 3 * stdDev;
25199
- this.upperFragmentLength = fMean + 3 * stdDev;
25200
-
25201
- //this.lowerFragmentLength = this.digest.percentile(this.lp);
25202
- //this.upperFragmentLength = this.digest.percentile(this.up);
25203
- //this.digest = undefined;
25204
- }
25205
- }
25206
- }
25207
-
25208
25093
  class SupplementaryAlignment {
25209
25094
 
25210
25095
  constructor(rec) {
@@ -25925,7 +25810,7 @@ const BamUtils = {
25925
25810
  const lseq = readInt(ba, offset + 20);
25926
25811
  const mateChrIdx = readInt(ba, offset + 24);
25927
25812
  const matePos = readInt(ba, offset + 28);
25928
- const tlen = readInt(ba, offset + 32);
25813
+ const fragmentLength = readInt(ba, offset + 32);
25929
25814
 
25930
25815
  let readName = [];
25931
25816
  for (let j = 0; j < nl - 1; ++j) {
@@ -25966,7 +25851,7 @@ const BamUtils = {
25966
25851
  alignment.readName = readName;
25967
25852
  alignment.cigar = cigar;
25968
25853
  alignment.lengthOnRef = lengthOnRef;
25969
- alignment.fragmentLength = tlen;
25854
+ alignment.fragmentLength = fragmentLength;
25970
25855
  alignment.mq = mq;
25971
25856
 
25972
25857
  BamUtils.bam_tag2cigar(ba, blockEnd, p, lseq, alignment, cigarArray);
@@ -26394,7 +26279,7 @@ class BamReaderNonIndexed {
26394
26279
  const header = this.header;
26395
26280
  const queryChr = header.chrAliasTable.hasOwnProperty(chr) ? header.chrAliasTable[chr] : chr;
26396
26281
  const qAlignments = this.alignmentCache.queryFeatures(queryChr, bpStart, bpEnd);
26397
- const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.samplingWindowSize, this.samplingDepth, this.pairsSupported, this.alleleFreqThreshold);
26282
+ const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);
26398
26283
  for (let a of qAlignments) {
26399
26284
  alignmentContainer.push(a);
26400
26285
  }
@@ -26421,13 +26306,13 @@ class BamReaderNonIndexed {
26421
26306
  const alignments = [];
26422
26307
  this.header = BamUtils.decodeBamHeader(data);
26423
26308
  BamUtils.decodeBamRecords(data, this.header.size, alignments, this.header.chrNames);
26424
- this.alignmentCache = new FeatureCache(alignments, this.genome);
26309
+ this.alignmentCache = new FeatureCache$1(alignments, this.genome);
26425
26310
  }
26426
26311
 
26427
26312
  fetchAlignments(chr, bpStart, bpEnd) {
26428
26313
  const queryChr = this.header.chrAliasTable.hasOwnProperty(chr) ? this.header.chrAliasTable[chr] : chr;
26429
26314
  const features = this.alignmentCache.queryFeatures(queryChr, bpStart, bpEnd);
26430
- const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.samplingWindowSize, this.samplingDepth, this.pairsSupported);
26315
+ const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);
26431
26316
  for (let feature of features) {
26432
26317
  alignmentContainer.push(feature);
26433
26318
  }
@@ -27418,9 +27303,7 @@ class BamReader {
27418
27303
  const chrToIndex = await this.getChrIndex();
27419
27304
  const queryChr = this.chrAliasTable.hasOwnProperty(chr) ? this.chrAliasTable[chr] : chr;
27420
27305
  const chrId = chrToIndex[queryChr];
27421
- const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd,
27422
- this.config.samplingWindowSize, this.config.samplingDepth,
27423
- this.config.pairsSupported, this.config.alleleFreqThreshold);
27306
+ const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);
27424
27307
 
27425
27308
  if (chrId === undefined) {
27426
27309
  return alignmentContainer
@@ -27552,7 +27435,7 @@ class ShardedBamReader {
27552
27435
  async readAlignments(chr, start, end) {
27553
27436
 
27554
27437
  if (!this.bamReaders.hasOwnProperty(chr)) {
27555
- return new AlignmentContainer(chr, start, end)
27438
+ return new AlignmentContainer(chr, start, end, this.config)
27556
27439
  } else {
27557
27440
 
27558
27441
  let reader = this.bamReaders[chr];
@@ -27643,7 +27526,7 @@ BamWebserviceReader.prototype.readAlignments = function (chr, bpStart, bpEnd) {
27643
27526
 
27644
27527
  header.chrToIndex[queryChr];
27645
27528
 
27646
- alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, self.samplingWindowSize, self.samplingDepth, self.pairsSupported, self.alleleFreqThreshold);
27529
+ alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, self.config);
27647
27530
 
27648
27531
  BamUtils.decodeSamRecords(sam, alignmentContainer, queryChr, bpStart, bpEnd, self.filter);
27649
27532
 
@@ -27897,7 +27780,7 @@ class HtsgetBamReader extends HtsgetReader {
27897
27780
  const ba = unbgzf(compressedData.buffer);
27898
27781
 
27899
27782
  const chrIdx = this.header.chrToIndex[chr];
27900
- const alignmentContainer = new AlignmentContainer(chr, start, end, this.samplingWindowSize, this.samplingDepth, this.pairsSupported, this.alleleFreqThreshold);
27783
+ const alignmentContainer = new AlignmentContainer(chr, start, end, this.config);
27901
27784
  BamUtils.decodeBamRecords(ba, this.header.size, alignmentContainer, this.header.chrNames, chrIdx, start, end);
27902
27785
  alignmentContainer.finish();
27903
27786
 
@@ -28063,8 +27946,7 @@ class CramReader {
28063
27946
  const header = await this.getHeader();
28064
27947
  const queryChr = header.chrAliasTable.hasOwnProperty(chr) ? header.chrAliasTable[chr] : chr;
28065
27948
  const chrIdx = header.chrToIndex[queryChr];
28066
- const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd,
28067
- this.samplingWindowSize, this.samplingDepth, this.pairsSupported, this.alleleFreqThreshold);
27949
+ const alignmentContainer = new AlignmentContainer(chr, bpStart, bpEnd, this.config);
28068
27950
 
28069
27951
  if (chrIdx === undefined) {
28070
27952
  return alignmentContainer
@@ -28680,7 +28562,7 @@ Ga4ghAlignmentReader.prototype.readAlignments = function (chr, bpStart, bpEnd) {
28680
28562
  "pageSize": "10000"
28681
28563
  },
28682
28564
  decode: decodeGa4ghReads,
28683
- results: new AlignmentContainer(chr, bpStart, bpEnd, self.samplingWindowSize, self.samplingDepth, self.pairsSupported, self.alleleFreqThreshold)
28565
+ results: new AlignmentContainer(chr, bpStart, bpEnd, self.config)
28684
28566
  })
28685
28567
  })
28686
28568
 
@@ -29013,7 +28895,6 @@ class BamSource {
29013
28895
 
29014
28896
  this.config = config;
29015
28897
  this.genome = genome;
29016
- this.alignmentContainer = undefined;
29017
28898
 
29018
28899
  if (isDataURL(config.url)) {
29019
28900
  if ("cram" === config.format) {
@@ -29061,19 +28942,11 @@ class BamSource {
29061
28942
  }
29062
28943
 
29063
28944
  setViewAsPairs(bool) {
29064
-
29065
- if (this.viewAsPairs !== bool) {
29066
- this.viewAsPairs = bool;
29067
- // if (this.alignmentContainer) {
29068
- // this.alignmentContainer.setViewAsPairs(bool);
29069
- // }
29070
- }
28945
+ this.viewAsPairs = bool;
29071
28946
  }
29072
28947
 
29073
28948
  setShowSoftClips(bool) {
29074
- if (this.showSoftClips !== bool) {
29075
- this.showSoftClips = bool;
29076
- }
28949
+ this.showSoftClips = bool;
29077
28950
  }
29078
28951
 
29079
28952
  async getAlignments(chr, bpStart, bpEnd) {
@@ -29081,33 +28954,28 @@ class BamSource {
29081
28954
  const genome = this.genome;
29082
28955
  const showSoftClips = this.showSoftClips;
29083
28956
 
29084
- if (this.alignmentContainer && this.alignmentContainer.contains(chr, bpStart, bpEnd)) {
29085
- return this.alignmentContainer
28957
+ const alignmentContainer = await this.bamReader.readAlignments(chr, bpStart, bpEnd);
28958
+ let alignments = alignmentContainer.alignments;
28959
+ if (!this.viewAsPairs) {
28960
+ alignments = unpairAlignments([{alignments: alignments}]);
28961
+ }
28962
+ const hasAlignments = alignments.length > 0;
28963
+ alignmentContainer.packedAlignmentRows = packAlignmentRows(alignments, alignmentContainer.start, alignmentContainer.end, showSoftClips);
29086
28964
 
29087
- } else {
29088
- const alignmentContainer = await this.bamReader.readAlignments(chr, bpStart, bpEnd);
29089
- let alignments = alignmentContainer.alignments;
29090
- if (!this.viewAsPairs) {
29091
- alignments = unpairAlignments([{alignments: alignments}]);
29092
- }
29093
- const hasAlignments = alignments.length > 0;
29094
- alignmentContainer.packedAlignmentRows = packAlignmentRows(alignments, alignmentContainer.start, alignmentContainer.end, showSoftClips);
29095
- alignmentContainer.alignments = undefined; // Don't need to hold onto these anymore
29096
-
29097
- this.alignmentContainer = alignmentContainer;
29098
-
29099
- if (hasAlignments) {
29100
- const sequence = await genome.sequence.getSequence(chr, alignmentContainer.start, alignmentContainer.end);
29101
- if (sequence) {
29102
- alignmentContainer.coverageMap.refSeq = sequence; // TODO -- fix this
29103
- alignmentContainer.sequence = sequence; // TODO -- fix this
29104
- return alignmentContainer
29105
- } else {
29106
- console.error("No sequence for: " + chr + ":" + alignmentContainer.start + "-" + alignmentContainer.end);
29107
- }
28965
+ this.alignmentContainer = alignmentContainer;
28966
+
28967
+ if (hasAlignments) {
28968
+ const sequence = await genome.sequence.getSequence(chr, alignmentContainer.start, alignmentContainer.end);
28969
+ if (sequence) {
28970
+ alignmentContainer.coverageMap.refSeq = sequence; // TODO -- fix this
28971
+ alignmentContainer.sequence = sequence; // TODO -- fix this
28972
+ return alignmentContainer
28973
+ } else {
28974
+ console.error("No sequence for: " + chr + ":" + alignmentContainer.start + "-" + alignmentContainer.end);
29108
28975
  }
29109
- return alignmentContainer
29110
28976
  }
28977
+ return alignmentContainer
28978
+
29111
28979
  }
29112
28980
  }
29113
28981
 
@@ -29287,7 +29155,7 @@ class TrackBase {
29287
29155
  return state
29288
29156
  }
29289
29157
 
29290
- supportsWholeGenome() {
29158
+ get supportsWholeGenome() {
29291
29159
  return false
29292
29160
  }
29293
29161
 
@@ -29425,7 +29293,7 @@ class TrackBase {
29425
29293
 
29426
29294
  // We use the cached features rather than method to avoid async load. If the
29427
29295
  // feature is not already loaded this won't work, but the user wouldn't be mousing over it either.
29428
- if (!features) features = clickState.viewport.getCachedFeatures();
29296
+ if (!features) features = clickState.viewport.cachedFeatures;
29429
29297
 
29430
29298
  if (!features || features.length === 0) {
29431
29299
  return []
@@ -29929,8 +29797,7 @@ class Locus {
29929
29797
  }
29930
29798
 
29931
29799
  overlaps(locus) {
29932
- return locus.chr === this.chr
29933
- && !(locus.end < this.start || locus.start > this.end)
29800
+ return locus.chr === this.chr && !(locus.end < this.start || locus.start > this.end)
29934
29801
  }
29935
29802
 
29936
29803
  extend(l) {
@@ -31807,6 +31674,22 @@ function makeCircViewChromosomes(genome) {
31807
31674
  return regions
31808
31675
  }
31809
31676
 
31677
+ function sendChords(chords, track, refFrame, alpha) {
31678
+
31679
+ const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? track.color : getChrColor(refFrame.chr), alpha);
31680
+ const trackColor = IGVColor.addAlpha(track.color || 'rgb(0,0,255)', alpha);
31681
+
31682
+ // name the chord set to include locus and filtering information
31683
+ const encodedName = track.name.replaceAll(' ', '%20');
31684
+ const chordSetName = "all" === refFrame.chr ? encodedName :
31685
+ `${encodedName} ${refFrame.chr}:${refFrame.start}-${refFrame.end}`;
31686
+ track.browser.circularView.addChords(chords, {track: chordSetName, color: chordSetColor, trackColor: trackColor});
31687
+
31688
+ // show circular view if hidden
31689
+ if(!track.browser.circularViewVisible) track.browser.circularViewVisible = true;
31690
+
31691
+ }
31692
+
31810
31693
 
31811
31694
  function createCircularView(el, browser) {
31812
31695
 
@@ -31816,42 +31699,105 @@ function createCircularView(el, browser) {
31816
31699
 
31817
31700
  const f1 = feature.data;
31818
31701
  const f2 = f1.mate;
31819
- const flanking = 2000;
31702
+ addFrameForFeature(f1);
31703
+ addFrameForFeature(f2);
31820
31704
 
31821
- const l1 = new Locus({chr: browser.genome.getChromosomeName(f1.refName), start: f1.start, end: f1.end});
31822
- const l2 = new Locus({chr: browser.genome.getChromosomeName(f2.refName), start: f2.start, end: f2.end});
31705
+ function addFrameForFeature(feature) {
31823
31706
 
31824
- let loci;
31707
+ feature.chr = browser.genome.getChromosomeName(feature.refName);
31708
+ let frameFound = false;
31709
+ for (let referenceFrame of browser.referenceFrameList) {
31710
+ const l = Locus.fromLocusString(referenceFrame.getLocusString());
31711
+ if (l.contains(feature)) {
31712
+ frameFound = true;
31713
+ break
31714
+ } else if (l.overlaps(feature)) {
31715
+ referenceFrame.extend(feature);
31716
+ frameFound = true;
31717
+ break
31718
+ }
31719
+ }
31720
+ if (!frameFound) {
31721
+ const flanking = 2000;
31722
+ const center = (feature.start + feature.end) / 2;
31723
+ browser.addMultiLocusPanel(feature.chr, center - flanking, center + flanking);
31825
31724
 
31826
- // If there is overlap with current loci
31725
+ }
31726
+ }
31727
+ }
31728
+ });
31827
31729
 
31828
- loci = browser.currentLoci().map(str => Locus.fromLocusString(str));
31730
+ return circularView
31731
+ }
31829
31732
 
31830
- if (loci.some(locus => locus.contains(l1)) || loci.some(locus => locus.contains(l2))) {
31831
- for (let l of [l1, l2]) {
31832
- if (!loci.some(locus => {
31833
- return locus.contains(l)
31834
- })) {
31835
- // add flanking
31836
- l.start = Math.max(0, l.start - flanking);
31837
- l.end += flanking;
31838
- loci.push(l);
31733
+ class PairedEndStats {
31734
+
31735
+ constructor(alignments, {minTLENPercentile, maxTLENPercentile}) {
31736
+ this.totalCount = 0;
31737
+ this.frCount = 0;
31738
+ this.rfCount = 0;
31739
+ this.ffCount = 0;
31740
+ this.sumF = 0;
31741
+ this.sumF2 = 0;
31742
+ this.lp = minTLENPercentile === undefined ? 0.1 : minTLENPercentile;
31743
+ this.up = maxTLENPercentile === undefined ? 99.5 : maxTLENPercentile;
31744
+ this.isizes = [];
31745
+ this.compute(alignments);
31746
+ }
31747
+
31748
+ compute(alignments) {
31749
+
31750
+ for (let alignment of alignments) {
31751
+ if (alignment.isProperPair()) {
31752
+ var tlen = Math.abs(alignment.fragmentLength);
31753
+ this.sumF += tlen;
31754
+ this.sumF2 += tlen * tlen;
31755
+ this.isizes.push(tlen);
31756
+
31757
+ var po = alignment.pairOrientation;
31758
+
31759
+ if (typeof po === "string" && po.length === 4) {
31760
+ var tmp = '' + po.charAt(0) + po.charAt(2);
31761
+ switch (tmp) {
31762
+ case 'FF':
31763
+ case 'RR':
31764
+ this.ffCount++;
31765
+ break
31766
+ case "FR":
31767
+ this.frCount++;
31768
+ break
31769
+ case"RF":
31770
+ this.rfCount++;
31839
31771
  }
31840
31772
  }
31841
- } else {
31842
- l1.start = Math.max(0, l1.start - flanking);
31843
- l1.end += flanking;
31844
- l2.start = Math.max(0, l2.start - flanking);
31845
- l2.end += flanking;
31846
- loci = [l1, l2];
31773
+ this.totalCount++;
31847
31774
  }
31848
-
31849
- const searchString = loci.map(l => l.getLocusString()).join(" ");
31850
- browser.search(searchString);
31851
31775
  }
31776
+
31777
+ if (this.ffCount / this.totalCount > 0.9) this.orienation = "ff";
31778
+ else if (this.frCount / this.totalCount > 0.9) this.orienation = "fr";
31779
+ else if (this.rfCount / this.totalCount > 0.9) this.orienation = "rf";
31780
+
31781
+ this.minTLEN = this.lp === 0 ? 0 : percentile(this.isizes, this.lp);
31782
+ this.maxTLEN = percentile(this.isizes, this.up);
31783
+
31784
+ // var fMean = this.sumF / this.totalCount
31785
+ // var stdDev = Math.sqrt((this.totalCount * this.sumF2 - this.sumF * this.sumF) / (this.totalCount * this.totalCount))
31786
+ // this.minTLEN = fMean - 3 * stdDev
31787
+ // this.maxTLEN = fMean + 3 * stdDev
31788
+
31789
+ }
31790
+ }
31791
+
31792
+ function percentile(array, p) {
31793
+
31794
+ if (array.length === 0) return undefined
31795
+ var k = Math.floor(array.length * (p / 100));
31796
+ array.sort(function (a, b) {
31797
+ return a - b
31852
31798
  });
31799
+ return array[k]
31853
31800
 
31854
- return circularView
31855
31801
  }
31856
31802
 
31857
31803
  /*
@@ -31918,8 +31864,6 @@ class BAMTrack extends TrackBase {
31918
31864
  this.showMismatches = false !== config.showMismatches;
31919
31865
  this.color = config.color;
31920
31866
  this.coverageColor = config.coverageColor;
31921
- this.minFragmentLength = config.minFragmentLength; // Optional, might be undefined
31922
- this.maxFragmentLength = config.maxFragmentLength || 1000;
31923
31867
 
31924
31868
  // The sort object can be an array in the case of multi-locus view, however if multiple sort positions
31925
31869
  // are present for a given reference frame the last one will take precedence
@@ -31947,12 +31891,24 @@ class BAMTrack extends TrackBase {
31947
31891
  return this._height
31948
31892
  }
31949
31893
 
31894
+ get minTemplateLength() {
31895
+ const configMinTLEN = this.config.minTLEN !== undefined ? this.config.minTLEN : this.config.minFragmentLength;
31896
+ return (configMinTLEN !== undefined) ? configMinTLEN :
31897
+ this._pairedEndStats ? this._pairedEndStats.minTLEN : 0
31898
+ }
31899
+
31900
+ get maxTemplateLength() {
31901
+ const configMaxTLEN = this.config.maxTLEN !== undefined ? this.config.maxTLEN : this.config.maxFragmentLength;
31902
+ return (configMaxTLEN !== undefined) ? configMaxTLEN :
31903
+ this._pairedEndStats ? this._pairedEndStats.maxTLEN : 1000
31904
+ }
31905
+
31950
31906
  sort(options) {
31951
31907
  options = this.assignSort(options);
31952
31908
 
31953
31909
  for (let vp of this.trackView.viewports) {
31954
31910
  if (vp.containsPosition(options.chr, options.position)) {
31955
- const alignmentContainer = vp.getCachedFeatures();
31911
+ const alignmentContainer = vp.cachedFeatures;
31956
31912
  if (alignmentContainer) {
31957
31913
  sortAlignmentRows(options, alignmentContainer);
31958
31914
  vp.repaint();
@@ -31987,14 +31943,13 @@ class BAMTrack extends TrackBase {
31987
31943
 
31988
31944
  const alignmentContainer = await this.featureSource.getAlignments(chr, bpStart, bpEnd);
31989
31945
 
31990
- if (alignmentContainer.alignments && alignmentContainer.alignments.length > 99) {
31991
- if (undefined === this.minFragmentLength) {
31992
- this.minFragmentLength = alignmentContainer.pairedEndStats.lowerFragmentLength;
31993
- }
31994
- if (undefined === this.maxFragmentLength) {
31995
- this.maxFragmentLength = alignmentContainer.pairedEndStats.upperFragmentLength;
31946
+ if (alignmentContainer.paired && !this._pairedEndStats && !this.config.maxFragmentLength) {
31947
+ const pairedEndStats = new PairedEndStats(alignmentContainer.alignments, this.config);
31948
+ if (pairedEndStats.totalCount > 99) {
31949
+ this._pairedEndStats = pairedEndStats;
31996
31950
  }
31997
31951
  }
31952
+ alignmentContainer.alignments = undefined; // Don't need to hold onto these anymore
31998
31953
 
31999
31954
  const sort = this.sortObject;
32000
31955
  if (sort) {
@@ -32091,7 +32046,7 @@ class BAMTrack extends TrackBase {
32091
32046
  if (this.alignmentTrack.hasPairs) {
32092
32047
  colorByMenuItems.push({key: 'firstOfPairStrand', label: 'first-of-pair strand'});
32093
32048
  colorByMenuItems.push({key: 'pairOrientation', label: 'pair orientation'});
32094
- colorByMenuItems.push({key: 'fragmentLength', label: 'insert size (TLEN)'});
32049
+ colorByMenuItems.push({key: 'tlen', label: 'insert size (TLEN)'});
32095
32050
  colorByMenuItems.push({key: 'unexpectedPair', label: 'pair orientation & insert size (TLEN)'});
32096
32051
  }
32097
32052
  const tagLabel = 'tag' + (this.alignmentTrack.colorByTag ? ' (' + this.alignmentTrack.colorByTag + ')' : '');
@@ -32197,32 +32152,17 @@ class BAMTrack extends TrackBase {
32197
32152
  });
32198
32153
  }
32199
32154
 
32200
- // Experimental JBrowse feature
32201
- if (this.browser.circularView && true === this.browser.circularViewVisible &&
32155
+ // Add chords to JBrowse circular view, if present
32156
+ if (this.browser.circularView &&
32202
32157
  (this.alignmentTrack.hasPairs || this.alignmentTrack.hasSupplemental)) {
32203
32158
  menuItems.push('<hr/>');
32204
32159
  if (this.alignmentTrack.hasPairs) {
32205
32160
  menuItems.push({
32206
32161
  label: 'Add discordant pairs to circular view',
32207
32162
  click: () => {
32208
- const maxFragmentLength = this.maxFragmentLength;
32209
- const inView = [];
32210
32163
  for (let viewport of this.trackView.viewports) {
32211
- for (let a of viewport.getCachedFeatures().allAlignments()) {
32212
- const referenceFrame = viewport.referenceFrame;
32213
- if (a.end >= referenceFrame.start
32214
- && a.start <= referenceFrame.end
32215
- && a.mate
32216
- && a.mate.chr
32217
- && (a.mate.chr !== a.chr || Math.max(a.fragmentLength) > maxFragmentLength)) {
32218
- inView.push(a);
32219
- }
32220
- }
32164
+ this.addPairedChordsForViewport(viewport);
32221
32165
  }
32222
- this.browser.circularViewVisible = true;
32223
- const chords = makePairedAlignmentChords(inView);
32224
- const color = IGVColor.addAlpha(this.color || 'rgb(0,0,255)', 0.02);
32225
- this.browser.circularView.addChords(chords, {track: this.name, color: color});
32226
32166
  }
32227
32167
  });
32228
32168
  }
@@ -32230,19 +32170,9 @@ class BAMTrack extends TrackBase {
32230
32170
  menuItems.push({
32231
32171
  label: 'Add split reads to circular view',
32232
32172
  click: () => {
32233
- const inView = [];
32234
32173
  for (let viewport of this.trackView.viewports) {
32235
- for (let a of viewport.getCachedFeatures().allAlignments()) {
32236
- const referenceFrame = viewport.referenceFrame;
32237
- const sa = a.hasTag('SA');
32238
- if (a.end >= referenceFrame.start && a.start <= referenceFrame.end && sa) {
32239
- inView.push(a);
32240
- }
32241
- }
32174
+ this.addSplitChordsForViewport(viewport);
32242
32175
  }
32243
- const chords = makeSupplementalAlignmentChords(inView);
32244
- const color = IGVColor.addAlpha(this.color || 'rgb(0,0,255)', 0.1);
32245
- this.browser.circularView.addChords(chords, {track: this.name, color: color});
32246
32176
  }
32247
32177
  });
32248
32178
  }
@@ -32354,7 +32284,7 @@ class BAMTrack extends TrackBase {
32354
32284
  }
32355
32285
 
32356
32286
  getCachedAlignmentContainers() {
32357
- return this.trackView.viewports.map(vp => vp.getCachedFeatures())
32287
+ return this.trackView.viewports.map(vp => vp.cachedFeatures)
32358
32288
  }
32359
32289
 
32360
32290
  get dataRange() {
@@ -32380,6 +32310,62 @@ class BAMTrack extends TrackBase {
32380
32310
  set autoscale(autoscale) {
32381
32311
  this.coverageTrack.autoscale = autoscale;
32382
32312
  }
32313
+
32314
+ /**
32315
+ * Add chords to the circular view for the given viewport, represented by its reference frame
32316
+ * @param refFrame
32317
+ */
32318
+ addPairedChordsForViewport(viewport) {
32319
+
32320
+ const maxTemplateLength = this.maxTemplateLength;
32321
+ const inView = [];
32322
+ const refFrame = viewport.referenceFrame;
32323
+ for (let a of viewport.cachedFeatures.allAlignments()) {
32324
+ if (a.end >= refFrame.start
32325
+ && a.start <= refFrame.end
32326
+ && a.mate
32327
+ && a.mate.chr
32328
+ && (a.mate.chr !== a.chr || Math.max(a.fragmentLength) > maxTemplateLength)) {
32329
+ inView.push(a);
32330
+ }
32331
+ }
32332
+ const chords = makePairedAlignmentChords(inView);
32333
+ sendChords(chords, this, refFrame, 0.02);
32334
+
32335
+ // const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? this.color : getChrColor(refFrame.chr), 0.02)
32336
+ // const trackColor = IGVColor.addAlpha(this.color || 'rgb(0,0,255)', 0.02)
32337
+ //
32338
+ // // name the chord set to include track name and locus
32339
+ // const encodedName = this.name.replaceAll(' ', '%20')
32340
+ // const chordSetName = "all" === refFrame.chr ? encodedName :
32341
+ // `${encodedName} (${refFrame.chr}:${refFrame.start}-${refFrame.end}`
32342
+ // this.browser.circularView.addChords(chords, {name: chordSetName, color: chordSetColor, trackColor: trackColor})
32343
+ }
32344
+
32345
+ addSplitChordsForViewport(viewport) {
32346
+
32347
+ const inView = [];
32348
+ const refFrame = viewport.referenceFrame;
32349
+ for (let a of viewport.cachedFeatures.allAlignments()) {
32350
+
32351
+ const sa = a.hasTag('SA');
32352
+ if (a.end >= refFrame.start && a.start <= refFrame.end && sa) {
32353
+ inView.push(a);
32354
+ }
32355
+ }
32356
+
32357
+ const chords = makeSupplementalAlignmentChords(inView);
32358
+ sendChords(chords, this, refFrame, 0.02);
32359
+
32360
+ // const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? this.color : getChrColor(refFrame.chr), 0.02)
32361
+ // const trackColor = IGVColor.addAlpha(this.color || 'rgb(0,0,255)', 0.02)
32362
+ //
32363
+ // // name the chord set to include track name and locus
32364
+ // const encodedName = this.name.replaceAll(' ', '%20')
32365
+ // const chordSetName = "all" === refFrame.chr ? encodedName :
32366
+ // `${encodedName} (${refFrame.chr}:${refFrame.start}-${refFrame.end}`
32367
+ // this.browser.circularView.addChords(chords, {name: chordSetName, color: chordSetColor, trackColor: trackColor})
32368
+ }
32383
32369
  }
32384
32370
 
32385
32371
 
@@ -32500,7 +32486,7 @@ class CoverageTrack {
32500
32486
 
32501
32487
  getClickedObject(clickState) {
32502
32488
 
32503
- let features = clickState.viewport.getCachedFeatures();
32489
+ let features = clickState.viewport.cachedFeatures;
32504
32490
  if (!features || features.length === 0) return
32505
32491
 
32506
32492
  const genomicLocation = Math.floor(clickState.genomicLocation);
@@ -32576,8 +32562,8 @@ class AlignmentTrack {
32576
32562
  this.skippedColor = config.skippedColor || "rgb(150, 170, 170)";
32577
32563
  this.pairConnectorColor = config.pairConnectorColor;
32578
32564
 
32579
- this.smallFragmentLengthColor = config.smallFragmentLengthColor || "rgb(0, 0, 150)";
32580
- this.largeFragmentLengthColor = config.largeFragmentLengthColor || "rgb(200, 0, 0)";
32565
+ this.smallTLENColor = config.smallTLENColor || config.smallFragmentLengthColor || "rgb(0, 0, 150)";
32566
+ this.largeTLENColor = config.largeTLENColor || config.largeFragmentLengthColor || "rgb(200, 0, 0)";
32581
32567
 
32582
32568
  this.pairOrientation = config.pairOrienation || 'fr';
32583
32569
  this.pairColors = {};
@@ -32585,7 +32571,7 @@ class AlignmentTrack {
32585
32571
  this.pairColors["RR"] = config.rrColor || "rgb(20, 50, 200)";
32586
32572
  this.pairColors["LL"] = config.llColor || "rgb(0, 150, 150)";
32587
32573
 
32588
- this.colorBy = config.colorBy || "pairOrientation";
32574
+ this.colorBy = config.colorBy || "unexpectedPair";
32589
32575
  this.colorByTag = config.colorByTag ? config.colorByTag.toUpperCase() : undefined;
32590
32576
  this.bamColorTag = config.bamColorTag === undefined ? "YC" : config.bamColorTag;
32591
32577
 
@@ -32692,7 +32678,7 @@ class AlignmentTrack {
32692
32678
  for (let alignment of alignmentRow.alignments) {
32693
32679
 
32694
32680
  this.hasPairs = this.hasPairs || alignment.isPaired();
32695
- if (this.browser.circularView && true === this.browser.circularViewVisible) {
32681
+ if (this.browser.circularView) {
32696
32682
  // This is an expensive check, only do it if needed
32697
32683
  this.hasSupplemental = this.hasSupplemental || alignment.hasTag('SA');
32698
32684
  }
@@ -32977,7 +32963,7 @@ class AlignmentTrack {
32977
32963
  direction: direction
32978
32964
  };
32979
32965
  this.parent.sortObject = newSortObject;
32980
- sortAlignmentRows(newSortObject, viewport.getCachedFeatures());
32966
+ sortAlignmentRows(newSortObject, viewport.cachedFeatures);
32981
32967
  viewport.repaint();
32982
32968
  };
32983
32969
  list.push('<b>Sort by...</b>');
@@ -33007,7 +32993,7 @@ class AlignmentTrack {
33007
32993
  };
33008
32994
  this.sortByTag = tag;
33009
32995
  this.parent.sortObject = newSortObject;
33010
- sortAlignmentRows(newSortObject, viewport.getCachedFeatures());
32996
+ sortAlignmentRows(newSortObject, viewport.cachedFeatures);
33011
32997
  viewport.repaint();
33012
32998
  }
33013
32999
  }
@@ -33034,7 +33020,11 @@ class AlignmentTrack {
33034
33020
  const referenceFrame = clickState.viewport.referenceFrame;
33035
33021
  if (this.browser.genome.getChromosome(clickedAlignment.mate.chr)) {
33036
33022
  this.highlightedAlignmentReadNamed = clickedAlignment.readName;
33037
- this.browser.presentMultiLocusPanel(clickedAlignment, referenceFrame);
33023
+ //this.browser.presentMultiLocusPanel(clickedAlignment, referenceFrame)
33024
+ const bpWidth = referenceFrame.end - referenceFrame.start;
33025
+ const frameStart = clickedAlignment.mate.position - bpWidth / 2;
33026
+ const frameEnd = clickedAlignment.mate.position + bpWidth / 2;
33027
+ this.browser.addMultiLocusPanel(clickedAlignment.mate.chr, frameStart, frameEnd, referenceFrame);
33038
33028
  } else {
33039
33029
  Alert.presentAlert(`Reference does not contain chromosome: ${clickedAlignment.mate.chr}`);
33040
33030
  }
@@ -33047,10 +33037,7 @@ class AlignmentTrack {
33047
33037
  list.push({
33048
33038
  label: 'View read sequence',
33049
33039
  click: () => {
33050
- const alignment = clickedAlignment;
33051
- if (!alignment) return
33052
-
33053
- const seqstring = alignment.seq; //.map(b => String.fromCharCode(b)).join("");
33040
+ const seqstring = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
33054
33041
  if (!seqstring || "*" === seqstring) {
33055
33042
  Alert.presentAlert("Read sequence: *");
33056
33043
  } else {
@@ -33062,11 +33049,16 @@ class AlignmentTrack {
33062
33049
  if (isSecureContext()) {
33063
33050
  list.push({
33064
33051
  label: 'Copy read sequence',
33065
- click: () => {
33066
- const alignment = clickedAlignment;
33067
- if (!alignment) return
33068
- const seqstring = alignment.seq; //.map(b => String.fromCharCode(b)).join("");
33069
- navigator.clipboard.writeText(seqstring);
33052
+ click: async () => {
33053
+ const seq = clickedAlignment.seq; //.map(b => String.fromCharCode(b)).join("");
33054
+ try {
33055
+ //console.log(`seq: ${seq}`)
33056
+ await navigator.clipboard.writeText(seq);
33057
+ } catch (e) {
33058
+ console.error(e);
33059
+ Alert.presentAlert(`error copying sequence to clipboard ${e}`);
33060
+ }
33061
+
33070
33062
  }
33071
33063
  });
33072
33064
  }
@@ -33076,25 +33068,12 @@ class AlignmentTrack {
33076
33068
  }
33077
33069
 
33078
33070
  // Experimental JBrowse feature
33079
- if (this.browser.circularView && true === this.browser.circularViewVisible
33080
- && (this.hasPairs || this.hasSupplemental)) {
33071
+ if (this.browser.circularView && (this.hasPairs || this.hasSupplemental)) {
33081
33072
  if (this.hasPairs) {
33082
33073
  list.push({
33083
33074
  label: 'Add discordant pairs to circular view',
33084
33075
  click: () => {
33085
- const maxFragmentLength = this.parent.maxFragmentLength;
33086
- const {referenceFrame} = viewport;
33087
- const inView = viewport.getCachedFeatures().allAlignments().filter(a => {
33088
- return a.end >= referenceFrame.start
33089
- && a.start <= referenceFrame.end
33090
- && a.mate
33091
- && a.mate.chr
33092
- && (a.mate.chr !== a.chr || Math.max(a.fragmentLength) > maxFragmentLength)
33093
- });
33094
- this.browser.circularViewVisible = true;
33095
- const chords = makePairedAlignmentChords(inView);
33096
- const color = IGVColor.addAlpha(this.parent.color || 'rgb(0,0,255)', 0.02);
33097
- this.browser.circularView.addChords(chords, {track: this.parent.name, color: color});
33076
+ this.parent.addPairedChordsForViewport(viewport);
33098
33077
  }
33099
33078
  });
33100
33079
  }
@@ -33102,17 +33081,7 @@ class AlignmentTrack {
33102
33081
  list.push({
33103
33082
  label: 'Add split reads to circular view',
33104
33083
  click: () => {
33105
- const inView = [];
33106
- for (let a of viewport.getCachedFeatures().allAlignments()) {
33107
- const referenceFrame = viewport.referenceFrame;
33108
- const sa = a.hasTag('SA');
33109
- if (a.end >= referenceFrame.start && a.start <= referenceFrame.end && sa) {
33110
- inView.push(a);
33111
- }
33112
- }
33113
- const chords = makeSupplementalAlignmentChords(inView);
33114
- const color = IGVColor.addAlpha(this.color || 'rgb(0,0,255)', 0.1);
33115
- this.browser.circularView.addChords(chords, {track: this.name, color: color});
33084
+ this.parent.addSplitChordsForViewport(viewport);
33116
33085
  }
33117
33086
  });
33118
33087
  }
@@ -33131,7 +33100,7 @@ class AlignmentTrack {
33131
33100
 
33132
33101
  const showSoftClips = this.parent.showSoftClips;
33133
33102
 
33134
- let features = viewport.getCachedFeatures();
33103
+ let features = viewport.cachedFeatures;
33135
33104
  if (!features || features.length === 0) return
33136
33105
 
33137
33106
  let packedAlignmentRows = features.packedAlignmentRows;
@@ -33222,14 +33191,17 @@ class AlignmentTrack {
33222
33191
  break
33223
33192
  }
33224
33193
 
33194
+ case "tlen":
33225
33195
  case "fragmentLength":
33226
33196
 
33227
- if (alignment.mate && alignment.isMateMapped() && alignment.mate.chr !== alignment.chr) {
33228
- color = getChrColor(alignment.mate.chr);
33229
- } else if (this.parent.minFragmentLength && Math.abs(alignment.fragmentLength) < this.parent.minFragmentLength) {
33230
- color = this.smallFragmentLengthColor;
33231
- } else if (this.parent.maxFragmentLength && Math.abs(alignment.fragmentLength) > this.parent.maxFragmentLength) {
33232
- color = this.largeFragmentLengthColor;
33197
+ if (alignment.mate && alignment.isMateMapped()) {
33198
+ if (alignment.mate.chr !== alignment.chr) {
33199
+ color = getChrColor(alignment.mate.chr);
33200
+ } else if (this.parent.minTemplateLength && Math.abs(alignment.fragmentLength) < this.parent.minTemplateLength) {
33201
+ color = this.smallTLENColor;
33202
+ } else if (this.parent.maxTemplateLength && Math.abs(alignment.fragmentLength) > this.parent.maxTemplateLength) {
33203
+ color = this.largeTLENColor;
33204
+ }
33233
33205
  }
33234
33206
  break
33235
33207
 
@@ -33271,11 +33243,6 @@ function sortAlignmentRows(options, alignmentContainer) {
33271
33243
  return true === direction ? i : -i
33272
33244
  });
33273
33245
 
33274
- // For debugging
33275
- // for(let r of alignmentContainer.packedAlignmentRows) {
33276
- // console.log(r.score);
33277
- // }
33278
-
33279
33246
  }
33280
33247
 
33281
33248
  function shadedBaseColor(qual, baseColor) {
@@ -33435,7 +33402,7 @@ class RulerViewport extends TrackViewport {
33435
33402
 
33436
33403
  this.$rulerLabel.click(async () => {
33437
33404
 
33438
- await this.browser.selectMultiLocusPanel(this.referenceFrame);
33405
+ await this.browser.gotoMultilocusPanel(this.referenceFrame);
33439
33406
 
33440
33407
  // const removals = this.browser.referenceFrameList.filter(r => this.referenceFrame !== r)
33441
33408
  // for (let referenceFrame of removals) {
@@ -33558,7 +33525,9 @@ class RulerViewport extends TrackViewport {
33558
33525
  currentViewport = this;
33559
33526
  this.$tooltip.show();
33560
33527
  } else if (currentViewport.guid !== this.guid) {
33561
- currentViewport.$tooltip.hide();
33528
+ if (currentViewport.$tooltip) {
33529
+ currentViewport.$tooltip.hide();
33530
+ }
33562
33531
  this.$tooltip.show();
33563
33532
  currentViewport = this;
33564
33533
  } else {
@@ -33585,7 +33554,9 @@ class RulerViewport extends TrackViewport {
33585
33554
 
33586
33555
  // hide tooltip when movement stops
33587
33556
  clearTimeout(timer);
33588
- timer = setTimeout(() => this.$tooltip.hide(), toolTipTimeout);
33557
+ timer = setTimeout(() => {
33558
+ if (this.$tooltip) this.$tooltip.hide();
33559
+ }, toolTipTimeout);
33589
33560
 
33590
33561
  }
33591
33562
 
@@ -33604,69 +33575,6 @@ class RulerViewport extends TrackViewport {
33604
33575
 
33605
33576
  }
33606
33577
 
33607
- const viewportColumnManager =
33608
- {
33609
- createColumns: (columnContainer, count) => {
33610
-
33611
- for (let i = 0; i < count; i++) {
33612
- if (0 === i) {
33613
- createColumn(columnContainer, 'igv-column');
33614
- } else {
33615
- columnContainer.appendChild(div$1({class: 'igv-column-shim'}));
33616
- createColumn(columnContainer, 'igv-column');
33617
- }
33618
- }
33619
-
33620
- },
33621
-
33622
- removeColumnAtIndex: (i, column) => {
33623
- const shim = 0 === i ? column.nextElementSibling : column.previousElementSibling;
33624
- column.remove();
33625
- shim.remove();
33626
- },
33627
-
33628
- insertAfter: referenceElement => {
33629
-
33630
- const shim = div$1({class: 'igv-column-shim'});
33631
- insertElementAfter(shim, referenceElement);
33632
-
33633
- const column = div$1({class: 'igv-column'});
33634
- insertElementAfter(column, shim);
33635
-
33636
- return column
33637
- },
33638
-
33639
- insertBefore: (referenceElement, count) => {
33640
-
33641
- for (let i = 0; i < count; i++) {
33642
-
33643
- const column = div$1({class: 'igv-column'});
33644
- insertElementBefore(column, referenceElement);
33645
-
33646
- if (count > 1 && i > 0) {
33647
- const columnShim = div$1({class: 'igv-column-shim'});
33648
- insertElementBefore(columnShim, column);
33649
- }
33650
-
33651
- }
33652
-
33653
- },
33654
-
33655
- indexOfColumn: (columnContainer, column) => {
33656
-
33657
- const allColumns = columnContainer.querySelectorAll('.igv-column');
33658
-
33659
- for (let i = 0; i < allColumns.length; i++) {
33660
- const c = allColumns[ i ];
33661
- if (c === column) {
33662
- return i
33663
- }
33664
- }
33665
-
33666
- return undefined
33667
- },
33668
- };
33669
-
33670
33578
  /*
33671
33579
  * The MIT License (MIT)
33672
33580
  *
@@ -33700,61 +33608,28 @@ class IdeogramViewport extends TrackViewport {
33700
33608
 
33701
33609
  initializationHelper() {
33702
33610
 
33703
- this.$ideogramCanvas = $$1('<canvas>', {class: 'igv-ideogram-canvas'});
33704
- this.$ideogramCanvas.insertBefore(this.$canvas);
33705
-
33706
- const canvas = this.$ideogramCanvas.get(0);
33707
- this.ideogram_ctx = canvas.getContext('2d');
33708
-
33709
- this.$canvas.remove();
33710
- this.canvas = undefined;
33711
- this.ctx = undefined;
33611
+ this.canvas = document.createElement('canvas');
33612
+ this.canvas.className = 'igv-ideogram-canvas';
33613
+ this.$content.append($$1(this.canvas));
33614
+ this.ideogram_ctx = this.canvas.getContext('2d');
33712
33615
 
33713
33616
  this.addMouseHandlers();
33714
-
33715
33617
  }
33716
33618
 
33717
33619
  addMouseHandlers() {
33718
- this.addBrowserObserver();
33719
33620
  this.addViewportClickHandler(this.$viewport.get(0));
33720
33621
  }
33721
33622
 
33722
- removeMouseHandlers() {
33723
- this.removeBrowserObserver();
33724
- this.removeViewportClickHandler(this.$viewport.get(0));
33725
-
33726
- }
33727
-
33728
- addBrowserObserver() {
33729
-
33730
- function observerHandler(referenceFrameList) {
33731
- const column = this.$viewport.get(0).parentElement;
33732
- if (null !== column) {
33733
- const index = viewportColumnManager.indexOfColumn(this.browser.columnContainer, column);
33734
- // console.log(`ideogram-viewport - locus-change-handler index(${ index }) ${ referenceFrameList[ index ].getLocusString() } ${ Date.now() } `)
33735
- this.update(this.ideogram_ctx, this.$viewport.width(), this.$viewport.height(), referenceFrameList[ index ]);
33736
- }
33737
- }
33738
-
33739
- this.boundObserverHandler = observerHandler.bind(this);
33740
- this.browser.on('locuschange', this.boundObserverHandler);
33741
- }
33742
-
33743
- removeBrowserObserver() {
33744
- this.browser.off('locuschange', this.boundObserverHandler);
33745
- }
33746
-
33747
33623
  addViewportClickHandler(viewport) {
33748
33624
 
33749
- function clickHandler(event) {
33625
+ this.boundClickHandler = clickHandler.bind(this);
33626
+ viewport.addEventListener('click', this.boundClickHandler);
33750
33627
 
33751
- const column = viewport.parentElement;
33752
- const index = viewportColumnManager.indexOfColumn(this.browser.columnContainer, column);
33753
- const referenceFrame = this.browser.referenceFrameList[ index ];
33628
+ function clickHandler(event) {
33754
33629
 
33755
33630
  const {xNormalized, width} = translateMouseCoordinates$1(event, this.ideogram_ctx.canvas);
33756
- const {bpLength} = this.browser.genome.getChromosome(referenceFrame.chr);
33757
- const locusLength = referenceFrame.bpPerPixel * width;
33631
+ const {bpLength} = this.browser.genome.getChromosome(this.referenceFrame.chr);
33632
+ const locusLength = this.referenceFrame.bpPerPixel * width;
33758
33633
  const chrCoveragePercentage = locusLength / bpLength;
33759
33634
 
33760
33635
  let xPercentage = xNormalized;
@@ -33769,21 +33644,14 @@ class IdeogramViewport extends TrackViewport {
33769
33644
  const ss = Math.round((xPercentage - (chrCoveragePercentage / 2.0)) * bpLength);
33770
33645
  const ee = Math.round((xPercentage + (chrCoveragePercentage / 2.0)) * bpLength);
33771
33646
 
33772
- referenceFrame.start = ss;
33773
- referenceFrame.end = ee;
33774
- referenceFrame.bpPerPixel = (ee - ss) / width;
33647
+ this.referenceFrame.start = ss;
33648
+ this.referenceFrame.end = ee;
33649
+ this.referenceFrame.bpPerPixel = (ee - ss) / width;
33775
33650
 
33776
- this.browser.updateViews(referenceFrame, this.browser.trackViews, true);
33651
+ this.browser.updateViews(this.referenceFrame, this.browser.trackViews, true);
33777
33652
 
33778
33653
  }
33779
33654
 
33780
- this.boundClickHandler = clickHandler.bind(this);
33781
- viewport.addEventListener('click', this.boundClickHandler);
33782
-
33783
- }
33784
-
33785
- removeViewportClickHandler(viewport) {
33786
- viewport.removeEventListener('click', this.boundClickHandler);
33787
33655
  }
33788
33656
 
33789
33657
  setWidth(width) {
@@ -33804,10 +33672,20 @@ class IdeogramViewport extends TrackViewport {
33804
33672
  context.restore();
33805
33673
  }
33806
33674
 
33807
- update(context, pixelWidth, pixelHeight, referenceFrame) {
33808
- this.$canvas.hide();
33809
- IGVGraphics.configureHighDPICanvas(context, pixelWidth, pixelHeight);
33810
- this.trackView.track.draw({ context, referenceFrame, pixelWidth, pixelHeight });
33675
+ repaint() {
33676
+ this.draw({referenceFrame: this.referenceFrame});
33677
+ }
33678
+
33679
+ draw({referenceFrame}) {
33680
+
33681
+ IGVGraphics.configureHighDPICanvas(this.ideogram_ctx, this.$viewport.width(), this.$viewport.height());
33682
+
33683
+ this.trackView.track.draw({
33684
+ context: this.ideogram_ctx,
33685
+ referenceFrame,
33686
+ pixelWidth: this.$viewport.width(),
33687
+ pixelHeight: this.$viewport.height()
33688
+ });
33811
33689
  }
33812
33690
 
33813
33691
  startSpinner() {
@@ -33848,7 +33726,7 @@ function createViewport(trackView, column, referenceFrame, width) {
33848
33726
 
33849
33727
  if ('ruler' === trackView.track.type) {
33850
33728
  return new RulerViewport(trackView, column, referenceFrame, width)
33851
- } else if ('ideogram' === trackView.track.type) {
33729
+ } else if ('ideogram' === trackView.track.id) {
33852
33730
  return new IdeogramViewport(trackView, column, referenceFrame, width)
33853
33731
  } else {
33854
33732
  return new TrackViewport(trackView, column, referenceFrame, width)
@@ -34371,15 +34249,10 @@ const colorPickerExclusionTypes = new Set(['ruler', 'sequence', 'ideogram']);
34371
34249
  class TrackView {
34372
34250
 
34373
34251
  constructor(browser, columnContainer, track) {
34374
-
34375
- this.namespace = `trackview-${guid$2()}`;
34376
-
34377
34252
  this.browser = browser;
34378
34253
  this.track = track;
34379
34254
  track.trackView = this;
34380
-
34381
34255
  this.addDOMToColumnContainer(browser, columnContainer, browser.referenceFrameList);
34382
-
34383
34256
  }
34384
34257
 
34385
34258
  /**
@@ -34403,7 +34276,7 @@ class TrackView {
34403
34276
  // Axis
34404
34277
  this.axis = this.createAxis(browser, this.track);
34405
34278
 
34406
- // Track Viewports
34279
+ // Create a viewport for each reference frame
34407
34280
  this.viewports = [];
34408
34281
  const viewportWidth = browser.calculateViewportWidth(referenceFrameList.length);
34409
34282
  const viewportColumns = columnContainer.querySelectorAll('.igv-column');
@@ -34476,7 +34349,6 @@ class TrackView {
34476
34349
 
34477
34350
  // Track Viewports
34478
34351
  for (let viewport of this.viewports) {
34479
- viewport.removeMouseHandlers();
34480
34352
  viewport.$viewport.remove();
34481
34353
  }
34482
34354
 
@@ -34542,19 +34414,14 @@ class TrackView {
34542
34414
  if (false === colorPickerExclusionTypes.has(this.track.type)) {
34543
34415
 
34544
34416
  const trackColors = [];
34545
-
34546
34417
  const color = this.track.color || this.track.defaultColor;
34547
-
34548
34418
  if (isString$3(color)) {
34549
34419
  trackColors.push(color);
34550
34420
  }
34551
-
34552
34421
  if (this.track.altColor && isString$3(this.track.altColor)) {
34553
34422
  trackColors.push(this.track.altColor);
34554
34423
  }
34555
-
34556
34424
  const defaultColors = trackColors.map(c => c.startsWith("#") ? c : c.startsWith("rgb(") ? IGVColor.rgbToHex(c) : IGVColor.colorNameToHex(c));
34557
-
34558
34425
  const colorHandlers =
34559
34426
  {
34560
34427
  color: color => {
@@ -34567,7 +34434,6 @@ class TrackView {
34567
34434
  }
34568
34435
 
34569
34436
  };
34570
-
34571
34437
  this.browser.genericColorPicker.configure(defaultColors, colorHandlers);
34572
34438
  this.browser.genericColorPicker.setActiveColorHandler(key);
34573
34439
  this.browser.genericColorPicker.show();
@@ -34581,7 +34447,6 @@ class TrackView {
34581
34447
  if (this.track.minHeight) {
34582
34448
  newHeight = Math.max(this.track.minHeight, newHeight);
34583
34449
  }
34584
-
34585
34450
  if (this.track.maxHeight) {
34586
34451
  newHeight = Math.min(this.track.maxHeight, newHeight);
34587
34452
  }
@@ -34601,9 +34466,8 @@ class TrackView {
34601
34466
 
34602
34467
  this.sampleNameViewport.viewport.style.height = `${newHeight}px`;
34603
34468
 
34604
- // If the track does not manage its own content height set it here
34469
+ // If the track does not manage its own content height set it equal to the viewport height here
34605
34470
  if (typeof this.track.computePixelHeight !== "function") {
34606
-
34607
34471
  for (let vp of this.viewports) {
34608
34472
  vp.setContentHeight(newHeight);
34609
34473
  }
@@ -34660,15 +34524,6 @@ class TrackView {
34660
34524
  }
34661
34525
  }
34662
34526
 
34663
- resize(viewportWidth) {
34664
-
34665
- for (let viewport of this.viewports) {
34666
- viewport.setWidth(viewportWidth);
34667
- }
34668
-
34669
- this.updateViews(true);
34670
- }
34671
-
34672
34527
  /**
34673
34528
  * Repaint all viewports without loading any new data. Use this for events that change visual aspect of data,
34674
34529
  * e.g. color, sort order, etc, but do not change the genomic state.
@@ -34676,7 +34531,9 @@ class TrackView {
34676
34531
  repaintViews() {
34677
34532
 
34678
34533
  for (let viewport of this.viewports) {
34679
- viewport.repaint();
34534
+ if (viewport.isVisible()) {
34535
+ viewport.repaint();
34536
+ }
34680
34537
  }
34681
34538
 
34682
34539
  if (typeof this.track.paintAxis === 'function') {
@@ -34685,7 +34542,6 @@ class TrackView {
34685
34542
 
34686
34543
  // Repaint sample names last
34687
34544
  this.repaintSamples();
34688
-
34689
34545
  }
34690
34546
 
34691
34547
  repaintSamples() {
@@ -34701,10 +34557,25 @@ class TrackView {
34701
34557
  this.viewports.forEach(viewport => viewport.setTrackLabel(name));
34702
34558
  }
34703
34559
 
34560
+ /**
34561
+ * Called in response to a window resize event, change in # of multilocus panels, or other event that changes
34562
+ * the width of the track view.
34563
+ *
34564
+ * @param viewportWidth The width of each viewport in this track view.
34565
+ */
34566
+ resize(viewportWidth) {
34567
+ for (let viewport of this.viewports) {
34568
+ viewport.setWidth(viewportWidth);
34569
+ }
34570
+ }
34571
+
34704
34572
  /**
34705
34573
  * Update viewports to reflect current genomic state, possibly loading additional data.
34574
+ *
34575
+ * @param force - if true, force a repaint even if no new data is loaded
34576
+ * @returns {Promise<void>}
34706
34577
  */
34707
- async updateViews(force) {
34578
+ async updateViews() {
34708
34579
 
34709
34580
  if (!(this.browser && this.browser.referenceFrameList)) return
34710
34581
 
@@ -34713,31 +34584,38 @@ class TrackView {
34713
34584
  // Shift viewports left/right to current genomic state (pans canvas)
34714
34585
  visibleViewports.forEach(viewport => viewport.shift());
34715
34586
 
34716
- const isDragging = this.browser.dragObject;
34717
-
34718
- if (isDragging) {
34587
+ // If dragging (panning) return
34588
+ if (this.browser.dragObject) {
34719
34589
  return
34720
34590
  }
34721
34591
 
34722
- // rpv: viewports whose image (canvas) does not fully cover current genomic range
34723
- const reloadableViewports = this.viewportsToReload(force);
34592
+ // Get viewports to repaint
34593
+ let viewportsToRepaint = (this.track.autoscale || this.track.autoscaleGroup || this.track.type === 'ruler') ?
34594
+ visibleViewports :
34595
+ visibleViewports.filter(vp => vp.needsRepaint());
34596
+
34597
+ // Filter zoomed out views. This has the side effect or turning off or no the zoomed out notice
34598
+ viewportsToRepaint = viewportsToRepaint.filter(viewport => viewport.checkZoomIn());
34599
+
34600
+ // Get viewports that require a data load
34601
+ const viewportsToReload = viewportsToRepaint.filter(viewport => viewport.needsReload());
34724
34602
 
34725
34603
  // Trigger viewport to load features needed to cover current genomic range
34726
34604
  // NOTE: these must be loaded synchronously, do not user Promise.all, not all file readers are thread safe
34727
- for (let viewport of reloadableViewports) {
34605
+ for (let viewport of viewportsToReload) {
34728
34606
  await viewport.loadFeatures();
34729
34607
  }
34730
-
34608
+
34731
34609
  if (this.disposed) return // Track was removed during load
34732
34610
 
34733
- // Very special case for variant tracks in multilocus view. The # of rows to allocate to the variant (site)
34611
+ // Special case for variant tracks in multilocus view. The # of rows to allocate to the variant (site)
34734
34612
  // section depends on data from all the views. We only need to adjust this however if any data was loaded
34735
34613
  // (i.e. reloadableViewports.length > 0)
34736
- if (this.track && typeof this.track.variantRowCount === 'function' && reloadableViewports.length > 0) {
34614
+ if (this.track && typeof this.track.variantRowCount === 'function' && viewportsToReload.length > 0) {
34737
34615
  let maxRow = 0;
34738
34616
  for (let viewport of this.viewports) {
34739
- if (viewport.tile && viewport.tile.features) {
34740
- maxRow = Math.max(maxRow, viewport.tile.features.reduce((a, f) => Math.max(a, f.row || 0), 0));
34617
+ if (viewport.featureCache && viewport.featureCache.features) {
34618
+ maxRow = Math.max(maxRow, viewport.featureCache.features.reduce((a, f) => Math.max(a, f.row || 0), 0));
34741
34619
  }
34742
34620
  }
34743
34621
  const current = this.track.nVariantRows;
@@ -34749,19 +34627,18 @@ class TrackView {
34749
34627
  }
34750
34628
  }
34751
34629
 
34752
-
34753
34630
  if (this.track.autoscale) {
34754
34631
  let allFeatures = [];
34755
34632
  for (let visibleViewport of visibleViewports) {
34756
34633
  const referenceFrame = visibleViewport.referenceFrame;
34757
34634
  const start = referenceFrame.start;
34758
34635
  const end = start + referenceFrame.toBP($$1(visibleViewport.contentDiv).width());
34759
- if (visibleViewport.tile && visibleViewport.tile.features) {
34760
- if (typeof visibleViewport.tile.features.getMax === 'function') {
34761
- const max = visibleViewport.tile.features.getMax(start, end);
34636
+ if (visibleViewport.featureCache && visibleViewport.featureCache.features) {
34637
+ if (typeof visibleViewport.featureCache.features.getMax === 'function') {
34638
+ const max = visibleViewport.featureCache.features.getMax(start, end);
34762
34639
  allFeatures.push({value: max});
34763
34640
  } else {
34764
- allFeatures = allFeatures.concat(FeatureUtils.findOverlapping(visibleViewport.tile.features, start, end));
34641
+ allFeatures = allFeatures.concat(FeatureUtils.findOverlapping(visibleViewport.featureCache.features, start, end));
34765
34642
  }
34766
34643
  }
34767
34644
  }
@@ -34772,15 +34649,8 @@ class TrackView {
34772
34649
  }
34773
34650
  }
34774
34651
 
34775
- // Must repaint all viewports if autoscaling
34776
- if (!isDragging && (this.track.autoscale || this.track.autoscaleGroup)) {
34777
- for (let visibleViewport of visibleViewports) {
34778
- visibleViewport.repaint();
34779
- }
34780
- } else {
34781
- for (let vp of reloadableViewports) {
34782
- vp.repaint();
34783
- }
34652
+ for (let vp of viewportsToRepaint) {
34653
+ vp.repaint();
34784
34654
  }
34785
34655
 
34786
34656
  this.adjustTrackHeight();
@@ -34808,34 +34678,29 @@ class TrackView {
34808
34678
  }
34809
34679
 
34810
34680
  /**
34811
- * Return a promise to get all in-view features. Used for group autoscaling.
34681
+ * Return a promise to get all in-view features across all viewports. Used for group autoscaling.
34812
34682
  */
34813
- async getInViewFeatures(force) {
34683
+ async getInViewFeatures() {
34814
34684
 
34815
34685
  if (!(this.browser && this.browser.referenceFrameList)) {
34816
34686
  return []
34817
34687
  }
34818
34688
 
34819
- // List of viewports that need reloading
34820
- const rpV = this.viewportsToReload(force);
34821
- const promises = rpV.map(function (vp) {
34822
- return vp.loadFeatures()
34823
- });
34824
-
34825
- await Promise.all(promises);
34826
-
34827
34689
  let allFeatures = [];
34828
34690
  for (let vp of this.viewports) {
34829
- if (vp.tile && vp.tile.features) {
34691
+ if (vp.needsReload()) {
34692
+ await vp.loadFeatures();
34693
+ }
34694
+ if (vp.featureCache && vp.featureCache.features) {
34830
34695
  const referenceFrame = vp.referenceFrame;
34831
34696
  const start = referenceFrame.start;
34832
34697
  const end = start + referenceFrame.toBP($$1(vp.contentDiv).width());
34833
34698
 
34834
- if (typeof vp.tile.features.getMax === 'function') {
34835
- const max = vp.tile.features.getMax(start, end);
34699
+ if (typeof vp.featureCache.features.getMax === 'function') {
34700
+ const max = vp.featureCache.features.getMax(start, end);
34836
34701
  allFeatures.push({value: max});
34837
34702
  } else {
34838
- allFeatures = allFeatures.concat(FeatureUtils.findOverlapping(vp.tile.features, start, end));
34703
+ allFeatures = allFeatures.concat(FeatureUtils.findOverlapping(vp.featureCache.features, start, end));
34839
34704
  }
34840
34705
  }
34841
34706
  }
@@ -34876,27 +34741,6 @@ class TrackView {
34876
34741
  }
34877
34742
  }
34878
34743
 
34879
- viewportsToReload(force) {
34880
-
34881
- // List of viewports that need reloading
34882
- const viewports = this.viewports.filter(viewport => {
34883
- if (!viewport.isVisible()) {
34884
- return false
34885
- }
34886
- if (!viewport.checkZoomIn()) {
34887
- return false
34888
- } else {
34889
- const referenceFrame = viewport.referenceFrame;
34890
- const chr = viewport.referenceFrame.chr;
34891
- const start = referenceFrame.start;
34892
- const end = start + referenceFrame.toBP($$1(viewport.contentDiv).width());
34893
- const bpPerPixel = referenceFrame.bpPerPixel;
34894
- return force || (!viewport.tile || viewport.tile.invalidate || !viewport.tile.containsRange(chr, start, end, bpPerPixel))
34895
- }
34896
- });
34897
- return viewports
34898
- }
34899
-
34900
34744
  createTrackScrollbar(browser) {
34901
34745
 
34902
34746
  const outerScroll = div$1();
@@ -35009,7 +34853,7 @@ class TrackView {
35009
34853
 
35010
34854
  addTrackDragMouseHandlers(browser) {
35011
34855
 
35012
- if ('ideogram' === this.track.type || 'ruler' === this.track.type) ; else {
34856
+ if ('ideogram' === this.track.id || 'ruler' === this.track.id) ; else {
35013
34857
 
35014
34858
  let currentDragHandle = undefined;
35015
34859
 
@@ -35086,7 +34930,7 @@ class TrackView {
35086
34930
 
35087
34931
  removeTrackDragMouseHandlers() {
35088
34932
 
35089
- if ('ideogram' === this.track.type || 'ruler' === this.track.type) ; else {
34933
+ if ('ideogram' === this.track.id || 'ruler' === this.track.id) ; else {
35090
34934
  this.dragHandle.removeEventListener('mousedown', this.boundTrackDragMouseDownHandler);
35091
34935
  document.removeEventListener('mouseup', this.boundDocumentTrackDragMouseUpHandler);
35092
34936
  this.dragHandle.removeEventListener('mouseup', this.boundTrackDragMouseEnterHandler);
@@ -39674,7 +39518,7 @@ class TextFeatureSource {
39674
39518
  mapProperties(features, config.mappings);
39675
39519
  }
39676
39520
  this.queryable = false;
39677
- this.featureCache = new FeatureCache(features, genome);
39521
+ this.featureCache = new FeatureCache$1(features, genome);
39678
39522
  } else if (config.reader) {
39679
39523
  // Explicit reader implementation
39680
39524
  this.reader = config.reader;
@@ -39841,14 +39685,14 @@ class TextFeatureSource {
39841
39685
  }
39842
39686
 
39843
39687
  // Note - replacing previous cache with new one. genomicInterval is optional (might be undefined => includes all features)
39844
- this.featureCache = new FeatureCache(features, this.genome, genomicInterval);
39688
+ this.featureCache = new FeatureCache$1(features, this.genome, genomicInterval);
39845
39689
 
39846
39690
  // If track is marked "searchable"< cache features by name -- use this with caution, memory intensive
39847
39691
  if (this.config.searchable || this.config.searchableFields) {
39848
39692
  this.addFeaturesToDB(features);
39849
39693
  }
39850
39694
  } else {
39851
- this.featureCache = new FeatureCache([], genomicInterval); // Empty cache
39695
+ this.featureCache = new FeatureCache$1([], genomicInterval); // Empty cache
39852
39696
  }
39853
39697
  }
39854
39698
 
@@ -41592,7 +41436,7 @@ class TDFSource {
41592
41436
  return features
41593
41437
  }
41594
41438
 
41595
- supportsWholeGenome() {
41439
+ get supportsWholeGenome() {
41596
41440
  return true
41597
41441
  }
41598
41442
  }
@@ -41713,38 +41557,6 @@ function FeatureSource(config, genome) {
41713
41557
  }
41714
41558
  }
41715
41559
 
41716
- const pairs =
41717
- [
41718
- ['A', 'T'],
41719
- ['G', 'C'],
41720
- ['Y', 'R'],
41721
- ['W', 'S'],
41722
- ['K', 'M'],
41723
- ['D', 'H'],
41724
- ['B', 'V']
41725
- ];
41726
-
41727
- const complements = new Map();
41728
- for (let p of pairs) {
41729
- const p1 = p[0];
41730
- const p2 = p[1];
41731
- complements.set(p1, p2);
41732
- complements.set(p2, p1);
41733
- complements.set(p1.toLowerCase(), p2.toLowerCase());
41734
- complements.set(p2.toLowerCase(), p1.toLowerCase());
41735
- }
41736
-
41737
- function reverseComplementSequence(sequence) {
41738
-
41739
- let comp = '';
41740
- let idx = sequence.length;
41741
- while (idx-- > 0) {
41742
- const base = sequence[idx];
41743
- comp += complements.has(base) ? complements.get(base) : base;
41744
- }
41745
- return comp
41746
- }
41747
-
41748
41560
  const GtexUtils = {
41749
41561
 
41750
41562
  getTissueInfo: function (datasetId, baseURL) {
@@ -42288,7 +42100,7 @@ class FeatureTrack extends TrackBase {
42288
42100
 
42289
42101
  }
42290
42102
 
42291
- supportsWholeGenome() {
42103
+ get supportsWholeGenome() {
42292
42104
  return (this.config.indexed === false || !this.config.indexURL) && this.config.supportsWholeGenome !== false
42293
42105
  }
42294
42106
 
@@ -42437,21 +42249,18 @@ class FeatureTrack extends TrackBase {
42437
42249
  const infoURL = this.infoURL || this.config.infoURL;
42438
42250
  for (let fd of featureData) {
42439
42251
  data.push(fd);
42440
- if (infoURL) {
42441
- if (fd.name &&
42442
- fd.name.toLowerCase() === "name" &&
42443
- fd.value &&
42444
- isString$3(fd.value) &&
42445
- !fd.value.startsWith("<")) {
42446
-
42447
-
42448
- const url = this.infoURL || this.config.infoURL;
42449
- const href = url.replace("$$", feature.name);
42450
- data.push({name: "Info", value: `<a target="_blank" href=${href}>${fd.value}</a>`});
42451
- }
42252
+ if (infoURL &&
42253
+ fd.name &&
42254
+ fd.name.toLowerCase() === "name" &&
42255
+ fd.value &&
42256
+ isString$3(fd.value) &&
42257
+ !fd.value.startsWith("<")) {
42258
+ const href = infoURL.replace("$$", feature.name);
42259
+ fd.value = `<a target=_blank href=${href}>${fd.value}</a>`;
42452
42260
  }
42453
42261
  }
42454
42262
 
42263
+
42455
42264
  //Array.prototype.push.apply(data, featureData);
42456
42265
 
42457
42266
  // If we have clicked over an exon number it.
@@ -42480,7 +42289,7 @@ class FeatureTrack extends TrackBase {
42480
42289
  }
42481
42290
 
42482
42291
  menuItemList() {
42483
-
42292
+
42484
42293
  const menuItems = [];
42485
42294
 
42486
42295
  if (this.render === renderSnp) {
@@ -42508,7 +42317,7 @@ class FeatureTrack extends TrackBase {
42508
42317
  menuItems.push(
42509
42318
  {
42510
42319
  object: $$1(createCheckbox$1(lut[displayMode], displayMode === this.displayMode)),
42511
- click: () => {
42320
+ click: () => {
42512
42321
  this.displayMode = displayMode;
42513
42322
  this.config.displayMode = displayMode;
42514
42323
  this.trackView.checkContentHeight();
@@ -42524,14 +42333,28 @@ class FeatureTrack extends TrackBase {
42524
42333
 
42525
42334
  contextMenuItemList(clickState) {
42526
42335
 
42527
- if (isSecureContext()) {
42528
- const features = this.clickedFeatures(clickState);
42529
- if (features.length > 1) {
42530
- features.sort((a, b) => (a.end - a.start) - (b.end - b.start));
42531
- }
42532
- const f = features[0]; // The longest feature
42533
- if ((f.end - f.start) <= 1000000) {
42534
- return [
42336
+ const features = this.clickedFeatures(clickState);
42337
+ if (features.length > 1) {
42338
+ features.sort((a, b) => (b.end - b.start) - (a.end - a.start));
42339
+ }
42340
+ const f = features[0]; // The shortest clicked feature
42341
+
42342
+ if ((f.end - f.start) <= 1000000) {
42343
+ const list = [{
42344
+ label: 'View feature sequence',
42345
+ click: async () => {
42346
+ let seq = await this.browser.genome.getSequence(f.chr, f.start, f.end);
42347
+ if (f.strand === '-') {
42348
+ seq = reverseComplementSequence(seq);
42349
+ }
42350
+ if (!seq) seq = "Unknown sequence";
42351
+ Alert.presentAlert(seq);
42352
+
42353
+ }
42354
+ }];
42355
+
42356
+ if (isSecureContext() && navigator.clipboard !== undefined) {
42357
+ list.push(
42535
42358
  {
42536
42359
  label: 'Copy feature sequence',
42537
42360
  click: async () => {
@@ -42539,17 +42362,23 @@ class FeatureTrack extends TrackBase {
42539
42362
  if (f.strand === '-') {
42540
42363
  seq = reverseComplementSequence(seq);
42541
42364
  }
42542
- navigator.clipboard.writeText(seq);
42365
+ try {
42366
+ await navigator.clipboard.writeText(seq);
42367
+ } catch (e) {
42368
+ console.error(e);
42369
+ Alert.presentAlert(`error copying sequence to clipboard ${e}`);
42370
+ }
42543
42371
  }
42544
- },
42545
- '<hr/>'
42546
- ]
42372
+ }
42373
+ );
42547
42374
  }
42548
- }
42375
+ list.push('<hr/>');
42376
+ return list
42377
+ } else {
42549
42378
 
42550
- // Either not a secure context (i.e. http: protocol), or feature is too long
42551
- return undefined
42379
+ return undefined
42552
42380
 
42381
+ }
42553
42382
  }
42554
42383
 
42555
42384
  description() {
@@ -42570,7 +42399,7 @@ class FeatureTrack extends TrackBase {
42570
42399
  desc += "</html>";
42571
42400
  return desc
42572
42401
  } else {
42573
- return super.description();
42402
+ return super.description()
42574
42403
  }
42575
42404
 
42576
42405
  };
@@ -42651,13 +42480,11 @@ class WigTrack extends TrackBase {
42651
42480
  this.paintAxis = paintAxis;
42652
42481
 
42653
42482
  const format = config.format ? config.format.toLowerCase() : config.format;
42483
+ this.flipAxis = config.flipAxis ? config.flipAxis : false;
42484
+ this.logScale = config.logScale ? config.logScale : false;
42654
42485
  if ("bigwig" === format) {
42655
- this.flipAxis = config.flipAxis ? config.flipAxis : false;
42656
- this.logScale = config.logScale ? config.logScale : false;
42657
42486
  this.featureSource = new BWSource(config, this.browser.genome);
42658
42487
  } else if ("tdf" === format) {
42659
- this.flipAxis = config.flipAxis ? config.flipAxis : false;
42660
- this.logScale = config.logScale ? config.logScale : false;
42661
42488
  this.featureSource = new TDFSource(config, this.browser.genome);
42662
42489
  } else {
42663
42490
  this.featureSource = FeatureSource(config, this.browser.genome);
@@ -42709,7 +42536,7 @@ class WigTrack extends TrackBase {
42709
42536
  let items = [];
42710
42537
  if (this.flipAxis !== undefined) {
42711
42538
  items.push({
42712
- label:"Flip y-axis",
42539
+ label: "Flip y-axis",
42713
42540
  click: () => {
42714
42541
  this.flipAxis = !this.flipAxis;
42715
42542
  this.trackView.repaintViews();
@@ -42893,7 +42720,7 @@ class WigTrack extends TrackBase {
42893
42720
  }
42894
42721
  }
42895
42722
 
42896
- supportsWholeGenome() {
42723
+ get supportsWholeGenome() {
42897
42724
  return !this.config.indexURL && this.config.supportsWholeGenome !== false
42898
42725
  }
42899
42726
 
@@ -43437,7 +43264,7 @@ class SegTrack extends TrackBase {
43437
43264
 
43438
43265
  const sortHandler = (sort) => {
43439
43266
  const viewport = clickState.viewport;
43440
- const features = viewport.getCachedFeatures();
43267
+ const features = viewport.cachedFeatures;
43441
43268
  this.sortSamples(sort.chr, sort.start, sort.end, sort.direction, features);
43442
43269
  };
43443
43270
 
@@ -43465,7 +43292,7 @@ class SegTrack extends TrackBase {
43465
43292
 
43466
43293
  }
43467
43294
 
43468
- supportsWholeGenome() {
43295
+ get supportsWholeGenome() {
43469
43296
  return (this.config.indexed === false || !this.config.indexURL) && this.config.supportsWholeGenome !== false
43470
43297
  }
43471
43298
 
@@ -43555,10 +43382,16 @@ const MUT_COLORS = {
43555
43382
  * THE SOFTWARE.
43556
43383
  */
43557
43384
 
43385
+ /**
43386
+ * Represents 2 or more wig tracks overlaid on a common viewport.
43387
+ */
43558
43388
  class MergedTrack extends TrackBase {
43559
43389
 
43560
43390
  constructor(config, browser) {
43561
43391
  super(config, browser);
43392
+ this.type = "merged";
43393
+ this.featureType = 'numeric';
43394
+ this.paintAxis = paintAxis;
43562
43395
  }
43563
43396
 
43564
43397
  init(config) {
@@ -43569,21 +43402,6 @@ class MergedTrack extends TrackBase {
43569
43402
  super.init(config);
43570
43403
  }
43571
43404
 
43572
- get height() {
43573
- return this._height
43574
- }
43575
-
43576
- set height(h) {
43577
- this._height = h;
43578
- if (this.tracks) {
43579
- for (let t of this.tracks) {
43580
- t.height = h;
43581
- t.config.height = h;
43582
- }
43583
- }
43584
- }
43585
-
43586
-
43587
43405
  async postInit() {
43588
43406
 
43589
43407
  this.tracks = [];
@@ -43603,11 +43421,55 @@ class MergedTrack extends TrackBase {
43603
43421
  }
43604
43422
  }
43605
43423
 
43606
- this.height = this.config.height || 100;
43424
+ this.flipAxis = this.config.flipAxis ? this.config.flipAxis : false;
43425
+ this.logScale = this.config.logScale ? this.config.logScale : false;
43426
+ this.autoscale = this.config.autoscale || this.config.max === undefined;
43427
+ if (!this.autoscale) {
43428
+ this.dataRange = {
43429
+ min: this.config.min || 0,
43430
+ max: this.config.max
43431
+ };
43432
+ }
43433
+ for (let t of this.tracks) {
43434
+ t.autoscale = false;
43435
+ t.dataRange = this.dataRange;
43436
+ }
43437
+
43438
+ this.height = this.config.height || 50;
43607
43439
 
43608
43440
  return Promise.all(p)
43609
43441
  }
43610
43442
 
43443
+ get height() {
43444
+ return this._height
43445
+ }
43446
+
43447
+ set height(h) {
43448
+ this._height = h;
43449
+ if (this.tracks) {
43450
+ for (let t of this.tracks) {
43451
+ t.height = h;
43452
+ t.config.height = h;
43453
+ }
43454
+ }
43455
+ }
43456
+
43457
+ menuItemList() {
43458
+ let items = [];
43459
+ if (this.flipAxis !== undefined) {
43460
+ items.push({
43461
+ label: "Flip y-axis",
43462
+ click: () => {
43463
+ this.flipAxis = !this.flipAxis;
43464
+ this.trackView.repaintViews();
43465
+ }
43466
+ });
43467
+ }
43468
+
43469
+ items = items.concat(MenuUtils.numericDataMenuItems(this.trackView));
43470
+
43471
+ return items
43472
+ }
43611
43473
 
43612
43474
  async getFeatures(chr, bpStart, bpEnd, bpPerPixel) {
43613
43475
 
@@ -43617,44 +43479,26 @@ class MergedTrack extends TrackBase {
43617
43479
 
43618
43480
  draw(options) {
43619
43481
 
43620
- var i, len, mergedFeatures, trackOptions, dataRange;
43621
-
43622
- mergedFeatures = options.features; // Array of feature arrays, 1 for each track
43623
-
43624
- dataRange = autoscale(options.referenceFrame.chr, mergedFeatures);
43482
+ const mergedFeatures = options.features; // Array of feature arrays, 1 for each track
43625
43483
 
43626
- //IGVGraphics.fillRect(options.context, 0, options.pixelTop, options.pixelWidth, options.pixelHeight, {'fillStyle': "rgb(255, 255, 255)"});
43627
-
43628
- for (i = 0, len = this.tracks.length; i < len; i++) {
43484
+ if (this.autoscale) {
43485
+ this.dataRange = autoscale(options.referenceFrame.chr, mergedFeatures);
43486
+ }
43629
43487
 
43630
- trackOptions = Object.assign({}, options);
43488
+ for (let i = 0, len = this.tracks.length; i < len; i++) {
43489
+ const trackOptions = Object.assign({}, options);
43631
43490
  trackOptions.features = mergedFeatures[i];
43632
- this.tracks[i].dataRange = dataRange;
43491
+ this.tracks[i].dataRange = this.dataRange;
43492
+ this.tracks[i].flipAxis = this.flipAxis;
43493
+ this.tracks[i].logScale = this.logScale;
43494
+ this.tracks[i].graphType = this.graphType;
43633
43495
  this.tracks[i].draw(trackOptions);
43634
43496
  }
43635
-
43636
- }
43637
-
43638
- paintAxis(ctx, pixelWidth, pixelHeight) {
43639
-
43640
- var i, len, autoscale, track;
43641
-
43642
- autoscale = true; // Hardcoded for now
43643
-
43644
- for (i = 0, len = this.tracks.length; i < len; i++) {
43645
-
43646
- track = this.tracks[i];
43647
-
43648
- if (typeof track.paintAxis === 'function') {
43649
- track.paintAxis(ctx, pixelWidth, pixelHeight);
43650
- if (autoscale) break
43651
- }
43652
- }
43653
43497
  }
43654
43498
 
43655
43499
  popupData(clickState, features) {
43656
43500
 
43657
- const featuresArray = features || clickState.viewport.getCachedFeatures();
43501
+ const featuresArray = features || clickState.viewport.cachedFeatures;
43658
43502
 
43659
43503
  if (featuresArray && featuresArray.length === this.tracks.length) {
43660
43504
  // Array of feature arrays, 1 for each track
@@ -43671,42 +43515,23 @@ class MergedTrack extends TrackBase {
43671
43515
  }
43672
43516
 
43673
43517
 
43674
- supportsWholeGenome() {
43675
- const b = this.tracks.every(track => track.supportsWholeGenome());
43676
- return b
43518
+ get supportsWholeGenome() {
43519
+ return this.tracks.every(track => track.supportsWholeGenome())
43677
43520
  }
43678
43521
  }
43679
43522
 
43680
43523
  function autoscale(chr, featureArrays) {
43681
43524
 
43682
-
43683
- var min = 0,
43684
- max = -Number.MAX_VALUE;
43685
-
43686
- // if (chr === 'all') {
43687
- // allValues = [];
43688
- // featureArrays.forEach(function (features) {
43689
- // features.forEach(function (f) {
43690
- // if (!Number.isNaN(f.value)) {
43691
- // allValues.push(f.value);
43692
- // }
43693
- // });
43694
- // });
43695
- //
43696
- // min = Math.min(0, IGVMath.percentile(allValues, .1));
43697
- // max = IGVMath.percentile(allValues, 99.9);
43698
- //
43699
- // }
43700
- // else {
43701
- featureArrays.forEach(function (features, i) {
43702
- features.forEach(function (f) {
43525
+ let min = 0;
43526
+ let max = -Number.MAX_VALUE;
43527
+ for(let features of featureArrays) {
43528
+ for(let f of features) {
43703
43529
  if (typeof f.value !== 'undefined' && !Number.isNaN(f.value)) {
43704
43530
  min = Math.min(min, f.value);
43705
43531
  max = Math.max(max, f.value);
43706
43532
  }
43707
- });
43708
- });
43709
- // }
43533
+ }
43534
+ }
43710
43535
  return {min: min, max: max}
43711
43536
  }
43712
43537
 
@@ -43815,7 +43640,7 @@ class InteractionTrack extends TrackBase {
43815
43640
  return this
43816
43641
  }
43817
43642
 
43818
- supportsWholeGenome() {
43643
+ get supportsWholeGenome() {
43819
43644
  return true
43820
43645
  }
43821
43646
 
@@ -44201,7 +44026,7 @@ class InteractionTrack extends TrackBase {
44201
44026
  items = items.concat(MenuUtils.numericDataMenuItems(this.trackView));
44202
44027
  }
44203
44028
 
44204
- if (this.browser.circularView && true === this.browser.circularViewVisible) {
44029
+ if (this.browser.circularView) {
44205
44030
  items.push('<hr/>');
44206
44031
  items.push({
44207
44032
  label: 'Add interactions to circular view',
@@ -44219,7 +44044,7 @@ class InteractionTrack extends TrackBase {
44219
44044
  contextMenuItemList(clickState) {
44220
44045
 
44221
44046
  // Experimental JBrowse feature
44222
- if (this.browser.circularView && true === this.browser.circularViewVisible) {
44047
+ if (this.browser.circularView ) {
44223
44048
  const viewport = clickState.viewport;
44224
44049
  const list = [];
44225
44050
 
@@ -44248,21 +44073,22 @@ class InteractionTrack extends TrackBase {
44248
44073
 
44249
44074
  // inView features are simply features that have been drawn, i.e. have a drawState
44250
44075
  const inView = cachedFeatures.filter(f => f.drawState);
44251
- if(inView.length === 0) erturn;
44076
+ if(inView.length === 0) return;
44252
44077
 
44253
- this.browser.circularViewVisible = true;
44254
44078
  const chords = makeBedPEChords(inView);
44255
-
44256
- // for filtered set, distinguishing the chromosomes is more critical than tracks
44257
- const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? this.color : getChrColor(refFrame.chr), 0.5);
44258
- const trackColor = IGVColor.addAlpha(this.color, 0.5);
44259
-
44260
- // name the chord set to include filtering information
44261
- const encodedName = this.name.replaceAll(' ', '%20');
44262
- const chordSetName = "all" === refFrame.chr ?
44263
- encodedName :
44264
- `${encodedName} (${refFrame.chr}:${refFrame.start}-${refFrame.end} ; range:${this.dataRange.min}-${this.dataRange.max})`;
44265
- this.browser.circularView.addChords(chords, {track: chordSetName, color: chordSetColor, trackColor: trackColor});
44079
+ sendChords(chords, this, refFrame, 0.5);
44080
+ //
44081
+ //
44082
+ // // for filtered set, distinguishing the chromosomes is more critical than tracks
44083
+ // const chordSetColor = IGVColor.addAlpha("all" === refFrame.chr ? this.color : getChrColor(refFrame.chr), 0.5)
44084
+ // const trackColor = IGVColor.addAlpha(this.color, 0.5)
44085
+ //
44086
+ // // name the chord set to include locus and filtering information
44087
+ // const encodedName = this.name.replaceAll(' ', '%20')
44088
+ // const chordSetName = "all" === refFrame.chr ?
44089
+ // encodedName :
44090
+ // `${encodedName} (${refFrame.chr}:${refFrame.start}-${refFrame.end} ; range:${this.dataRange.min}-${this.dataRange.max})`
44091
+ // this.browser.circularView.addChords(chords, {track: chordSetName, color: chordSetColor, trackColor: trackColor})
44266
44092
  }
44267
44093
 
44268
44094
  doAutoscale(features) {
@@ -44327,7 +44153,7 @@ class InteractionTrack extends TrackBase {
44327
44153
 
44328
44154
  // We use the cached features rather than method to avoid async load. If the
44329
44155
  // feature is not already loaded this won't work, but the user wouldn't be mousing over it either.
44330
- const featureList = features || clickState.viewport.getCachedFeatures();
44156
+ const featureList = features || clickState.viewport.cachedFeatures;
44331
44157
  const candidates = [];
44332
44158
  if (featureList) {
44333
44159
  const proportional = (this.arcType === "proportional" || this.arcType === "inView" || this.arcType === "partialInView");
@@ -44661,7 +44487,7 @@ class VariantTrack extends TrackBase {
44661
44487
 
44662
44488
  }
44663
44489
 
44664
- supportsWholeGenome() {
44490
+ get supportsWholeGenome() {
44665
44491
  return this.config.indexed === false || this.config.supportsWholeGenome === true
44666
44492
  }
44667
44493
 
@@ -44853,7 +44679,7 @@ class VariantTrack extends TrackBase {
44853
44679
  }
44854
44680
 
44855
44681
  } else if (this._color) {
44856
- variantColor = (typeof this._color === "function") ? this._color(v) : this._color;
44682
+ variantColor = this.color;
44857
44683
  } else if ("NONVARIANT" === v.type) {
44858
44684
  variantColor = this.nonRefColor;
44859
44685
  } else if ("MIXED" === v.type) {
@@ -44864,6 +44690,10 @@ class VariantTrack extends TrackBase {
44864
44690
  return variantColor
44865
44691
  }
44866
44692
 
44693
+ get color() {
44694
+ return this._color ? ((typeof this._color === "function") ? this._color(v) : this._color) : this.defaultColor
44695
+ }
44696
+
44867
44697
  clickedFeatures(clickState, features) {
44868
44698
 
44869
44699
  let featureList = super.clickedFeatures(clickState, features);
@@ -45090,25 +44920,15 @@ class VariantTrack extends TrackBase {
45090
44920
  }
45091
44921
 
45092
44922
  // Experimental JBrowse circular view integration
45093
- if (this.browser.circularView && true === this.browser.circularViewVisible) {
44923
+ if (this.browser.circularView) {
45094
44924
 
45095
44925
  menuItems.push('<hr>');
45096
44926
  menuItems.push({
45097
44927
  label: 'Add SVs to circular view',
45098
44928
  click: () => {
45099
- const inView = [];
45100
44929
  for (let viewport of this.trackView.viewports) {
45101
- const refFrame = viewport.referenceFrame;
45102
- for (let f of viewport.getCachedFeatures()) {
45103
- if (f.end >= refFrame.start && f.start <= refFrame.end) {
45104
- inView.push(f);
45105
- }
45106
- }
44930
+ this.sendChordsForViewport(viewport);
45107
44931
  }
45108
-
45109
- const chords = makeVCFChords(inView);
45110
- const color = IGVColor.addAlpha(this._color || this.defaultColor, 0.5);
45111
- this.browser.circularView.addChords(chords, {track: this.name, color: color});
45112
44932
  }
45113
44933
  });
45114
44934
  }
@@ -45120,20 +44940,14 @@ class VariantTrack extends TrackBase {
45120
44940
  contextMenuItemList(clickState) {
45121
44941
 
45122
44942
  // Experimental JBrowse circular view integration
45123
- if (this.browser.circularView && true === this.browser.circularViewVisible) {
44943
+ if (this.browser.circularView) {
45124
44944
  const viewport = clickState.viewport;
45125
44945
  const list = [];
45126
44946
 
45127
44947
  list.push({
45128
44948
  label: 'Add SVs to Circular View',
45129
44949
  click: () => {
45130
- const refFrame = viewport.referenceFrame;
45131
- const inView = "all" === refFrame.chr ?
45132
- this.featureSource.getAllFeatures() :
45133
- this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end);
45134
- const chords = makeVCFChords(inView);
45135
- const color = IGVColor.addAlpha(this._color || this.defaultColor, 0.5);
45136
- this.browser.circularView.addChords(chords, {track: this.name, color: color});
44950
+ this.sendChordsForViewport(viewport);
45137
44951
  }
45138
44952
  });
45139
44953
 
@@ -45143,6 +44957,15 @@ class VariantTrack extends TrackBase {
45143
44957
  }
45144
44958
 
45145
44959
 
44960
+ sendChordsForViewport(viewport) {
44961
+ const refFrame = viewport.referenceFrame;
44962
+ const inView = "all" === refFrame.chr ?
44963
+ this.featureSource.getAllFeatures() :
44964
+ this.featureSource.featureCache.queryFeatures(refFrame.chr, refFrame.start, refFrame.end);
44965
+ const chords = makeVCFChords(inView);
44966
+ sendChords(chords, this, refFrame, 0.5);
44967
+ }
44968
+
45146
44969
  /**
45147
44970
  * Create a "color by" checkbox menu item, optionally initially checked
45148
44971
  * @param menuItem
@@ -45439,7 +45262,7 @@ class EqtlTrack extends TrackBase {
45439
45262
  */
45440
45263
  popupData(clickState) {
45441
45264
 
45442
- let features = clickState.viewport.getCachedFeatures();
45265
+ let features = clickState.viewport.cachedFeatures;
45443
45266
  if (!features || features.length === 0) return []
45444
45267
 
45445
45268
  const tolerance = 3;
@@ -45658,7 +45481,7 @@ class GWASTrack extends TrackBase {
45658
45481
  }
45659
45482
 
45660
45483
 
45661
- supportsWholeGenome() {
45484
+ get supportsWholeGenome() {
45662
45485
  return true
45663
45486
  }
45664
45487
 
@@ -45769,7 +45592,7 @@ class GWASTrack extends TrackBase {
45769
45592
 
45770
45593
  let data = [];
45771
45594
  const track = clickState.viewport.trackView.track;
45772
- const features = clickState.viewport.getCachedFeatures();
45595
+ const features = clickState.viewport.cachedFeatures;
45773
45596
 
45774
45597
  if (features) {
45775
45598
  let count = 0;
@@ -46172,7 +45995,7 @@ class GCNVTrack extends TrackBase {
46172
45995
  return items
46173
45996
  }
46174
45997
 
46175
- supportsWholeGenome() {
45998
+ get supportsWholeGenome() {
46176
45999
  return false
46177
46000
  }
46178
46001
  }
@@ -46452,7 +46275,7 @@ class RNAFeatureSource {
46452
46275
 
46453
46276
  const data = await igvxhr.loadString(this.config.url, options);
46454
46277
 
46455
- this.featureCache = new FeatureCache(parseBP(data), genome);
46278
+ this.featureCache = new FeatureCache$1(parseBP(data), genome);
46456
46279
 
46457
46280
  return this.featureCache.queryFeatures(chr, start, end)
46458
46281
 
@@ -46559,21 +46382,18 @@ class RNAFeatureSource {
46559
46382
  * THE SOFTWARE.
46560
46383
  */
46561
46384
 
46385
+ /**
46386
+ * Class represents an ideogram of a chromsome cytobands. It is used for the header of a track panel.
46387
+ *
46388
+ */
46562
46389
  class IdeogramTrack {
46563
46390
  constructor(browser) {
46564
-
46565
46391
  this.browser = browser;
46566
-
46567
46392
  this.type = 'ideogram';
46568
- this.id = this.type;
46569
-
46570
46393
  this.height = 16;
46571
-
46572
46394
  this.order = Number.MIN_SAFE_INTEGER;
46573
-
46574
46395
  this.disableButtons = true;
46575
46396
  this.ignoreTrackMenu = true;
46576
-
46577
46397
  }
46578
46398
 
46579
46399
  async getFeatures(chr, start, end) {
@@ -46840,7 +46660,7 @@ class SpliceJunctionTrack extends TrackBase {
46840
46660
 
46841
46661
  }
46842
46662
 
46843
- supportsWholeGenome() {
46663
+ get supportsWholeGenome() {
46844
46664
  return false
46845
46665
  }
46846
46666
 
@@ -47734,6 +47554,15 @@ class ReferenceFrame {
47734
47554
  this.id = guid$2();
47735
47555
  }
47736
47556
 
47557
+ extend(locus) {
47558
+ const newStart = Math.min(locus.start, this.start);
47559
+ const newEnd = Math.max(locus.end, this.end);
47560
+ const ratio = (newEnd - newStart) / (this.end - this.start);
47561
+ this.start = newStart;
47562
+ this.end = newEnd;
47563
+ this.bpPerPixel *= ratio;
47564
+ }
47565
+
47737
47566
  calculateEnd(pixels) {
47738
47567
  return this.start + this.bpPerPixel * pixels
47739
47568
  }
@@ -47820,7 +47649,7 @@ class ReferenceFrame {
47820
47649
 
47821
47650
  const viewChanged = start !== this.start || bpPerPixel !== this.bpPerPixel;
47822
47651
  if (viewChanged) {
47823
- await browser.updateViews(this);
47652
+ await browser.updateViews(true);
47824
47653
  }
47825
47654
 
47826
47655
  }
@@ -47896,30 +47725,6 @@ function createReferenceFrameList(loci, genome, browserFlanking, minimumBases, v
47896
47725
  })
47897
47726
  }
47898
47727
 
47899
- function adjustReferenceFrame(scaleFactor, referenceFrame, viewportWidth, alignmentStart, alignmentLength) {
47900
-
47901
- referenceFrame.bpPerPixel *= scaleFactor;
47902
-
47903
- const alignmentEE = alignmentStart + alignmentLength;
47904
- const alignmentCC = (alignmentStart + alignmentEE) / 2;
47905
-
47906
- referenceFrame.start = alignmentCC - (referenceFrame.bpPerPixel * (viewportWidth / 2));
47907
- referenceFrame.end = referenceFrame.start + (referenceFrame.bpPerPixel * viewportWidth);
47908
- referenceFrame.locusSearchString = referenceFrame.getLocusString();
47909
- }
47910
-
47911
- function createReferenceFrameWithAlignment(genome, chromosomeName, bpp, viewportWidth, alignmentStart, alignmentLength) {
47912
-
47913
- const alignmentEE = alignmentStart + alignmentLength;
47914
- const alignmentCC = (alignmentStart + alignmentEE) / 2;
47915
-
47916
- const ss = alignmentCC - (bpp * (viewportWidth / 2));
47917
- const ee = ss + (bpp * viewportWidth);
47918
-
47919
- return new ReferenceFrame(genome, chromosomeName, ss, ee, bpp)
47920
-
47921
- }
47922
-
47923
47728
  const defaultNucleotideColors = {
47924
47729
  "A": "rgb( 0, 200, 0)",
47925
47730
  "C": "rgb( 0,0,200)",
@@ -49019,6 +48824,69 @@ const SVGSaveControl = function (parent, browser) {
49019
48824
  button.addEventListener('click', () => browser.saveSVGtoFile({}));
49020
48825
  };
49021
48826
 
48827
+ const viewportColumnManager =
48828
+ {
48829
+ createColumns: (columnContainer, count) => {
48830
+
48831
+ for (let i = 0; i < count; i++) {
48832
+ if (0 === i) {
48833
+ createColumn(columnContainer, 'igv-column');
48834
+ } else {
48835
+ columnContainer.appendChild(div$1({class: 'igv-column-shim'}));
48836
+ createColumn(columnContainer, 'igv-column');
48837
+ }
48838
+ }
48839
+
48840
+ },
48841
+
48842
+ removeColumnAtIndex: (i, column) => {
48843
+ const shim = 0 === i ? column.nextElementSibling : column.previousElementSibling;
48844
+ column.remove();
48845
+ shim.remove();
48846
+ },
48847
+
48848
+ insertAfter: referenceElement => {
48849
+
48850
+ const shim = div$1({class: 'igv-column-shim'});
48851
+ insertElementAfter(shim, referenceElement);
48852
+
48853
+ const column = div$1({class: 'igv-column'});
48854
+ insertElementAfter(column, shim);
48855
+
48856
+ return column
48857
+ },
48858
+
48859
+ insertBefore: (referenceElement, count) => {
48860
+
48861
+ for (let i = 0; i < count; i++) {
48862
+
48863
+ const column = div$1({class: 'igv-column'});
48864
+ insertElementBefore(column, referenceElement);
48865
+
48866
+ if (count > 1 && i > 0) {
48867
+ const columnShim = div$1({class: 'igv-column-shim'});
48868
+ insertElementBefore(columnShim, column);
48869
+ }
48870
+
48871
+ }
48872
+
48873
+ },
48874
+
48875
+ indexOfColumn: (columnContainer, column) => {
48876
+
48877
+ const allColumns = columnContainer.querySelectorAll('.igv-column');
48878
+
48879
+ for (let i = 0; i < allColumns.length; i++) {
48880
+ const c = allColumns[ i ];
48881
+ if (c === column) {
48882
+ return i
48883
+ }
48884
+ }
48885
+
48886
+ return undefined
48887
+ },
48888
+ };
48889
+
49022
48890
  /*
49023
48891
  * The MIT License (MIT)
49024
48892
  *
@@ -49251,7 +49119,7 @@ class RulerTrack {
49251
49119
  }
49252
49120
  }
49253
49121
 
49254
- supportsWholeGenome() {
49122
+ get supportsWholeGenome() {
49255
49123
  return true
49256
49124
  };
49257
49125
 
@@ -49622,8 +49490,8 @@ class Browser {
49622
49490
  this.svgSaveControl = new SVGSaveControl($toggle_button_container.get(0), this);
49623
49491
  }
49624
49492
 
49625
- if(config.customButtons) {
49626
- for(let b of config.customButtons) {
49493
+ if (config.customButtons) {
49494
+ for (let b of config.customButtons) {
49627
49495
  new CustomButton($toggle_button_container.get(0), this, b);
49628
49496
  }
49629
49497
  }
@@ -49784,7 +49652,6 @@ class Browser {
49784
49652
  return undefined
49785
49653
  }
49786
49654
  }
49787
-
49788
49655
  }
49789
49656
  }
49790
49657
 
@@ -49829,7 +49696,9 @@ class Browser {
49829
49696
  // Create ideogram and ruler track. Really this belongs in browser initialization, but creation is
49830
49697
  // deferred because ideogram and ruler are treated as "tracks", and tracks require a reference frame
49831
49698
  if (false !== session.showIdeogram) {
49832
- this.trackViews.push(new TrackView(this, this.columnContainer, new IdeogramTrack(this)));
49699
+ const ideogramTrack = new IdeogramTrack(this);
49700
+ ideogramTrack.id = 'ideogram';
49701
+ this.trackViews.push(new TrackView(this, this.columnContainer, ideogramTrack));
49833
49702
  }
49834
49703
 
49835
49704
  if (false !== session.showRuler) {
@@ -49874,6 +49743,11 @@ class Browser {
49874
49743
 
49875
49744
  await this.loadTrackList(trackConfigurations);
49876
49745
 
49746
+ // The ruler and ideogram tracks are not explicitly loaded, but needs updated nonetheless.
49747
+ for (let rtv of this.trackViews.filter((tv) => tv.track.type === 'ruler' || tv.track.type === 'ideogram')) {
49748
+ rtv.updateViews();
49749
+ }
49750
+
49877
49751
  this.updateUIWithReferenceFrameList();
49878
49752
 
49879
49753
  }
@@ -50052,23 +49926,19 @@ class Browser {
50052
49926
 
50053
49927
  async loadTrackList(configList) {
50054
49928
 
50055
- try {
50056
- const promises = [];
50057
- for (let config of configList) {
50058
- promises.push(this.loadTrack(config, false));
50059
- }
49929
+ const promises = [];
49930
+ for (let config of configList) {
49931
+ promises.push(this.loadTrack(config));
49932
+ }
50060
49933
 
50061
- const loadedTracks = await Promise.all(promises);
50062
- const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
50063
- return trackView.track.autoscaleGroup
50064
- });
50065
- if (groupAutoscaleViews.length > 0) {
50066
- this.updateViews(groupAutoscaleViews);
50067
- }
50068
- return loadedTracks
50069
- } finally {
50070
- await this.resize();
49934
+ const loadedTracks = await Promise.all(promises);
49935
+ const groupAutoscaleViews = this.trackViews.filter(function (trackView) {
49936
+ return trackView.track.autoscaleGroup
49937
+ });
49938
+ if (groupAutoscaleViews.length > 0) {
49939
+ this.updateViews();
50071
49940
  }
49941
+ return loadedTracks
50072
49942
  }
50073
49943
 
50074
49944
  async loadROI(config) {
@@ -50082,6 +49952,8 @@ class Browser {
50082
49952
  } else {
50083
49953
  this.roi.push(new ROI(config, this.genome));
50084
49954
  }
49955
+ // Force reload all views (force = true) to insure ROI features are loaded. Wasteful but this function is
49956
+ // rarely called.
50085
49957
  await this.updateViews(true);
50086
49958
  }
50087
49959
 
@@ -50093,14 +49965,14 @@ class Browser {
50093
49965
  }
50094
49966
  }
50095
49967
  for (let tv of this.trackViews) {
50096
- tv.updateViews(true);
49968
+ tv.repaintViews();
50097
49969
  }
50098
49970
  }
50099
49971
 
50100
49972
  clearROIs() {
50101
49973
  this.roi = [];
50102
49974
  for (let tv of this.trackViews) {
50103
- tv.updateViews(true);
49975
+ tv.repaintViews();
50104
49976
  }
50105
49977
  }
50106
49978
 
@@ -50112,24 +49984,12 @@ class Browser {
50112
49984
  /**
50113
49985
  * Return a promise to load a track.
50114
49986
  *
50115
- * Each track is associated with the following DOM elements
50116
- *
50117
- * leftHandGutter - div on the left for track controls and legend
50118
- * contentDiv - a div element wrapping all the track content. Height can be > viewportDiv height
50119
- * viewportDiv - a div element through which the track is viewed. This might have a vertical scrollbar
50120
- * canvas - canvas element upon which the track is drawn. Child of contentDiv
50121
- *
50122
- * The width of all elements should be equal. Height of the viewportDiv is controlled by the user, but never
50123
- * greater than the contentDiv height. Height of contentDiv and canvas are equal, and governed by the data
50124
- * loaded.
50125
- *
50126
- *
50127
49987
  * @param config
50128
49988
  * @param doResize - undefined by default
50129
49989
  * @returns {*}
50130
49990
  */
50131
49991
 
50132
- async loadTrack(config, doResize) {
49992
+ async loadTrack(config) {
50133
49993
 
50134
49994
 
50135
49995
  // config might be json
@@ -50197,11 +50057,6 @@ class Browser {
50197
50057
  }
50198
50058
  msg += (": " + config.url);
50199
50059
  Alert.presentAlert(new Error(msg), undefined);
50200
- } finally {
50201
- // TODO: If loadTrack() is called individually - not via loadTrackList() - call this.resize()
50202
- if (false === doResize) ; else {
50203
- await this.resize();
50204
- }
50205
50060
  }
50206
50061
  }
50207
50062
 
@@ -50425,44 +50280,11 @@ class Browser {
50425
50280
  this.navbarManager.navbarDidResize(this.$navigation.width(), isWGV);
50426
50281
  }
50427
50282
 
50428
- await this.resize();
50429
- }
50430
-
50431
- async resize() {
50432
-
50433
- const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
50434
-
50435
- for (let referenceFrame of this.referenceFrameList) {
50436
-
50437
- const index = this.referenceFrameList.indexOf(referenceFrame);
50438
-
50439
- const {chr, genome} = referenceFrame;
50440
-
50441
- const {bpLength} = genome.getChromosome(referenceFrame.chr);
50442
-
50443
- const viewportWidthBP = referenceFrame.toBP(viewportWidth);
50444
-
50445
- // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
50446
- if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
50447
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
50448
- referenceFrame.bpPerPixel = bpLength / viewportWidth;
50449
- } else {
50450
- // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
50451
- referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
50452
- }
50453
-
50454
- for (let {viewports} of this.trackViews) {
50455
- viewports[index].setWidth(viewportWidth);
50456
- }
50457
-
50458
- }
50459
-
50460
- await this.updateViews(true);
50461
-
50462
- this.updateUIWithReferenceFrameList();
50283
+ resize.call(this);
50284
+ await this.updateViews();
50463
50285
  }
50464
50286
 
50465
- async updateViews(force) {
50287
+ async updateViews() {
50466
50288
 
50467
50289
  const trackViews = this.trackViews;
50468
50290
 
@@ -50475,7 +50297,7 @@ class Browser {
50475
50297
  // Don't autoscale while dragging.
50476
50298
  if (this.dragObject) {
50477
50299
  for (let trackView of trackViews) {
50478
- await trackView.updateViews(force);
50300
+ await trackView.updateViews();
50479
50301
  }
50480
50302
  } else {
50481
50303
  // Group autoscale
@@ -50520,14 +50342,14 @@ class Browser {
50520
50342
  for (let trackView of groupTrackViews) {
50521
50343
  trackView.track.dataRange = dataRange;
50522
50344
  trackView.track.autoscale = false;
50523
- p.push(trackView.updateViews(force));
50345
+ p.push(trackView.updateViews());
50524
50346
  }
50525
50347
  await Promise.all(p);
50526
50348
  }
50527
50349
 
50528
50350
  }
50529
50351
 
50530
- await Promise.all(otherTracks.map(tv => tv.updateViews(force)));
50352
+ await Promise.all(otherTracks.map(tv => tv.updateViews()));
50531
50353
  // for (let trackView of otherTracks) {
50532
50354
  // await trackView.updateViews(force);
50533
50355
  // }
@@ -50535,6 +50357,12 @@ class Browser {
50535
50357
 
50536
50358
  }
50537
50359
 
50360
+ repaintViews() {
50361
+ for (let trackView of this.trackViews) {
50362
+ trackView.repaintViews();
50363
+ }
50364
+ }
50365
+
50538
50366
  updateLocusSearchWidget() {
50539
50367
 
50540
50368
  const referenceFrameList = this.referenceFrameList;
@@ -50599,49 +50427,50 @@ class Browser {
50599
50427
  }
50600
50428
  }
50601
50429
 
50602
- async presentMultiLocusPanel(alignment, referenceFrameLeft) {
50430
+ /**
50431
+ * Add a new multi-locus panel for the specified region
50432
+ * @param chr
50433
+ * @param start
50434
+ * @param end
50435
+ * @param referenceFrameLeft - optional, if supplied new panel should be placed to the immediate right
50436
+ */
50437
+ async addMultiLocusPanel(chr, start, end, referenceFrameLeft) {
50603
50438
 
50604
50439
  // account for reduced viewport width as a result of adding right mate pair panel
50605
50440
  const viewportWidth = this.calculateViewportWidth(1 + this.referenceFrameList.length);
50606
-
50607
50441
  const scaleFactor = this.calculateViewportWidth(this.referenceFrameList.length) / this.calculateViewportWidth(1 + this.referenceFrameList.length);
50608
- adjustReferenceFrame(scaleFactor, referenceFrameLeft, viewportWidth, alignment.start, alignment.lengthOnRef);
50609
-
50610
- // create right mate pair reference frame
50611
- const mateChrName = this.genome.getChromosomeName(alignment.mate.chr);
50612
-
50613
- const referenceFrameRight = createReferenceFrameWithAlignment(this.genome, mateChrName, referenceFrameLeft.bpPerPixel, viewportWidth, alignment.mate.position, alignment.lengthOnRef);
50442
+ for (let refFrame of this.referenceFrameList) {
50443
+ refFrame.bpPerPixel *= scaleFactor;
50444
+ }
50614
50445
 
50615
- // add right mate panel beside left mate panel
50616
- const indexLeft = this.referenceFrameList.indexOf(referenceFrameLeft);
50617
- const indexRight = 1 + (this.referenceFrameList.indexOf(referenceFrameLeft));
50446
+ const bpp = (end - start) / viewportWidth;
50447
+ const newReferenceFrame = new ReferenceFrame(this.genome, chr, start, end, bpp);
50448
+ const indexLeft = referenceFrameLeft ? this.referenceFrameList.indexOf(referenceFrameLeft) : this.referenceFrameList.length - 1;
50449
+ const indexRight = 1 + indexLeft;
50618
50450
 
50451
+ // TODO -- this is really ugly
50619
50452
  const {$viewport} = this.trackViews[0].viewports[indexLeft];
50620
50453
  const viewportColumn = viewportColumnManager.insertAfter($viewport.get(0).parentElement);
50621
50454
 
50622
50455
  if (indexRight === this.referenceFrameList.length) {
50623
-
50624
- this.referenceFrameList.push(referenceFrameRight);
50625
-
50456
+ this.referenceFrameList.push(newReferenceFrame);
50626
50457
  for (let trackView of this.trackViews) {
50627
- const viewport = createViewport(trackView, viewportColumn, referenceFrameRight);
50458
+ const viewport = createViewport(trackView, viewportColumn, newReferenceFrame);
50628
50459
  trackView.viewports.push(viewport);
50629
50460
  }
50630
-
50631
50461
  } else {
50632
-
50633
- this.referenceFrameList.splice(indexRight, 0, referenceFrameRight);
50634
-
50462
+ this.referenceFrameList.splice(indexRight, 0, newReferenceFrame);
50635
50463
  for (let trackView of this.trackViews) {
50636
- const viewport = createViewport(trackView, viewportColumn, referenceFrameRight);
50464
+ const viewport = createViewport(trackView, viewportColumn, newReferenceFrame);
50637
50465
  trackView.viewports.splice(indexRight, 0, viewport);
50638
50466
  }
50639
-
50640
50467
  }
50641
50468
 
50469
+
50642
50470
  this.centerLineList = this.createCenterLineList(this.columnContainer);
50643
50471
 
50644
- await this.resize();
50472
+ resize.call(this);
50473
+ await this.updateViews(true);
50645
50474
  }
50646
50475
 
50647
50476
  async removeMultiLocusPanel(referenceFrame) {
@@ -50670,7 +50499,13 @@ class Browser {
50670
50499
 
50671
50500
  }
50672
50501
 
50673
- async selectMultiLocusPanel(referenceFrame) {
50502
+ /**
50503
+ * Goto the locus represented by the selected referenceFrame, discarding all other panels
50504
+ *
50505
+ * @param referenceFrame
50506
+ * @returns {Promise<void>}
50507
+ */
50508
+ async gotoMultilocusPanel(referenceFrame) {
50674
50509
 
50675
50510
  const referenceFrameIndex = this.referenceFrameList.indexOf(referenceFrame);
50676
50511
 
@@ -50724,7 +50559,7 @@ class Browser {
50724
50559
 
50725
50560
  this.updateUIWithReferenceFrameList();
50726
50561
 
50727
- await this.updateViews(true);
50562
+ await this.updateViews();
50728
50563
 
50729
50564
  }
50730
50565
 
@@ -50947,7 +50782,6 @@ class Browser {
50947
50782
  }
50948
50783
 
50949
50784
 
50950
-
50951
50785
  json["tracks"] = trackJson;
50952
50786
 
50953
50787
  return json // This is an object, not a json string
@@ -50966,16 +50800,6 @@ class Browser {
50966
50800
  return surl
50967
50801
  }
50968
50802
 
50969
- currentLoci() {
50970
- const loci = [];
50971
- const anyTrackView = this.trackViews[0];
50972
- for (let {referenceFrame} of anyTrackView.viewports) {
50973
- const locusString = referenceFrame.getLocusString();
50974
- loci.push(locusString);
50975
- }
50976
- return loci
50977
- }
50978
-
50979
50803
  /**
50980
50804
  * Record a mouse click on a specific viewport. This might be the start of a drag operation. Dragging
50981
50805
  * (panning) is handled here so that the mouse can move out of a specific viewport (e.g. stray into another
@@ -51013,12 +50837,22 @@ class Browser {
51013
50837
 
51014
50838
  }
51015
50839
 
50840
+ /**
50841
+ * Track drag here refers to vertical dragging to reorder tracks, not horizontal panning.
50842
+ *
50843
+ * @param trackView
50844
+ */
51016
50845
  startTrackDrag(trackView) {
51017
50846
 
51018
50847
  this.dragTrack = trackView;
51019
50848
 
51020
50849
  }
51021
50850
 
50851
+ /**
50852
+ * Track drag here refers to vertical dragging to reorder tracks, not horizontal panning.
50853
+ *
50854
+ * @param dragDestination
50855
+ */
51022
50856
  updateTrackDrag(dragDestination) {
51023
50857
 
51024
50858
  if (dragDestination && this.dragTrack) {
@@ -51093,12 +50927,9 @@ class Browser {
51093
50927
  }
51094
50928
 
51095
50929
  addWindowResizeHandler() {
51096
- this.boundWindowResizeHandler = windowResizeHandler.bind(this);
50930
+ // Create a copy of the prototype "resize" function bound to this instance. Neccessary to support removing.
50931
+ this.boundWindowResizeHandler = resize.bind(this);
51097
50932
  window.addEventListener('resize', this.boundWindowResizeHandler);
51098
-
51099
- function windowResizeHandler() {
51100
- this.resize();
51101
- }
51102
50933
  }
51103
50934
 
51104
50935
  removeWindowResizeHandler() {
@@ -51181,7 +51012,7 @@ class Browser {
51181
51012
  id: this.genome.id,
51182
51013
  chromosomes: makeCircViewChromosomes(this.genome)
51183
51014
  });
51184
- this.circularViewVisible = show === true;
51015
+ this.circularViewVisible = show;
51185
51016
 
51186
51017
  }
51187
51018
 
@@ -51197,6 +51028,50 @@ class Browser {
51197
51028
  }
51198
51029
  }
51199
51030
 
51031
+ /**
51032
+ * Function called win window is resized, or visibility changed (e.g. "show" from a tab). This is a function rather
51033
+ * than class method because it needs to be copied and bound to specific instances of browser to support listener
51034
+ * removal
51035
+ *
51036
+ * @returns {Promise<void>}
51037
+ */
51038
+ async function resize() {
51039
+
51040
+ const viewportWidth = this.calculateViewportWidth(this.referenceFrameList.length);
51041
+
51042
+ for (let referenceFrame of this.referenceFrameList) {
51043
+
51044
+ const index = this.referenceFrameList.indexOf(referenceFrame);
51045
+
51046
+ const {chr, genome} = referenceFrame;
51047
+
51048
+ const {bpLength} = genome.getChromosome(referenceFrame.chr);
51049
+
51050
+ const viewportWidthBP = referenceFrame.toBP(viewportWidth);
51051
+
51052
+ // viewportWidthBP > bpLength occurs when locus is full chromosome and user widens browser
51053
+ if (GenomeUtils.isWholeGenomeView(chr) || viewportWidthBP > bpLength) {
51054
+ // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) bpp. viewport ${ StringUtils.numberFormatter(viewportWidthBP) } > ${ StringUtils.numberFormatter(bpLength) }.`)
51055
+ referenceFrame.bpPerPixel = bpLength / viewportWidth;
51056
+ } else {
51057
+ // console.log(`${ Date.now() } Recalc referenceFrame(${ index }) end.`)
51058
+ referenceFrame.end = referenceFrame.start + referenceFrame.toBP(viewportWidth);
51059
+ }
51060
+
51061
+ for (let {viewports} of this.trackViews) {
51062
+ viewports[index].setWidth(viewportWidth);
51063
+ }
51064
+
51065
+ }
51066
+
51067
+ this.updateUIWithReferenceFrameList();
51068
+
51069
+ //TODO -- update view only if needed. Reducing size never needed. Increasing size maybe
51070
+
51071
+ await this.updateViews(true);
51072
+ }
51073
+
51074
+
51200
51075
  function handleMouseMove(e) {
51201
51076
 
51202
51077
  e.preventDefault();