@gmod/jbrowse
Version:
JBrowse - client-side genome browser
308 lines (268 loc) • 13.2 kB
JavaScript
define( ['dojo/_base/declare',
'dojo/_base/array',
'JBrowse/View/Track/Wiggle/XYPlot',
'JBrowse/Util',
'JBrowse/View/Track/_AlignmentsMixin',
'JBrowse/Store/SeqFeature/SNPCoverage'
],
function( declare, array, WiggleXY, Util, AlignmentsMixin, SNPCoverageStore ) {
var dojof = Util.dojof;
return declare( [WiggleXY, AlignmentsMixin],
{
constructor: function() {
// force conf variables that are meaningless for this kind of track, and maybe harmful
delete this.config.bicolor_pivot;
delete this.config.scale;
delete this.config.align;
var thisB = this;
this.store = new SNPCoverageStore(
{ store: this.store,
config: {
mismatchScale: this.config.mismatchScale,
indicatorProp: this.config.indicatorProp,
indicatorDepth: this.config.indicatorDepth
},
browser: this.browser,
filter: function( f ) {
return thisB.filterFeature( f );
}
});
},
_defaultConfig: function() {
return Util.deepUpdate(
dojo.clone( this.inherited(arguments) ),
{
autoscale: 'local',
min_score: 0,
mismatchScale: 1/10,
indicatorProp: 0.5,
indicatorDepth: 1,
hideDuplicateReads: true,
logScaleOption: false,
hideQCFailingReads: true,
hideSecondary: true,
hideSupplementary: true,
hideMissingMatepairs: false,
hideImproperPairs: false,
hideUnmapped: true
}
);
},
/*
* Draw a set of features on the canvas.
* @private
*/
_drawFeatures: function( scale, leftBase, rightBase, block, canvas, features, featureRects, dataScale ) {
var thisB = this;
var context = canvas.getContext('2d');
var canvasHeight = canvas.height;
var ratio = Util.getResolution( context, this.browser.config.highResolutionMode );
var toY = dojo.hitch( this, function( val ) {
return canvasHeight * ( 1-dataScale.normalize(val) ) / ratio;
});
var originY = toY( dataScale.origin );
// a canvas element below the histogram that will contain indicators of likely SNPs
var snpCanvasHeight = 20;
var snpCanvas = dojo.create('canvas',
{height: snpCanvasHeight,
width: canvas.width,
style: {
cursor: 'default',
width: "100%",
height: snpCanvasHeight + "px"
},
innerHTML: 'Your web browser cannot display this type of track.',
className: 'SNP-indicator-track'
}, block.domNode);
var snpContext = snpCanvas.getContext('2d');
// finally query the various pixel ratios
var ratio = Util.getResolution( snpContext, this.browser.config.highResolutionMode );
// upscale canvas if the two ratios don't match
if ( this.browser.config.highResolutionMode !='disabled' && ratio!=1 ) {
var oldWidth = snpCanvas.width;
var oldHeight = snpCanvas.height;
snpCanvas.width = oldWidth * ratio;
snpCanvas.height = oldHeight * ratio;
//c.style.width = oldWidth + 'px';
snpCanvas.style.height = oldHeight + 'px';
// now scale the context to counter
// the fact that we've manually scaled
// our canvas element
snpContext.scale(ratio, ratio);
}
var negColor = this.config.style.neg_color;
var clipColor = this.config.style.clip_marker_color;
var bgColor = this.config.style.bg_color;
var disableClipMarkers = this.config.disable_clip_markers;
var drawRectangle = function(ID, yPos, height, fRect) {
if( yPos <= canvasHeight ) { // if the rectangle is visible at all
context.fillStyle = thisB.colorForBase(ID);
if( yPos <= originY ) {
// bar goes upward
thisB._fillRectMod( context, fRect.l, yPos, fRect.w, height);
if( !disableClipMarkers && yPos < 0 ) { // draw clip marker if necessary
context.fillStyle = clipColor || negColor;
thisB._fillRectMod( context, fRect.l, 0, fRect.w, 2 );
}
}
else {
// bar goes downward
thisB._fillRectMod( context, fRect.l, originY, fRect.w, height );
if( !disableClipMarkers && yPos >= canvasHeight ) { // draw clip marker if necessary
context.fillStyle = clipColor || thisB.colorForBase(ID);
thisB._fillRectMod( context, fRect.l, canvasHeight-3, fRect.w, 2 );
}
}
}
};
// Note: 'reference' is done first to ensure the grey part of the graph is on top
dojo.forEach( features, function(f,i) {
var fRect = featureRects[i];
var score = f.get('score');
// draw the background color if we are configured to do so
if( bgColor ) {
context.fillStyle = bgColor;
thisB._fillRectMod( context, fRect.l, 0, fRect.w, canvasHeight );
}
drawRectangle( 'reference', toY( score.total() ), originY-toY( score.get('reference'))+1, fRect);
});
var indicatorMinHeightProp = this.config.indicatorProp;
var indicatorMinHeight = this.config.indicatorDepth;
dojo.forEach( features, function(f,i) {
var fRect = featureRects[i];
var score = f.get('score');
var totalHeight = score.total();
// draw indicators of SNPs if base coverage is greater than 50% of total coverage
score.forEach( function( count, category ) {
if ( !{reference:true,skip:true,deletion:true}[category] && count >= indicatorMinHeightProp*totalHeight && count >= indicatorMinHeight ) {
snpContext.save();
if( thisB.browser.config.highResolutionMode != 'disabled' )
snpContext.scale(ratio, 1);
snpContext.beginPath();
snpContext.arc( (fRect.l + 0.5*fRect.w),
0.40*snpCanvas.height/ratio,
0.20*snpCanvas.height/ratio,
1.75 * Math.PI,
1.25 * Math.PI,
false);
snpContext.lineTo(fRect.l + 0.5*fRect.w, 0);
snpContext.closePath();
snpContext.fillStyle = thisB.colorForBase(category);
snpContext.fill();
snpContext.lineWidth = 1;
snpContext.strokeStyle = 'black';
snpContext.stroke();
if( thisB.browser.config.highResolutionMode != 'disabled' )
snpContext.restore();
}
});
totalHeight -= score.get('reference');
score.forEach( function( count, category ) {
if ( category != 'reference' ) {
drawRectangle( category, toY(totalHeight), originY-toY( count )+1, fRect);
totalHeight -= count;
}
});
}, this );
},
// Overwrites the method from WiggleBase
_draw: function( scale, leftBase, rightBase, block, canvas, features, featureRects, dataScale, pixels, spans ) {
// Note: pixels currently has no meaning, as the function that generates it is not yet defined for this track
this._preDraw( scale, leftBase, rightBase, block, canvas, features, featureRects, dataScale );
this._drawFeatures( scale, leftBase, rightBase, block, canvas, features, featureRects, dataScale );
if ( spans ) {
this._maskBySpans( scale, leftBase, canvas, spans );
}
this._postDraw( scale, leftBase, rightBase, block, canvas, features, featureRects, dataScale );
},
/* If it's a boolean track, mask accordingly */
_maskBySpans: function( scale, leftBase, canvas, spans ) {
var context = canvas.getContext('2d');
var canvasHeight = canvas.height;
var booleanAlpha = this.config.style.masked_transparancy || 0.17;
this.config.style.masked_transparancy = booleanAlpha;
// make a temporary canvas to store image data
var tempCan = dojo.create( 'canvas', {height: canvasHeight, width: canvas.width} );
var ctx2 = tempCan.getContext('2d');
for ( var index in spans ) {
if (spans.hasOwnProperty(index)) {
var w = Math.round(( spans[index].end - spans[index].start ) * scale );
var l = Math.round(( spans[index].start - leftBase ) * scale );
if (l+w >= canvas.width)
w = canvas.width-l; // correct possible rounding errors
if (w==0)
continue; // skip if there's no width.
ctx2.drawImage(canvas, l, 0, w, canvasHeight, l, 0, w, canvasHeight);
context.globalAlpha = booleanAlpha;
// clear masked region and redraw at lower opacity.
context.clearRect(l, 0, w, canvasHeight);
context.drawImage(tempCan, l, 0, w, canvasHeight, l, 0, w, canvasHeight);
context.globalAlpha = 1;
}
}
},
/*
* The following method is required to override the equivalent method in "WiggleBase.js"
* It displays more complete data.
*/
_showPixelValue: function( scoreDisplay, score ) {
if( ! score || ! score.score )
return false;
score = score.score;
function fmtNum( num ) {
return parseFloat( num ).toPrecision(6).replace(/0+$/,'').replace(/\.$/,'');
}
function pctString( count ) {
count = Math.round(count/total*100);
if( typeof count == 'number' && ! isNaN(count) )
return count+'%';
return '';
}
if( score.snpsCounted ) {
var total = score.total();
var scoreSummary = '<table>';
score.forEach( function( count, category ) {
// if this count has more nested categories, do counts of those
var subdistribution = '';
if( count.forEach ) {
subdistribution = [];
count.forEach( function( count, category ) {
subdistribution.push( fmtNum(count) + ' '+category );
});
subdistribution = subdistribution.join(', ');
if( subdistribution )
subdistribution = '('+subdistribution+')';
}
category = { '*': 'del', reference: 'Ref', skip: 'Skip/intron' }[category] || category;
scoreSummary += '<tr><td>'+category + '</td><td class="count">' + fmtNum(count) + '</td><td class="pct">'
+pctString(count)+'</td><td class="subdist">'+subdistribution + '</td></tr>';
});
scoreSummary += '<tr class="total"><td>Total</td><td class="count">'+fmtNum(total)+'</td><td class="pct"> </td><td class="subdist"> </td></tr>';
scoreDisplay.innerHTML = scoreSummary+'</table>';
return true;
} else {
scoreDisplay.innerHTML = '<table><tr><td>Total</td><td class="count">'+fmtNum(score)+'</td></tr></table>';
return true;
}
},
_trackMenuOptions: function() {
var thisB = this;
var displayOptions = [];
displayOptions.push({
label: 'View alignments',
onClick: function(event) {
thisB.config.type = 'JBrowse/View/Track/Alignments2'
thisB.config._oldSnpCoverageHeight = thisB.config.style.height
thisB.config.style.height = thisB.config._oldAlignmentsHeight
thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
}
});
return Promise.all([ this.inherited(arguments), this._alignmentsFilterTrackMenuOptions(), displayOptions ])
.then( function( options ) {
var o = options.shift();
options.unshift({ type: 'dijit/MenuSeparator' } );
return o.concat.apply( o, options );
});
}
});
});