/* global d3: false */
/* global $: false */
/* global _: false */
/* global document: false */
/* global textures: false */
/* global circles: false */
/* global bootbox: false */
/* jshint funcscope:true */
// use const instead of var as soon as EcmaScript 6 (ES6 is widely used)
var AliTV_VERSION = "1.0.6";
/**
* Creates an object of type AliTV for drawing whole genome alignment visualizations
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @constructor
* @param {Object} svg - jQuery object containing a svg DOM element. Visualizations will be drawn on this svg. Size may be changed by object methods. Previous content will be deleted.
* @example
* // initializes an AliTV object (wga) on the svg element with id 'canvas'
* var svg = $('#canvas');
* var wga = new AliTV(svg);
*/
function AliTV(svg) {
/**
* property to contain the svg DOM element as jQuery Object
*/
this.svg = svg;
/**
* property to contain the svg DOM element as d3 Object
*/
this.svgD3 = d3.selectAll(svg);
/**
* property to store the data
* @property {Object} karyo - the chromosome information
* @property {Object} karyo.chromosomes - the chromosome details, karyo IDs as keys
* @property {Number} karyo.chromosomes.genome_id - number of genome to which this chromosome belongs
* @property {Number} karyo.chromosomes.length - length in bp
* @property {String} karyo.chromosomes.seq - sequence of the chromosome
* @property {String} karyo.chromosomes.name - name of the chromosome
* @property {Object} features - the feature information, feature type as keys
* @property {Object} features.link - the link feature information, feature IDs as keys
* @property {String} features.link.karyo - the karyo ID
* @property {Number} features.link.start - start position on the sequence
* @property {Number} features.link.end - end position on the sequence
* @property {Array} features.<type> - the feature information fot type <type>
* @property {String} features.<type>.karyo - the karyo ID
* @property {String} features.<type>.name - the name of the feature
* @property {Number} features.<type>.start - start position on the sequence
* @property {Number} features.<type>.end - end position on the sequence
* @property {Object} links - the link information, <source_genome> (sgen) as keys
* @property {Object} links.<sgen> - the link information, <target_genome> (tgen) as keys
* @property {Object} links.<sgen>.<tgen> - the link information, feature_id (fid) as key
* @property {Object} links.<sgen>.<tgen>.<fid> - the link information, feature_id (fid) as key
* @property {String} links.<sgen>.<tgen>.<fid>.source - source feature of the link
* @property {String} links.<sgen>.<tgen>.<fid>.target - target feature of the link
* @property {Number} links.<sgen>.<tgen>.<fid>.identity - identity of the link
*/
this.data = {};
/**
* property to store data specific drawing options (structure highly dependent on data structure)
* @property {Object} filters - the data dependent displaying information
* @property {Object} filters.karyo - the chromosome dependent displaying information
* @property {Boolean} filters.skipChromosomesWithoutVisibleLinks - If a chromosome has no visible links, because they are filtered, it is possible to skip this chromosome.
* @property {Boolean} filters.showAllChromosomes - Allows to show all chromosomes, even if when they are set not visible.
* @property {Boolean} filters.onlyShowAdjacentLinks - Allows to show only adjacent links or all links.
* @property {Array} filters.karyo.order - array of chromosome IDs in the desired order (circular layout)
* @property {Array} filters.karyo.genome_order - array of genome IDs in the desired order (linear layout)
* @property {Object} filters.karyo.chromosomes - the chromosome drawing details, karyo IDs as keys
* @property {Boolean} filters.karyo.chromosomes.reverse - should the sequence be treated as its reverse (complement)
* @property {Boolean} filters.karyo.chromosomes.visible - should the sequence be displayed at all
* @property {Object} filters.karyo.genome_region - An object that can contain genome_ids as keys and regions as values
* @property {Object} filters.karyo.genome_region.<genome_id> - region object with optional "start" and "end" values
* @property {Object} filters.karyo.genome_region.<genome_id>.start - start value in bp, this position in the genome scale (including gaps between chromosomes) will be at the start of the drawing area (left).
* @property {Object} filters.karyo.genome_region.<genome_id>.end - end value in bp, this position in the genome scale (including gaps between chromosomes) will be at the end of the drawing area (right).
* @property {Number} filters.links.minLinkIdentity - The minimum identity of links which should be draw.
* @property {Number} filters.links.maxLinkIdentity - The maximum identity of links which should be draw.
* @property {Number} filters.links.minLinkLength - The minimum length of links, which should be draw in bp.
* @property {Number} filters.links.maxLinkLength - The maximum length of links, which should be draw in bp.
*/
this.filters = {};
/**
* property to store configuration options
* @property {Object} linear - The configuration options for the linear layout.
* @property {String} linear.startLineColor - The start color of the color gradient for drawing karyos according to their genomeId
* @property {String} linear.endLineColor - The end color of the color gradient.
* @property {String} linear.hideHalfVisibleLinks - If true - do not show links with only one end in a visible region.
* @property {Object} circular - The configuration options for the circular layout.
* @property {Number} circular.tickSize - The size of the ticks in pixels.
* @property {Number} minLinkIdentity - The minimum of the link identity the user wants to color.
* @property {Number} maxLinkIdentity - The maximum of the link identity the user wants to color.
* @property {Number} midLinkIdentity - The middle of the link identity the user wants to color.
* @property {String} minLinkIdentityColor - The color of the minimum link.
* @property {String} maxLinkIdentityColor - The color of the maximum link.
* @property {String} midLinkIdentityColor - The color of the middle link.
* @property {Number} minLinkLength - The minimum length of a link:
* @property {Number} maxLinkLength - The maximum length of a link.
* @property {Object} graphicalParameters - The configuration options for all graphical parameters.
* @property {Number} graphicalParameters.canvasWidth - The width of the alignment drawing area in px.
* @property {Number} graphicalParameters.canvasHeight - The height of the alignment drawing area in px.
* @property {Number} graphicalParameters.karyoHeight - The height of each chromosome in px.
* @property {Number} graphicalParameters.karyoDistance - The horizontal distance between adjacent chromosomes of the same genome in bp.
* @property {Number} graphicalParameters.linkKaryoDistance - The vertical distance between chromosomes and links in px.
* @property {Number} graphicalParameters.tickDistance - The distance in bp of ticks on the drawn chromosomes.
* @property {Number} graphicalParameters.treeWidth - The width of the svg drawing area, where the tree should be shown.
* @property {Number} graphicalParameters.genomeLabelWidth - The width of the svg drawing area, where the genome labels should be shown.
* @property {Number} graphicalParameters.linkOpacity - The value which is used as default opacity of links.
* @property {Number} graphicalParameters.fade - The value which is used for the opacity of links by the fadeLinks method.
* @property {Number} graphicalParameters.buttonWidth - The width of the drawing area for the offset buttons.
* @property {String} layout - Contains the current layout, this means linear or circular.
* @property {Object} tree - Contains the configuration objects for drawing a tree.
* @property {Boolean} tree.drawTree - With this option it is possible to draw a phylogenetic tree ext to the chromosomes.
* @property {Boolean} tree.orientation - Defines where the tree should be drawn.
* @property {Object} features - Contains the configuration for feature groups.
* @property {Boolean} features.showAllFeatures - Defines if all features are drawn or not.
* @property {Object} features.gene - Contains the configuration for genes.
* @property {String} features.gene.form - Defines the shape of a gene.
* @property {String} features.gene.color - Defines the color of a gene.
* @property {Number} features.gene.height - Defines the height of the drawn gene onto the chromosome.
* @property {Boolean} features.gene.visible - Defines if a gene is drawn or not.
* @property {Object} features.invertedRepeat - Contains the configuration for inverted repeats.
* @property {String} features.invertedRepeat.form - Defines the shape of an inverted repeat.
* @property {String} features.invertedRepeat.color - Defines the color of an inverted repeat.
* @property {Number} features.invertedRepeat.height - Defines the height of the drawn inverted repeat onto the chromosome.
* @property {Boolean} features.invertedRepeat.visible - Defines if an inverted repeat is drawn or not.
* @property {Object} features.nStretch - Contains the configuration for n stretch.
* @property {String} features.nStretch.form - Defines the shape of a n stretch.
* @property {String} features.nStretch.color - Defines the color of a n stretch.
* @property {Number} features.nStretch.height - Defines the height of the drawn n stretch onto the chromosome.
* @property {Boolean} features.nStretch.visible - Defines if an inverted n stretch is drawn or not.
* @property {Object} features.repeat - Contains the configuration for inverted repeats.
* @property {String} features.repeat.form - Defines the shape of a repeat.
* @property {String} features.repeat.color - Defines the color of a repeat.
* @property {Number} features.repeat.height - Defines the height of the drawn repeat onto the chromosome.
* @property {Boolean} features.repeat.visible - Defines if an repeat is drawn or not.
* @property {Object} features.fallback - Contains the configuration for non-supported feature classes.
* @property {String} features.fallback.form - Defines the shape of a non-supported feature groups.
* @property {String} features.fallback.color - Defines the color of a non-supported feature group.
* @property {Number} features.fallback.height - Defines the height of the drawn non-supported feature group onto the chromosome.
* @property {Boolean} features.fallback.visible - Defines if an non-supported feature group is drawn or not.
* @property {Object} labels - The configuration options for the text labels.
* @property {Boolean} labels.ticks - Contains the configuration for the labeling of the chromosome scale.
* @property {Boolean} labels.ticks.showTicks - Defines if ticks are drawn.
* @property {Boolean} labels.ticks.showTickLabels - Defines if tick labels are drawn.
* @property {String} labels.ticks.color - Defines the color of ticks and their labels.
* @property {Object} labels.genome - Contains the configurations for the genome labels.
* @property {Boolean} labels.genome.showGenomeLabels - Defines if genome labels are shown or not.
* @property {String} labels.genome.color - Defines the color of genome labels.
* @property {Number} labels.genome.size - Defines the size of genome labels.
* @property {Object} offset - Contains values for the offset
* @property {Boolean} offset.isSet - Defines if an offset is setted or not.
* @property {Number} offset.distance - The value for shifting the chromosomes.
*/
this.conf = {
linear: {
startLineColor: "#49006a",
endLineColor: "#1d91c0",
hideHalfVisibleLinks: false
},
circular: {
tickSize: 5
},
graphicalParameters: {
canvasWidth: 1000,
canvasHeight: 1000,
karyoHeight: 30,
karyoDistance: 10,
linkKaryoDistance: 20,
tickLabelFrequency: 10,
tickDistance: 100,
treeWidth: 300,
genomeLabelWidth: 150,
linkOpacity: 0.9,
fade: 0.1,
buttonWidth: 90
},
minLinkIdentity: 40,
maxLinkIdentity: 100,
midLinkIdentity: 60,
minLinkIdentityColor: "#D21414",
maxLinkIdentityColor: "#1DAD0A",
midLinkIdentityColor: "#FFEE05",
minLinkLength: 100,
maxLinkLength: 5000,
layout: "linear",
tree: {
drawTree: false,
orientation: "left"
},
features: {
showAllFeatures: false,
supportedFeatures: {
gene: {
form: "rect",
color: "#E2EDFF",
height: 30,
visible: false
},
invertedRepeat: {
form: "arrow",
color: "#e7d3e2",
height: 30,
visible: false,
pattern: "woven"
},
nStretch: {
form: "rect",
color: "#000000",
height: 30,
visible: false,
pattern: "lines"
},
repeat: {
form: "rect",
color: "#56cd0f",
height: 30,
visible: false,
pattern: "woven"
}
},
fallbackStyle: {
form: "rect",
color: "#787878",
height: 30,
visible: false
}
},
labels: {
ticks: {
showTicks: true,
showTickLabels: true,
color: "#000000",
size: 10
},
genome: {
showGenomeLabels: true,
color: "#000000",
size: 25
},
features: {
showFeatureLabels: true,
color: "#000000",
size: 25
}
},
offset: {
isSet: false,
distance: 1000
}
};
/**
* property to cache calculated values
* @property {Object} cache - the data dependent displaying information
* @property {Object} cache.linear - the chromosome dependent displaying information
* @property {Object} cache.linear.maxGenomeSize - the chromosome dependent displaying information
*/
this.cache = {
'linear': {}
};
/**
* array of registered onChange callback functions
* @property {Array} onChangeCallbacks - array of callback functions
*/
this.onChangeCallbacks = [];
/**
* boolean that indicates whether AliTV is inside transaction, see functions startTransaction and endTransaction
* @property {boolean} inTransaction - is AliTV currently in transaction
*/
this.inTransaction = false;
// Initialize svg size
this.setSvgWidth(this.getCanvasWidth());
this.setSvgHeight(this.getCanvasHeight());
var that = this;
// add mouse event handlers for the selection rect (inspired by http://bl.ocks.org/lgersman/5311083)
this.svgD3.on("mousedown", function() {
// only procede in the linear case and if no selection rect exists.
if (that.conf.layout !== "linear" || that.svgD3.selectAll("rect.selection").size() > 0) {
return;
}
var p = d3.mouse(this);
that.svgD3.append("rect")
.attr({
rx: 6,
ry: 6,
class: "selection",
x: p[0],
y: p[1],
width: 0,
height: 0
});
}).on("mousemove", function() {
var s = that.svgD3.select("rect.selection");
if (!s.empty()) {
var p = d3.mouse(this),
d = {
x: Number(s.attr("x")),
y: Number(s.attr("y")),
width: Number(s.attr("width")),
height: Number(s.attr("height"))
},
move = {
x: p[0] - d.x,
y: p[1] - d.y
};
// this somewhat strange code is required to correct for lag between multiple calls from mousemove
// this code is likely executed more than once concurrently when the mouse pointer is moved too fast.
// therefore the selection box may loose the right coordinates if the mouse is moved rapidly.
if (move.x < 1 || (move.x * 2 < d.width)) {
d.x = p[0];
d.width -= move.x;
} else {
d.width = move.x;
}
if (move.y < 1 || (move.y * 2 < d.height)) {
d.y = p[1];
d.height -= move.y;
} else {
d.height = move.y;
}
s.attr(d);
}
}).on("mouseup", function() {
var s = that.svgD3.selectAll("rect.selection");
if (s.size() > 0) {
var rect = {
x: Number(s.attr("x")),
y: Number(s.attr("y")),
width: Number(s.attr("width")),
height: Number(s.attr("height"))
};
if (rect.width >= 10) {
that.updateGenomeRegionBySvgRect(rect);
that.drawLinear();
}
s.remove();
}
});
}
/**
* Extends the existing conf of the AliTV object.
* New features override the existing ones if conflicting. Non-conflicting configuration values are kept.
* For the required format see the documentation of the conf property.
* If you need to override the entire conf object write directly to the conf property of your AliTV object.
* This is not recommended as AliTV will not function properly if some conf values are not set.
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @param {Object} conf - Object containing conf values
*/
AliTV.prototype.setConf = function(conf) {
jQuery.extend(true, this.conf, conf);
this.triggerChange();
};
/**
* Sets the data of the AliTV object.
* For the required format see the documentation of the data property
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @param {Object} data - Object containing karyo, link and feature information
* @example
* var svg = $('#canvas');
* var wga = new AliTV(svg);
* var karyo = {
* 'chromosomes': {
* 'c1': {'genome_id': 0, 'length': 2000, 'seq': null},
* 'c2': {'genome_id': 1, 'length': 1000, 'seq': null}
* }
* };
* var features = {
* 'f1': {'karyo': 'c1', 'start': 300, 'end': 800},
* 'f2': {'karyo': 'c2', 'start': 100, 'end': 600}
* };
* var links = { "l1":
* {'source': 'f1', 'target': 'f2', 'identity': 90}
* };
* wga.setData({'karyo': karyo, 'features': features, 'links': links};
*/
AliTV.prototype.setData = function(data) {
this.data = data;
this.triggerChange();
};
/**
* Sets the filters of the AliTV object.
* For the required format see the documentation of the filters property
* The filters are highly dependent on the data object and have to resemble its layout
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @param {Object} filters - Object containing data specific drawing information
* @example
* var svg = $('#canvas');
* var wga = new AliTV(svg);
* var karyo = {
* 'chromosomes': {
* 'c1': {'genome_id': 0, 'length': 2000, 'seq': null},
* 'c2': {'genome_id': 1, 'length': 1000, 'seq': null}
* }
* };
* var features = {
* 'f1': {'karyo': 'c1', 'start': 300, 'end': 800},
* 'f2': {'karyo': 'c2', 'start': 100, 'end': 600}
* };
* var links = {"l1":
* {'source': 'f1', 'target': 'f2', 'identity': 90}
* };
* wga.setData({'karyo': karyo, 'features': features, 'links': links};
* var filters = {
* 'karyo': {
* 'order': ['c1', 'c2'],
* 'genome_order': ['0', '1'],
* 'chromosomes': {
* 'c1': {'reverse': false, 'visible': true},
* 'c2': {'reverse': false, 'visible': true}
* }
* }
* };
* wga.setFilters(filters);
* wga.drawLinear();
* wga.drawCircular();
*/
AliTV.prototype.setFilters = function(filters) {
this.filters = filters;
if (this.filters.links === undefined) {
this.filters.links = {};
}
if (this.filters.features === undefined) {
this.filters.features = {};
}
if (this.filters.links.invisibleLinks === undefined) {
this.filters.links.invisibleLinks = {};
}
if (this.filters.features.invisibleFeatures === undefined) {
this.filters.features.invisibleFeatures = {};
}
if (this.data.karyo !== undefined) {
this.filters.links.maxLinkLength = this.getMaxChromosomeLength();
}
this.triggerChange();
};
/**
* Calculates coordinates for the chromosomes to draw in the linear layout.
* This function operates on the data property of the object and therefore needs no parameters.
* This function is primarily meant for internal usage, the user should not need to call this directly.
* The function also updates the value cache.linear.maxGenomeSize
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @returns {Array} Array containing one Object for each element in data.karyo of the form {karyo: 'karyo_name', x:0, y:0, width:10, height:10}
*/
AliTV.prototype.getLinearKaryoCoords = function() {
var linearKaryoCoords = [];
var genome_order = this.filters.karyo.genome_order;
var conf = this.conf;
var genomeDistance = this.getGenomeDistance();
var that = this;
var visibleChromosomes = that.filterChromosomes();
var orderOfVisibleChromosomes = that.filterChromosomeOrder(visibleChromosomes);
var genomeScale = {};
var total = [];
var current = [];
var i;
// Initialize total with the negative of one karyoDistance - as there is one space less then karyos per genome
for (i = 0; i < genome_order.length; i++) {
total.push(-conf.graphicalParameters.karyoDistance);
current.push(0);
}
$.each(visibleChromosomes, function(key, value) {
total[genome_order.indexOf(value.genome_id)] += value.length + conf.graphicalParameters.karyoDistance;
});
var maxTotalSize = Math.max.apply(null, total);
that.cache.linear.maxGenomeSize = maxTotalSize;
// Calculate genome specific scales
var getGenomeScale = function(gid) {
var genomeSvgScale = d3.scale.linear()
.domain([0, maxTotalSize]);
var genome_start = 0;
var genome_region = that.filters.karyo.genome_region || {};
if (typeof(genome_region[gid] || {}).start !== 'undefined') {
genome_start = genome_region[gid].start;
}
var genome_end = maxTotalSize;
if (typeof(genome_region[gid] || {}).end !== 'undefined') {
genome_end = genome_region[gid].end;
} else {
genome_end += genome_start;
}
// The calculation of the range for the scale depends on ideas of the intercept theorem
genomeSvgScale.range([conf.graphicalParameters.canvasWidth * genome_start / (genome_start - genome_end),
conf.graphicalParameters.canvasWidth * (maxTotalSize - genome_start) / (genome_end - genome_start)
]);
return genomeSvgScale;
};
for (i = 0; i < genome_order.length; i++) {
genomeScale[genome_order[i]] = getGenomeScale(genome_order[i]);
}
for (i = 0; i < orderOfVisibleChromosomes.length; i++) {
var key = orderOfVisibleChromosomes[i];
var value = visibleChromosomes[key];
var coord = {
'karyo': key,
'y': genome_order.indexOf(value.genome_id) * genomeDistance,
'height': conf.graphicalParameters.karyoHeight,
'genome': value.genome_id
};
var genome2svgScale = genomeScale[value.genome_id];
if (this.filters.karyo.chromosomes[key].reverse === false) {
coord.width = genome2svgScale(value.length) - genome2svgScale(0);
coord.x = genome2svgScale(current[genome_order.indexOf(value.genome_id)]);
} else {
coord.x = genome2svgScale(current[genome_order.indexOf(value.genome_id)] + value.length);
coord.width = genome2svgScale(0) - genome2svgScale(value.length);
}
current[genome_order.indexOf(value.genome_id)] += value.length + conf.graphicalParameters.karyoDistance;
linearKaryoCoords.push(coord);
}
return linearKaryoCoords;
};
/**
* Calculate coordinates for the links to draw in the linear layout and uses link-data and karyo-coordinates
* this function should also check if links are adjacent or not and save this information in the link property "adjacent"
* This function is primarily meant for internal usage, the user should not need to call this directly
* @author Sonja Hohlfeld
* @param {Array} The array containing the coordinates as returned by getLinearKaryoCoords()
* @returns {Array} Returns an Array which is presented in the following example
* @example [
* {"linkID": "l1", "source0": {"x":0, "y":10}, "target0": {"x": 0, "y":20}, "source1": {"x":10, "y":10}, "target1": {"x":10, "y":20}, "adjacent": true}
* ]
*/
AliTV.prototype.getLinearLinkCoords = function(coords) {
var linearLinkCoords = [];
if (typeof coords === 'undefined') {
return linearLinkCoords;
}
var that = this;
var conf = this.conf;
var visibleChromosomes = that.filterChromosomes();
var visibleLinks = that.filterLinks(visibleChromosomes);
var karyoMap = {};
$.each(coords, function(key, value) {
karyoMap[value.karyo] = key;
});
$.each(visibleLinks, function(key, value) {
var link = {};
link.linkID = key;
link.source0 = {};
link.source1 = {};
link.target0 = {};
link.target1 = {};
var splitLink = {};
splitLink.linkID = key;
splitLink.source0 = {};
splitLink.source1 = {};
splitLink.target0 = {};
splitLink.target1 = {};
var splitPart;
var linkSource;
var linkTarget;
var feature1 = that.data.features.link[value.source];
var feature2 = that.data.features.link[value.target];
var karyo1 = that.data.karyo.chromosomes[feature1.karyo];
var karyo2 = that.data.karyo.chromosomes[feature2.karyo];
var karyo1Coords = coords[karyoMap[feature1.karyo]];
var karyo2Coords = coords[karyoMap[feature2.karyo]];
var genomePosition1 = that.filters.karyo.genome_order.indexOf(karyo1.genome_id);
var genomePosition2 = that.filters.karyo.genome_order.indexOf(karyo2.genome_id);
var lengthOfFeature1 = Math.abs(that.data.features.link[value.source].end - that.data.features.link[value.source].start);
var lengthOfFeature2 = Math.abs(that.data.features.link[value.target].end - that.data.features.link[value.target].start);
if (genomePosition1 > genomePosition2) {
var tmp = feature1;
feature1 = feature2;
feature2 = tmp;
tmp = karyo1;
karyo1 = karyo2;
karyo2 = tmp;
tmp = karyo1Coords;
karyo1Coords = karyo2Coords;
karyo2Coords = tmp;
}
var shift1 = (that.filters.karyo.chromosomes[feature1.karyo].offset === undefined ? 0 : that.filters.karyo.chromosomes[feature1.karyo].offset);
var shift2 = (that.filters.karyo.chromosomes[feature2.karyo].offset === undefined ? 0 : that.filters.karyo.chromosomes[feature2.karyo].offset);
var linkTargetScale = d3.scale.linear()
.domain([0, karyo2.length])
.range([karyo2Coords.x, karyo2Coords.x + karyo2Coords.width]);
var linkSourceScale = d3.scale.linear()
.domain([0, karyo1.length])
.range([karyo1Coords.x, karyo1Coords.x + karyo1Coords.width]);
link.source0.x = linkSourceScale((feature1.start + shift1 + karyo1.length) % karyo1.length) === linkSourceScale(0) && (feature1.start > feature1.end && feature1.start + shift1 === karyo1.length) ? linkSourceScale(karyo1.length) : linkSourceScale((feature1.start + shift1 + karyo1.length) % karyo1.length);
link.source0.y = karyo1Coords.y + karyo1Coords.height + conf.graphicalParameters.linkKaryoDistance;
link.source1.x = linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length) === linkSourceScale(0) && !(feature1.start > feature1.end && feature1.end + shift1 === 0) ? linkSourceScale(karyo1.length) : linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length);
link.source1.y = karyo1Coords.y + karyo1Coords.height + conf.graphicalParameters.linkKaryoDistance;
link.target0.x = linkTargetScale((feature2.start + shift2 + karyo2.length) % karyo2.length) === linkTargetScale(0) && (feature2.start + shift2 === karyo2.length && feature2.start > feature2.end) ? linkTargetScale(karyo2.length) : linkTargetScale((feature2.start + shift2 + karyo2.length) % karyo2.length);
link.target0.y = karyo2Coords.y - conf.graphicalParameters.linkKaryoDistance;
link.target1.x = linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length) === linkTargetScale(0) && !(feature2.start > feature2.end && feature2.end + shift2 === 0) ? linkTargetScale(karyo2.length) : linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length);
link.target1.y = karyo2Coords.y - conf.graphicalParameters.linkKaryoDistance;
if ((feature1.start < feature1.end && link.source0.x > link.source1.x && that.filters.karyo.chromosomes[feature1.karyo].reverse === false) || (feature1.start < feature1.end && link.source0.x < link.source1.x && that.filters.karyo.chromosomes[feature1.karyo].reverse === true)) {
splitPart = (feature1.end + shift1 + karyo1.length) % karyo1.length / Math.abs(feature1.start - feature1.end);
linkTarget = link.target1.x;
link.source1.x = linkSourceScale(karyo1.length);
link.target1.x = linkTargetScale(feature2.start > feature2.end ? Math.min(feature2.end, feature2.start) + splitPart * Math.abs(feature2.start - feature2.end) : Math.min(feature2.end, feature2.start) + (1 - splitPart) * Math.abs(feature2.start - feature2.end));
splitLink.source0.x = linkSourceScale(0);
splitLink.source0.y = link.source0.y;
splitLink.source1.x = linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length) === linkSourceScale(0) && !(feature1.start > feature1.end && feature1.end + shift1 === 0) ? linkSourceScale(karyo1.length) : linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length);
splitLink.source1.y = link.source1.y;
splitLink.target0.x = linkTargetScale(feature2.start > feature2.end ? Math.min(feature2.end, feature2.start) + splitPart * Math.abs(feature2.start - feature2.end) : Math.min(feature2.end, feature2.start) + (1 - splitPart) * Math.abs(feature2.start - feature2.end));
splitLink.target0.y = link.target0.y;
splitLink.target1.x = linkTarget;
splitLink.target1.y = link.target1.y;
linearLinkCoords.push(splitLink);
linearLinkCoords.push(link);
} else if ((feature2.start < feature2.end && link.target0.x > link.target1.x && that.filters.karyo.chromosomes[feature2.karyo].reverse === false) || (feature2.start < feature2.end && link.target0.x < link.target1.x && that.filters.karyo.chromosomes[feature2.karyo].reverse === true)) {
splitPart = (feature2.end + shift2 + karyo2.length) % karyo2.length / Math.abs(feature2.start - feature2.end);
linkSource = link.source1.x;
link.source1.x = linkSourceScale(feature1.start > feature1.end ? Math.min(feature1.start, feature1.end) + splitPart * Math.abs(feature1.start - feature1.end) : Math.min(feature1.start, feature1.end) + (1 - splitPart) * Math.abs(feature1.start - feature1.end));
link.target1.x = linkTargetScale(karyo2.length);
splitLink.source0.x = linkSourceScale(feature1.start > feature1.end ? Math.min(feature1.start, feature1.end) + splitPart * Math.abs(feature1.start - feature1.end) : Math.min(feature1.start, feature1.end) + (1 - splitPart) * Math.abs(feature1.start - feature1.end));
splitLink.source0.y = link.source0.y;
splitLink.source1.x = linkSource;
splitLink.source1.y = link.source1.y;
splitLink.target0.x = linkTargetScale(0);
splitLink.target0.y = link.target0.y;
splitLink.target1.x = linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length) === linkTargetScale(0) && !(feature2.start > feature2.end && feature2.end + shift2 === 0) ? linkTargetScale(karyo2.length) : linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length);
splitLink.target1.y = link.target1.y;
linearLinkCoords.push(splitLink);
linearLinkCoords.push(link);
} else if ((feature1.start > feature1.end && link.source0.x < link.source1.x && that.filters.karyo.chromosomes[feature1.karyo].reverse === false) || (feature1.start > feature1.end && link.source0.x > link.source1.x && that.filters.karyo.chromosomes[feature1.karyo].reverse === true)) {
splitPart = (feature1.start + shift1 + karyo1.length) % karyo1.length / Math.abs(feature1.start - feature1.end);
linkTarget = link.target1.x;
link.source1.x = linkSourceScale(0);
link.target1.x = linkTargetScale(feature2.start + splitPart * (Math.abs(feature2.start - feature2.end)));
splitLink.source0.x = linkSourceScale(karyo1.length);
splitLink.source0.y = link.source0.y;
splitLink.source1.x = linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length) === linkSourceScale(0) && !(feature1.start > feature1.end && feature1.end + shift1 === 0) ? linkSourceScale(karyo1.length) : linkSourceScale((feature1.end + shift1 + karyo1.length) % karyo1.length);
splitLink.source1.y = link.source1.y;
splitLink.target0.x = linkTargetScale(feature2.start + splitPart * (Math.abs(feature2.start - feature2.end)));
splitLink.target0.y = link.target0.y;
splitLink.target1.x = linkTarget;
splitLink.target1.y = link.target1.y;
linearLinkCoords.push(splitLink);
linearLinkCoords.push(link);
} else if ((feature2.start > feature2.end && link.target0.x < link.target1.x && that.filters.karyo.chromosomes[feature2.karyo].reverse === false) || (feature2.start > feature2.end && link.target0.x > link.target1.x && that.filters.karyo.chromosomes[feature2.karyo].reverse === true)) {
splitPart = (feature2.start + shift2 + karyo2.length) % karyo2.length / Math.abs(feature2.start - feature2.end);
linkSource = link.source1.x;
link.source1.x = linkSourceScale(feature1.start + splitPart * (Math.abs(feature1.start - feature1.end)));
link.target1.x = linkTargetScale(0);
splitLink.source0.x = linkSourceScale(feature1.start + splitPart * (Math.abs(feature1.start - feature1.end)));
splitLink.source0.y = link.source0.y;
splitLink.source1.x = linkSource;
splitLink.source1.y = link.source1.y;
splitLink.target0.x = linkTargetScale(karyo2.length);
splitLink.target0.y = link.target0.y;
splitLink.target1.x = linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length) === linkTargetScale(0) && !(feature2.start > feature2.end && feature2.end + shift2 === 0) ? linkTargetScale(karyo2.length) : linkTargetScale((feature2.end + shift2 + karyo2.length) % karyo2.length);
splitLink.target1.y = link.target1.y;
linearLinkCoords.push(splitLink);
linearLinkCoords.push(link);
} else {
linearLinkCoords.push(link);
}
});
linearLinkCoords = this.removeLinksOutsideVisibleRegion(linearLinkCoords, this.conf.linear.hideHalfVisibleLinks);
return linearLinkCoords;
};
/**
* This function draws the karyos in the linear layout, color them according to their genome_id and add some events to the chromosome.
* @author Markus Ankenbrand and Sonja Hohlfeld
* @param {Array} The array containing the coordinates as returned by getLinearKaryoCoords()
*/
AliTV.prototype.drawLinearKaryo = function(linearKaryoCoords) {
var that = this;
function dragEvent() {
that.svgD3.selectAll('.karyoGroup')
.attr("x", d3.event.x - parseInt(that.svgD3.selectAll('.karyoGroup').attr("width")) / 2)
.attr("y", d3.event.y - parseInt(that.svgD3.selectAll('.karyoGroup').attr("height")) / 2);
}
var drag = d3.behavior.drag()
.on("drag", dragEvent);
that.svgD3.selectAll(".karyoGroup").remove();
that.getAlignmentRegion().append("g")
.attr("class", "karyoGroup")
.selectAll("path")
.data(linearKaryoCoords)
.enter()
.append("rect")
.attr("class", "karyo")
.attr("id", function(d) {
return that.data.karyo.chromosomes[d.karyo].genome_id + ", " + d.karyo;
})
.attr("x", function(d) {
if (d.width < 0) {
return d.x + d.width;
} else {
return d.x;
}
})
.attr("y", function(d) {
return d.y;
})
.attr("width", function(d) {
return Math.abs(d.width);
})
.attr("height", function(d) {
return d.height;
})
.on("mouseover", function(g) {
that.fadeLinks(g, that.conf.graphicalParameters.fade);
})
.on("mouseout", function(g) {
that.fadeLinks(g, that.getLinkOpacity());
})
.on("click", function(g) {
that.changeChromosomeOrientation(g.karyo);
that.drawLinear();
})
.style("fill", function(d) {
return that.colorKaryoByGenomeId(that.data.karyo.chromosomes[d.karyo].genome_id);
})
.call(drag);
};
/**
* This function color links according to their identity and is called by drawLinearLinks within the style attribute
* It operates on the identity value of the links and therefore the identity should be assigned to the function
* The identity is assigned to a color which is used by the drawLinearLinks function, so the returned value is the RGB farbcode
* @author Sonja Hohlfeld
*/
AliTV.prototype.colorLinksByIdentity = function(identity) {
var that = this;
var linkIdentityDomain = [0, that.conf.minLinkIdentity, that.conf.midLinkIdentity, that.conf.maxLinkIdentity, 100];
var linkIdentityColorRange = [that.conf.minLinkIdentityColor, that.conf.minLinkIdentityColor, that.conf.midLinkIdentityColor, that.conf.maxLinkIdentityColor, that.conf.maxLinkIdentityColor];
var color = d3.scale.linear()
.domain(linkIdentityDomain)
.range(linkIdentityColorRange);
return color(identity);
};
/**
* This function color karyos according to their genome_id and is called by drawLinearKaryo within the style attribute
* It operates on the genome_id of the links and therefore the genome_id should be assigned to the function
* The genome_id is assigned to a color which is used by the drawLinearKaryo function, so the returned value is the RGB farbcode
* @author Sonja Hohlfeld
*/
AliTV.prototype.colorKaryoByGenomeId = function(genomeId) {
var that = this;
var genomeOrder = [0, (that.filters.karyo.genome_order.length - 1)];
var colorRange = [that.conf.linear.startLineColor, that.conf.linear.endLineColor];
var color = d3.scale.linear()
.domain(genomeOrder)
.range(colorRange);
return color(that.filters.karyo.genome_order.indexOf(genomeId));
};
/**
* This function calculates the tick coords and operates on the chromosomes and need the length in bp and the width in px of the karyo.
* @author Sonja Hohlfeld
* @param {Array} The array containing the coordinates as returned by getLinearKaryoCoords()
* @return {Array} The array containing the tick coordinates as shown in the following example.
* Ticks for one chromosome have to be adjacent and in the order from start to end.
* @example linearTickCoords = [{id: 'c1', x1: 0, x2: 0, y1: 860, y2: 910}, {id: 'c2', x1: 0, x2: 0, y1: 660, y2:710}]
*/
AliTV.prototype.getLinearTickCoords = function(karyoCoords) {
var that = this;
var linearTickCoords = [];
$.each(karyoCoords, function(key, value) {
var ticks = [];
var shift;
shift = (that.filters.karyo.chromosomes[value.karyo].offset === undefined ? 0 : that.filters.karyo.chromosomes[value.karyo].offset);
var scale = d3.scale.linear()
.domain([0, that.data.karyo.chromosomes[value.karyo].length])
.range([value.x, value.x + value.width]);
var chromosomePosition = 0;
for (var i = 0; chromosomePosition < that.data.karyo.chromosomes[value.karyo].length; i++) {
var currentTick = scale((chromosomePosition + shift + that.data.karyo.chromosomes[value.karyo].length) % that.data.karyo.chromosomes[value.karyo].length);
chromosomePosition += that.conf.graphicalParameters.tickDistance;
var coords = {};
coords.id = value.karyo;
coords.x1 = currentTick;
coords.x2 = currentTick;
if (i % that.conf.graphicalParameters.tickLabelFrequency === 0 && (that.conf.labels.ticks.showTickLabels === true)) {
coords.y1 = value.y - 10;
coords.y2 = value.y + value.height + 10;
} else {
coords.y1 = value.y - 5;
coords.y2 = value.y + value.height + 5;
}
linearTickCoords.push(coords);
}
});
return linearTickCoords;
};
/**
* This function draw the ticks in the linear layout.
* @author Sonja Hohlfeld
* @param {Array} The array containing the coordinates as returned by getLinearTickCoords()
*/
AliTV.prototype.drawLinearTicks = function(linearTickCoords) {
var that = this;
this.getAlignmentRegion().selectAll(".tickGroup").remove();
that.getAlignmentRegion().append("g")
.attr("class", "tickGroup")
.selectAll("path")
.data(linearTickCoords)
.enter()
.append("line")
.attr("class", "tick")
.attr("x1", function(d) {
return d.x1;
})
.attr("y1", function(d) {
return d.y1;
})
.attr("x2", function(d) {
return d.x2;
})
.attr("y2", function(d) {
return d.y2;
})
.style("stroke", that.getTickLabelColor());
};
/**
* This method is supposed to label the ticks with configurable tick labels.
* @author Sonja Hohlfeld
* @param linearTickCoords
*/
AliTV.prototype.drawLinearTickLabels = function(linearTickCoords) {
var that = this;
var lastID = '';
var counter = 0;
var filteredLinearTickCoords = [];
$.each(linearTickCoords, function(key, value) {
if (lastID !== value.id) {
lastID = value.id;
counter = 0;
}
if (counter % that.conf.graphicalParameters.tickLabelFrequency === 0) {
var myValue = {};
$.extend(true, myValue, value);
myValue.counter = counter;
filteredLinearTickCoords.push(myValue);
}
counter++;
});
var labels = that.getAlignmentRegion().append("g")
.attr("class", "tickLabelGroup")
.selectAll("path")
.data(filteredLinearTickCoords)
.enter();
labels.append("text")
.attr("class", "tickLabel")
.attr("x", function(d) {
return d.x1 - 3;
})
.attr("y", function(d) {
return d.y1;
})
.text(function(d) {
return d.counter * that.conf.graphicalParameters.tickDistance + " bp";
})
.attr("font-size", that.getTickLabelSize() + "px")
.attr("fill", that.getTickLabelColor());
labels.append("text")
.attr("class", "tickLabel")
.attr("x", function(d) {
return d.x2 - 3;
})
.attr("y", function(d) {
return d.y2 + 6;
})
.text(function(d) {
return d.counter * that.conf.graphicalParameters.tickDistance + " bp";
})
.attr("font-size", that.getTickLabelSize() + "px")
.attr("fill", that.getTickLabelColor());
};
/**
* This function is called by a mouse event.
* If the mouse pointer enters the area of a chromosome all links should be faded out except the the links of the chromosome the mouse points to.
* If the mouse pointer leaves the area of a chromosome all links should be faded in.
* @param {Number} The opacity value is a number between 0 and 1 and indicates the degree of the colored link opacity.
*/
AliTV.prototype.fadeLinks = function(g, opacity) {
var that = this;
that.svgD3.selectAll(".link")
.filter(function(d) {
return that.data.features.link[that.visibleLinks[d.linkID].source].karyo != g.karyo && that.data.features.link[that.visibleLinks[d.linkID].target].karyo != g.karyo;
})
.transition()
.style("opacity", opacity);
};
/**
* This function draws adjacent links in the linear layout
* @author Sonja Hohlfeld
* @param {Array} The array linearLinkCoords containing the coordinates of all links as returned by getLinearLinkCoords()
*/
AliTV.prototype.drawLinearLinks = function(linearLinkCoords) {
var that = this;
var coordsToPath = function(link) {
var diagonal = d3.svg.diagonal().source(function(d) {
return d.source;
}).target(function(d) {
return d.target;
});
var path1 = diagonal({
source: link.source0,
target: link.target0
});
var path2 = diagonal({
source: link.target1,
target: link.source1
}).replace(/^M/, 'L');
var shape = path1 + path2 + 'Z';
return shape;
};
this.getAlignmentRegion().selectAll(".linkGroup").remove();
this.getAlignmentRegion().append("g")
.attr("class", "linkGroup")
.selectAll("path")
.data(linearLinkCoords)
.enter()
.append("path")
.attr("class", "link")
.attr("id", function(d) {
return d.linkID;
})
.attr("d", coordsToPath)
.style("fill", function(d) {
return that.colorLinksByIdentity(that.visibleLinks[d.linkID].identity);
})
.style("opacity", that.getLinkOpacity())
.style("display", function(d) {
if (d.linkID in that.filters.links.invisibleLinks) {
return "none";
}
});
};
/**
* This function draws the data in the linear layout.
* It operates on the data of the object and therefore needs no parameters.
* It draws directly on the svg and therefore has no return value.
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
*/
AliTV.prototype.drawLinear = function() {
this.clearAli();
this.getAlignmentRegion().remove();
this.getAlignmentRegion();
var karyoCoords = this.getLinearKaryoCoords();
var linearTickCoords = this.getLinearTickCoords(karyoCoords);
this.drawLinearTicks(linearTickCoords);
this.drawLinearKaryo(karyoCoords);
var linkCoords = this.getLinearLinkCoords(karyoCoords);
this.drawLinearLinks(linkCoords);
this.drawFeatureLegend();
this.drawLinkIdentityLegend();
if (this.conf.labels.ticks.showTickLabels === true) {
this.drawLinearTickLabels(linearTickCoords);
}
if (this.conf.labels.genome.showGenomeLabels === true) {
var linearGenomeLabelCoords = this.getGenomeLabelCoords();
this.drawLinearGenomeLabels(linearGenomeLabelCoords);
this.setSvgWidth(this.conf.graphicalParameters.canvasWidth + this.conf.graphicalParameters.genomeLabelWidth);
}
var linearFeatureCoords = this.getLinearFeatureCoords(karyoCoords);
this.drawLinearFeatures(linearFeatureCoords);
if (this.conf.labels.features.showFeatureLabels === true) {
var linearFeatureLabelCoords = this.getFeatureLabelCoords(linearFeatureCoords);
this.drawLinearFeatureLabels(linearFeatureLabelCoords);
}
if (this.conf.tree.drawTree === true && this.hasTree() === true) {
this.drawPhylogeneticTree();
this.setSvgWidth(this.conf.graphicalParameters.canvasWidth + this.conf.graphicalParameters.treeWidth);
}
if (this.conf.tree.drawTree === true && this.conf.labels.genome.showGenomeLabels) {
this.setSvgWidth(this.conf.graphicalParameters.canvasWidth + this.conf.graphicalParameters.treeWidth + this.conf.graphicalParameters.genomeLabelWidth);
}
if (this.conf.tree.drawTree === true && this.conf.tree.orientation === "left") {
this.getAlignmentRegion().attr("transform", "translate(" + this.conf.graphicalParameters.treeWidth + ", 0)");
}
if (this.conf.tree.drawTree === true && this.conf.tree.orientation === "right" && this.conf.labels.genome.showGenomeLabels === false) {
this.svgD3.selectAll(".treeGroup").attr("transform", "translate(" + this.conf.graphicalParameters.canvasWidth + ", 0)");
}
if (this.conf.labels.genome.showGenomeLabels === true) {
this.getAlignmentRegion().attr("transform", "translate(" + this.conf.graphicalParameters.genomeLabelWidth + ", 0)");
if (this.conf.tree.drawTree === true && this.conf.tree.orientation === "right") {
this.svgD3.selectAll(".treeGroup").attr("transform", "translate(" + (this.conf.graphicalParameters.canvasWidth + this.conf.graphicalParameters.genomeLabelWidth) + ", 0)");
}
}
if ((this.conf.labels.genome.showGenomeLabels === true) && this.conf.tree.drawTree === true && this.conf.tree.orientation === "left") {
this.getAlignmentRegion().attr("transform", "translate(" + (this.conf.graphicalParameters.treeWidth + this.conf.graphicalParameters.genomeLabelWidth) + ", 0)");
}
// move feature legend below the alignment area and adjust svg height
this.getLegendRegion().attr("transform", "translate(0," + this.conf.graphicalParameters.canvasHeight + ")");
if (typeof this.getLegendRegion().node().getBBox !== "undefined") {
this.setSvgHeight(this.conf.graphicalParameters.canvasHeight + this.getLegendRegion().node().getBBox().height + this.getLegendRegion().node().getBBox().y);
}
this.conf.layout = "linear";
};
/**
* Calculates coordinates for the chromosomes to draw in the circular layout.
* This function operates on the data property of the object and therefore needs no parameters.
* This function is primarily meant for internal usage, the user should not need to call this directly.
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
* @returns {Array} Array containing one Object for each element in data.karyo of the form {karyo: 'karyo_name', startAngle:0, endAngle:1}
*/
AliTV.prototype.getCircularKaryoCoords = function() {
var circularKaryoCoords = [];
var total = 0;
var spacer = this.conf.graphicalParameters.karyoDistance;
var current = -spacer;
$.each(this.data.karyo.chromosomes, function(key, value) {
total += value.length + spacer;
});
for (var i = 0; i < this.filters.karyo.order.length; i++) {
var key = this.filters.karyo.order[i];
var value = this.data.karyo.chromosomes[key];
var data = {
"karyo": key,
"startAngle": ((current + spacer) / total) * (2 * Math.PI),
};
current += value.length + spacer;
data.endAngle = (current / total) * (2 * Math.PI);
if (this.filters.karyo.chromosomes[key].reverse === true) {
var startAngle = data.startAngle;
var endAngle = data.endAngle;
data.startAngle = endAngle;
data.endAngle = startAngle;
}
circularKaryoCoords.push(data);
}
return circularKaryoCoords;
};
/**
* Calculate coordinates for the links to draw in the cirular layout and uses link-data and karyo-coordinates
* This function is primarily meant for internal usage, the user should not need to call this directly
* @author Markus Ankenbrand
* @param {Array} The array containing the coordinates as returned by getCircularKaryoCoords()
* @returns {Array} Returns an Array which is presented in the following example
* @example [
* {"linkID": "l1", "source": {"startAngle":1, "endAngle":3}, "target": {"startAngle":4, "endAngle":6}}
* ]
*/
AliTV.prototype.getCircularLinkCoords = function(coords) {
var circularLinkCoords = [];
if (typeof coords === 'undefined') {
return circularLinkCoords;
}
var that = this;
var karyoMap = {};
$.each(coords, function(key, value) {
karyoMap[value.karyo] = key;
});
var visibleChromosomes = that.filterChromosomes();
var visibleLinks = that.filterLinks(visibleChromosomes);
$.each(visibleLinks, function(key, value) {
var link = {};
link.linkID = key;
var feature1 = that.data.features.link[value.source];
var feature2 = that.data.features.link[value.target];
var karyo1 = that.data.karyo.chromosomes[feature1.karyo];
var karyo2 = that.data.karyo.chromosomes[feature2.karyo];
var karyo1Coords = coords[karyoMap[feature1.karyo]];
var karyo2Coords = coords[karyoMap[feature2.karyo]];
var sourceScale = d3.scale.linear().domain([0, karyo1.length]).range([karyo1Coords.startAngle, karyo1Coords.endAngle]);
var targetScale = d3.scale.linear().domain([0, karyo2.length]).range([karyo2Coords.startAngle, karyo2Coords.endAngle]);
link.source = {
startAngle: sourceScale(feature1.start),
endAngle: sourceScale(feature1.end)
};
link.target = {
startAngle: targetScale(feature2.start),
endAngle: targetScale(feature2.end)
};
circularLinkCoords.push(link);
});
return circularLinkCoords;
};
/**
* This function calculates the coordinates (angles) for the ticks in the circular layout
* @author Markus Ankenbrand
* @param {Array} The array containing the coordinates as returned by getCircularKaryoCoords()
* @returns {Array} Returns an Array of angles
*/
AliTV.prototype.getCircularTickCoords = function(coords) {
var that = this;
var circularTickCoords = [];
$.each(coords, function(key, value) {
var karyoLength = that.data.karyo.chromosomes[value.karyo].length;
var baseToAngle = d3.scale.linear().domain([0, karyoLength]).range([value.startAngle, value.endAngle]);
var chromosomePosition = 0;
while (chromosomePosition <= karyoLength) {
circularTickCoords.push(baseToAngle(chromosomePosition));
chromosomePosition += that.conf.graphicalParameters.tickDistance;
}
});
return circularTickCoords;
};
/**
* This function draws the karyos in the circular layout, color them according to their genome_id and add some eventHandlers.
* @author Markus Ankenbrand
* @param {Array} The array containing the coordinates as returned by getCircularKaryoCoords()
*/
AliTV.prototype.drawCircularKaryo = function(coords) {
var that = this;
this.svgD3.selectAll(".karyoGroup").remove();
var outerRadius = this.getOuterRadius();
this.svgD3.append("g")
.attr("class", "karyoGroup")
.attr("transform", "translate(" + this.conf.graphicalParameters.canvasWidth / 2 + "," + this.conf.graphicalParameters.canvasHeight / 2 + ")")
.selectAll("path")
.data(coords)
.enter()
.append("path")
.attr("d", d3.svg.arc().innerRadius(outerRadius - this.conf.graphicalParameters.karyoHeight).outerRadius(outerRadius))
.attr("class", "karyo")
.style("fill", function(d) {
return that.colorKaryoByGenomeId(that.data.karyo.chromosomes[d.karyo].genome_id);
})
.on("mouseover", function(g) {
that.fadeLinks(g, 0.1);
})
.on("mouseout", function(g) {
that.fadeLinks(g, 1);
})
.on("click", function(g) {
that.filters.karyo.chromosomes[g.karyo].reverse = !that.filters.karyo.chromosomes[g.karyo].reverse;
that.drawCircular();
});
};
/**
* This function draws the ticks to the karyos in the circular layout
* @author Markus Ankenbrand
* @param {Array} The array containing the coordinates as returned by getCircularTickCoords()
*/
AliTV.prototype.drawCircularTicks = function(coords) {
var that = this;
that.svgD3.selectAll(".tickGroup").remove();
that.svgD3.append("g")
.attr("class", "tickGroup")
.attr("transform", "translate(" + this.conf.graphicalParameters.canvasWidth / 2 + "," + this.conf.graphicalParameters.canvasHeight / 2 + ")")
.selectAll("path")
.data(coords)
.enter()
.append("path")
.attr("d", function(d) {
var startPoint = d3.svg.line.radial()([
[that.getOuterRadius() + that.conf.circular.tickSize, d]
]);
var endPoint = d3.svg.line.radial()([
[that.getOuterRadius(), d]
]);
endPoint = endPoint.replace(/^M/, 'L');
return startPoint + endPoint + "Z";
})
.style("stroke", "#000");
};
/**
* This function draws links in the circular layout
* @author Markus Ankenbrand
* @param {Array} The array circularLinkCoords containing the coordinates of all links as returned by getCircularLinkCoords()
*/
AliTV.prototype.drawCircularLinks = function(circularLinkCoords) {
var that = this;
this.svgD3.selectAll(".linkGroup").remove();
this.svgD3.append("g")
.attr("class", "linkGroup")
.attr("transform", "translate(" + this.conf.graphicalParameters.canvasWidth / 2 + "," + this.conf.graphicalParameters.canvasHeight / 2 + ")")
.selectAll("path")
.data(circularLinkCoords)
.enter()
.append("path")
.attr("class", "link")
.attr("d", d3.svg.chord().radius(this.getOuterRadius() - this.conf.graphicalParameters.karyoHeight - this.conf.graphicalParameters.linkKaryoDistance))
.style("fill", function(d) {
return that.colorLinksByIdentity(that.visibleLinks[d.linkID].identity);
});
};
/**
* This function draws the data in the circular layout.
* It operates on the data of the object and therefore needs no parameters.
* It draws directly on the svg and therefore has no return value.
* @author Markus Ankenbrand <markus.ankenbrand@uni-wuerzburg.de>
*/
AliTV.prototype.drawCircular = function() {
this.clearAli();
var karyoCoords = this.getCircularKaryoCoords();
var tickCoords = this.getCircularTickCoords(karyoCoords);
this.drawCircularTicks(tickCoords);
this.drawCircularKaryo(karyoCoords);
var linkCoords = this.getCircularLinkCoords(karyoCoords);
this.drawCircularLinks(linkCoords);
this.conf.layout = "circular";
this.triggerChange();
};
/**
* This function returns the information of the spacer between two chromosomes which is set in the configuration.
* @returns {Number} The actual spacer.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getKaryoSpacer = function() {
return this.conf.graphicalParameters.karyoDistance;
};
/**
* This function replaces the old spacer with the new spacer in the config-object.
* It is called by a blur()-event, when the decription field loses focus.
* When the method gets a wrong spacer it throws an error message.
* @param {Number} The function gets the spacer which can be set by the user.
* @throws Will throw an error if the argument is Sorry, you entered an empty value. Please try it again..
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setKaryoSpacer = function(spacer) {
if (spacer === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(spacer)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (spacer <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
this.conf.graphicalParameters.karyoDistance = spacer;
this.triggerChange();
return this.conf.graphicalParameters.karyoDistance;
}
};
/**
* This function returns the height of the chromosomes between two genomes which is set in the configuration.
* @returns {Number} The actual height of chromosomes.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getKaryoHeight = function() {
return this.conf.graphicalParameters.karyoHeight;
};
/**
* This function replaces the old height of the chromosomes with the new value in the config-object.
* It is called by a blur()-event, when the decription field loses focus.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the height of chromosomes which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setKaryoHeight = function(height) {
if (height === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(height)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (height <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
height = Number(height);
this.conf.graphicalParameters.karyoHeight = height;
this.triggerChange();
return this.conf.graphicalParameters.karyoHeight;
}
};
/**
* This function returns the width of the svg drawing area.
* @returns {Number} The width of canvas.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getCanvasWidth = function() {
return this.conf.graphicalParameters.canvasWidth;
};
/**
* This function replaces the old width of the drawing area for the alignment.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the width of the svg drawing area which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setCanvasWidth = function(width) {
if (width === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(width)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (width <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
width = Number(width);
this.conf.graphicalParameters.canvasWidth = width;
this.triggerChange();
return this.conf.graphicalParameters.canvasWidth;
}
};
/**
* This function returns the height of the svg drawing area.
* @returns {Number} The height of canvas.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getCanvasHeight = function() {
return this.conf.graphicalParameters.canvasHeight;
};
/**
* This function replaces the old height of the drawing area for the alignment.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the height of the svg drawing area which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setCanvasHeight = function(height) {
if (height === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(height)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (height <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
height = Number(height);
this.conf.graphicalParameters.canvasHeight = height;
this.triggerChange();
return this.conf.graphicalParameters.canvasHeight;
}
};
/**
* This function returns the distance of the chromosome ticks in bp.
* @returns {Number} The tick distance in bp.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getTickDistance = function() {
var json = this.getJSON();
return json.conf.graphicalParameters.tickDistance;
};
/**
* This function replaces the old distance between ticks with the new distance in the config-object.
* It is called by a blur()-event, when the decription field loses focus.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the distance between ticks which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setTickDistance = function(distance) {
if (distance === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(distance)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (distance <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
distance = Number(distance);
this.conf.graphicalParameters.tickDistance = distance;
this.triggerChange();
return this.conf.graphicalParameters.tickDistance;
}
};
/**
* This function returns the current layout.
* @returns {String} The current layout: linear or circular.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getLayout = function() {
return this.conf.layout;
};
/**
* This function should draw the equal layout according to the current layout.
* @param {String} The current layout, this means circular or linear.
* @author Sonja Hohlfeld
*/
AliTV.prototype.drawEqualLayout = function(layout) {
if (layout === "linear") {
this.drawLinear();
return this.conf.layout;
} else {
this.drawCircular();
return this.conf.layout;
}
};
/**
* This function returns the current width of the phylogenetic tree.
* @returns {Number} The current tree width.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getTreeWidth = function() {
return this.conf.graphicalParameters.treeWidth;
};
/**
* This function replaces the old tree width with the new tree width in the config-object.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the width of a phylogenetic tree which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setTreeWidth = function(treeWidth) {
if (treeWidth === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(treeWidth)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (treeWidth <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
treeWidth = Number(treeWidth);
this.conf.graphicalParameters.treeWidth = treeWidth;
this.triggerChange();
return this.conf.graphicalParameters.treeWidth;
}
};
/**
* This function calculates the appropriate outerRadius of the circular layout for the current svg dimensions.
* @returns {Number} outerRadius - the outer radius in px
* @author Markus Ankenbrand
*/
AliTV.prototype.getOuterRadius = function() {
var outerRadius = 0.45 * Math.min(this.getCanvasHeight(), this.getCanvasWidth());
return outerRadius;
};
/**
* This function calculates the appropriate genomeDistance of the linear layout for the current svg height.
* @returns {Number} genomeDistance - the distance between genomes in the linear layout.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getGenomeDistance = function() {
var genomeDistance = (this.getCanvasHeight() - this.getKaryoHeight()) / (this.filters.karyo.genome_order.length - 1);
return Math.round(genomeDistance);
};
/**
* This function returns the current frequency of tick labels.
* @returns {Number} Returns the frequency of the tick labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getTickLabelFrequency = function() {
var tickLabelFrequency = this.conf.graphicalParameters.tickLabelFrequency;
return tickLabelFrequency;
};
/**
* This function replaces the old frequency of tick labels with the new tick label frequency in the config-object.
* @param tickLabelFrequency: the frequency of tick labels which is returned by getTickLabelFrequency.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setTickLabelFrequency = function(tickLabelFrequency) {
if (tickLabelFrequency === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(tickLabelFrequency)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (tickLabelFrequency <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
tickLabelFrequency = Number(tickLabelFrequency);
this.conf.graphicalParameters.tickLabelFrequency = tickLabelFrequency;
this.triggerChange();
return this.conf.graphicalParameters.tickLabelFrequency;
}
};
/**
* This function returns the specified property of the given supported feature.
* @param {String} groupId - The group ID of the desired supported feature.
* @param {String} property - The desired property of the feature (e.g. color, form, ...).
* @throws Will throw an error if the feature groupId is not supported.
* @throws Will throw an error if the property is undefined in the feature group.
* @returns {String} The value of the property of the given supported feature.
* @author Markus Ankenbrand
*/
AliTV.prototype.getFeatureProperty = function(groupId, property) {
if (typeof this.conf.features.supportedFeatures[groupId] === 'undefined') {
throw "Not a supported feature.";
}
if (typeof this.conf.features.supportedFeatures[groupId][property] === 'undefined') {
throw "Not a supported property.";
}
var prop = this.conf.features.supportedFeatures[groupId][property];
return prop;
};
/**
* This function replaces the old color of the specified supported feature with the new color in the config-object.
* @param {String} groupId - the supported feature groupId for which the color should be set.
* @param {String} property - The desired property of the feature (e.g. color, form, ...).
* @param value: the new value for the property of the supported feature.
* @throws Will throw an error if the feature is not supported.
* @throws Will throw an error if the argument is empty.
* @author Markus Ankenbrand
*/
AliTV.prototype.setFeatureProperty = function(groupId, property, val) {
if (typeof this.conf.features.supportedFeatures[groupId] === "undefined") {
throw "Not a supported feature.";
}
if (val === "") {
throw "Sorry, you entered an empty value. Please try it again.";
}
this.conf.features.supportedFeatures[groupId][property] = val;
this.triggerChange();
return this.conf.features.supportedFeatures[groupId][property];
};
/**
* This function returns an array which contains the color of the first and the last genome.
* The colors are defined in the conf-object.
* @returns {Array} The color of the first and the last genome.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getGenomeColor = function() {
var color = [];
color.push(this.conf.linear.startLineColor);
color.push(this.conf.linear.endLineColor);
return color;
};
/**
* This function replaces the old colors of the start genome and the end genome with the new ones.
* @param color: the array contains the startLineColor and the endLineColor which is returned by getGenomeColor.
* @throws Will throw an error if the argument is empty.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setGenomeColor = function(color) {
var newColor = [];
if (color === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else {
this.conf.linear.startLineColor = color[0];
this.conf.linear.endLineColor = color[1];
newColor.push(this.conf.linear.startLineColor);
newColor.push(this.conf.linear.endLineColor);
this.triggerChange();
return newColor;
}
};
/**
* This function returns an array which contains the color of the minLinkIdentity and the maxLinkIdentity.
* The colors are defined in the conf-object.
* @returns {Array} The color of the links with the minimal and maximal identity.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getLinkColor = function() {
var color = [];
color.push(this.conf.minLinkIdentityColor);
color.push(this.conf.midLinkIdentityColor);
color.push(this.conf.maxLinkIdentityColor);
return color;
};
/**
* This function replaces the old colors for the minimal and maximal link identity by the new ones.
* @param color: the array contains the minLinkIdentityColor and the maxLinkIdentityColor which is returned by getLinkColor.
* @throws Will throw an error if the argument is empty.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setLinkColor = function(color) {
var newColor = [];
if (color === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else {
this.conf.minLinkIdentityColor = color[0];
this.conf.midLinkIdentityColor = color[1];
this.conf.maxLinkIdentityColor = color[2];
newColor.push(this.conf.minLinkIdentityColor);
newColor.push(this.conf.midLinkIdentityColor);
newColor.push(this.conf.maxLinkIdentityColor);
this.triggerChange();
return newColor;
}
};
/**
* This method should call other filter functions in order to filter the visible chromosomes.
* @returns visibleChromosomes: returns only chromosomes which are visible
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterChromosomes = function() {
var visibleChromosomes = this.data.karyo.chromosomes;
if (this.filters.showAllChromosomes === false) {
visibleChromosomes = this.filterVisibleChromosomes(visibleChromosomes);
} else {
return visibleChromosomes;
}
if (this.filters.skipChromosomesWithoutVisibleLinks === true) {
visibleChromosomes = this.filterChromosomeWithoutVisibleLinks(visibleChromosomes);
}
return visibleChromosomes;
};
/**
*This method should filter all chromosome which are set visible in conf.filters.karyo.chromosomes[<chromosome>].visible
* @param visibleChromosomes: the method gets all current visible chromosomes.
* @returns filteredChromosomes: the method returns only chromosomes whose visibility is set true
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterVisibleChromosomes = function(visibleChromosomes) {
var that = this;
var filteredChromosomes = {};
$.each(visibleChromosomes, function(key, value) {
if (that.filters.karyo.chromosomes[key].visible === true) {
filteredChromosomes[key] = value;
}
});
return filteredChromosomes;
};
/**
* This method should filter all chromosome which have no visible links with the current configurations
* @param visibleChromosomes: the method gets all current visible chromosomes.
* @returns filteredChromosomes: the method returns only chromosomes which have visible links
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterChromosomeWithoutVisibleLinks = function(visibleChromosomes) {
var that = this;
var filteredChromosomes = {};
var filteredLinks = that.filterLinks(visibleChromosomes);
$.each(visibleChromosomes, function(key, value) {
var currentChromosome = key;
var valueOfCurrentChromosome = value;
$.each(filteredLinks, function(key, value) {
if (that.data.features.link[value.source].karyo === currentChromosome && (currentChromosome in filteredChromosomes) === false || that.data.features.link[value.target].karyo === currentChromosome && (currentChromosome in filteredChromosomes) === false) {
filteredChromosomes[currentChromosome] = valueOfCurrentChromosome;
}
});
});
return filteredChromosomes;
};
/**
* This method is supposed to filter the order of chromosomes according to all visible chromosomes.
* @param visibleChromosomes: gets all visible chromosomes
* @return chromosomeOrder: returns the order of the visible chromosomes
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterChromosomeOrder = function(visibleChromosomes) {
var orderOfVisibleChromosomes = [];
var keysOfVisibleChromosomes = [];
$.each(visibleChromosomes, function(key, value) {
keysOfVisibleChromosomes.push(key);
});
$.each(this.filters.karyo.order, function(key, value) {
if (keysOfVisibleChromosomes.indexOf(value) !== -1) {
orderOfVisibleChromosomes.push(value);
}
});
return orderOfVisibleChromosomes;
};
/**
* This method should call functions in order to filter the links.
* The nested link data structure with genome_ids as keys is returned as a flat structure with link_ids as keys.
* The filteredLinks are also saved as an object property.
* @param visibleChromosomes: gets the chromosomes which are visible in the current configurations.
* @returns visibleLinks: return all links which are visible
* @author Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.filterLinks = function(visibleChromosomes) {
this.visibleLinks = this.filterLinksByAdjacency();
this.visibleLinks = this.filterVisibleLinks(this.visibleLinks, visibleChromosomes);
this.visibleLinks = this.filterLinksByIdentity(this.visibleLinks);
this.visibleLinks = this.filterLinksByLength(this.visibleLinks);
return this.visibleLinks;
};
/**
* This method should filter the visible links according to visible chromosomes
* @param visibleLinks: contains all currently visible links.
* @param visibleChromosomes: contains the chromosomes, which are visible in the current configurations in order to filter all links, which have no target or source chromosome.
* @return visibleLinks: returns only links which source or target are in visible chromosomes
* @author Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.filterVisibleLinks = function(visibleLinks, visibleChromosomes) {
var that = this;
var filteredLinks = {};
var listOfVisibleChromosomes = [];
$.each(visibleChromosomes, function(key, value) {
listOfVisibleChromosomes.push(key);
});
$.each(visibleLinks, function(key, value) {
var targetKaryo = that.data.features.link[value.target].karyo;
var sourceKaryo = that.data.features.link[value.source].karyo;
if (listOfVisibleChromosomes.indexOf(targetKaryo) !== -1 && listOfVisibleChromosomes.indexOf(sourceKaryo) !== -1 && (value in filteredLinks) === false) {
filteredLinks[key] = value;
}
});
return filteredLinks;
};
/**
* This method should filter links according to their identity.
* @returns filteredLinks: return all links which are visible with the current configuration.
* @param visibleLinks: gets all current visible links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterLinksByIdentity = function(visibleLinks) {
var minIdentity = this.filters.links.minLinkIdentity;
var maxIdentity = this.filters.links.maxLinkIdentity;
var filteredLinks = {};
$.each(visibleLinks, function(key, value) {
var currentLink = value;
if (currentLink.identity >= minIdentity && currentLink.identity <= maxIdentity) {
filteredLinks[key] = currentLink;
}
});
return filteredLinks;
};
/**
* This method should filter links according to their length.
* @returns filteredLinks: return all links which are visible with the current configuration.
* @param visibleLinks: gets all current visible links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.filterLinksByLength = function(visibleLinks) {
var minLength = this.filters.links.minLinkLength;
var maxLength = this.filters.links.maxLinkLength;
var that = this;
var filteredLinks = {};
$.each(visibleLinks, function(key, value) {
var currentLink = value;
var sourceFeature = currentLink.source;
var targetFeature = currentLink.target;
var lengthOfSourceFeature = Math.abs(that.data.features.link[sourceFeature].end - that.data.features.link[sourceFeature].start);
var lengthOfTargetFeature = Math.abs(that.data.features.link[targetFeature].end - that.data.features.link[targetFeature].start);
if (lengthOfSourceFeature >= minLength && lengthOfSourceFeature <= maxLength || lengthOfTargetFeature >= minLength && lengthOfTargetFeature <= maxLength) {
filteredLinks[key] = currentLink;
}
});
return filteredLinks;
};
/**
* This method should filter links according to their adjacency (if the according option is set and the layout is linear).
* The nested link data structure with genome_ids as keys is returned as a flat structure with link_ids as keys.
* @return filteredLinks: returns only links which are between chromosomes of adjacent genomes (if needed) in a flat link object.
* @author Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.filterLinksByAdjacency = function() {
var that = this;
var filteredLinks = {};
if (this.filters.onlyShowAdjacentLinks === true && this.conf.layout === 'linear') {
for (var i = 0; i < that.filters.karyo.genome_order.length - 1; i++) {
var genome0 = that.filters.karyo.genome_order[i];
var genome1 = that.filters.karyo.genome_order[i + 1];
var links01 = ((typeof that.data.links[genome0] === 'undefined' || typeof that.data.links[genome0][genome1] === 'undefined') ? {} : that.data.links[genome0][genome1]);
var links10 = ((typeof that.data.links[genome1] === 'undefined' || typeof that.data.links[genome1][genome0] === 'undefined') ? {} : that.data.links[genome1][genome0]);
filteredLinks = $.extend(filteredLinks, links01);
filteredLinks = $.extend(filteredLinks, links10);
}
} else {
// combine all links into a single object
$.each(that.data.links, function(key, value) {
$.each(value, function(k, v) {
if (key !== k || that.filters.showIntraGenomeLinks) {
filteredLinks = $.extend(filteredLinks, v);
}
});
});
}
return filteredLinks;
};
/**
* This method is supposed to draw a phylogenetic tree next to the chromosomes.
* In the default configuration the tree is not drawn, but the user can set drawTree equal true and this method wil be called.
* @author {Sonja Hohlfeld}
*/
AliTV.prototype.drawPhylogeneticTree = function() {
var that = this;
var treeData;
try {
treeData = this.rotateTreeToGenomeOrder();
} catch (err) {
that.svgD3.append("g")
.attr("class", "treeGroup")
.append("text")
.attr("class", "treeWarningLabel")
.attr("x", that.conf.graphicalParameters.treeWidth / 2)
.attr("y", that.conf.graphicalParameters.canvasHeight / 2)
.text("WARNING: Genome order not concordant with tree")
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "#ff0000")
.attr("transform", "rotate(-90, " + (that.conf.graphicalParameters.treeWidth / 2) + "," + (that.conf.graphicalParameters.canvasHeight / 2) + ")")
.style("text-anchor", "middle");
return;
}
// Create a tree "canvas"
var genomeDistance = that.getGenomeDistance();
//Initialize the tree size. Every node of the tree has its own "spacer", therefore it is important not only use the canvas height, but you need
// the canveas height and the genome distance - the heigth of one karyo in order to draw the branches in the right position. So we have exactly 6 branches, but one is not in the drawing area.
var tree = d3.layout.tree()
.size([that.conf.graphicalParameters.canvasHeight + genomeDistance - that.conf.graphicalParameters.karyoHeight, that.conf.graphicalParameters.treeWidth])
.separation(function() {
return 1;
});
// Preparing the data for the tree layout, convert data into an array of nodes
var nodes = tree.nodes(treeData);
// Create an array with all the links
var links = tree.links(nodes);
//Now you want to draw every branch in the middle of a chromosome. Therefore you must move it the negative half of a chromosome height and negative the half of the genome distance in y direction.
if (this.conf.tree.orientation === "left") {
that.svgD3.append("g")
.attr("class", "treeGroup")
.attr("style", "fill:none;stroke:#000;stroke-width:2px;")
.selectAll("path")
.data(links)
.enter()
.append("path")
.attr("class", "branch")
.attr("d", function(d) {
return "M" + d.source.y + "," + d.source.x + "H" + d.target.y + "V" + d.target.x;
})
.attr("transform", "translate(0, " + 0.5 * (that.conf.graphicalParameters.karyoHeight - genomeDistance) + ")");
} else {
that.svgD3.append("g")
.attr("class", "treeGroup")
.attr("style", "fill:none;stroke:#000;stroke-width:2px;")
.attr("transform", "translate(" + that.conf.graphicalParameters.canvasWidth + ", 0)")
.selectAll("path")
.data(links)
.enter()
.append("path")
.attr("class", "branch")
.attr("d", function(d) {
return "M" + (that.conf.graphicalParameters.treeWidth - d.source.y) + "," + d.source.x + "H" + (that.conf.graphicalParameters.treeWidth - d.target.y) + "V" + d.target.x;
})
.attr("transform", "translate(0, " + 0.5 * (that.conf.graphicalParameters.karyoHeight - genomeDistance) + ")");
}
};
/**
* This method should check if the user provides tree data.
* @returns {Boolean} Returns true when tree data exists and false when there is no tree data.
* @author Sonja Hohlfeld
*/
AliTV.prototype.hasTree = function() {
if (typeof this.data.tree === "undefined" || $.isEmptyObject(this.data.tree) === true || this.data.tree === null) {
return false;
} else {
return true;
}
};
/**
* Calculates coordinates for different shapes according to the different feature classes in order to draw in the linear layout.
* This function operates on the linearKaryoCoords.
* This function is primarily meant for internal usage, the user should not need to call this directly.
* @author Sonja Hohlfeld
* @param {Array} linearKaryoCoords: contains the coordinates for all chromosomes of the form: {karyo: 'karyo_name', x:0, y:0, width:10, height:10}.
* @returns {Array} linearFeatureCoords: contains the coordinates for feature classes of the form: {id: "featureId", x:0, y:0, width: 45, height: 10}
*/
AliTV.prototype.getLinearFeatureCoords = function(linearKaryoCoords) {
var that = this;
var linearFeatureCoords = [];
$.each(that.data.features, function(type, features) {
// skip link features
if (type === "link") {
return true;
}
$.each(features, function(key, value) {
var featureKaryo = value.karyo;
var currentY;
var currentWidth;
var currentX;
var currentFeature = {};
var featureId = value.name;
var start;
var end;
var splitFeature;
var shift = (that.filters.karyo.chromosomes[featureKaryo].offset === undefined ? 0 : that.filters.karyo.chromosomes[featureKaryo].offset);
$.each(linearKaryoCoords, function(key, value) {
if (featureKaryo === value.karyo) {
currentY = value.y;
currentX = value.x;
currentWidth = value.width;
}
});
var featureStyle = ((typeof that.conf.features.supportedFeatures[type] !== 'undefined') ? that.conf.features.supportedFeatures[type] : that.conf.features.fallbackStyle);
if (featureStyle.visible === false && that.conf.features.showAllFeatures === false) {
// skip if the feature type should not be visible
return true;
}
var featureScale = d3.scale.linear()
.domain([0, that.data.karyo.chromosomes[featureKaryo].length])
.range([currentX, currentX + currentWidth]);
if (featureStyle.form === "rect") {
currentFeature = {
"id": featureId,
"type": type,
"karyo": value.karyo
};
if (value.strand === undefined) {
currentFeature.y = currentY;
currentFeature.height = that.conf.graphicalParameters.karyoHeight;
} else if (value.strand === "+") {
currentFeature.y = currentY;
currentFeature.height = 1 / 5 * that.conf.graphicalParameters.karyoHeight;
} else if (value.strand === "-") {
currentFeature.y = currentY + 4 / 5 * that.conf.graphicalParameters.karyoHeight;
currentFeature.height = 1 / 5 * that.conf.graphicalParameters.karyoHeight;
}
currentFeature.width = featureScale(Math.max(value.end, value.start)) - featureScale(Math.min(value.start, value.end));
currentFeature.x = featureScale((Math.min(value.start, value.end) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length);
start = featureScale((Math.min(value.end, value.start) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length);
end = featureScale((Math.max(value.end, value.start) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length);
if (that.filters.karyo.chromosomes[featureKaryo].reverse === false && (start > end) && end > 0) {
currentFeature.width = featureScale(that.data.karyo.chromosomes[featureKaryo].length) - start;
linearFeatureCoords.push(currentFeature);
splitFeature = {
"x": featureScale(0),
"width": end - featureScale(0),
"id": featureId,
"type": type,
"karyo": value.karyo,
"height": that.conf.graphicalParameters.karyoHeight,
"y": currentY
};
linearFeatureCoords.push(splitFeature);
} else if (that.filters.karyo.chromosomes[featureKaryo].reverse === true &&
(start < end) &&
// the next line should really compare end != featureScale(0) but as those are floats this comparison is not reliable
// a better solution would be to check for the difference to be greater than Number.EPSILON (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON)
// however, this is not supported in all browsers yet, therefore, in this case 0.01 can be used as if there is a real difference it will be close to one and if there is not it will be very close to 0
(Math.abs(end - featureScale(0)) > 0.01)) {
currentFeature.x = end;
currentFeature.width = featureScale(0) - end;
linearFeatureCoords.push(currentFeature);
splitFeature = {
"x": featureScale(that.data.karyo.chromosomes[featureKaryo].length),
"width": start,
"id": featureId,
"type": type,
"karyo": value.karyo,
"height": that.conf.graphicalParameters.karyoHeight,
"y": currentY
};
linearFeatureCoords.push(splitFeature);
} else {
linearFeatureCoords.push(currentFeature);
}
} else if (featureStyle.form === "arrow") {
currentFeature = {
"type": type,
"id": value.name,
"karyo": value.karyo
};
currentFeature.path = [];
if (value.strand === undefined) {
start = featureScale((Math.abs(value.start) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length) === featureScale(0) ? featureScale(that.data.karyo.chromosomes[featureKaryo].length) : featureScale((Math.abs(value.start) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length);
end = featureScale((Math.abs(value.end) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length) === featureScale(0) ? featureScale(that.data.karyo.chromosomes[featureKaryo].length) : featureScale((Math.abs(value.end) + shift + that.data.karyo.chromosomes[featureKaryo].length) % that.data.karyo.chromosomes[featureKaryo].length);
if (value.start < value.end && start > end && that.filters.karyo.chromosomes[featureKaryo].reverse === false) {
currentFeature.path.push({
x: start,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start + (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - start),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start + (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - start),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
splitFeature = {
"type": type,
"id": value.name,
"karyo": value.karyo
};
splitFeature.path = [];
splitFeature.path.push({
x: featureScale(0),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) + 5 / 6 * (end - featureScale(0)),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) + 5 / 6 * (end - featureScale(0)),
y: currentY
}, {
x: end,
y: currentY + 1 / 2 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) + 5 / 6 * (end - featureScale(0)),
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) + 5 / 6 * (end - featureScale(0)),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
linearFeatureCoords.push(splitFeature);
} else if (value.start > value.end && start < end && that.filters.karyo.chromosomes[featureKaryo].reverse === false) {
currentFeature.path.push({
x: featureScale(0),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
splitFeature = {
"type": type,
"id": value.name,
"karyo": value.karyo
};
splitFeature.path = [];
splitFeature.path.push({
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + 1 / 2 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
linearFeatureCoords.push(splitFeature);
} else if (value.start < value.end && start < end && that.filters.karyo.chromosomes[featureKaryo].reverse === true) {
currentFeature.path.push({
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) + start,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) + start,
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
splitFeature = {
"type": type,
"id": value.name,
"karyo": value.karyo
};
splitFeature.path = [];
splitFeature.path.push({
x: featureScale(0),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) - 5 / 6 * (featureScale(0) - end),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) - 5 / 6 * (featureScale(0) - end),
y: currentY
}, {
x: featureScale(0) - (featureScale(0) - end),
y: currentY + 1 / 2 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) - 5 / 6 * (featureScale(0) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0) - 5 / 6 * (featureScale(0) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
linearFeatureCoords.push(splitFeature);
} else if (value.start > value.end && start > end && that.filters.karyo.chromosomes[featureKaryo].reverse === true) {
currentFeature.path.push({
x: featureScale(0),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(0),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
splitFeature = {
"type": type,
"id": value.name,
"karyo": value.karyo
};
splitFeature.path = [];
splitFeature.path.push({
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + 1 / 2 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length) - 5 / 6 * (featureScale(that.data.karyo.chromosomes[featureKaryo].length) - end),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: featureScale(that.data.karyo.chromosomes[featureKaryo].length),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
linearFeatureCoords.push(splitFeature);
} else {
currentFeature.path.push({
x: start,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start + 5 / 6 * (featureScale(value.end) - featureScale(value.start)),
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start + 5 / 6 * (featureScale(value.end) - featureScale(value.start)),
y: currentY
}, {
x: end,
y: currentY + 1 / 2 * that.conf.graphicalParameters.karyoHeight
}, {
x: start + 5 / 6 * (featureScale(value.end) - featureScale(value.start)),
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: start + 5 / 6 * (featureScale(value.end) - featureScale(value.start)),
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: start,
y: currentY + that.conf.graphicalParameters.karyoHeight - 1 / 5 * that.conf.graphicalParameters.karyoHeight
});
}
} else if (value.strand === "+") {
currentFeature.path.push({
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 25 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 25 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 10 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight - 1 / 25 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 1 / 5 * that.conf.graphicalParameters.karyoHeight - 1 / 25 * that.conf.graphicalParameters.karyoHeight
});
} else if (value.strand === "-") {
currentFeature.path.push({
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + (4 / 5 + 1 / 25) * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + (4 / 5 + 1 / 25) * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 4 / 5 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + (4 / 5 + 1 / 10) * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length + 5 / 6 * ((value.end - value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 24 / 25 * that.conf.graphicalParameters.karyoHeight
}, {
x: currentX + (Math.abs(value.start) * currentWidth) / that.data.karyo.chromosomes[featureKaryo].length,
y: currentY + 24 / 25 * that.conf.graphicalParameters.karyoHeight
});
}
linearFeatureCoords.push(currentFeature);
}
});
});
return linearFeatureCoords;
};
/**
* This function draws the features on the karyos in the linear layout, color them according to the configuration.
* @author Sonja Hohlfeld
* @param {Array} The array containing the coordinates of the features as returned by getLinearFeatureCoords()
*/
AliTV.prototype.drawLinearFeatures = function(linearFeatureCoords) {
var that = this;
that.getAlignmentRegion().selectAll(".featureGroup").remove();
var shapes = that.getAlignmentRegion().append("g")
.attr("class", "featureGroup")
.selectAll("path")
.data(linearFeatureCoords)
.enter();
var lines = textures.lines()
.background(function(d) {
if (d.type in that.conf.features.supportedFeatures === true) {
return that.conf.features.supportedFeatures[d.type].color;
} else {
return that.conf.features.fallbackStyle.color;
}
})
.thicker();
shapes.call(lines);
var woven = textures.paths()
.d("woven")
.background("orange")
.thicker();
shapes.call(woven);
var counter = 0;
shapes.append("rect")
.filter(function(d) {
if (d.type in that.conf.features.supportedFeatures === true) {
return that.conf.features.supportedFeatures[d.type].form === "rect" && (that.conf.features.supportedFeatures[d.type].visible === true || that.conf.features.showAllFeatures === true);
} else {
return that.conf.features.fallbackStyle.form === "rect";
}
})
.attr("class", "feature")
.attr("id", function(d, i) {
var position = that.data.features[d.type].indexOf(that.data.features[d.type][counter]);
if (counter < that.data.features[d.type].length - 1) {
counter++;
} else {
counter = 0;
}
return position + "_" + d.type;
})
.attr("x", function(d) {
if (d.width < 0) {
return d.x + d.width;
} else {
return d.x;
}
})
.attr("y", function(d) {
return d.y;
})
.attr("width", function(d) {
return Math.abs(d.width);
})
.attr("height", function(d) {
return d.height;
})
.style("fill", function(d) {
var pattern;
var color;
if (d.type in that.conf.features.supportedFeatures === true) {
pattern = that.conf.features.supportedFeatures[d.type].pattern;
if (pattern === "lines") {
return lines.url();
} else if (pattern === "woven") {
return woven.url();
} else {
color = that.conf.features.supportedFeatures[d.type].color;
return color;
}
} else {
pattern = that.conf.features.fallbackStyle.pattern;
if (pattern === "lines") {
return lines.url();
} else if (pattern === "woven") {
return woven.url();
} else {
color = that.conf.features.fallbackStyle.color;
return color;
}
}
})
.style("display", function(d) {
var featureId = $(this).attr("id");
if (featureId in that.filters.features.invisibleFeatures) {
return "none";
}
});
var lineFunction = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.interpolate("linear");
shapes.append("path")
.filter(function(d) {
if (d.type in that.conf.features.supportedFeatures === true) {
return that.conf.features.supportedFeatures[d.type].form === "arrow" && (that.conf.features.supportedFeatures[d.type].visible === true || that.conf.features.showAllFeatures === true);
}
})
.each(function(d, i) {
d3.select(this)
.attr("class", "feature")
.attr("id", function(d) {
var position = that.data.features[d.type].indexOf(that.data.features[d.type][counter]);
if (counter < that.data.features[d.type].length - 1) {
counter++;
} else {
counter = 0;
}
return position + "_" + d.type;
})
.attr("d", lineFunction(d.path))
.attr("fill", function(d) {
var pattern;
var color;
pattern = that.conf.features.supportedFeatures[d.type].pattern;
if (pattern === "lines") {
return lines.url();
} else if (pattern === "woven") {
return woven.url();
} else {
color = that.conf.features.supportedFeatures[d.type].color;
return color;
}
})
.style("display", function(d) {
var featureId = $(this).attr("id");
if (featureId in that.filters.features.invisibleFeatures) {
return "none";
}
});
});
};
/**
* This method is supposed to calculate the coordinates for genome labels.
* This is called if the configuration of addGenomeLables is true.
* @returns genomeLabelCoords: returns an array which contains the coords for the genome labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getGenomeLabelCoords = function() {
var that = this;
var linearGenomeLabelCoords = [];
var genomeDistance = that.getGenomeDistance();
$.each(that.filters.karyo.genome_order, function(key, value) {
var genome = {
name: value,
x: 1 / 2 * that.conf.graphicalParameters.genomeLabelWidth,
y: key * genomeDistance + 0.9 * that.conf.graphicalParameters.karyoHeight
};
linearGenomeLabelCoords.push(genome);
});
return linearGenomeLabelCoords;
};
/**
* This function is supposed to draw the text labels for genomes.
* @param linearGenomeLabelCoords: gets the coords of the genome labels whcih is returned by getGenomeLabelCoords.
* @author Sonja Hohlfeld
*/
AliTV.prototype.drawLinearGenomeLabels = function(linearGenomeLabelCoords) {
var that = this;
this.svgD3.selectAll(".genomeLabelGroup").remove();
that.svgD3.append("g")
.attr("class", "genomeLabelGroup")
.selectAll("path")
.data(linearGenomeLabelCoords)
.enter()
.append("text")
.attr("class", "genomeLabel")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text(function(d) {
return d.name;
})
.attr("font-family", "sans-serif")
.attr("font-size", that.getGenomeLabelSize() + "px")
.attr("fill", that.getGenomeLabelColor())
.style("text-anchor", "middle");
if (that.conf.tree.drawTree === true && that.conf.tree.orientation === "left") {
that.svgD3.selectAll(".genomeLabelGroup").attr("transform", "translate(" + that.conf.graphicalParameters.treeWidth + ", 0)");
}
};
/**
* This function returns the width of the svg.
* @returns {Number} The width of svg.
* @author Markus Ankenbrand
*/
AliTV.prototype.getSvgWidth = function() {
return Number(this.svg.attr("width"));
};
/**
* This function sets the width of the svg - it does not effect the size of the canvas, tree, etc.
* When the method gets a wrong value it throws an error message.
* @param {Number} width - the desired width of the svg set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Markus Ankenbrand
*/
AliTV.prototype.setSvgWidth = function(width) {
if (width === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(width)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (width <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
width = Number(width);
if (this.conf.offset.isSet === true) {
this.svg.attr("width", width + this.conf.graphicalParameters.buttonWidth);
} else {
this.svg.attr("width", width);
}
this.triggerChange();
}
};
/**
* This function returns the height of the svg.
* @returns {Number} The height of svg.
* @author Markus Ankenbrand
*/
AliTV.prototype.getSvgHeight = function() {
return Number(this.svg.attr("height"));
};
/**
* This function sets the height of the svg - it does not effect the size of the canvas, tree, etc.
* When the method gets a wrong value it throws an error message.
* @param {Number} height - the desired height of the svg set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Markus Ankenbrand
*/
AliTV.prototype.setSvgHeight = function(height) {
if (height === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(height)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (height <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
height = Number(height);
this.svg.attr("height", height);
this.triggerChange();
}
};
/**
* This function returns the content of the svg as a text string.
* @returns {String} The content of the svg.
* @author Markus Ankenbrand
*/
AliTV.prototype.getSvgAsText = function() {
var svgText = this.svg[0].outerHTML;
svgText = svgText.replace(/"/g, "");
return svgText;
};
/**
* This function returns the content of the AliTV object as a single object containing data, filters and conf.
* @returns {Object} The content of the AliTV object.
* @author Markus Ankenbrand
*/
AliTV.prototype.getJSON = function() {
return {
data: this.data,
conf: this.conf,
filters: this.filters
};
};
/**
* This is a convenience function to set data, filters and conf of the AliTV object with a single call.
* @param {Object} json - Object containing any of data, filters and conf.
* @author Markus Ankenbrand
*/
AliTV.prototype.setJSON = function(json) {
if (typeof json.data !== 'undefined') {
this.setData(json.data);
}
if (typeof json.filters !== 'undefined') {
this.setFilters(json.filters);
}
if (typeof json.conf !== 'undefined') {
this.setConf(json.conf);
}
this.triggerChange();
};
/**
* This function returns the color of the genomeLabels.
* The color is defined in the conf-object.
* @returns The color of the genome labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getGenomeLabelColor = function() {
this.getJSON();
return this.conf.labels.genome.color;
};
/**
* This function set a new color for the genome labels.
* @param color: the current color of genome labels which is returned by getGenomeLabelColor.
* @throws Will throw an error if the argument is empty.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setGenomeLabelColor = function(color) {
if (color === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else {
this.conf.labels.genome.color = color;
this.triggerChange();
return this.conf.labels.genome.color;
}
};
/**
* This function returns the size of the genomeLabels.
* The size is defined in the conf-object.
* @returns The size of the genome labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getGenomeLabelSize = function() {
return this.conf.labels.genome.size;
};
/**
* This function set a new size for the genome labels.
* @param size: the current size of genome labels which is returned by getGenomeLabelSize.
* @param {Number} The function gets the size of genome labels which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setGenomeLabelSize = function(size) {
if (size === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(size)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (size <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
size = Number(size);
this.conf.labels.genome.size = size;
this.triggerChange();
return this.conf.labels.genome.size;
}
};
/**
* This function returns the color of the tick Labels.
* The color is defined in the conf-object.
* @returns The color of the tick labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getTickLabelColor = function() {
return this.conf.labels.ticks.color;
};
/**
* This function set a new color for the tick labels.
* @param color: the current color of tick labels which is returned by getTickLabelColor.
* @throws Will throw an error if the argument is empty.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setTickLabelColor = function(color) {
if (color === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else {
this.conf.labels.ticks.color = color;
this.triggerChange();
return this.conf.labels.ticks.color;
}
};
/**
* This function returns the size of the tick Labels.
* The size is defined in the conf-object.
* @returns The size of the tick labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getTickLabelSize = function() {
return this.conf.labels.ticks.size;
};
/**
* This function set a new size for the tick labels.
* @param size: the current size of tick labels which is returned by getTickLabelSize.
* @param {Number} The function gets the size of tick labels which can be set by the user.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is less than 0 or equal to 0.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setTickLabelSize = function(size) {
if (size === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(size)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (size <= 0) {
throw "Sorry, the entered value is too small. Please, insert one which is not less than 0.";
} else {
size = Number(size);
this.conf.labels.ticks.size = size;
this.triggerChange();
return this.conf.labels.ticks.size;
}
};
/**
* This function gets the id of a selected link, hides it and pushes it to ali.filters.links.selectedLinks.
* @param selectedLinkID: gets the id of the selected link
* @returns ali.filters.links.invisibleLinks: returns the links which are invisible in the current settings of ali.filters.links.invisibleLinks.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setLinkInvisible = function(selectedLinkID) {
$("#" + selectedLinkID).hide();
var selectedLink = this.visibleLinks[selectedLinkID];
this.filters.links.invisibleLinks[selectedLinkID] = selectedLink;
this.triggerChange();
return this.filters.links.invisibleLinks;
};
/**
* This functions gets the number of all links which are in ali.filters.links.invisibleLinks
* @returns invisibleLinkSize: the number of all Links which are invisible.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getInvisibleLinks = function() {
var keylist = [];
$.each(this.filters.links.invisibleLinks, function(key, value) {
keylist.push(key);
});
var invisibleLinkSize = keylist.length;
return invisibleLinkSize;
};
/**
* This function is supposed to get the ID of a selected link, which is hidden and should be restored.
* The function show the hidden link and delete it from the invisibleLinks-object in ali.filters.links.invisibleLinks
* @param selectedLinkID: the ID of the links which should be restored.
* @returns ali.filters.links.hiddenLinks: the current links which are set invisible.
* @author Sonja Hohlfeld
*/
AliTV.prototype.showInvisibleLink = function(selectedLinkID) {
$("#" + selectedLinkID).show();
delete this.filters.links.invisibleLinks[selectedLinkID];
this.triggerChange();
return this.filters.links.invisibleLinks;
};
/**
* This function is supposed to return the value of the longest chromosome.
* @return maxLinkLength: the value of the longest chromosome in bp.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getMaxChromosomeLength = function() {
var length = [];
$.each(this.data.karyo.chromosomes, function(key, value) {
length.push(value.length);
});
var maxLength = Math.max.apply(Math, length);
return maxLength;
};
/**
* This function is supposed to clear the complete drawing area by removing all children from the svg.
* @author Sonja Hohlfeld
*/
AliTV.prototype.clearAli = function() {
this.svgD3.selectAll(".treeGroup").remove();
this.svgD3.selectAll(".featureGroup").remove();
this.svgD3.selectAll(".genomeLabelGroup").remove();
this.svgD3.selectAll(".tickLabelGroup").remove();
this.svgD3.selectAll(".tickGroup").remove();
this.svgD3.selectAll(".karyoGroup").remove();
this.svgD3.selectAll(".linkGroup").remove();
};
/**
* This function gets a selected feature ID and pushes it to ali.filters.Features.invisibleFeatures.
* The function marks all features, which are set invisible.
* @param featureID: gets the ID of the selected feature.
* @param group: gets the group of the selected feature for example invertedRepeat.
* @param karyo: gets the chromsome which belongs to the selected feature.
* @returns ali.filters.features.invisibleFeatures: returns the features which are invisible in the current settings.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setFeatureInvisible = function(feature) {
$("#" + feature).hide();
var split = feature.split("_");
var id = split[0];
var group = split[1];
this.filters.features.invisibleFeatures[feature] = this.data.features[group][id];
this.triggerChange();
return this.filters.features.invisibleFeatures;
};
/**
* This functions gets the number of all features which are in ali.filters.features.invisibleFeatures
* @returns invisibleFeatureSize: the number of all features which are invisible.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getInvisibleFeatures = function() {
var keylist = [];
$.each(this.filters.features.invisibleFeatures, function(key, value) {
keylist.push(key);
});
var invisibleFeatureSize = keylist.length;
return invisibleFeatureSize;
};
/**
* This function is supposed to get the ID of a selected feature, which is hidden and should be restored.
* The function show the hidden feature and delete it from the invisibleFeatures-object in ali.filters.features.invisibleFeatures
* @param selectedFeatureId: the Id of the features which should be restored.
* @returns ali.filters.features.invisibleFeatures: the current features which are set invisible.
* @author Sonja Hohlfeld
*/
AliTV.prototype.showInvisibleFeature = function(selectedFeatureId) {
$("#" + selectedFeatureId).show();
delete this.filters.features.invisibleFeatures[selectedFeatureId];
this.triggerChange();
return this.filters.features.invisibleFeatures;
};
/**
* This function returns the internal alignmentRegion g element as d3 selection. It is created if it does not exist.
* It also creates and adds the clipPath if it does not exist.
* @returns {Object} internal alignmentRegion g as d3 selection.
* @author Markus Ankenbrand
*/
AliTV.prototype.getAlignmentRegion = function() {
var alignmentRegion = this.svgD3.selectAll(".alignmentRegion");
if (alignmentRegion.size() < 1) {
this.svgD3.selectAll("#clip").remove();
this.svgD3.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr("width", this.conf.graphicalParameters.canvasWidth)
.attr("height", this.conf.graphicalParameters.canvasHeight);
this.svgD3.append("g")
.attr("class", "alignmentRegion")
.attr("clip-path", "url(#clip)");
//.attr("width", 500) //this.conf.graphicalParameters.canvasWidth)
//.attr("height", this.conf.graphicalParameters.canvasHeight);
alignmentRegion = this.svgD3.selectAll(".alignmentRegion");
}
return alignmentRegion;
};
/**
* This function returns the internal legendRegion g element as d3 selection. It is created if it does not exist.
* @returns {Object} internal legendRegion g as d3 selection.
* @author Markus Ankenbrand
*/
AliTV.prototype.getLegendRegion = function() {
var legendRegion = this.svgD3.selectAll(".legendRegion");
if (legendRegion.size() < 1) {
this.svgD3.append("g")
.attr("class", "legendRegion");
legendRegion = this.svgD3.selectAll(".legendRegion");
}
return legendRegion;
};
/**
* This function returns linkCoords with those removed that have their ends outside the visible region.
* Optionally also links with only one end in the visible region can be removed.
* @param {Array} - linearLinkCoords as returned by getLinearLinkCoords
* @param {boolean} - if true also links with one end inside the visual region will be removed
* @returns {Array} - filtered linearLinkCoords
* @author Markus Ankenbrand
*/
AliTV.prototype.removeLinksOutsideVisibleRegion = function(linkCoords, removeHalfVisible) {
var filteredCoords = [];
var canvasWidth = this.getCanvasWidth();
var tooMuch = (removeHalfVisible ? 1 : 2);
$.each(linkCoords, function(key, value) {
var out = 0;
if (Math.max(value.source0.x, value.source1.x) <= 0 || Math.min(value.source0.x, value.source1.x) >= canvasWidth) {
out++;
}
if (Math.max(value.target0.x, value.target1.x) <= 0 || Math.min(value.target0.x, value.target1.x) >= canvasWidth) {
out++;
}
if (out < tooMuch) {
filteredCoords.push(value);
}
});
return filteredCoords;
};
/**
* This function updates the genome_region filter according to the specified region on the svg
* @param {Object} - rect an object with properties x, y, width and height (relative to the svg)
* @author Markus Ankenbrand
*/
AliTV.prototype.updateGenomeRegionBySvgRect = function(rect) {
var that = this;
var distance = that.getGenomeDistance();
var karyoHeight = that.getKaryoHeight();
if (typeof that.filters.karyo.genome_region === 'undefined') {
that.filters.karyo.genome_region = {};
}
for (var i = 0; i < that.filters.karyo.genome_order.length; i++) {
var genome = that.filters.karyo.genome_order[i];
if (typeof that.filters.karyo.genome_region[genome] === 'undefined') {
that.filters.karyo.genome_region[genome] = {};
}
var yPosCurrentGenome = i * distance + karyoHeight / 2;
if (yPosCurrentGenome >= rect.y && yPosCurrentGenome <= rect.y + rect.height) {
var region = that.filters.karyo.genome_region[genome];
var start = (region.start || 0);
var end = (region.end || that.cache.linear.maxGenomeSize + start);
var translateX = d3.transform(that.svgD3.select('.alignmentRegion').attr("transform")).translate[0];
var transformToGenomeScale = d3.scale.linear().domain([0 + translateX, that.getCanvasWidth() + translateX]).range([start, end]);
region.start = transformToGenomeScale(rect.x);
region.end = transformToGenomeScale(rect.x + rect.width);
}
}
this.triggerChange();
};
/**
* This function resets the genome_region filter to default for the specified genome
* @param {String} - genome_id
* @author Markus Ankenbrand
*/
AliTV.prototype.resetGenomeRegion = function(genome_id) {
if (typeof this.filters.karyo.genome_region !== "undefined") {
this.filters.karyo.genome_region[genome_id] = {};
}
this.triggerChange();
};
/**
* This function is supposed to change the visibility of a selected chromosome.
* The function gets the name of a chromosome and set his visibility in filters.karyo.chromosomes equal false or true.
* @param {String} chromosomeName: the name of the selected chromosome.
* @author Sonja Hohlfeld
*/
AliTV.prototype.changeChromosomeVisibility = function(chromosomeId) {
this.filters.karyo.chromosomes[chromosomeId].visible = !this.filters.karyo.chromosomes[chromosomeId].visible;
this.triggerChange();
return this.filters.karyo.chromosomes;
};
/**
* This functions gets the number of all chromosomes which are set invisible.
* @returns invisibleChromosomeSize: the number of all chromosomes which are invisible.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getInvisibleChromosomes = function() {
var invisibleChromosomeSize = 0;
$.each(this.filters.karyo.chromosomes, function(key, value) {
if (value.visible === false) {
invisibleChromosomeSize = invisibleChromosomeSize + 1;
}
});
return invisibleChromosomeSize;
};
/**
* This function is supposed to swap a genome with its adjacent genomes according to the order of all genomes which is defined in ali.filters.karyo.genome_order.
* @param {String} name: the name of the selected genome.
* @param {Number} value: +1 or -1. The number defines if the genome is moved one genome up or down.
* @author Sonja Hohlfeld
*/
AliTV.prototype.changeGenomeOrder = function(name, value) {
var that = this;
var genomePosition = that.filters.karyo.genome_order.indexOf(name);
var tmp;
if ((genomePosition !== 0 || (genomePosition === 0 && value === -1)) && (genomePosition !== (that.filters.karyo.genome_order.length - 1) || (genomePosition === (that.filters.karyo.genome_order.length - 1) && value === +1))) {
var adjacentGenomePosition = genomePosition - value;
tmp = that.filters.karyo.genome_order[genomePosition];
that.filters.karyo.genome_order[genomePosition] = that.filters.karyo.genome_order[adjacentGenomePosition];
that.filters.karyo.genome_order[adjacentGenomePosition] = tmp;
} else if (genomePosition === 0 && value === +1) {
tmp = that.filters.karyo.genome_order.shift();
that.filters.karyo.genome_order.push(tmp);
} else {
tmp = that.filters.karyo.genome_order.pop();
that.filters.karyo.genome_order.unshift(tmp);
}
this.triggerChange();
return that.filters.karyo.genome_order;
};
/**
* This function is supposed to change the orientation of an assigned chromosome from reverse equal false or reverse equal true.
* @param {String} chromosome: the selected chromosome which orientation should be changed.
* @retrun {String} ali.filters: return the current settings for the filters.
* @author {Sonja Hohlfeld}
*/
AliTV.prototype.changeChromosomeOrientation = function(chromosome) {
this.filters.karyo.chromosomes[chromosome].reverse = !this.filters.karyo.chromosomes[chromosome].reverse;
this.triggerChange();
return this.filters.karyo.chromosomes[chromosome].reverse;
};
/**
* This function is supposed to change the order of chromosomes according to their genome.
* If a genome has only one chromosomes it is not possible to change the order.
* @param id: The name of the selected chromosome.
* @param value: +1 (moves right) and -1 (moves left).
* @author: Sonja Hohlfeld
*/
AliTV.prototype.changeChromosomeOrder = function(id, value) {
var that = this;
var chromosomePosition = that.filters.karyo.order.indexOf(id);
var order = that.filters.karyo.order;
var i, tmp;
if (value === +1) {
i = (chromosomePosition + 1) % order.length;
while (that.data.karyo.chromosomes[order[i]].genome_id !== that.data.karyo.chromosomes[id].genome_id) {
i = (i + 1) % order.length;
}
} else if (value === -1) {
i = chromosomePosition - 1;
i = (i === -1 ? order.length - 1 : i);
while (that.data.karyo.chromosomes[order[i]].genome_id !== that.data.karyo.chromosomes[id].genome_id) {
i = (i === 0 ? order.length - 1 : (i - 1));
}
}
if (value === +1 && i < chromosomePosition) {
// move over right border
tmp = order.splice(chromosomePosition, 1)[0];
order.splice(i, 0, tmp);
} else if (value === -1 && i > chromosomePosition) {
// move over left border
tmp = order.splice(chromosomePosition, 1)[0];
order.splice(i, 0, tmp);
} else {
// ususal case
tmp = order[i];
order[i] = order[chromosomePosition];
order[chromosomePosition] = tmp;
}
this.triggerChange();
return that.filters.karyo.order;
};
/**
* This function returns the offset for a specific chromosome.
* @returns {Number} The offset in bp of a chromosome.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getOffset = function(karyoId) {
return this.filters.karyo.chromosomes[karyoId].offset;
};
/**
* This function sets a new offset for specific chromosome.
* When the method gets a wrong value it throws an error message.
* @param {Number} The function gets the distance for shifting chromosomes which can be set by the user.
* @param {String} The function gets the Id of the chromosome which should be shifted.
* @throws Will throw an error if the argument is empty.
* @throws Will throw an error if the argument is not a number.
* @author Sonja Hohlfeld
*/
AliTV.prototype.setOffset = function(offset, karyoId) {
if (offset === "") {
throw "Sorry, you entered an empty value. Please try it again.";
} else if (isNaN(offset)) {
throw "Sorry, you entered not a number. Please try it again.";
} else {
this.filters.karyo.chromosomes[karyoId].offset = offset;
return this.filters.karyo.chromosomes[karyoId].offset;
}
};
/**
* This function is supposed to return an array of supported features
* @returns {Array} supportedFeatures
* @author: Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.getSupportedFeatures = function() {
return Object.keys(this.conf.features.supportedFeatures);
};
/**
* This function is supposed to register callback function which are called upon data change
* @param {function} callback - callback to be called upon data change
* @author: Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.onDataChange = function(callback) {
if (typeof callback !== "function") {
throw "Not a function.";
}
this.onChangeCallbacks.push(callback);
};
/**
* This function is supposed to call all registered callback functions
* @author: Sonja Hohlfeld and Markus Ankenbrand
*/
AliTV.prototype.triggerChange = function() {
if (this.inTransaction) {
return;
}
$.each(this.onChangeCallbacks, function(key, value) {
value();
});
};
/**
* This function is supposed to start a transaction and prevent changes from being triggered during that time
* @author: Markus Ankenbrand
*/
AliTV.prototype.startTransaction = function() {
this.inTransaction = true;
};
/**
* This function is supposed to end a transaction, call triggerChange once and allow for new changes to trigger directly.
* @author: Markus Ankenbrand
*/
AliTV.prototype.endTransaction = function() {
this.inTransaction = false;
this.triggerChange();
};
/**
* This function is supposed to get the parameters for adding a new feature group in conf.features.supportedFeatures.
* @param group: the name of the new feature group
* @param form: the form for the group which is used by drawFeatures
* @param color: the color for the new group
* @returns {ali.conf.features.supportedFeatures} The current feature groups which are supported by AliTV
* @author Sonja Hohlfeld
*/
AliTV.prototype.setNewFeature = function(group, form, color) {
this.conf.features.supportedFeatures[group] = {
form: form,
color: color,
visible: true,
height: 30
};
this.triggerChange();
return this.conf.features.supportedFeatures[group];
};
/**
* This function is supposed to generate a tree from the supplied one that matches the current genome_order.
* @returns {object} tree - the tree in data rotated to match the current genome_order.
* @author Markus Ankenbrand
*/
AliTV.prototype.rotateTreeToGenomeOrder = function() {
if (typeof this.data.tree === "undefined") {
throw "No tree in data.";
}
// decorate the tree nodes with lists of their leaf nodes
var getLeafs = function(subtree) {
var names = [];
for (var i = 0; i < subtree.length; i++) {
if (typeof subtree[i].name !== "undefined") {
names.push(subtree[i].name);
subtree[i].leafs = [subtree[i].name];
} else {
var leafs = getLeafs(subtree[i].children);
subtree[i].leafs = leafs;
names = names.concat(leafs);
}
}
return names;
};
var startTree = {};
jQuery.extend(true, startTree, this.data.tree);
startTree.leafs = getLeafs(startTree.children);
// offset from the beginning of the array at which this subtree starts
startTree.offset = 0;
var genome_order = this.filters.karyo.genome_order;
var rotateTree = function(subtree) {
var j = subtree.offset;
if (typeof subtree.children !== 'undefined') {
var ordered_children = [];
while (j < subtree.offset + subtree.leafs.length) {
var fail = true;
for (var i = 0; i < subtree.children.length; i++) {
if (typeof subtree.children[i].leafs === 'undefined') {
continue;
}
if (subtree.children[i].leafs.indexOf(genome_order[j]) !== -1) {
fail = false;
ordered_children.push(subtree.children[i]);
subtree.children[i].offset = j;
j += subtree.children[i].leafs.length;
rotateTree(subtree.children[i]);
subtree.children.splice(i, 1);
break;
}
}
if (fail) {
throw "No rotation can lead to current genome_order.";
}
}
subtree.children = ordered_children;
}
delete subtree.offset;
delete subtree.leafs;
};
rotateTree(startTree);
return startTree;
};
/**
* This function is supposed to draw a legend for all the supportedFeatures.
* @author Markus Ankenbrand
*/
AliTV.prototype.drawFeatureLegend = function() {
var rect = [];
var rectCol = [];
var arrow = [];
var arrowCol = [];
var that = this;
$.each(that.conf.features.supportedFeatures, function(key, val) {
if (!val.visible) {
return;
}
if (val.form === "rect") {
rect.push(key);
rectCol.push(val.color);
} else if (val.form === "arrow") {
arrow.push(key);
arrowCol.push(val.color);
}
});
this.getLegendRegion().selectAll(".legendRect").remove();
this.getLegendRegion().selectAll(".legendArrow").remove();
this.getLegendRegion().append("g")
.attr("class", "legendRect")
.attr("transform", "translate(0,20)");
this.getLegendRegion().append("g")
.attr("class", "legendArrow")
.attr("transform", "translate(" + (this.getSvgWidth() / 3) + ",20)");
var rectScale = d3.scale.ordinal()
.domain(rect)
.range(rectCol);
var arrowScale = d3.scale.ordinal()
.domain(arrow)
.range(arrowCol);
var arrowShape = [
[0, 0],
[10, 0],
[10, -5],
[18, 5],
[10, 15],
[10, 10],
[0, 10]
];
var lineGenerator = d3.svg.line().x(function(d) {
return d[0];
}).y(function(d) {
return d[1];
});
// this does not work in test cases - most likely the d3-legend library is not properly loaded
// to avoid meaningless fails in tests skip this region if d3.legend is not defined
if (typeof d3.legend === 'undefined') {
return;
}
var legendArrow = d3.legend.color()
//d3 symbol creates a path-string, for example
//"M0,-8.059274488676564L9.306048591020996,
//8.059274488676564 -9.306048591020996,8.059274488676564Z"
.shape("path", lineGenerator(arrowShape)) //d3.svg.symbol().type("triangle-up").size(150)())
.shapePadding(10)
.scale(arrowScale);
var legendRect = d3.legend.color()
.shape("rect")
.shapePadding(10)
.scale(rectScale);
this.getLegendRegion().select(".legendRect")
.call(legendRect);
this.getLegendRegion().select(".legendArrow")
.call(legendArrow);
};
/**
* This function is supposed to draw a legend for the color code of the link identity.
* @author Markus Ankenbrand
*/
AliTV.prototype.drawLinkIdentityLegend = function() {
var legendRegion = this.getLegendRegion();
legendRegion.selectAll(".legendLinkIdentity").remove();
legendRegion.selectAll("#linkIdentityGradient").remove();
var legend = legendRegion.append("defs")
.append("svg:linearGradient")
.attr("id", "linkIdentityGradient")
.attr("x1", "0%")
.attr("y1", "100%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
var transformPercent = d3.scale.linear().range([0, 100]).domain([this.conf.minLinkIdentity, this.conf.maxLinkIdentity]);
legend.append("stop").attr("offset", "0%").attr("stop-color", this.conf.minLinkIdentityColor).attr("stop-opacity", 1);
legend.append("stop").attr("offset", transformPercent(this.conf.midLinkIdentity) + "%").attr("stop-color", this.conf.midLinkIdentityColor).attr("stop-opacity", 1);
legend.append("stop").attr("offset", "100%").attr("stop-color", this.conf.maxLinkIdentityColor).attr("stop-opacity", 1);
var legendLinkIdentityGroup = legendRegion.append("g")
.attr("class", "legendLinkIdentity")
.attr("transform", "translate(" + (this.getSvgWidth() * (2 / 3)) + ", 20)");
legendLinkIdentityGroup.append("rect").attr("width", 300).attr("height", 20).style("fill", "url(#linkIdentityGradient)");
var x = d3.scale.linear().range([0, 300]).domain([this.conf.minLinkIdentity, this.conf.maxLinkIdentity]);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
legendLinkIdentityGroup.append("g").attr("class", "x axis").attr("style", "font: 10px sans-serif; fill: none; stroke: #000; shape-rendering: crispEdges;").attr("transform", "translate(0, 20)").call(xAxis).append("text").attr("y", 20).attr("x", 150).attr("dy", ".71em").style("text-anchor", "middle").text("Link Identity %");
};
/**
* This method is supposed to return the current minimal identity of links.
* @returns minimalLinkIdentity: the minimal identity of links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getMinLinkIdentity = function() {
var minimalLinkIdentity = this.filters.links.minLinkIdentity;
return minimalLinkIdentity;
};
/**
* This method is supposed to return the current maximal identity of links.
* @returns maximalLinkIdentity: the maximal identity of links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getMaxLinkIdentity = function() {
var maximalLinkIdentity = this.filters.links.maxLinkIdentity;
return maximalLinkIdentity;
};
/**
* This method is supposed to return the current minimal length of links.
* @returns minimalLinkLength: the minimal lenght of links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getMinLinkLength = function() {
var minimalLinkLength = this.filters.links.minLinkLength;
return minimalLinkLength;
};
/**
* This method is supposed to return the current maximal length of links.
* @returns maximalLinkLength: the maximal length of links.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getMaxLinkLength = function() {
var maximalLinkLength = this.filters.links.maxLinkLength;
return maximalLinkLength;
};
/**
* This method is supposed to calculate the coordinates for feature labels.
* This method is called if the configuration of addFeatureLabels or showAllLabels is true.
* @param gets the coordinates of the drawn features.
* @returns featureLabelCoords: returns an array which contains the coords for the feature labels.
* @author Sonja Hohlfeld
*/
AliTV.prototype.getFeatureLabelCoords = function(linearFeatureCoords) {
var that = this;
var linearFeatureLabelCoords = [];
$.each(linearFeatureCoords, function(key, value) {
var feature = {
name: value.id
};
if (value.type in that.conf.features.supportedFeatures === true) {
if (that.conf.features.supportedFeatures[value.type].form === "rect" && (that.conf.labels.showAllLabels === true || that.conf.labels.features.showFeatureLabels === true || that.conf.features.supportedFeatures[value.type].labeling === true)) {
feature.x = value.x + 1 / 2 * value.width;
feature.y = value.y + 0.85 * that.conf.graphicalParameters.karyoHeight;
}
if (that.conf.features.supportedFeatures[value.type].form === "arrow" && (that.conf.labels.showAllLabels === true || that.conf.labels.features.showFeatureLabels === true || that.conf.features.supportedFeatures[value.type].labeling === true)) {
feature.x = value.path[0].x + 1 / 2 * (value.path[3].x - value.path[0].x);
feature.y = value.path[0].y + 1 / 2 * that.conf.graphicalParameters.karyoHeight;
}
} else {
feature.x = value.x + 1 / 2 * value.width;
feature.y = value.y + 0.85 * that.conf.graphicalParameters.karyoHeight;
}
linearFeatureLabelCoords.push(feature);
});
return linearFeatureLabelCoords;
};
/**
* This method is supposed to draw labels to all features.
* @param linearFeatureLabelCoords: get the coords for the feature labels which are returned by getFeatureLabelCoords.
* @author Sonja Hohlfeld
*/
AliTV.prototype.drawLinearFeatureLabels = function(linearFeatureLabelCoords) {
var that = this;
this.svgD3.selectAll(".featureLabelGroup").remove();
that.svgD3.append("g")
.attr("class", "featureLabelGroup")
.selectAll("path")
.data(linearFeatureLabelCoords)
.enter()
.append("text")
.attr("class", "featureLabel")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text(function(d) {
return d.name;
})
.attr("font-family", "sans-serif")
.attr("font-size", 2 / 3 * that.conf.graphicalParameters.karyoHeight + "px")
.attr("fill", "black")
.style("text-anchor", "middle");
if (that.conf.labels.showAllLabels === true || that.conf.labels.genome.showGenomeLabels === true) {
that.svgD3.selectAll(".featureLabelGroup").attr("transform", "translate(" + that.conf.graphicalParameters.genomeLabelWidth + ", 0)");
}
if ((that.conf.labels.showAllLabels === true || that.conf.labels.genome.showGenomeLabels === true) && that.conf.tree.drawTree === true && that.conf.tree.orientation === "left") {
that.svgD3.selectAll(".featureLabelGroup").attr("transform", "translate(" + (that.conf.graphicalParameters.treeWidth + that.conf.graphicalParameters.genomeLabelWidth) + ", 0)");
}
};
/**
* This method is supposed to return the default link opacity.
* @returns linkOpacity: the default link opacity.
* @author Markus Ankenbrand
*/
AliTV.prototype.getLinkOpacity = function() {
var linkOpacity = this.conf.graphicalParameters.linkOpacity;
return linkOpacity;
};
/**
* This function sets a new default link opacity.
* When the method gets a wrong value it throws an error message.
* @param {Number} The new opacity (value between 0 and 1)
* @throws Will throw an error if the argument is not a number.
* @throws Will throw an error if the argument is out of range [0,1].
* @author Markus Ankenbrand
*/
AliTV.prototype.setLinkOpacity = function(linkOpacity) {
if (linkOpacity === "" || isNaN(linkOpacity)) {
throw "Sorry, you entered not a number. Please try it again.";
} else if (linkOpacity > 1 || linkOpacity < 0) {
throw "Sorry, this value is out of range. Please enter a number between 0 and 1.";
} else {
this.conf.graphicalParameters.linkOpacity = linkOpacity;
}
};
/* helper function for transpiled ES6 function */
function _toConsumableArray(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {
arr2[i] = arr[i];
}
return arr2;
} else {
return Array.from(arr);
}
}
/**
* This function re-orders the karyos in all genomes according to the longest hit to the reference genome (determined by globally longest sequence)
* This function is transpiled using babel to make tests with phantomJS work (does not support ES6)
* To see the original ES6 implementation you can check out fb68a0fb290ab38ccc381bfebd4323f267f97ca6
* @author Markus Ankenbrand
*/
AliTV.prototype.orderKaryosAutomatically = function() {
// get genome with longest chromosome
var chr = this.data.karyo.chromosomes;
var referenceGenome = chr[Object.keys(chr).sort(function(a, b) {
return chr[b].length - chr[a].length;
})[0]].genome_id;
// for each karyo safe position of longest hit to reference
var links = this.data.links;
var bestHitOnRef = {};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError;
try {
for (var _iterator = Object.keys(links)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var s = _step.value;
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3;
try {
for (var _iterator3 = Object.keys(links[s])[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var t = _step3.value;
if (s !== referenceGenome && t !== referenceGenome) {
continue;
}
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4;
try {
for (var _iterator4 = Object.keys(links[s][t])[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var link = _step4.value;
var source = links[s][t][link].source;
var target = links[s][t][link].target;
var sourceFeature = this.data.features.link[source];
var targetFeature = this.data.features.link[target];
// swap source and target if source is not ref
var sourceIsRef = this.data.karyo.chromosomes[sourceFeature.karyo].genome_id === referenceGenome;
if (!sourceIsRef) {
var _ref = [target, source];
source = _ref[0];
target = _ref[1];
var _ref2 = [targetFeature, sourceFeature];
sourceFeature = _ref2[0];
targetFeature = _ref2[1];
}
var rc = (sourceFeature.start - sourceFeature.end) * (targetFeature.start - targetFeature.end) < 0;
var refSeq = sourceFeature.karyo;
var refPos = _.min([sourceFeature.start, sourceFeature.end]);
var length = Math.abs(sourceFeature.start - sourceFeature.end);
if (!(targetFeature.karyo in bestHitOnRef) || bestHitOnRef[targetFeature.karyo].len < length) {
bestHitOnRef[targetFeature.karyo] = {
len: length,
rc: rc,
refSeq: refSeq,
refPos: refPos
};
}
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
// get all reference karyos and add them to the new_order array
// split non-ref karyos into those with hit to ref and those without
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
var new_order = [];
var with_hit = [];
var without_hit = [];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2;
try {
for (var _iterator2 = this.filters.karyo.order[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var karyo = _step2.value;
if (!(karyo in this.data.karyo.chromosomes)) {
continue;
}
if (this.data.karyo.chromosomes[karyo].genome_id === referenceGenome) {
new_order.unshift(karyo);
} else {
if (karyo in bestHitOnRef) {
with_hit.push(karyo);
this.filters.karyo.chromosomes[karyo].reverse = bestHitOnRef[karyo].rc;
} else {
without_hit.push(karyo);
}
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
with_hit = with_hit.sort(function(a, b) {
var bestA = bestHitOnRef[a];
var bestB = bestHitOnRef[b];
if (bestA.refSeq !== bestB.refSeq) {
return new_order.indexOf(bestA.refSeq) - new_order.indexOf(bestB.refSeq);
} else {
return bestA.refPos - bestB.refPos;
}
});
new_order = [].concat(_toConsumableArray(new_order), _toConsumableArray(with_hit), without_hit);
this.filters.karyo.order = new_order;
this.triggerChange();
this.drawLinear();
};