sequence-viewer
Version:
A protein sequence viewer written in javascript
439 lines (392 loc) • 20.7 kB
JavaScript
var Sequence = (function () {
function Sequence(sequence, isoformName) {
var self = this;
this.events = {
MOUSE_SELECTION_EVENT: "sequence-viewer-mouse-selection",
SEQUENCE_SELECTED_EVENT: "sequence-viewer-substring-selected"
};
var isoName;
if (isoformName !== undefined) isoName = isoformName;
else isoName = "";
console.log(isoName);
var sequence = sequence;
var seqInit = "";
var lineJump = 0;
var title;
var divID;
var sequenceOptions;
var el;
var seqHeight;
var showBadge;
this.render = function (divId, options) {
//identifier should be a string in other cases we might get strange effects
if (typeof divId !== 'string') {
throw new Error("Div identifier must be a string");
}
divID = divId;
el = document.getElementById(divId.substring(1));
if (el === null) {
throw new Error("Cannot find element with id: " + divId.substring(1));
}
if (typeof options === 'undefined') {
var options = {
'showLineNumbers': true,
'wrapAminoAcids': true,
'charsPerLine': 30,
'search': false,
'toolbar': false,
'title': "Protein Sequence",
'sequenceMaxHeight': "400px",
'badge': true
}
}
else sequenceOptions = options;
(typeof options.charsPerLine === 'undefined') ? lineJump = 30 : lineJump = options.charsPerLine;
(typeof options.title === 'undefined') ? title = "Protein Sequence" : title = options.title;
(typeof options.sequenceMaxHeight === 'undefined') ? seqHeight = "400px" : seqHeight = options.sequenceMaxHeight;
(typeof options.badge === 'undefined') ? showBadge = true : showBadge = options.badge;
var badge = "<div style=\"display:inline-block;\">" +
"<span class=\"badge\" style=\"border-radius:70%;border: 2px solid black;color:#C50063;padding:8px 5px;background-color:white;margin-right:10px;vertical-align:middle;\">{{sequenceLength}}</span>" +
"</div>";
var displayBadge = showBadge ? badge : "";
var sources = "<div class=\"sequenceHeader row\" style=\"border-bottom: 1px solid #E7EAEC;padding-bottom:5px;margin:0px 0px 15px\">" +
displayBadge + "<h4 style=\"display:inline-block;vertical-align:middle;\">" + title + "</h4>" +
"</div>" +
"<div class=\"sequenceBody\" style=\"margin-top: 5px;\">" +
"<div class=\"scroller\" style=\"max-height:" + seqHeight + ";overflow:auto;white-space: nowrap;padding-right:20px;margin-right:10px;s\">" +
"<div class=\"charNumbers\" style=\"font-family: monospace;font-size: 13px;display:inline-block;text-align:right; padding-right:5px; border-right:1px solid LightGray;\"></div>" +
"<div id='teste' class=\"fastaSeq\" display-option=\"" + lineJump + "\" style=\"font-family: monospace;font-size: 13px;display:inline-block;padding:5px;\">{{{sequence}}}</div></div>" +
"<div class=\"coverageLegend\" style=\"margin-top: 10px;margin-left:15px;\"></div>" +
"</div>";
var template = Handlebars.compile(sources);
var html = template({
"sequence": sequence,
"sequenceLength": sequence.length
});
$(divId).html(html);
if (!(options.wrapAminoAcids === false)) {
sequenceLayout(divId + " .fastaSeq");
}
else $(divId + " .scroller").css("overflow-x", "auto");
if (!(options.showLineNumbers === false))
lineNumbers(divId + " .fastaSeq", divId + " .charNumbers");
if (options.search) {
addSequenceSearch();
}
if (options.toolbar) {
addToolbar();
if (isoName !== "") {
$(".sequenceToolbar").append(
"<a class=\"btn btn-default\" href=\"http://www.nextprot.org/db/entry/" + isoName.split("-")[0] + "/fasta?isoform=" + isoName.slice(3) + "\" style=\"margin-left:15px;\">view Fasta</a>" +
"<a class=\"btn btn-default disabled\" href=\"\" style=\"margin-left:15px;\">Blast sequence</a>" +
"<a class=\"btn btn-default disabled\" href=\"\" style=\"margin-left:15px;\">Blast selection</a>"
);
}
}
seqInit = $(divId + " .fastaSeq").html();
mouseSelectionListener();
};
this.selection = function (start, end, color, options) {
var positions = [start, end];
var hlSeq = seqInit;
subpartSelection([{start:start,end:end}]);
positions[0] = positions[0] + ~~(positions[0] / 10) + 4 * (~~(positions[0] / lineJump));
positions[1] = positions[1] + ~~(positions[1] / 10) + 4 * (~~(positions[1] / lineJump));
var highlightColor = color;
hlSeq = hlSeq.substring(0, positions[0]) +
"<span class='stringSelected' style=\"background:" + color + ";color:white;\">" +
hlSeq.substring(positions[0], positions[1]) +
"</span>" +
hlSeq.substring(positions[1], hlSeq.length);
$(divID + " .fastaSeq").html(hlSeq);
};
function multiHighlighting(ArrayHL, color, options) {
var startTime2 = new Date().getTime();
if (ArrayHL.length === 0) {
$(divID + " .fastaSeq").html(seqInit);
}
var hlSeq = seqInit;
var seqTemp = hlSeq.toString();
var positionStart = 0;
var positionEnd = 0;
for (i in ArrayHL) {
positionStart = jTranslation(ArrayHL[i].start);
positionEnd = jTranslation(ArrayHL[i].end);
seqTemp = seqTemp.substring(0, positionStart) +
"<span class='stringsSelected' style=\"background:" + color + ";color:white;\">" +
seqTemp.substring(positionStart, positionEnd) +
"</span>" +
seqTemp.substring(positionEnd, seqTemp.length);
}
$(divID + " .fastaSeq").html(seqTemp);
}
this.addLegend = function (hashLegend) {
for (var i = 0; i < hashLegend.length; i++) {
if (hashLegend[i].underscore === true) {
$(divID + " .coverageLegend").append("<div style=\"display:inline-block;background:" + hashLegend[i].color + ";width:20px;height:20px;vertical-align:middle;margin:0px 5px 0px 10px;border-radius:50%; border: 1px solid grey;text-align:center; line-height:0.8;\">_</div><p style=\"display:inline-block;font-weight:bold;font-size:11px;font-style:italic;margin:0;padding-top:3px;vertical-align:top;\">" + hashLegend[i].name + "</p></div>");
}
else {
$(divID + " .coverageLegend").append("<div style=\"display:inline-block;background:" + hashLegend[i].color + ";width:20px;height:20px;vertical-align:middle;margin:0px 5px 0px 10px;border-radius:50%;\"></div><p style=\"display:inline-block;font-weight:bold;font-size:11px;font-style:italic;margin:0;padding-top:3px;vertical-align:top;\">" + hashLegend[i].name + "</p>");
}
}
};
function jTranslation(i) {
var j = i + ~~(i / 10) + 4 * (~~(i / lineJump));
return j;
}
function fillGap(list) {
var listCloned = list.slice();
for (var i=0;i<=list.length-1;i++) {
if (i===0){
if (list[0].start > 0) {
listCloned.unshift({start: 0, end: list[0].start, color: "black", underscore: false});
}
if (i === list.length-1) {
listCloned.push({start: list[i].end, end: sequence.length, color: "black", underscore: false});
}
}
else if (i === list.length-1){
if (list[i-1].end < list[i].start){
listCloned.push({start: list[i-1].end, end: list[i].start, color: "black", underscore: false});
}
if (list[i].end < sequence.length-1){
listCloned.push({start: list[i].end, end: sequence.length, color: "black", underscore: false});
}
}
else {
if (list[i-1].end < list[i].start){
listCloned.push({start: list[i-1].end, end: list[i].start, color: "black", underscore: false});
}
}
if (i !== 0 && list[i-1].end > list[i].start){
console.warn("WARNING (error): Some positions in the coverage list are overlapping");
}
}
listCloned.sort(function (a, b) {
return a.start - b.start;
});
return listCloned;
}
this.coverage = function (HashAA, start, end, highlightColor) {
HashAA.sort(function (a, b) {
return a.start - b.start;
});
HashAA=fillGap(HashAA);
console.log(HashAA);
var timestart = new Date().getTime();
if (!start) var start = 0;
if (!end) var end = 0;
if (!highlightColor) var highlightColor = "#FFE5A3";
var source = "";
var pre = "";
for (var i = 0; i < HashAA.length; i++) {
var tooltipStr = "";
if (HashAA[i].tooltip !== undefined) {
tooltipStr = " title=\""+HashAA[i].tooltip+"\"";
}
var bgcolorStr = "";
if (HashAA[i].bgcolor !== undefined) {
bgcolorStr = "background:"+HashAA[i].bgcolor+";";
}
//in html string there is no easy way to put function, therefore
//assign to onclick event handler identifier of the region,
//and after all DOM objects will be created we will replace this handlers
//with proper functions
var onclickStr = "";
var cursorStr = "";
if (HashAA[i].onclick !== undefined) {
onclickStr = " onclick=\" return "+i+";\" ";
cursorStr = "cursor: pointer;";
}
if (HashAA[i].underscore) {
pre = "<span style=\"text-decoration:underline;color:" + HashAA[i].color + ";" + bgcolorStr + cursorStr+ "\"" + tooltipStr + onclickStr + ">";
source += pre;
}
else {
pre = "<span style=\"color:" + HashAA[i].color + ";" + bgcolorStr + cursorStr + "\"" + tooltipStr + onclickStr + ">";
source += pre;
}
if (end) {
if (start >= HashAA[i].start && start < HashAA[i].end && end >= HashAA[i].start && end < HashAA[i].end) {
source += seqInit.substring(jTranslation(HashAA[i].start), jTranslation(start)) + "<span class=\"peptideHighlighted\" style=\"background:" + highlightColor + ";\">" + seqInit.substring(jTranslation(start), jTranslation(end + 1));
source += "</span>" + seqInit.substring(jTranslation(end + 1), jTranslation(HashAA[i].end)) + "</span>";
}
else if (start >= HashAA[i].start && start < HashAA[i].end) {
source += seqInit.substring(jTranslation(HashAA[i].start), jTranslation(start)) + "</span><span class=\"peptideHighlighted\" style=\"background:" + highlightColor + ";\">" + pre + seqInit.substring(jTranslation(start), jTranslation(HashAA[i].end)) + "</span>";
}
else if (end >= HashAA[i].start && end < HashAA[i].end) {
source += seqInit.substring(jTranslation(HashAA[i].start), jTranslation(end + 1)) + "</span></span>" + pre + seqInit.substring(jTranslation(end + 1), jTranslation(HashAA[i].end)) + "</span>";
}
else {
source += seqInit.substring(jTranslation(HashAA[i].start), jTranslation(HashAA[i].end)) + "</span>";
}
}
else {
source += seqInit.substring(jTranslation(HashAA[i].start), jTranslation(HashAA[i].end)) + "</span>";
}
}
var fastaDiv = $(divID + " .fastaSeq");
fastaDiv.html(source);
//and now (after we created dom objects) we can assign onclick events
var onclickObjects = $("[onclick]",fastaDiv);
for (var i=0;i<onclickObjects.length;i++) {
//get the id that was assigned to onclick event in the simple html
var onclickId = onclickObjects[i].onclick();
//and replace it with proper function
onclickObjects[i].onclick = HashAA[onclickId].onclick;
}
};
function lineNumbers(textAreaID, lineNumberID) {
var textContent = $(textAreaID).html().split("<br>");
var NBC = parseInt($(textAreaID).attr("display-option"));
var newTextContent = [];
var charPerLine = 0;
for (var i = 0; i < textContent.length; i++) {
newTextContent.push((charPerLine + 1) + ("<br>"));
charPerLine += NBC;
}
$(lineNumberID).html(newTextContent.join(""));
};
function sequenceLayout(textAreaID) {
var newLines = parseInt($(textAreaID).attr("display-option"));
newLines = (newLines + (newLines / 10)).toString();
var seqFormat = $(textAreaID).html();
seqFormat = seqFormat.toString().match(/.{1,10}/g).join(' ').match(new RegExp('.{1,' + newLines + '}', 'g')).join('<br>');
$(textAreaID).html(seqFormat);
}
function addSequenceSearch() {
$(divID + " .sequenceHeader").append('<input class=\"inputSearchSeq form-control pull-right\" type=\"text\" style=\"width:40%;margin-top:3px;\" placeholder=\"Search in sequence.. (Regex supported)\">');
sequenceSearch();
}
function sequenceSearch() {
$(divID + " .inputSearchSeq").keyup(function () {
var text = $(this).val();
if (text !== "") {
var text2 = new RegExp(text, "gi");
var match;
var matches = [];
while ((match = text2.exec(sequence)) != null) {
matches.push({start: match.index, end: match.index + match[0].length});
}
matches.sort(function (a, b) {
return b.start - a.start;
});
subpartSelection(matches);
multiHighlighting(matches, "#C50063");
}
else {
$(divID + " .fastaSeq").html(seqInit);
}
});
}
function subpartSelection(list) {
var selectedParts = getSequenceOfSelection(list, sequence);
if (selectedParts) triggerSequenceSelectedEvent(selectedParts);
}
function getSequenceOfSelection(selection, sequence) {
var selectionList = [];
if (selection.length)
{
selection.forEach(function (s) {
selectionList.push({
start: s.start + 1,
end: s.end,
sequence: sequence.substring(s.start, s.end)
});
});
}
return selectionList;
}
function triggerSequenceSelectedEvent(selection) {
if (CustomEvent) {
var event = new CustomEvent(self.events.SEQUENCE_SELECTED_EVENT, {
detail: selection
});
el.dispatchEvent(event);
} else {
console.warn("CustomEvent is not defined....");
}
if (self.trigger) self.trigger(self.events.SEQUENCE_SELECTED_EVENT, selection);
}
function mouseSelectionListener() {
$(divID + " .fastaSeq").mouseup(function () {
var selectedSubpart = getSelectedText();
if (selectedSubpart) triggerMouseSelectionEvent(selectedSubpart);
});
}
this.onMouseSelection = function (listener) {
el.addEventListener(self.events.MOUSE_SELECTION_EVENT, listener);
//$(document).on(self.events.FEATURE_SELECTED_EVENT, listener);
};
this.onSubpartSelected = function (listener) {
el.addEventListener(self.events.SEQUENCE_SELECTED_EVENT, listener);
//$(document).on(self.events.FEATURE_SELECTED_EVENT, listener);
};
function getSelectedText() {
var text = window.getSelection().toString().replace(/\s+/g, '');
var selection = window.getSelection();
var element = $(divID + " .fastaSeq")[0];
var caretOffset = 0;
var seqText = $(divID + " .fastaSeq").text();
var range = selection.getRangeAt(0);
var preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
var pos_end_parsed = seqText.substring(0,caretOffset).replace(/\s+/g, '').length;
var pos_start_parsed = pos_end_parsed - (text.length -1);
var text_selected = {
text: text,
start: pos_start_parsed,
end: pos_end_parsed
}
return text_selected;
}
function triggerMouseSelectionEvent(subseq) {
if (CustomEvent) {
var event = new CustomEvent(self.events.MOUSE_SELECTION_EVENT, {
detail: {
'selection': subseq.text,
'start':subseq.start,
'end':subseq.end
}
});
el.dispatchEvent(event);
} else {
console.warn("CustomEvent is not defined....");
}
if (self.trigger) self.trigger(self.events.MOUSE_SELECTION_EVENT, {'selection': subseq});
}
function changeCharsPerLine(selection) {
var options = sequenceOptions;
options.charsPerLine = selection.value;
self.render(divID, options);
}
function addToolbar() {
var listOfCharsPerLine = ["50", "60", "70", "80", "90", "100"];
var source = "<form class=\"form-inline\" role=\"form\">" +
"<div class=\"sequenceToolbar row\" style=\"margin-bottom:15px;\">" +
"<div class=\"input-group\" style=\"margin-left:20px;\"> <span class=\"input-group-addon\">Char per line</i></span>" +
"<select class=\"CPLChoice form-control\" style=\"border-top-left-radius: 0px;border-bottom-left-radius: 0px;\">" +
"<option>Select</option>" +
"{{#each CPL}}<option value={{this}}>{{this}}</option>{{/each}}" +
"</select>" +
"</div>" +
"</div>" +
"</form>";
var template = Handlebars.compile(source);
var html = template({
"CPL": listOfCharsPerLine
});
$(divID + " .sequenceBody").prepend(html);
$(divID + " .CPLChoice").change(function () {
changeCharsPerLine(this);
$(divID + " .CPLChoice" + " option:selected").text($(this).val());
});
}
}
return Sequence;
})();
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = Sequence;
}