Skip to content

Commit 3fc96a0

Browse files
authored
Bug Fix: Vanishing 2D Annotations Fix (#394)
* Bug fix underway: Vanishing 2D Annotations * Bug fix: Vanishing 2D Annotations * 2D Annotation rendering: add trivial rejection * 2D Tracks: Restore legacy rendering for reference ONLY. Legacy 2D track rendering NOLONGER used. * contactMatrixView.setBackgroundColor() - now calls update() rather then repaint() * Refactor - added render2DTracks() * bug fix
1 parent 252d2a3 commit 3fc96a0

File tree

2 files changed

+167
-99
lines changed

2 files changed

+167
-99
lines changed

examples/bug-daphne-qin-vanishing-2d-annotation.html

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -27,38 +27,38 @@
2727
const config_2d_annotation_vanish =
2828
{
2929
"browsers": [
30-
{
31-
"backgroundColor": "58,58,58",
32-
"url": "https://s3.us-east-1.wasabisys.com/aiden-suhas/hic_files/FINAL_GRCh38_processing/LCL/GM12878/diploid/r.hic",
33-
"name": "r.hic",
34-
"state": "13,13,10,42716.19,42716.19,640,640,1.067481115221243,NONE",
35-
"colorScale": "11,84,84,83",
36-
"nvi": "43762184328,25419",
37-
"tracks": [
38-
{
39-
"url": "https://www.dropbox.com/scl/fi/mweycro8eyhubrjrhv6oo/fosl1_loops_with_snps_nothresh.bedpe?rlkey=1dv8wbc7c57m9muob7tlxvm89&e=1&st=2c4adj56&dl=0",
40-
"name": "fosl1_loops_with_snps_nothresh.bedpe",
41-
"color": "#83f902",
42-
"displayMode": "COLLAPSED"
43-
}
44-
]
45-
},
46-
{
47-
"backgroundColor": "58,58,58",
48-
"url": "https://s3.us-east-1.wasabisys.com/aiden-suhas/hic_files/FINAL_GRCh38_processing/LCL/GM12878/diploid/a.hic",
49-
"name": "a.hic",
50-
"state": "13,13,10,42716.19,42716.19,640,640,1.067481115221243,NONE",
51-
"colorScale": "11,84,84,83",
52-
"nvi": "43835341324,25419",
53-
"tracks": [
54-
{
55-
"url": "https://www.dropbox.com/scl/fi/mweycro8eyhubrjrhv6oo/fosl1_loops_with_snps_nothresh.bedpe?rlkey=1dv8wbc7c57m9muob7tlxvm89&e=1&st=2c4adj56&dl=0",
56-
"name": "fosl1_loops_with_snps_nothresh.bedpe",
57-
"color": "#ff39ff",
58-
"displayMode": "COLLAPSED"
59-
}
60-
]
61-
},
30+
// {
31+
// "backgroundColor": "58,58,58",
32+
// "url": "https://s3.us-east-1.wasabisys.com/aiden-suhas/hic_files/FINAL_GRCh38_processing/LCL/GM12878/diploid/r.hic",
33+
// "name": "r.hic",
34+
// "state": "13,13,10,42716.19,42716.19,640,640,1.067481115221243,NONE",
35+
// "colorScale": "11,84,84,83",
36+
// "nvi": "43762184328,25419",
37+
// "tracks": [
38+
// {
39+
// "url": "https://www.dropbox.com/scl/fi/mweycro8eyhubrjrhv6oo/fosl1_loops_with_snps_nothresh.bedpe?rlkey=1dv8wbc7c57m9muob7tlxvm89&e=1&st=2c4adj56&dl=0",
40+
// "name": "fosl1_loops_with_snps_nothresh.bedpe",
41+
// "color": "#83f902",
42+
// "displayMode": "COLLAPSED"
43+
// }
44+
// ]
45+
// },
46+
// {
47+
// "backgroundColor": "58,58,58",
48+
// "url": "https://s3.us-east-1.wasabisys.com/aiden-suhas/hic_files/FINAL_GRCh38_processing/LCL/GM12878/diploid/a.hic",
49+
// "name": "a.hic",
50+
// "state": "13,13,10,42716.19,42716.19,640,640,1.067481115221243,NONE",
51+
// "colorScale": "11,84,84,83",
52+
// "nvi": "43835341324,25419",
53+
// "tracks": [
54+
// {
55+
// "url": "https://www.dropbox.com/scl/fi/mweycro8eyhubrjrhv6oo/fosl1_loops_with_snps_nothresh.bedpe?rlkey=1dv8wbc7c57m9muob7tlxvm89&e=1&st=2c4adj56&dl=0",
56+
// "name": "fosl1_loops_with_snps_nothresh.bedpe",
57+
// "color": "#ff39ff",
58+
// "displayMode": "COLLAPSED"
59+
// }
60+
// ]
61+
// },
6262
{
6363
"backgroundColor": "58,58,58",
6464
"url": "https://s3.us-east-1.wasabisys.com/aiden-suhas/hic_files/FINAL_GRCh38_processing/LCL/LCL_combined/9_12_23/inter_30.hic",

js/contactMatrixView.js

Lines changed: 135 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@ import {IGVColor, StringUtils} from '../node_modules/igv-utils/src/index.js'
3030
import ColorScale from './colorScale.js'
3131
import HICEvent from './hicEvent.js'
3232
import * as hicUtils from './hicUtils.js'
33+
import {getLocus} from "./genomicUtils.js"
3334

3435
const DRAG_THRESHOLD = 2
3536
const DOUBLE_TAP_DIST_THRESHOLD = 20
3637
const DOUBLE_TAP_TIME_THRESHOLD = 300
3738

3839
const imageTileDimension = 685
3940

40-
class ContactMatrixView {
41+
const doLegacyTrack2DRendering = false
4142

43+
class ContactMatrixView {
4244

4345
constructor(browser, $viewport, sweepZoom, scrollbarWidget, colorScale, ratioColorScale, backgroundColor) {
4446

@@ -86,7 +88,7 @@ class ContactMatrixView {
8688
setBackgroundColor(rgb) {
8789
this.backgroundColor = rgb
8890
this.backgroundRGBString = IGVColor.rgbColor(rgb.r, rgb.g, rgb.b)
89-
this.repaint()
91+
this.update()
9092
}
9193

9294
stringifyBackgroundColor() {
@@ -177,6 +179,10 @@ class ContactMatrixView {
177179

178180
await this.repaint()
179181

182+
if (this.browser.dataset && false === doLegacyTrack2DRendering){
183+
await this.render2DTracks(this.browser.tracks2D, this.browser.dataset, this.browser.state)
184+
}
185+
180186
}
181187

182188
async repaint() {
@@ -416,95 +422,83 @@ class ContactMatrixView {
416422
}
417423

418424
ctx.putImageData(id, 0, 0)
419-
} else {
420-
//console.log("No block for " + blockNumber);
421425
}
422426

423-
//Draw 2D tracks
424-
ctx.save()
425-
ctx.lineWidth = 2
426427

428+
if (true === doLegacyTrack2DRendering) {
427429

428-
const onDiagonalTile = sameChr && row === column
430+
//Draw 2D tracks
431+
ctx.save()
432+
ctx.lineWidth = 2
429433

430-
for (let track2D of this.browser.tracks2D) {
431-
const skip =
432-
!track2D.isVisible ||
433-
(sameChr && "lower" === track2D.displayMode && row < column) ||
434-
(sameChr && "upper" === track2D.displayMode && row > column)
434+
const onDiagonalTile = sameChr && row === column
435435

436-
if (!skip) {
436+
for (let track2D of this.browser.tracks2D) {
437+
const skip =
438+
!track2D.isVisible ||
439+
(sameChr && "lower" === track2D.displayMode && row < column) ||
440+
(sameChr && "upper" === track2D.displayMode && row > column)
437441

438-
const chr1Name = zd.chr1.name
439-
const chr2Name = zd.chr2.name
442+
if (!skip) {
440443

441-
const features = track2D.getFeatures(chr1Name, chr2Name)
444+
const chr1Name = zd.chr1.name
445+
const chr2Name = zd.chr2.name
442446

443-
if (features) {
447+
const features = track2D.getFeatures(chr1Name, chr2Name)
444448

445-
// console.log(`contactMatrixView - getImageTile(): render 2D annotations ${ features.length }`)
449+
if (features) {
446450

447-
for (let {chr1, x1, x2, y1, y2, color} of features) {
451+
for (let {chr1, x1, x2, y1, y2, color} of features) {
448452

449-
ctx.strokeStyle = track2D.color || color
453+
ctx.strokeStyle = track2D.color || color
450454

451-
// Chr name order -- test for equality of zoom data chr1 and feature chr1
452-
const flip = chr1Name !== chr1
455+
// Chr name order -- test for equality of zoom data chr1 and feature chr1
456+
const flip = chr1Name !== chr1
453457

454-
//Note: transpose = sameChr && row < column
455-
const fx1 = transpose || flip ? y1 : x1
456-
const fx2 = transpose || flip ? y2 : x2
457-
const fy1 = transpose || flip ? x1 : y1
458-
const fy2 = transpose || flip ? x2 : y2
458+
//Note: transpose = sameChr && row < column
459+
const fx1 = transpose || flip ? y1 : x1
460+
const fx2 = transpose || flip ? y2 : x2
461+
const fy1 = transpose || flip ? x1 : y1
462+
const fy2 = transpose || flip ? x2 : y2
459463

460-
let px1 = (fx1 - x0bp) / zd.zoom.binSize
461-
let px2 = (fx2 - x0bp) / zd.zoom.binSize
462-
let py1 = (fy1 - y0bp) / zd.zoom.binSize
463-
let py2 = (fy2 - y0bp) / zd.zoom.binSize
464-
let w = Math.round(Math.max(1, px2 - px1))
465-
let h = Math.round(Math.max(1, py2 - py1))
464+
let px1 = (fx1 - x0bp) / zd.zoom.binSize
465+
let px2 = (fx2 - x0bp) / zd.zoom.binSize
466+
let py1 = (fy1 - y0bp) / zd.zoom.binSize
467+
let py2 = (fy2 - y0bp) / zd.zoom.binSize
468+
let w = px2 - px1
469+
let h = py2 - py1
466470

467-
const dim = Math.max(image.width, image.height)
468-
if (px2 > 0 && px1 < dim && py2 > 0 && py1 < dim) {
471+
const dim = Math.max(image.width, image.height)
472+
if (px2 > 0 && px1 < dim && py2 > 0 && py1 < dim) {
469473

470474

471-
if (!onDiagonalTile || "upper" !== track2D.displayMode) {
472-
const xx = Math.round(px1)
473-
const yy = Math.round(py1)
474-
const startStr = `xStart ${ StringUtils.numberFormatter(xx) } yStart ${ StringUtils.numberFormatter(yy) }`
475-
const endStr = `width ${ StringUtils.numberFormatter(w) } height ${ StringUtils.numberFormatter(h) }`
476-
// console.log(`render ${ chr1 } ${ startStr } ${ endStr }`)
477-
ctx.strokeRect(xx, yy, w, h)
478-
}
475+
if (!onDiagonalTile || "upper" !== track2D.displayMode) {
476+
ctx.strokeRect(px1, py1, w, h)
477+
}
479478

480-
// By convention intra-chromosome data is always stored in lower diagonal coordinates.
481-
// If we are on a diagonal tile, draw the symettrical reflection unless display mode is lower
482-
if (onDiagonalTile && "lower" !== track2D.displayMode) {
483-
const xx = Math.round(py1)
484-
const yy = Math.round(px1)
485-
const startStr = `xStart ${ StringUtils.numberFormatter(xx) } yStart ${ StringUtils.numberFormatter(yy) }`
486-
const endStr = `width ${ StringUtils.numberFormatter(h) } height ${ StringUtils.numberFormatter(w) }`
487-
// console.log(`render ${ chr1 } ${ startStr } ${ endStr }`)
488-
ctx.strokeRect(xx, yy, h, w)
479+
// By convention intra-chromosome data is always stored in lower diagonal coordinates.
480+
// If we are on a diagonal tile, draw the symmetrical reflection unless display mode is lower
481+
if (onDiagonalTile && "lower" !== track2D.displayMode) {
482+
ctx.strokeRect(py1, px1, h, w)
483+
}
489484
}
490485
}
491-
}
492486

493-
} // if (features)
487+
} // if (features)
494488

495-
} // if (track2D.isVisible)
496-
}
489+
}
490+
}
497491

492+
ctx.restore()
498493

499-
ctx.restore()
494+
} // if (true === doLegacyTrack2DRendering)
500495

501496
// Uncomment to reveal tile boundaries for debugging.
502-
// ctx.fillStyle = "rgb(255,255,255)"
497+
// ctx.strokeStyle = "rgb(255,255,255)"
498+
// ctx.strokeStyle = "pink"
503499
// ctx.strokeRect(0, 0, image.width - 1, image.height - 1)
504500

505-
506-
var imageTile = {row: row, column: column, blockBinCount: imageTileDimension, image: image}
507-
501+
const imageTile = { row, column, blockBinCount: imageTileDimension, image }
508502

509503
if (this.imageTileCacheLimit > 0) {
510504
if (this.imageTileCacheKeys.length > this.imageTileCacheLimit) {
@@ -518,7 +512,6 @@ class ContactMatrixView {
518512
return imageTile
519513

520514
} finally {
521-
//console.log("Finish load for " + key)
522515
this.drawsInProgress.delete(key)
523516
this.stopSpinner()
524517
}
@@ -533,7 +526,7 @@ class ContactMatrixView {
533526
imageData.data[index + 3] = a
534527
}
535528

536-
};
529+
}
537530

538531
/**
539532
* Return a promise to adjust the color scale, if needed. This function might need to load the contact
@@ -678,7 +671,6 @@ class ContactMatrixView {
678671
this.spinnerCount = Math.max(0, this.spinnerCount) // This should not be neccessary
679672
}
680673

681-
682674
addMouseHandlers($viewport) {
683675

684676
let isMouseDown = false
@@ -856,7 +848,6 @@ class ContactMatrixView {
856848
}
857849
}
858850

859-
860851
/**
861852
* Add touch handlers. Touches are mapped to one of the following application level events
862853
* - double tap, equivalent to double click
@@ -1023,6 +1014,84 @@ class ContactMatrixView {
10231014

10241015
}
10251016

1017+
async render2DTracks(track2DList, dataset, state) {
1018+
1019+
const matrix = await dataset.getMatrix(state.chr1, state.chr2)
1020+
const zoomData = matrix.getZoomDataByIndex(state.zoom, 'BP')
1021+
1022+
const { width, height } = this.getViewDimensions()
1023+
const bpPerPixel = zoomData.zoom.binSize/state.pixelSize
1024+
const { xStartBP, yStartBP, xEndBP, yEndBP } = getLocus(dataset, state, width, height, bpPerPixel)
1025+
1026+
const chr1Name = zoomData.chr1.name
1027+
const chr2Name = zoomData.chr2.name
1028+
1029+
const sameChr = zoomData.chr1.index === zoomData.chr2.index
1030+
1031+
this.ctx.save()
1032+
this.ctx.lineWidth = 2
1033+
1034+
const renderFeatures = (xS, xE, yS, yE) => {
1035+
1036+
if (xE < xStartBP || xS > xEndBP || yE < yStartBP || yS > yEndBP) {
1037+
// trivially reject
1038+
} else {
1039+
const w = Math.max(1, (xE - xS)/bpPerPixel)
1040+
const h = Math.max(1, (yE - yS)/bpPerPixel)
1041+
const x = Math.floor((xS - xStartBP)/bpPerPixel)
1042+
const y = Math.floor((yS - yStartBP)/bpPerPixel)
1043+
this.ctx.strokeRect(x, y, w, h)
1044+
}
1045+
1046+
}
1047+
1048+
const renderLowerFeatures = (track2D, features) => {
1049+
for (const { chr1, x1:xS, x2:xE, y1:yS, y2:yE, color } of features) {
1050+
1051+
const flip = chr1Name !== chr1
1052+
1053+
this.ctx.strokeStyle = track2D.color || color
1054+
renderFeatures(xS, xE, yS, yE)
1055+
}
1056+
}
1057+
1058+
const renderUpperFeatures = (track2D, features) => {
1059+
for (const { chr1, x1:yS, x2:yE, y1:xS, y2:xE, color } of features) {
1060+
1061+
const flip = chr1Name !== chr1
1062+
1063+
this.ctx.strokeStyle = track2D.color || color
1064+
renderFeatures(xS, xE, yS, yE)
1065+
}
1066+
}
1067+
1068+
for (const track2D of track2DList) {
1069+
1070+
if (false === track2D.isVisible) {
1071+
continue
1072+
}
1073+
1074+
const features = track2D.getFeatures(zoomData.chr1.name, zoomData.chr1.name)
1075+
1076+
if (features) {
1077+
1078+
if ('COLLAPSED' === track2D.displayMode || undefined === track2D.displayMode) {
1079+
renderLowerFeatures(track2D, features)
1080+
renderUpperFeatures(track2D, features)
1081+
} else if ('lower' === track2D.displayMode) {
1082+
renderLowerFeatures(track2D, features)
1083+
} else if ('upper' === track2D.displayMode) {
1084+
renderUpperFeatures(track2D, features)
1085+
}
1086+
1087+
}
1088+
1089+
1090+
}
1091+
1092+
this.ctx.restore()
1093+
1094+
}
10261095
}
10271096

10281097
ContactMatrixView.defaultBackgroundColor = {r: 255, g: 255, b: 255}
@@ -1079,7 +1148,6 @@ function getMatrices(chr1, chr2) {
10791148
return Promise.all(promises)
10801149
}
10811150

1082-
10831151
function computePercentile(records, p) {
10841152
const counts = records.map(r => r.counts)
10851153
counts.sort(function (a, b) {

0 commit comments

Comments
 (0)