@gmod/jbrowse
Version:
JBrowse - client-side genome browser
397 lines (365 loc) • 15.4 kB
JavaScript
/**
* Mixin with methods used for displaying alignments and their mismatches.
*/
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/lang',
'dojo/when',
'JBrowse/Util',
'JBrowse/Store/SeqFeature/_MismatchesMixin',
'JBrowse/View/Track/_NamedFeatureFiltersMixin'
],
function(
declare,
array,
lang,
when,
Util,
MismatchesMixin,
NamedFeatureFiltersMixin
) {
return declare([ MismatchesMixin, NamedFeatureFiltersMixin ], {
/**
* Make a default feature detail page for the given feature.
* @returns {HTMLElement} feature detail page HTML
*/
defaultFeatureDetail: function( /** JBrowse.Track */ track, /** Object */ f, /** HTMLElement */ div ) {
var container = dojo.create('div', {
className: 'detail feature-detail feature-detail-'+track.name.replace(/\s+/g,'_').toLowerCase(),
innerHTML: ''
});
var fmt = dojo.hitch( this, function( name, value, feature ) {
name = Util.ucFirst( name.replace(/_/g,' ') );
return this.renderDetailField(container, name, value, feature);
});
fmt( 'Name', f.get('name'), f );
fmt( 'Type', f.get('type'), f );
fmt( 'Score', f.get('score'), f );
fmt( 'Description', f.get('note'), f );
fmt(
'Position',
Util.assembleLocString({ start: f.get('start'),
end: f.get('end'),
ref: this.refSeq.name })
+ ({'1':' (+)', '-1': ' (-)', 0: ' (no strand)' }[f.get('strand')] || ''),
f
);
if( f.get('seq') ) {
fmt('Sequence and Quality', this._renderSeqQual( f ), f );
}
var renameTags = { length_on_ref: 'seq_length_on_ref' };
var additionalTags = array.filter(
f.tags(), function(t) {
return ! {name:1,score:1,start:1,end:1,strand:1,note:1,subfeatures:1,type:1,cram_read_features:1}[t.toLowerCase()];
}
)
.map( function(tagName) {
return [
renameTags[tagName] || tagName,
f.get(tagName)
]
})
.sort( function(a,b) { return a[0].localeCompare(b[0]) })
dojo.forEach( additionalTags, function(t) {
fmt( t[0], t[1], f );
});
// genotypes in a separate section
if(this.config.renderAlignment || this.config.renderPrettyAlignment) {
this._renderTable( container, track, f, div );
}
return container;
},
// takes a feature, returns an HTML representation of its 'seq'
// and 'qual', if it has at least a seq. empty string otherwise.
_renderSeqQual: function( feature ) {
var seq = feature.get('seq'),
qual = feature.get('qual') || '';
if( !seq )
return '';
qual = qual.split(/\s+/);
var html = '';
for( var i = 0; i < seq.length; i++ ) {
html += '<div class="basePosition" title="position '+(i+1)+'"><span class="seq">'
+ seq[i]+'</span>';
if( qual[i] )
html += '<span class="qual">'+qual[i]+'</span>';
html += '</div>';
}
return '<div class="baseQuality">'+html+'</div>';
},
// recursively find all the stylesheets that are loaded in the
// current browsing session, traversing imports and such
_getStyleSheets: function( inSheets ) {
var outSheets = []
array.forEach(inSheets, sheet => {
try {
let rules = sheet.cssRules || sheet.rules
let includedSheets = [sheet]
array.forEach(rules, rule => {
if (rule.styleSheet)
includedSheets.push(...this._getStyleSheets([rule.styleSheet]))
})
outSheets.push(...includedSheets)
} catch(e) {
//console.warn('could not read stylesheet',sheet)
}
})
return outSheets;
},
// get the appropriate HTML color string to use for a given base
// letter. case insensitive. 'reference' gives the color to draw matches with the reference.
colorForBase: function( base ) {
// get the base colors out of CSS
this._baseStyles = this._baseStyles || function() {
var colors = {};
try {
var styleSheets = this._getStyleSheets( document.styleSheets );
array.forEach( styleSheets, function( sheet ) {
// avoid modifying cssRules for plugins which generates SecurityException on Firefox
var classes = sheet.rules || sheet.cssRules;
if( ! classes ) return;
array.forEach( classes, function( c ) {
var match = /^\.jbrowse\s+\.base_([^\s_]+)$/.exec( c.selectorText );
if( match && match[1] ) {
var base = match[1];
match = /\#[0-9a-f]{3,6}|(?:rgb|hsl)a?\([^\)]*\)/gi.exec( c.cssText );
if( match && match[0] ) {
colors[ base.toLowerCase() ] = match[0];
colors[ base.toUpperCase() ] = match[0];
}
}
});
});
} catch(e) {
console.error(e)
/* catch errors from cross-domain stylesheets */
}
return colors;
}.call(this);
return this._baseStyles[base] || '#999';
},
// filters for BAM alignments according to some flags
_getNamedFeatureFilters: function() {
return lang.mixin( {}, this.inherited( arguments ),
{
hideDuplicateReads: {
desc: 'Hide PCR/Optical duplicate reads',
func: function( f ) {
return ! f.get('duplicate');
}
},
hideQCFailingReads: {
desc: 'Hide reads failing vendor QC',
func: function( f ) {
return ! f.get('qc_failed');
}
},
hideSecondary: {
desc: 'Hide secondary alignments',
func: function( f ) {
return ! f.get('secondary_alignment');
}
},
hideSupplementary: {
desc: 'Hide supplementary alignments',
func: function( f ) {
return ! f.get('supplementary_alignment');
}
},
hideMissingMatepairs: {
desc: 'Hide reads with missing mate pairs',
func: function( f ) {
return ! ( f.get('multi_segment_template') && ! f.get('multi_segment_all_aligned') );
}
},
hideUnmapped: {
desc: 'Hide unmapped reads',
func: function( f ) {
return ! f.get('unmapped');
}
},
hideForwardStrand: {
desc: 'Hide reads aligned to the forward strand',
func: function( f ) {
return f.get('strand') != 1;
}
},
hideReverseStrand: {
desc: 'Hide reads aligned to the reverse strand',
func: function( f ) {
return f.get('strand') != -1;
}
},
hideUnsplicedReads: {
desc: 'Hide unspliced reads',
func: function ( f ) {
return f.get('cigar').indexOf("N") != -1;
}
}
});
},
_alignmentsFilterTrackMenuOptions: function() {
// add toggles for feature filters
var track = this;
return when( this._getNamedFeatureFilters() )
.then( function( filters ) {
return track._makeFeatureFilterTrackMenuItems(
[
'hideDuplicateReads',
'hideQCFailingReads',
'hideMissingMatepairs',
'hideSecondary',
'hideSupplementary',
'hideUnmapped',
'SEPARATOR',
'hideForwardStrand',
'hideReverseStrand',
'hideUnsplicedReads'
],
filters );
});
},
_renderTable: function( parentElement, track, feat, featDiv ) {
var thisB = this;
var mismatches = track._getMismatches(feat);
var seq = feat.get('seq');
if(!seq) {
var gContainer = dojo.create('div', {
className: 'renderTable',
innerHTML: '<h2 class="sectiontitle">Matches</h2><div style=\"font-family: Courier; white-space: pre;\">'
+'No sequence on feature, cannot render alignment</div>'
}, parentElement );
return;
}
var start = feat.get('start');
var query_str = '', align_str = '', refer_str = '';
var curr_mismatch = 0;
var genome_pos = 0;
var curr_pos = 0;
mismatches.sort(function(a,b) {
return a.start - b.start;
});
for(var i = 0; curr_pos < seq.length; i++) {
var f = false;
var mismatchesAtCurrentPosition = [];
for(var j = curr_mismatch; j < mismatches.length; j++) {
var mismatch = mismatches[j];
if(genome_pos == mismatch.start) {
mismatchesAtCurrentPosition.push(mismatch);
}
}
mismatchesAtCurrentPosition.sort(function(a,b) {
if(a.type == "insertion") return -1;
else if(a.type == "deletion") return 1;
else if(a.type == "mismatch") return 1;
else if(a.type == "skip") return 1;
else return 0;
});
for(var k = 0; k < mismatchesAtCurrentPosition.length; k++) {
var mismatch = mismatchesAtCurrentPosition[k];
curr_mismatch++;
if(mismatch.type == "softclip") {
for(var l = 0; l < mismatch.cliplen; l++) {
query_str += seq[curr_pos + l];
align_str += ' ';
refer_str += '.';
}
curr_pos += mismatch.cliplen;
f = true;
}
else if(mismatch.type == "insertion") {
for(var l = 0; l < +mismatch.base; l++) {
query_str += seq[curr_pos + l];
align_str += ' ';
refer_str += '-';
}
curr_pos += +mismatch.base||mismatch.base.length;
f = true;
}
else if(mismatch.type == "deletion") {
for(var l = 0; l < mismatch.length; l++) {
query_str += '-';
align_str += ' ';
refer_str += (mismatch.seq||{})[l] || ".";
}
genome_pos += mismatch.length;
f = true;
}
else if(mismatch.type == "skip") {
for(var l = 0; l < Math.min(mismatch.length, 10000); l++) {
query_str += '.';
align_str += ' ';
refer_str += 'N';
}
genome_pos += mismatch.length;
f = true;
}
else if(mismatch.type == "mismatch") {
query_str += mismatch.base;
align_str += ' ';
refer_str += mismatch.altbase;
curr_pos++;
genome_pos++;
f = true;
}
}
if(!f) {
query_str += seq[curr_pos];
align_str += '|';
refer_str += seq[curr_pos];
genome_pos++;
curr_pos++;
}
}
if(this.config.renderPrettyAlignment) {
var s1, s2, s3, ret_str;
s1 = s2 = s3 = ret_str ='';
var qpos = 0;
var rpos = (mismatches.length && mismatches[0].type == 'softclip') ? (start-mismatches[0].cliplen) : start;
var w = this.config.renderAlignmentWidth || 50;
for(var i = 0; i < query_str.length; i += w) {
s1 = query_str.substring(i, i+w);
s2 = align_str.substring(i, i+w);
s3 = refer_str.substring(i, i+w);
var padding = (rpos).toString().replace(/./g," ");
var offset1 = s1.length - (s1.match(/[-N\.]/g) || []).length;
var offset2 = s3.length - (s3.match(/[-]/g) || []).length
ret_str += 'Query ' + this.pad(padding, qpos, true) + ': ' + s1 + ' ' + (qpos + offset1) + '<br>';
ret_str += ' ' + padding + ' ' + s2+' <br>';
ret_str += 'Ref: ' + (rpos) + ': ' + s3 + ' ' + (rpos + offset2) + ' <br><br>';
qpos += offset1;
rpos += offset2;
}
var gContainer = dojo.create('div', {
className: 'renderTable',
innerHTML: '<h2 class="sectiontitle">Matches</h2><div style=\"font-family: Courier; white-space: pre;\">'
+ret_str+'</div>'
}, parentElement );
} else if(this.config.renderAlignment) {
var gContainer = dojo.create('div', {
className: 'renderTable',
innerHTML: '<h2 class="sectiontitle">Matches</h2><div style=\"font-family: Courier; white-space: pre;\">'
+'Query: '+query_str+' <br>'
+' '+align_str+' <br>'
+'Ref: '+refer_str+' </div>'
}, parentElement );
}
return {
val1: query_str,
val2: align_str,
val3: refer_str
};
},
//stackoverflow http://stackoverflow.com/questions/2686855/is-there-a-javascript-function-that-can-pad-a-string-to-get-to-a-determined-leng
pad: function(pad, str, padLeft) {
if (typeof str === 'undefined')
return pad;
if (padLeft) {
return (pad + str).slice(-pad.length);
} else {
return (str + pad).substring(0, pad.length);
}
}
});
});