UNPKG

ideogram

Version:

Chromosome visualization with D3.js

1,649 lines (1,302 loc) 83.6 kB
// 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