ideogram
Version:
Chromosome visualization with D3.js
1,649 lines (1,302 loc) • 83.6 kB
JavaScript
// Developed by Eric Weitz (https://github.com/eweitz)
// https://github.com/stefanpenner/es6-promise
(function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){G=t}function r(t){Q=t}function o(){return function(){process.nextTick(a)}}function i(){return function(){B(a)}}function s(){var t=0,e=new X(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){t.port2.postMessage(0)}}function c(){return function(){setTimeout(a,1)}}function a(){for(var t=0;J>t;t+=2){var e=tt[t],n=tt[t+1];e(n),tt[t]=void 0,tt[t+1]=void 0}J=0}function f(){try{var t=require,e=t("vertx");return B=e.runOnLoop||e.runOnContext,i()}catch(n){return c()}}function l(t,e){var n=this,r=new this.constructor(p);void 0===r[rt]&&k(r);var o=n._state;if(o){var i=arguments[o-1];Q(function(){x(o,r,i,n._result)})}else E(n,r,t,e);return r}function h(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(p);return g(n,t),n}function p(){}function _(){return new TypeError("You cannot resolve a promise with itself")}function d(){return new TypeError("A promises callback cannot return that same promise.")}function v(t){try{return t.then}catch(e){return ut.error=e,ut}}function y(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function m(t,e,n){Q(function(t){var r=!1,o=y(n,e,function(n){r||(r=!0,e!==n?g(t,n):S(t,n))},function(e){r||(r=!0,j(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,j(t,o))},t)}function b(t,e){e._state===it?S(t,e._result):e._state===st?j(t,e._result):E(e,void 0,function(e){g(t,e)},function(e){j(t,e)})}function w(t,n,r){n.constructor===t.constructor&&r===et&&constructor.resolve===nt?b(t,n):r===ut?j(t,ut.error):void 0===r?S(t,n):e(r)?m(t,n,r):S(t,n)}function g(e,n){e===n?j(e,_()):t(n)?w(e,n,v(n)):S(e,n)}function A(t){t._onerror&&t._onerror(t._result),T(t)}function S(t,e){t._state===ot&&(t._result=e,t._state=it,0!==t._subscribers.length&&Q(T,t))}function j(t,e){t._state===ot&&(t._state=st,t._result=e,Q(A,t))}function E(t,e,n,r){var o=t._subscribers,i=o.length;t._onerror=null,o[i]=e,o[i+it]=n,o[i+st]=r,0===i&&t._state&&Q(T,t)}function T(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,o,i=t._result,s=0;s<e.length;s+=3)r=e[s],o=e[s+n],r?x(n,r,o,i):o(i);t._subscribers.length=0}}function M(){this.error=null}function P(t,e){try{return t(e)}catch(n){return ct.error=n,ct}}function x(t,n,r,o){var i,s,u,c,a=e(r);if(a){if(i=P(r,o),i===ct?(c=!0,s=i.error,i=null):u=!0,n===i)return void j(n,d())}else i=o,u=!0;n._state!==ot||(a&&u?g(n,i):c?j(n,s):t===it?S(n,i):t===st&&j(n,i))}function C(t,e){try{e(function(e){g(t,e)},function(e){j(t,e)})}catch(n){j(t,n)}}function O(){return at++}function k(t){t[rt]=at++,t._state=void 0,t._result=void 0,t._subscribers=[]}function Y(t){return new _t(this,t).promise}function q(t){var e=this;return new e(I(t)?function(n,r){for(var o=t.length,i=0;o>i;i++)e.resolve(t[i]).then(n,r)}:function(t,e){e(new TypeError("You must pass an array to race."))})}function F(t){var e=this,n=new e(p);return j(n,t),n}function D(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function K(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function L(t){this[rt]=O(),this._result=this._state=void 0,this._subscribers=[],p!==t&&("function"!=typeof t&&D(),this instanceof L?C(this,t):K())}function N(t,e){this._instanceConstructor=t,this.promise=new t(p),this.promise[rt]||k(this.promise),I(e)?(this._input=e,this.length=e.length,this._remaining=e.length,this._result=new Array(this.length),0===this.length?S(this.promise,this._result):(this.length=this.length||0,this._enumerate(),0===this._remaining&&S(this.promise,this._result))):j(this.promise,U())}function U(){return new Error("Array Methods must be provided an Array")}function W(){var t;if("undefined"!=typeof global)t=global;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=pt)}var z;z=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var B,G,H,I=z,J=0,Q=function(t,e){tt[J]=t,tt[J+1]=e,J+=2,2===J&&(G?G(a):H())},R="undefined"!=typeof window?window:void 0,V=R||{},X=V.MutationObserver||V.WebKitMutationObserver,Z="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),$="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,tt=new Array(1e3);H=Z?o():X?s():$?u():void 0===R&&"function"==typeof require?f():c();var et=l,nt=h,rt=Math.random().toString(36).substring(16),ot=void 0,it=1,st=2,ut=new M,ct=new M,at=0,ft=Y,lt=q,ht=F,pt=L;L.all=ft,L.race=lt,L.resolve=nt,L.reject=ht,L._setScheduler=n,L._setAsap=r,L._asap=Q,L.prototype={constructor:L,then:et,"catch":function(t){return this.then(null,t)}};var _t=N;N.prototype._enumerate=function(){for(var t=this.length,e=this._input,n=0;this._state===ot&&t>n;n++)this._eachEntry(e[n],n)},N.prototype._eachEntry=function(t,e){var n=this._instanceConstructor,r=n.resolve;if(r===nt){var o=v(t);if(o===et&&t._state!==ot)this._settledAt(t._state,e,t._result);else if("function"!=typeof o)this._remaining--,this._result[e]=t;else if(n===pt){var i=new n(p);w(i,t,o),this._willSettleAt(i,e)}else this._willSettleAt(new n(function(e){e(t)}),e)}else this._willSettleAt(r(t),e)},N.prototype._settledAt=function(t,e,n){var r=this.promise;r._state===ot&&(this._remaining--,t===st?j(r,n):this._result[e]=n),0===this._remaining&&S(r,this._result)},N.prototype._willSettleAt=function(t,e){var n=this;E(t,void 0,function(t){n._settledAt(it,e,t)},function(t){n._settledAt(st,e,t)})};var dt=W,vt={Promise:pt,polyfill:dt};"function"==typeof define&&define.amd?define(function(){return vt}):"undefined"!=typeof module&&module.exports?module.exports=vt:"undefined"!=typeof this&&(this.ES6Promise=vt),dt()}).call(this);
// https://github.com/kristw/d3.promise
!function(a,b){"function"==typeof define&&define.amd?define(["d3"],b):"object"==typeof exports?module.exports=b(require("d3")):a.d3.promise=b(a.d3)}(this,function(a){var b=function(){function b(a,b){return function(){var c=Array.prototype.slice.call(arguments);return new Promise(function(d,e){var f=function(a,b){return a?void e(Error(a)):void d(b)};b.apply(a,c.concat(f))})}}var c={};return["csv","tsv","json","xml","text","html"].forEach(function(d){c[d]=b(a,a[d])}),c}();return a.promise=b,b});
// https://github.com/overset/javascript-natural-sort
function naturalSort(a,b){var q,r,c=/(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g,d=/^\s+|\s+$/g,e=/\s+/g,f=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,g=/^0x[0-9a-f]+$/i,h=/^0/,i=function(a){return(naturalSort.insensitive&&(""+a).toLowerCase()||""+a).replace(d,"")},j=i(a),k=i(b),l=j.replace(c,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),m=k.replace(c,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0"),n=parseInt(j.match(g),16)||1!==l.length&&Date.parse(j),o=parseInt(k.match(g),16)||n&&k.match(f)&&Date.parse(k)||null,p=function(a,b){return(!a.match(h)||1==b)&&parseFloat(a)||a.replace(e," ").replace(d,"")||0};if(o){if(n<o)return-1;if(n>o)return 1}for(var s=0,t=l.length,u=m.length,v=Math.max(t,u);s<v;s++){if(q=p(l[s]||"",t),r=p(m[s]||"",u),isNaN(q)!==isNaN(r))return isNaN(q)?1:-1;if(/[^\x00-\x80]/.test(q+r)&&q.localeCompare){var w=q.localeCompare(r);return w/Math.abs(w)}if(q<r)return-1;if(q>r)return 1}}
/* Constructs a prototypal Ideogram class */
var Ideogram = function(config) {
// Clone the config object, to allow multiple instantiations
// without picking up prior ideogram's settings
this.config = JSON.parse(JSON.stringify(config));
this.debug = false;
if (!this.config.bandDir) {
this.config.bandDir = "../data/bands/";
}
if (!this.config.container) {
this.config.container = "body";
}
if (!this.config.resolution) {
this.config.resolution = 850;
}
if ("showChromosomeLabels" in this.config === false) {
this.config.showChromosomeLabels = true;
}
if (!this.config.chrMargin) {
this.config.chrMargin = 10;
}
if (!this.config.orientation) {
var orientation = "vertical";
this.config.orientation = orientation;
}
if (!this.config.chrHeight) {
var chrHeight,
container = this.config.container,
rect = document.querySelector(container).getBoundingClientRect();
if (orientation === "vertical") {
chrHeight = rect.height;
} else {
chrHeight = rect.width;
}
if (container == "body") {
chrHeight = 500;
}
this.config.chrHeight = chrHeight;
}
if (!this.config.chrWidth) {
var chrWidth = 10,
chrHeight = this.config.chrHeight;
if (900 > chrHeight && chrHeight > 500) {
chrWidth = Math.round(chrHeight / 40);
} else if (chrHeight >= 900) {
chrWidth = Math.round(chrHeight / 45);
}
this.config.chrWidth = chrWidth;
}
if (!this.config.showBandLabels) {
this.config.showBandLabels = false;
}
if (!this.config.brush) {
this.config.brush = false;
}
if (!this.config.rows) {
this.config.rows = 1;
}
this.bump = Math.round(this.config.chrHeight / 125);
this.adjustedBump = false;
if (this.config.chrHeight < 200) {
this.adjustedBump = true;
this.bump = 4;
}
if (config.showBandLabels) {
this.config.chrMargin += 20;
}
if (config.chromosome) {
this.config.chromosomes = [config.chromosome];
if ("showBandLabels" in config === false) {
this.config.showBandLabels = true;
}
if ("rotatable" in config === false) {
this.config.rotatable = false;
}
}
if (!this.config.showNonNuclearChromosomes) {
this.config.showNonNuclearChromosomes = false;
}
this.initAnnotSettings();
this.config.chrMargin = (
this.config.chrMargin +
this.config.chrWidth +
this.config.annotTracksHeight * 2
);
if (config.onLoad) {
this.onLoadCallback = config.onLoad;
}
if (config.onDrawAnnots) {
this.onDrawAnnotsCallback = config.onDrawAnnots;
}
if (config.onBrushMove) {
this.onBrushMoveCallback = config.onBrushMove;
}
this.coordinateSystem = "iscn";
this.maxLength = {
"bp": 0,
"iscn": 0
};
// The E-Utilies In Depth: Parameters, Syntax and More:
// https://www.ncbi.nlm.nih.gov/books/NBK25499/
var eutils = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/";
this.esearch = eutils + "esearch.fcgi?retmode=json";
this.esummary = eutils + "esummary.fcgi?retmode=json";
this.elink = eutils + "elink.fcgi?retmode=json";
this.organisms = {
"9606": {
"commonName": "Human",
"scientificName": "Homo sapiens",
"scientificNameAbbr": "H. sapiens",
"assemblies": { // technically, primary assembly unit of assembly
"default": "GCF_000001305.14", // GRCh38
"GRCh38": "GCF_000001305.14",
"GRCh37": "GCF_000001305.13",
}
},
"10090": {
"commonName": "Mouse",
"scientificName": "Mus musculus",
"scientificNameAbbr": "M. musculus",
"assemblies": {
"default": "GCF_000000055.19"
}
},
"7227": {
"commonName": "Fly",
"scientificName": "Drosophlia melanogaster",
"scientificNameAbbr": "D. melanogaster"
}
};
// A flat array of chromosomes
// (this.chromosomes is an object of
// arrays of chromosomes, keyed by organism)
this.chromosomesArray = [];
this.bandsToShow = [];
this.chromosomes = {};
this.numChromosomes = 0;
this.bandData = {};
this.init();
};
/**
* Gets chromosome band data from a
* TSV file, or, if band data is prefetched, from an array
*
* UCSC: #chrom chromStart chromEnd name gieStain
* http://genome.ucsc.edu/cgi-bin/hgTables
* - group: Mapping and Sequencing
* - track: Chromosome Band (Ideogram)
*
* NCBI: #chromosome arm band iscn_start iscn_stop bp_start bp_stop stain density
* ftp://ftp.ncbi.nlm.nih.gov/pub/gdp/ideogram_9606_GCF_000001305.14_550_V1
*/
Ideogram.prototype.getBands = function(content, taxid, chromosomes) {
var lines = {},
delimiter, tsvLines, columns, line, stain, chr,
i, prefetched, init, tsvLinesLength, source,
start, stop, firstColumn;
if (content.slice(0, 8) === "chrBands") {
source = "native";
}
if (typeof chrBands === "undefined" && source !== "native") {
delimiter = /\t/;
tsvLines = content.split(/\r\n|\n/);
init = 1;
} else {
delimiter = / /;
if (source === "native") {
tsvLines = eval(content);
} else {
tsvLines = content;
}
init = 0;
}
firstColumn = tsvLines[0].split(delimiter)[0];
if (firstColumn == '#chromosome') {
source = 'ncbi';
} else if (firstColumn == '#chrom'){
source = 'ucsc';
} else {
source = 'native';
}
tsvLinesLength = tsvLines.length;
if (source === 'ncbi' || source === 'native') {
for (i = init; i < tsvLinesLength; i++) {
columns = tsvLines[i].split(delimiter);
chr = columns[0];
if (
// If a specific set of chromosomes has been requested, and
// the current chromosome
typeof(chromosomes) !== "undefined" &&
chromosomes.indexOf(chr) === -1
) {
continue;
}
if (chr in lines === false) {
lines[chr] = [];
}
stain = columns[7];
if (columns[8]) {
// For e.g. acen and gvar, columns[8] (density) is undefined
stain += columns[8];
}
line = {
"chr": chr,
"bp": {
"start": parseInt(columns[5], 10),
"stop": parseInt(columns[6], 10)
},
"iscn": {
"start": parseInt(columns[3], 10),
"stop": parseInt(columns[4], 10)
},
"px": {
"start": -1,
"stop": -1,
"width": -1
},
"name": columns[1] + columns[2],
"stain": stain,
"taxid": taxid
};
lines[chr].push(line);
}
} else if (source === 'ucsc') {
for (i = init; i < tsvLinesLength; i++) {
// #chrom chromStart chromEnd name gieStain
// e.g. for fly:
// chr4 69508 108296 102A1 n/a
columns = tsvLines[i].split(delimiter);
if (columns[0] !== 'chr' + chromosomeName) {
continue;
}
stain = columns[4];
if (stain === 'n/a') {
stain = 'gpos100';
}
start = parseInt(columns[1], 10);
stop = parseInt(columns[2], 10);
line = {
"chr": columns[0].split('chr')[1],
"bp": {
"start": start,
"stop": stop
},
"iscn": {
"start": start,
"stop": stop
},
"px": {
"start": -1,
"stop": -1,
"width": -1
},
"name": columns[3],
"stain": stain,
"taxid": taxid
};
lines[chr].push(line);
}
}
return lines;
};
/**
* Fills cytogenetic arms -- p-arm and q-arm -- with specified colors
*/
Ideogram.prototype.colorArms = function(pArmColor, qArmColor) {
var ideo = this;
ideo.chromosomesArray.forEach(function(chr, chrIndex){
var bands = chr.bands,
pcen = bands[chr.pcenIndex],
qcen = bands[chr.pcenIndex + 1],
chrID = chr.id,
chrMargin = ideo.config.chrMargin * (chrIndex + 1),
chrWidth = ideo.config.chrWidth;
pcenStart = pcen.px.start;
qcenStop = qcen.px.stop;
d3.select("#" + chrID)
.append("line")
.attr("x1", pcenStart)
.attr("y1", chrMargin + 0.2)
.attr("x2", pcenStart)
.attr("y2", chrMargin + chrWidth - 0.2)
.style("stroke", pArmColor)
d3.select("#" + chrID)
.append("line")
.attr("x1", qcenStop)
.attr("y1", chrMargin + 0.2)
.attr("x2", qcenStop)
.attr("y2", chrMargin + chrWidth - 0.2)
.style("stroke", qArmColor)
d3.selectAll("#" + chrID + " .band")
.data(chr.bands)
.style("fill", function(d, i) {
if (i <= chr.pcenIndex) {
return pArmColor;
} else {
return qArmColor;
}
});
});
d3.selectAll(".p-ter.chromosomeBorder").style("fill", pArmColor);
d3.selectAll(".q-ter.chromosomeBorder").style("fill", qArmColor);
};
/**
* Generates a model object for each chromosome
* containing information on its name, DOM ID,
* length in base pairs or ISCN coordinates,
* cytogenetic bands, centromere position, etc.
*/
Ideogram.prototype.getChromosomeModel = function(bands, chromosome, taxid, chrIndex) {
var chr = {},
band, scale,
width, pxStop,
startType, stopType,
chrHeight = this.config.chrHeight,
maxLength = this.maxLength,
chrLength,
cs, hasBands;
cs = this.coordinateSystem;
hasBands = (typeof bands !== "undefined");
if (hasBands) {
chr["name"] = chromosome;
chr["length"] = bands[bands.length - 1][cs].stop;
chr["type"] = "nuclear";
} else {
chr = chromosome;
}
chr["chrIndex"] = chrIndex;
chr["id"] = "chr" + chr.name + "-" + taxid;
if (this.config.fullChromosomeLabels === true) {
var orgName = this.organisms[taxid].scientificNameAbbr;
chr["name"] = orgName + " chr" + chr.name;
}
chrLength = chr["length"];
pxStop = 0;
if (hasBands) {
for (var i = 0; i < bands.length; i++) {
band = bands[i];
width = chrHeight * chr["length"]/maxLength[cs] * (band[cs].stop - band[cs].start)/chrLength;
bands[i]["px"] = {"start": pxStop, "stop": pxStop + width, "width": width};
pxStop = bands[i].px.stop;
if (hasBands && band.stain === "acen" && band.name[0] === "p") {
chr["pcenIndex"] = i;
}
}
} else {
pxStop = chrHeight * chr["length"]/maxLength[cs];
}
chr["width"] = pxStop;
chr["scale"] = {};
// TODO:
//
// A chromosome-level scale property is likely
// nonsensical for any chromosomes that have cytogenetic band data.
// Different bands tend to have ratios between number of base pairs
// and physical length.
//
// However, a chromosome-level scale property is likely
// necessary for chromosomes that do not have band data.
//
// This needs further review.
if (this.config.multiorganism === true) {
chr["scale"].bp = 1;
//chr["scale"].bp = band.iscn.stop / band.bp.stop;
chr["scale"].iscn = chrHeight * chrLength/maxLength.bp;
} else {
chr["scale"].bp = chrHeight / maxLength.bp;
if (hasBands) {
chr["scale"].iscn = chrHeight / maxLength.iscn;
}
}
chr["bands"] = bands;
chr["centromerePosition"] = "";
if (hasBands && bands[0].bp.stop - bands[0].bp.start == 1) {
// As with mouse
chr["centromerePosition"] = "telocentric";
// Remove placeholder pter band
chr["bands"] = chr["bands"].slice(1);
}
return chr;
};
/**
* Draws labels for each chromosome, e.g. "1", "2", "X".
* If ideogram configuration has 'fullChromosomeLabels: True',
* then labels includes name of taxon, which can help when
* depicting orthologs.
*/
Ideogram.prototype.drawChromosomeLabels = function(chromosomes) {
var i, chr, chrs, taxid, ideo,
chrMargin2,
ideo = this,
chrMargin = ideo.config.chrMargin,
chrWidth = ideo.config.chrWidth;
chrs = ideo.chromosomesArray;
chrMargin2 = chrWidth/2 + chrMargin - 8;
if (ideo.config.orientation === "vertical" && ideo.config.showBandLabels === true) {
chrMargin2 = chrMargin + 17;
}
if (ideo.config.orientation === "vertical") {
d3.selectAll(".chromosome")
.append("text")
.data(chrs)
.attr("class", "chrLabel")
.attr("transform", "rotate(-90)")
.attr("y", -16)
.each(function (d, i) {
var i, chrMarginI, x, cls;
var arr = d.name.split(" ");
var lines = [];
if (arr != undefined) {
lines.push(arr.slice(0, arr.length - 1).join(" "));
lines.push(arr[arr.length - 1]);
if (!ideo.config.showBandLabels) {
i += 1;
}
chrMarginI = chrMargin * i;
x = -(chrMarginI + chrMargin2 - chrWidth - 2) + ideo.config.annotTracksHeight * 2;
for (var i = 0; i < lines.length; i++) {
cls = "";
if (i == 0 && ideo.config.fullChromosomeLabels) {
cls = "italic";
}
d3.select(this).append("tspan")
.text(lines[i])
.attr("dy", i ? "1.2em" : 0)
.attr("x", x)
.attr("text-anchor", "middle")
.attr("class", cls);
}
}
});
} else {
d3.selectAll(".chromosome")
.append("text")
.data(chrs)
.attr("class", "chrLabel")
.attr("x", -5)
.each(function (d, i) {
var i, chrMarginI, y, cls;
var arr = d.name.split(" ");
var lines = [];
if (arr != undefined) {
lines.push(arr.slice(0, arr.length - 1).join(" "));
lines.push(arr[arr.length - 1]);
chrMarginI = chrMargin * i;
y = (chrMarginI + chrMargin2);
for (var i = 0; i < lines.length; i++) {
cls = "";
if (i == 0 && ideo.config.fullChromosomeLabels) {
cls = "italic";
}
d3.select(this).append("tspan")
.text(lines[i])
.attr("dy", i ? "1.2em" : 0)
.attr("y", y)
.attr("x", -8)
.attr("text-anchor", "middle")
.attr("class", cls);
}
}
});
}
};
/**
* Draws labels and stalks for cytogenetic bands.
*
* Band labels are text like "p11.11".
* Stalks are small lines that visually connect labels to their bands.
*/
Ideogram.prototype.drawBandLabels = function(chromosomes) {
var i, chr, chrs, taxid, ideo,
chrMargin2, chrModel;
ideo = this;
chrs = [];
for (taxid in chromosomes) {
for (chr in chromosomes[taxid]) {
chrs.push(chromosomes[taxid][chr]);
}
}
var textOffsets = {};
chrIndex = 0;
for (var i = 0; i < chrs.length; i++) {
chrIndex += 1;
chrModel = chrs[i];
chr = d3.select("#" + chrModel.id);
var chrMargin = this.config.chrMargin * chrIndex,
lineY1, lineY2,
ideo = this;
lineY1 = chrMargin;
lineY2 = chrMargin - 8;
if (
chrIndex == 1 &&
"perspective" in this.config && this.config.perspective == "comparative"
) {
lineY1 += 18;
lineY2 += 18;
}
textOffsets[chrModel.id] = [];
chr.selectAll("text")
.data(chrModel.bands)
.enter()
.append("g")
.attr("class", function(d, i) { return "bandLabel bsbsl-" + i; })
.attr("transform", function(d) {
var x, y;
x = ideo.round(-8 + d.px.start + d.px.width/2);
textOffsets[chrModel.id].push(x + 13);
y = chrMargin - 10;
return "translate(" + x + "," + y + ")";
})
.append("text")
.text(function(d) { return d.name; });
chr.selectAll("line.bandLabelStalk")
.data(chrModel.bands)
.enter()
.append("g")
.attr("class", function(d, i) { return "bandLabelStalk bsbsl-" + i; })
.attr("transform", function(d) {
var x = ideo.round(d.px.start + d.px.width/2);
return "translate(" + x + ", " + lineY1 + ")";
})
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", -8);
}
for (var i = 0; i < chrs.length; i++) {
chrModel = chrs[i];
var textsLength = textOffsets[chrModel.id].length,
overlappingLabelXRight,
index,
indexesToShow = [],
prevHiddenBoxIndex,
prevTextBox,
xLeft,
prevLabelXRight,
textPadding;
overlappingLabelXRight = 0;
textPadding = 5;
for (index = 0; index < textsLength; index++) {
// Ensures band labels don't overlap
xLeft = textOffsets[chrModel.id][index];
if (xLeft < overlappingLabelXRight + textPadding === false) {
indexesToShow.push(index);
} else {
prevHiddenBoxIndex = index;
overlappingLabelXRight = prevLabelXRight;
continue;
}
if (prevHiddenBoxIndex !== index) {
// This getBoundingClientRect() forces Chrome's
// 'Recalculate Style' and 'Layout', which takes 30-40 ms on Chrome.
// TODO: This forced synchronous layout would be nice to eliminate.
//prevTextBox = texts[index].getBoundingClientRect();
//prevLabelXRight = prevTextBox.left + prevTextBox.width;
// TODO: Account for number of characters in prevTextBoxWidth,
// maybe also zoom.
prevTextBoxLeft = textOffsets[chrModel.id][index];
prevTextBoxWidth = 36;
prevLabelXRight = prevTextBoxLeft + prevTextBoxWidth;
}
if (
xLeft < prevLabelXRight + textPadding
) {
prevHiddenBoxIndex = index;
overlappingLabelXRight = prevLabelXRight;
} else {
indexesToShow.push(index);
}
}
var selectorsToShow = [],
ithLength = indexesToShow.length,
j;
for (var j = 0; j < ithLength; j++) {
index = indexesToShow[j];
selectorsToShow.push("#" + chrModel.id + " .bsbsl-" + index);
}
this.bandsToShow = this.bandsToShow.concat(selectorsToShow);
}
};
/**
* Rotates chromosome labels by 90 degrees, e.g. upon clicking a chromosome to focus.
*/
Ideogram.prototype.rotateChromosomeLabels = function(chr, chrIndex, orientation, scale) {
var chrMargin, chrWidth, ideo, x, y,
numAnnotTracks, scaleSvg, tracksHeight;
chrWidth = this.config.chrWidth;
chrMargin = this.config.chrMargin * chrIndex;
numAnnotTracks = this.config.numAnnotTracks;
ideo = this;
if (typeof(scale) !== "undefined" && scale.hasOwnProperty("x") && !(scale.x == 1 && scale.y == 1)) {
scaleSvg = "scale(" + scale.x + "," + scale.y + ")";
x = -6;
y = (scale === "" ? -16 : -14);
} else {
x = -8;
y = -16;
scale = {"x": 1, "y": 1};
scaleSvg = "";
}
if (orientation == "vertical" || orientation == "") {
chr.selectAll("text.chrLabel")
.attr("transform", scaleSvg)
.selectAll("tspan")
.attr("x", x)
.attr("y", function(d, i) {
var ci = chrIndex - 1;
if (numAnnotTracks > 1 || orientation == "") {
ci -= 1;
}
chrMargin2 = -4;
if (ideo.config.showBandLabels === true) {
chrMargin2 = ideo.config.chrMargin + chrWidth + 26;
}
var chrMargin = ideo.config.chrMargin * ci;
if (numAnnotTracks > 1 == false) {
chrMargin += 1;
}
return chrMargin + chrMargin2;
});
} else {
chrIndex -= 1;
chrMargin2 = -chrWidth - 2;
if (ideo.config.showBandLabels === true) {
chrMargin2 = ideo.config.chrMargin + 8;
}
tracksHeight = ideo.config.annotTracksHeight;
if (ideo.config.annotationsLayout !== "overlay") {
tracksHeight = tracksHeight * 2;
}
chr.selectAll("text.chrLabel")
.attr("transform", "rotate(-90)" + scaleSvg)
.selectAll("tspan")
.attr("x", function(d, i) {
chrMargin = ideo.config.chrMargin * chrIndex;
x = -(chrMargin + chrMargin2) + 3 + tracksHeight;
x = x/scale.x;
return x;
})
.attr("y", y);
}
};
/**
* Rotates band labels by 90 degrees, e.g. upon clicking a chromosome to focus.
*
* This method includes proportional scaling, which ensures that
* while the parent chromosome group is scaled strongly in one dimension to fill
* available space, the text in the chromosome's band labels is
* not similarly distorted, and remains readable.
*/
Ideogram.prototype.rotateBandLabels = function(chr, chrIndex, scale) {
var chrMargin, chrWidth, scaleSvg,
orientation, bandLabels,
ideo = this;
bandLabels = chr.selectAll(".bandLabel");
chrWidth = this.config.chrWidth;
chrMargin = this.config.chrMargin * chrIndex;
orientation = chr.attr("data-orientation");
if (typeof(scale) == "undefined") {
scale = {x: 1, y: 1};
scaleSvg = "";
} else {
scaleSvg = "scale(" + scale.x + "," + scale.y + ")";
}
if (
chrIndex == 1 &&
"perspective" in this.config && this.config.perspective == "comparative"
) {
bandLabels
.attr("transform", function(d) {
var x, y;
x = (8 - chrMargin) - 26;
y = ideo.round(2 + d.px.start + d.px.width/2);
return "rotate(-90)translate(" + x + "," + y + ")";
})
.selectAll("text")
.attr("text-anchor", "end");
} else if (orientation == "vertical") {
bandLabels
.attr("transform", function(d) {
var x, y;
x = 8 - chrMargin;
y = ideo.round(2 + d.px.start + d.px.width/2);
return "rotate(-90)translate(" + x + "," + y + ")";
})
.selectAll("text")
.attr("transform", scaleSvg);
} else {
bandLabels
.attr("transform", function(d) {
var x, y;
x = ideo.round(-8*scale.x + d.px.start + d.px.width/2);
y = chrMargin - 10;
return "translate(" + x + "," + y + ")";
})
.selectAll("text")
.attr("transform", scaleSvg);
chr.selectAll(".bandLabelStalk line")
.attr("transform", scaleSvg);
}
};
Ideogram.prototype.round = function(coord) {
// Rounds an SVG coordinates to two decimal places
// e.g. 42.1234567890 -> 42.12
// Per http://stackoverflow.com/a/9453447, below method is fastest
return Math.round(coord * 100) / 100;
};
Ideogram.prototype.drawChromosomeNoBands = function(chrModel, chrIndex) {
var chr,
bump, chrMargin, chrWidth, width,
ideo = this;
bump = ideo.bump;
chrMargin = ideo.config.chrMargin * chrIndex;
chrWidth = ideo.config.chrWidth;
width = chrModel.width;
chr = d3.select("svg")
.append("g")
.attr("id", chrModel.id)
.attr("class", "chromosome noBands");
if (width < 1) {
// Applies to mitochrondial and chloroplast chromosomes
return;
}
chr.append('path')
.attr("class", "upstream chromosomeBorder")
.attr("d",
"M " + (bump - bump/2 + 0.1) + " " + chrMargin + " " +
"q -" + bump + " " + (chrWidth/2) + " 0 " + chrWidth);
chr.append('path')
.attr("class", "downstream chromosomeBorder")
.attr("d",
"M " + width + " " + chrMargin + " " +
"q " + bump + " " + chrWidth/2 + " 0 " + chrWidth);
chr.append('line')
.attr("class", "cb-top chromosomeBorder")
.attr('x1', bump/2)
.attr('y1', chrMargin)
.attr('x2', width)
.attr("y2", chrMargin);
chr.append('line')
.attr("class", "cb-bottom chromosomeBorder")
.attr('x1', bump/2)
.attr('y1', chrWidth + chrMargin)
.attr('x2', width)
.attr("y2", chrWidth + chrMargin);
chr.append('path')
.attr("class", "chromosomeBody")
.attr("d",
"M " + bump/2 + " " + chrMargin + " " +
"l " + (width - bump/2) + " 0 " +
"l 0 " + chrWidth + " " +
"l -" + (width - bump/2) + " 0 z");
}
/**
* Renders all the bands and outlining boundaries of a chromosome.
*/
Ideogram.prototype.drawChromosome = function(chrModel, chrIndex) {
var chr, chrWidth, width,
pArmWidth, selector, qArmStart, qArmWidth,
pTerPad, chrClass,
annotHeight, numAnnotTracks, annotTracksHeight,
bump, ideo,
bumpTweak, borderTweak,
ideo = this;
if (typeof chrModel.bands === "undefined") {
ideo.drawChromosomeNoBands(chrModel, chrIndex);
return;
}
bump = ideo.bump;
// p-terminal band padding
if (chrModel.centromerePosition != "telocentric") {
pTerPad = bump;
} else {
pTerPad = Math.round(bump/4) + 3;
}
chr = d3.select("svg")
.append("g")
.attr("id", chrModel.id)
.attr("class", "chromosome");
chrWidth = ideo.config.chrWidth;
width = chrModel.width;
var chrMargin = ideo.config.chrMargin * chrIndex;
// Draw chromosome bands
chr.selectAll("path")
.data(chrModel.bands)
.enter()
.append("path")
.attr("id", function(d) {
// e.g. 1q31
var band = d.name.replace(".", "-");
return chrModel.id + "-" + band;
})
.attr("class", function(d) {
var cls = "band " + d.stain;
if (d.stain == "acen") {
var arm = d.name[0]; // e.g. p in p11
cls += " " + arm + "-cen";
}
return cls;
})
.attr("d", function(d, i) {
var x = ideo.round(d.px.width),
left = ideo.round(d.px.start),
curveStart, curveMid, curveEnd,
curveTweak,
innerBump = bump;
curveTweak = 0;
if (d.stain == "acen") {
// Pericentromeric bands get curved
if (ideo.adjustedBump) {
curveTweak = 0.35;
x = 0.2;
left -= 0.1;
if (d.name[0] === "q") {
left += 1.2;
}
} else {
x -= bump/2;
}
curveStart = chrMargin + curveTweak;
curveMid = chrWidth/2 - curveTweak*2;
curveEnd = chrWidth - curveTweak*2;
if (d.name[0] == "p") {
// p arm
d =
"M " + left + " " + curveStart + " " +
"l " + x + " 0 " +
"q " + bump + " " + curveMid + " 0 " + curveEnd + " " +
"l -" + x + " 0 z";
} else {
if (ideo.adjustedBump) {
x += 0.2;
}
// q arm
d =
"M " + (left + x + bump/2) + " " + curveStart + " " +
"l -" + x + " 0 " +
"q -" + (bump + 0.5) + " " + curveMid + " 0 " + curveEnd + " " +
"l " + x + " 0 z";
}
} else {
// Normal bands
if (i == 0) {
left += pTerPad - bump/2;
// TODO: this is a minor kludge to preserve visible
// centromeres in mouse, when viewing mouse and
// human chromosomes for e.g. orthology analysis
if (ideo.config.multiorganism === true) {
left += pTerPad;
}
}
if (ideo.adjustedBump && d.name[0] === "q") {
left += 1.8;
}
if (i == chrModel.bands.length - 1) {
left -= pTerPad - bump/2;
}
d =
"M " + left + " " + chrMargin + " " +
"l " + x + " 0 " +
"l 0 " + chrWidth + " " +
"l -" + x + " 0 z";
}
return d;
});
if (chrModel.centromerePosition != "telocentric") {
// As in human
chr.append('path')
.attr("class", "p-ter chromosomeBorder " + chrModel.bands[0].stain)
.attr("d",
"M " + (pTerPad - bump/2 + 0.1) + " " + chrMargin + " " +
"q -" + pTerPad + " " + (chrWidth/2) + " 0 " + chrWidth);
} else {
// As in mouse
chr.append('path')
.attr("class", "p-ter chromosomeBorder " + chrModel.bands[0].stain)
.attr("d",
"M " + (pTerPad - 3) + " " + chrMargin + " " +
"l -" + (pTerPad - 2) + " 0 " +
"l 0 " + chrWidth + " " +
"l " + (pTerPad - 2) + " 0 z");
chr.insert('path', ':first-child')
.attr("class", "acen")
.attr("d",
"M " + (pTerPad - 3) + " " + (chrMargin + chrWidth * 0.1) + " " +
"l " + (pTerPad + bump/2 + 1) + " 0 " +
"l 0 " + chrWidth * 0.8 + " " +
"l -" + (pTerPad + bump/2 + 1) + " 0 z");
}
if (ideo.adjustedBump) {
borderTweak = 1.8;
} else {
borderTweak = 0;
}
var pcenIndex = chrModel["pcenIndex"],
pcen = chrModel.bands[pcenIndex],
qcen = chrModel.bands[pcenIndex + 1],
pBump, qArmEnd;
// Why does human chromosome 11 lack a centromeric p-arm band?
// Answer: because of a bug in the data. Hack removed; won't work
// for human 550 resolution until data is fixed.
if (pcenIndex > 0) {
pArmWidth = pcen.px.start;
qArmStart = qcen.px.stop + borderTweak;
pBump = bump;
} else {
// For telocentric centromeres, as in many mouse chromosomes
pArmWidth = 2;
pBump = 0;
qArmStart = document.querySelectorAll("#" + chrModel.id + " .band")[0].getBBox().x;
}
qArmWidth = chrModel.width - qArmStart + borderTweak*1.3;
qArmEnd = qArmStart + qArmWidth - bump/2 - 0.5;
chr.append('line')
.attr("class", "cb-p-arm-top chromosomeBorder")
.attr('x1', bump/2)
.attr('y1', chrMargin)
.attr('x2', pArmWidth)
.attr("y2", chrMargin);
chr.append('line')
.attr("class", "cb-p-arm-bottom chromosomeBorder")
.attr('x1', bump/2)
.attr('y1', chrWidth + chrMargin)
.attr('x2', pArmWidth)
.attr("y2", chrWidth + chrMargin);
chr.append('line')
.attr("class", "cb-q-arm-top chromosomeBorder")
.attr('x1', qArmStart)
.attr('y1', chrMargin)
.attr('x2', qArmEnd)
.attr("y2", chrMargin);
chr.append('line')
.attr("class", "cb-q-arm-bottom chromosomeBorder")
.attr('x1', qArmStart)
.attr('y1', chrWidth + chrMargin)
.attr('x2', qArmEnd)
.attr("y2", chrWidth + chrMargin);
chr.append('path')
.attr("class", "q-ter chromosomeBorder " + chrModel.bands[chrModel.bands.length - 1].stain)
.attr("d",
"M " + qArmEnd + " " + chrMargin + " " +
"q " + bump + " " + chrWidth/2 + " 0 " + chrWidth
);
};
/**
* Rotates and translates chromosomes upon initialization as needed.
*/
Ideogram.prototype.initTransformChromosome = function(chr, chrIndex) {
if (this.config.orientation == "vertical") {
var chrMargin, chrWidth, tPadding;
chrWidth = this.config.chrWidth;
chrMargin = this.config.chrMargin * chrIndex;
if (!this.config.showBandLabels) {
chrIndex += 2;
}
tPadding = chrMargin + (chrWidth-4)*(chrIndex - 1);
chr
.attr("data-orientation", "vertical")
.attr("transform", "rotate(90, " + (tPadding - 30) + ", " + (tPadding) + ")");
this.rotateBandLabels(chr, chrIndex);
} else {
chr.attr("data-orientation", "horizontal");
}
};
/**
* Rotates a chromosome 90 degrees and shows or hides all other chromosomes
* Useful for focusing or defocusing a particular chromosome
*/
Ideogram.prototype.rotateAndToggleDisplay = function(chromosomeID) {
var id, chr, chrModel, chrIndex, chrMargin, chrWidth,
chrHeight, ideoBox, ideoWidth, ideoHeight, scaleX, scaleY,
initOrientation, currentOrientation,
cx, cy, cy2,
ideo = this;
id = chromosomeID;
chr = d3.select("#" + id);
chrModel = ideo.chromosomes[ideo.config.taxid][id.split("-")[0].split("chr")[1]];
chrIndex = chrModel["chrIndex"];
otherChrs = d3.selectAll("g.chromosome").filter(function(d, i) { return this.id !== id; });
initOrientation = ideo.config.orientation;
currentOrientation = chr.attr("data-orientation");
chrMargin = this.config.chrMargin * chrIndex;
chrWidth = this.config.chrWidth;
ideoBox = d3.select("#_ideogram").nodes()[0].getBoundingClientRect();
ideoHeight = ideoBox.height;
ideoWidth = ideoBox.width;
if (initOrientation == "vertical") {
chrLength = chr.nodes()[0].getBoundingClientRect().height;
scaleX = (ideoWidth/chrLength)*0.97;
scaleY = 1.5;
scale = "scale(" + scaleX + ", " + scaleY + ")";
inverseScaleX = 2/scaleX;
inverseScaleY = 1;
if (!this.config.showBandLabels) {
chrIndex += 2;
}
cx = chrMargin + (chrWidth-4)*(chrIndex - 1) - 30;
cy = cx + 30;
verticalTransform = "rotate(90, " + cx + ", " + cy + ")";
cy2 = -1*(chrMargin - this.config.annotTracksHeight)*scaleY;
if (this.config.showBandLabels) {
cy2 += 25;
}
horizontalTransform =
"rotate(0)" +
"translate(20, " + cy2 + ")" +
scale;
} else {
chrLength = chr.nodes()[0].getBoundingClientRect().width;
scaleX = (ideoHeight/chrLength)*0.97;
scaleY = 1.5;
scale = "scale(" + scaleX + ", " + scaleY + ")";
inverseScaleX = 2/scaleX;
inverseScaleY = 1;
var bandPad = 20;
if (!this.config.showBandLabels) {
chrIndex += 2;
bandPad = 15;
}
cx = chrMargin + (chrWidth-bandPad)*(chrIndex - 2);
cy = cx + 5;
if (!this.config.showBandLabels) {
cx += bandPad;
cy += bandPad;
}
verticalTransform = (
"rotate(90, " + cx + ", " + cy + ")" +
scale
);
horizontalTransform = "";
}
inverseScale = "scale(" + inverseScaleX + "," + inverseScaleY + ")";
if (currentOrientation != "vertical") {
if (initOrientation == "horizontal") {
otherChrs.style("display", "none");
}
chr.selectAll(".annot>path")
.attr("transform", (initOrientation == "vertical" ? "" : inverseScale));
chr
.attr("data-orientation", "vertical")
.transition()
.attr("transform", verticalTransform)
.on("end", function() {
if (initOrientation == "vertical") {
scale = "";
} else {
scale = {"x": inverseScaleY, "y": inverseScaleX};
}
ideo.rotateBandLabels(chr, chrIndex, scale);
ideo.rotateChromosomeLabels(chr, chrIndex, "horizontal", scale);
if (initOrientation == "vertical") {
otherChrs.style("display", "");
}
});
} else {
chr.attr("data-orientation", "");
if (initOrientation == "vertical") {
otherChrs.style("display", "none");
}
chr.selectAll(".annot>path")
.transition()
.attr("transform", (initOrientation == "vertical" ? inverseScale : ""));
chr
.transition()
.attr("transform", horizontalTransform)
.on("end", function() {
if (initOrientation == "horizontal") {
if (currentOrientation == "vertical") {
inverseScale = {"x": 1, "y": 1};
} else {
inverseScale = "";
}
} else {
inverseScale = {"x": inverseScaleX, "y": inverseScaleY};
}
ideo.rotateBandLabels(chr, chrIndex, inverseScale);
ideo.rotateChromosomeLabels(chr, chrIndex, "", inverseScale);
if (initOrientation == "horizontal") {
otherChrs.style("display", "");
}
});
}
};
/**
* Converts base pair coordinates to pixel offsets.
* Bp-to-pixel scales differ among cytogenetic bands.
*/
Ideogram.prototype.convertBpToPx = function(chr, bp) {
var i, band, bpToIscnScale, iscn, px;
for (i = 0; i < chr.bands.length; i++) {
band = chr.bands[i];
if (bp >= band.bp.start && bp <= band.bp.stop) {
bpToIscnScale = (band.iscn.stop - band.iscn.start)/(band.bp.stop - band.bp.start);
iscn = band.iscn.start + (bp - band.bp.start) * bpToIscnScale;
px = 30 + band.px.start + (band.px.width * (iscn - band.iscn.start)/(band.iscn.stop - band.iscn.start));
return px;
}
}
throw new Error(
"Base pair out of range. " +
"bp: " + bp + "; length of chr" + chr.name + ": " + band.bp.stop
);
};
/**
* Converts base pair coordinates to pixel offsets.
* Bp-to-pixel scales differ among cytogenetic bands.
*/
Ideogram.prototype.convertPxToBp = function(chr, px) {
var i, band, prevBand, bpToIscnScale, iscn;
for (i = 0; i < chr.bands.length; i++) {
band = chr.bands[i];
if (px >= band.px.start && px <= band.px.stop) {
pxToIscnScale = (band.iscn.stop - band.iscn.start)/(band.px.stop - band.px.start);
iscn = band.iscn.start + (px - band.px.start) * pxToIscnScale;
bp = band.bp.start + ((band.bp.stop - band.bp.start) * (iscn - band.iscn.start)/(band.iscn.stop - band.iscn.start));
return Math.round(bp);
}
}
throw new Error(
"Pixel out of range. " +
"px: " + bp + "; length of chr" + chr.name + ": " + band.px.stop
);
};
/**
* Draws a trapezoid connecting a genomic range on
* one chromosome to a genomic range on another chromosome;
* a syntenic region.
*/
Ideogram.prototype.drawSynteny = function(syntenicRegions) {
var t0 = new Date().getTime();
var r1, r2,
c1Box, c2Box,
chr1Plane, chr2Plane,
polygon,
region,
syntenies, synteny,
i, svg, color, opacity,
regionID,
ideo = this;
syntenies = d3.select("svg")
.insert("g", ":first-child")
.attr("class", "synteny");
for (i = 0; i < syntenicRegions.length; i++) {
regions = syntenicRegions[i];
r1 = regions.r1;
r2 = regions.r2;
color = "#CFC";
if ("color" in regions) {
color = regions.color;
}
opacity = 1;
if ("opacity" in regions) {
opacity = regions.opacity;
}
r1.startPx = this.convertBpToPx(r1.chr, r1.start);
r1.stopPx = this.convertBpToPx(r1.chr, r1.stop);
r2.startPx = this.convertBpToPx(r2.chr, r2.start);
r2.stopPx = this.convertBpToPx(r2.chr, r2.stop);
c1Box = document.querySelectorAll("#" + r1.chr.id + " path")[0].getBBox();
c2Box = document.querySelectorAll("#" + r2.chr.id + " path")[0].getBBox();
chr1Plane = c1Box.y - 31;
chr2Plane = c2Box.y - 28;
regionID = (
r1.chr.id + "_" + r1.start + "_" + r1.stop + "_" +
"__" +
r2.chr.id + "_" + r2.start + "_" + r2.stop
);
syntenicRegion = syntenies.append("g")
.attr("class", "syntenicRegion")
.attr("id", regionID)
.on("click", function() {
var activeRegion = this;
var others = d3.selectAll(".syntenicRegion")
.filter(function(d, i) {
return (this !== activeRegion);
});
others.classed("hidden", !others.classed("hidden"));
})
.on("mouseover", function() {
var activeRegion = this;
d3.selectAll(".syntenicRegion")
.filter(function(d, i) {
return (this !== activeRegion);
})
.classed("ghost", true);
})
.on("mouseout", function() {
d3.selectAll(".syntenicRegion").classed("ghost", false);
});
syntenicRegion.append("polygon")
.attr("points",
chr1Plane + ', ' + r1.startPx + ' ' +
chr1Plane + ', ' + r1.stopPx + ' ' +
chr2Plane + ', ' + r2.stopPx + ' ' +
chr2Plane + ', ' + r2.startPx
)
.attr('style', "fill: " + color + "; fill-opacity: " + opacity);
syntenicRegion.append("line")
.attr("class", "syntenyBorder")
.attr("x1", chr1Plane)
.attr("x2", chr2Plane)
.attr("y1", r1.startPx)
.attr("y2", r2.startPx);
syntenicRegion.append("line")
.attr("class", "syntenyBorder")
.attr("x1", chr1Plane)
.attr("x2", chr2Plane)
.attr("y1", r1.stopPx)
.attr("y2", r2.stopPx);
}
var t1 = new Date().getTime();
if (ideo.debug) {
console.log("Time in drawSyntenicRegions: " + (t1 - t0) + " ms");
}
};
/**
* Initializes various annotation settings. Constructor help function.
*/
Ideogram.prototype.initAnnotSettings = function() {
if (this.config.annotationsPath || this.config.localAnnotationsPath
|| this.annots || this.config.annotations) {
if (!this.config.annotationHeight) {
var annotHeight = Math.round(this.config.chrHeight/100);
this.config.annotationHeight = annotHeight;
}
if (this.config.annotationTracks) {
this.config.numAnnotTracks = this.config.annotationTracks.length;
} else {
this.config.numAnnotTracks = 1;
}
this.config.annotTracksHeight = this.config.annotationHeight * this.config.numAnnotTracks;
if (typeof this.config.barWidth === "undefined") {
this.config.barWidth = 3;
}
} else {
this.config.annotTracksHeight = 0;
}
if (typeof this.config.annotationsColor === "undefined") {
this.config.annotationsColor = "#F00";
}
};
/**
* Draws annotations defined by user
*/
Ideogram.prototype.drawAnnots = function(friendlyAnnots) {
var ideo = this,
i, j, annot,
rawAnnots = [],
rawAnnot, keys,
chr,
chrs = ideo.chromosomes[ideo.config.taxid]; // TODO: multiorganism
// Occurs when filtering
if ("annots" in friendlyAnnots[0]) {
return ideo.drawProcessedAnnots(friendlyAnnots);
}
for (chr in chrs) {
rawAnnots.push({"chr": chr, "annots": []});
}
for (i = 0; i < friendlyAnnots.length; i++) {
annot = friendlyAnnots[i];
for (j = 0; j < rawAnnots.length; j++) {
if (annot.chr === rawAnnots[j].chr) {
rawAnnot = [
annot.name,
annot.start,
annot.stop - annot.start
];
if ("color" in annot) {
rawAnnot.push(annot.color);
}
if ("shape" in annot) {
rawAnnot.push(annot.shape);
}
rawAnnots[j]["annots"].push(rawAnnot);
break;
}
}
}
keys = ["name", "start", "length"];
if ("color" in friendlyAnnots[0]) {
keys.push("color");
}
if ("shape" in friendlyAnnots[0]) {
keys.push("shape");
}
ideo.rawAnnots = {"keys": keys, "annots": rawAnnots};
ideo.annots = ideo.processAnnotData(ideo.rawAnnots);
ideo.drawProcessedAnnots(ideo.annots);
};
/**
* Proccesses genome annotation data.
* Genome annotations represent features like a gene, SNP, etc. as
* a small graphical object on or beside a chromosome.
* Converts raw an