tnt.board
Version:
TnT track-based board display
1,884 lines (1,592 loc) • 164 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
if (typeof tnt === "undefined") {
module.exports = tnt = {};
}
tnt.board = require("./index.js");
},{"./index.js":2}],2:[function(require,module,exports){
// if (typeof tnt === "undefined") {
// module.exports = tnt = {}
// }
// tnt.utils = require("tnt.utils");
// tnt.tooltip = require("tnt.tooltip");
// tnt.board = require("./src/index.js");
module.exports = require("./src/index");
},{"./src/index":13}],3:[function(require,module,exports){
module.exports = require("./src/api.js");
},{"./src/api.js":4}],4:[function(require,module,exports){
var api = function (who) {
var _methods = function () {
var m = [];
m.add_batch = function (obj) {
m.unshift(obj);
};
m.update = function (method, value) {
for (var i=0; i<m.length; i++) {
for (var p in m[i]) {
if (p === method) {
m[i][p] = value;
return true;
}
}
}
return false;
};
m.add = function (method, value) {
if (m.update (method, value) ) {
} else {
var reg = {};
reg[method] = value;
m.add_batch (reg);
}
};
m.get = function (method) {
for (var i=0; i<m.length; i++) {
for (var p in m[i]) {
if (p === method) {
return m[i][p];
}
}
}
};
return m;
};
var methods = _methods();
var api = function () {};
api.check = function (method, check, msg) {
if (method instanceof Array) {
for (var i=0; i<method.length; i++) {
api.check(method[i], check, msg);
}
return;
}
if (typeof (method) === 'function') {
method.check(check, msg);
} else {
who[method].check(check, msg);
}
return api;
};
api.transform = function (method, cbak) {
if (method instanceof Array) {
for (var i=0; i<method.length; i++) {
api.transform (method[i], cbak);
}
return;
}
if (typeof (method) === 'function') {
method.transform (cbak);
} else {
who[method].transform(cbak);
}
return api;
};
var attach_method = function (method, opts) {
var checks = [];
var transforms = [];
var getter = opts.on_getter || function () {
return methods.get(method);
};
var setter = opts.on_setter || function (x) {
for (var i=0; i<transforms.length; i++) {
x = transforms[i](x);
}
for (var j=0; j<checks.length; j++) {
if (!checks[j].check(x)) {
var msg = checks[j].msg ||
("Value " + x + " doesn't seem to be valid for this method");
throw (msg);
}
}
methods.add(method, x);
};
var new_method = function (new_val) {
if (!arguments.length) {
return getter();
}
setter(new_val);
return who; // Return this?
};
new_method.check = function (cbak, msg) {
if (!arguments.length) {
return checks;
}
checks.push ({check : cbak,
msg : msg});
return this;
};
new_method.transform = function (cbak) {
if (!arguments.length) {
return transforms;
}
transforms.push(cbak);
return this;
};
who[method] = new_method;
};
var getset = function (param, opts) {
if (typeof (param) === 'object') {
methods.add_batch (param);
for (var p in param) {
attach_method (p, opts);
}
} else {
methods.add (param, opts.default_value);
attach_method (param, opts);
}
};
api.getset = function (param, def) {
getset(param, {default_value : def});
return api;
};
api.get = function (param, def) {
var on_setter = function () {
throw ("Method defined only as a getter (you are trying to use it as a setter");
};
getset(param, {default_value : def,
on_setter : on_setter}
);
return api;
};
api.set = function (param, def) {
var on_getter = function () {
throw ("Method defined only as a setter (you are trying to use it as a getter");
};
getset(param, {default_value : def,
on_getter : on_getter}
);
return api;
};
api.method = function (name, cbak) {
if (typeof (name) === 'object') {
for (var p in name) {
who[p] = name[p];
}
} else {
who[name] = cbak;
}
return api;
};
return api;
};
module.exports = exports = api;
},{}],5:[function(require,module,exports){
module.exports = require("./src/index.js");
},{"./src/index.js":6}],6:[function(require,module,exports){
// require('fs').readdirSync(__dirname + '/').forEach(function(file) {
// if (file.match(/.+\.js/g) !== null && file !== __filename) {
// var name = file.replace('.js', '');
// module.exports[name] = require('./' + file);
// }
// });
// Same as
var utils = require("./utils.js");
utils.reduce = require("./reduce.js");
utils.png = require("./png.js");
module.exports = exports = utils;
},{"./png.js":7,"./reduce.js":8,"./utils.js":9}],7:[function(require,module,exports){
var png = function () {
var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
var scale_factor = 1;
// var filename = 'image.png';
// Restrict the css to apply to the following array (hrefs)
// TODO: substitute this by an array of regexp
var css; // If undefined, use all stylesheets
// var inline_images_opt = true; // If true, inline images
var img_cbak = function () {};
var png_export = function (from_svg) {
from_svg = from_svg.node();
// var svg = div.querySelector('svg');
var inline_images = function (cbak) {
var images = d3.select(from_svg)
.selectAll('image');
var remaining = images[0].length;
if (remaining === 0) {
cbak();
}
images
.each (function () {
var image = d3.select(this);
var img = new Image();
img.onload = function () {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
var uri = canvas.toDataURL('image/png');
image.attr('href', uri);
remaining--;
if (remaining === 0) {
cbak();
}
};
img.src = image.attr('href');
});
};
var move_children = function (src, dest) {
var children = src.children || src.childNodes;
while (children.length > 0) {
var child = children[0];
if (child.nodeType !== 1/*Node.ELEMENT_NODE*/) continue;
dest.appendChild(child);
}
return dest;
};
var styling = function (dom) {
var used = "";
var sheets = document.styleSheets;
// var sheets = [];
for (var i=0; i<sheets.length; i++) {
var href = sheets[i].href || "";
if (css) {
var skip = true;
for (var c=0; c<css.length; c++) {
if (href.indexOf(css[c]) > -1) {
skip = false;
break;
}
}
if (skip) {
continue;
}
}
var rules = sheets[i].cssRules || [];
for (var j = 0; j < rules.length; j++) {
var rule = rules[j];
if (typeof(rule.style) != "undefined") {
var elems = dom.querySelectorAll(rule.selectorText);
if (elems.length > 0) {
used += rule.selectorText + " { " + rule.style.cssText + " }\n";
}
}
}
}
// Check if there are <defs> already
var defs = dom.querySelector("defs") || document.createElement('defs');
var s = document.createElement('style');
s.setAttribute('type', 'text/css');
s.innerHTML = "<![CDATA[\n" + used + "\n]]>";
// var defs = document.createElement('defs');
defs.appendChild(s);
return defs;
};
inline_images (function () {
// var svg = div.querySelector('svg');
var outer = document.createElement("div");
var clone = from_svg.cloneNode(true);
var width = parseInt(clone.getAttribute('width'));
var height = parseInt(clone.getAttribute('height'));
clone.setAttribute("version", "1.1");
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
clone.setAttribute("width", width * scale_factor);
clone.setAttribute("height", height * scale_factor);
var scaling = document.createElement("g");
scaling.setAttribute("transform", "scale(" + scale_factor + ")");
clone.appendChild(move_children(clone, scaling));
outer.appendChild(clone);
clone.insertBefore (styling(clone), clone.firstChild);
var svg = doctype + outer.innerHTML;
svg = svg.replace ("none", "block"); // In case the svg is not being displayed, it is ignored in FF
var image = new Image();
image.src = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svg)));
image.onload = function() {
var canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
var context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
var src = canvas.toDataURL('image/png');
img_cbak (src);
// var a = document.createElement('a');
// a.download = filename;
// a.href = canvas.toDataURL('image/png');
// document.body.appendChild(a);
// a.click();
};
});
};
png_export.scale_factor = function (f) {
if (!arguments.length) {
return scale_factor;
}
scale_factor = f;
return this;
};
png_export.callback = function (cbak) {
if (!arguments.length) {
return img_cbak;
}
img_cbak = cbak;
return this;
};
png_export.stylesheets = function (restrictCss) {
if (!arguments.length) {
return css;
}
css = restrictCss;
return this;
};
// png_export.filename = function (f) {
// if (!arguments.length) {
// return filename;
// }
// filename = f;
// return png_export;
// };
return png_export;
};
var download = function () {
var filename = 'image.png';
var max_size = {
limit: Infinity,
onError: function () {
console.log("image too large");
}
};
var png_export = png()
.callback (function (src) {
var a = document.createElement('a');
a.download = filename;
a.href = src;
document.body.appendChild(a);
if (a.href.length > max_size.limit) {
a.parentNode.removeChild(a);
max_size.onError();
} else {
a.click();
}
// setTimeout(function () {
// a.click();
// }, 3000);
});
png_export.filename = function (fn) {
if (!arguments.length) {
return filename;
}
filename = fn;
return png_export;
};
png_export.limit = function (l) {
if (!arguments.length) {
return max_size;
}
max_size = l;
return this;
};
return png_export;
};
module.exports = exports = download;
},{}],8:[function(require,module,exports){
var reduce = function () {
var smooth = 5;
var value = 'val';
var redundant = function (a, b) {
if (a < b) {
return ((b-a) <= (b * 0.2));
}
return ((a-b) <= (a * 0.2));
};
var perform_reduce = function (arr) {return arr;};
var reduce = function (arr) {
if (!arr.length) {
return arr;
}
var smoothed = perform_smooth(arr);
var reduced = perform_reduce(smoothed);
return reduced;
};
var median = function (v, arr) {
arr.sort(function (a, b) {
return a[value] - b[value];
});
if (arr.length % 2) {
v[value] = arr[~~(arr.length / 2)][value];
} else {
var n = ~~(arr.length / 2) - 1;
v[value] = (arr[n][value] + arr[n+1][value]) / 2;
}
return v;
};
var clone = function (source) {
var target = {};
for (var prop in source) {
if (source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
return target;
};
var perform_smooth = function (arr) {
if (smooth === 0) { // no smooth
return arr;
}
var smooth_arr = [];
for (var i=0; i<arr.length; i++) {
var low = (i < smooth) ? 0 : (i - smooth);
var high = (i > (arr.length - smooth)) ? arr.length : (i + smooth);
smooth_arr[i] = median(clone(arr[i]), arr.slice(low,high+1));
}
return smooth_arr;
};
reduce.reducer = function (cbak) {
if (!arguments.length) {
return perform_reduce;
}
perform_reduce = cbak;
return reduce;
};
reduce.redundant = function (cbak) {
if (!arguments.length) {
return redundant;
}
redundant = cbak;
return reduce;
};
reduce.value = function (val) {
if (!arguments.length) {
return value;
}
value = val;
return reduce;
};
reduce.smooth = function (val) {
if (!arguments.length) {
return smooth;
}
smooth = val;
return reduce;
};
return reduce;
};
var block = function () {
var red = reduce()
.value('start');
var value2 = 'end';
var join = function (obj1, obj2) {
return {
'object' : {
'start' : obj1.object[red.value()],
'end' : obj2[value2]
},
'value' : obj2[value2]
};
};
// var join = function (obj1, obj2) { return obj1 };
red.reducer( function (arr) {
var value = red.value();
var redundant = red.redundant();
var reduced_arr = [];
var curr = {
'object' : arr[0],
'value' : arr[0][value2]
};
for (var i=1; i<arr.length; i++) {
if (redundant (arr[i][value], curr.value)) {
curr = join(curr, arr[i]);
continue;
}
reduced_arr.push (curr.object);
curr.object = arr[i];
curr.value = arr[i].end;
}
reduced_arr.push(curr.object);
// reduced_arr.push(arr[arr.length-1]);
return reduced_arr;
});
reduce.join = function (cbak) {
if (!arguments.length) {
return join;
}
join = cbak;
return red;
};
reduce.value2 = function (field) {
if (!arguments.length) {
return value2;
}
value2 = field;
return red;
};
return red;
};
var line = function () {
var red = reduce();
red.reducer ( function (arr) {
var redundant = red.redundant();
var value = red.value();
var reduced_arr = [];
var curr = arr[0];
for (var i=1; i<arr.length-1; i++) {
if (redundant (arr[i][value], curr[value])) {
continue;
}
reduced_arr.push (curr);
curr = arr[i];
}
reduced_arr.push(curr);
reduced_arr.push(arr[arr.length-1]);
return reduced_arr;
});
return red;
};
module.exports = reduce;
module.exports.line = line;
module.exports.block = block;
},{}],9:[function(require,module,exports){
module.exports = {
iterator : function(init_val) {
var i = init_val || 0;
var iter = function () {
return i++;
};
return iter;
},
script_path : function (script_name) { // script_name is the filename
var script_scaped = script_name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var script_re = new RegExp(script_scaped + '$');
var script_re_sub = new RegExp('(.*)' + script_scaped + '$');
// TODO: This requires phantom.js or a similar headless webkit to work (document)
var scripts = document.getElementsByTagName('script');
var path = ""; // Default to current path
if(scripts !== undefined) {
for(var i in scripts) {
if(scripts[i].src && scripts[i].src.match(script_re)) {
return scripts[i].src.replace(script_re_sub, '$1');
}
}
}
return path;
},
defer_cancel : function (cbak, time) {
var tick;
var defer_cancel = function () {
var args = Array.prototype.slice.call(arguments);
var that = this;
clearTimeout(tick);
tick = setTimeout (function () {
cbak.apply (that, args);
}, time);
};
return defer_cancel;
}
};
},{}],10:[function(require,module,exports){
var apijs = require ("tnt.api");
var deferCancel = require ("tnt.utils").defer_cancel;
var board = function() {
"use strict";
//// Private vars
var svg;
var div_id;
var tracks = [];
var min_width = 50;
var height = 0; // This is the global height including all the tracks
var width = 920;
var height_offset = 20;
var loc = {
species : undefined,
chr : undefined,
from : 0,
to : 500
};
// Limit caps
var caps = {
left : undefined,
right : undefined
};
var cap_width = 3;
// TODO: We have now background color in the tracks. Can this be removed?
// It looks like it is used in the too-wide pane etc, but it may not be needed anymore
var bgColor = d3.rgb('#F8FBEF'); //#F8FBEF
var pane; // Draggable pane
var svg_g;
var xScale;
var zoomEventHandler = d3.behavior.zoom();
var limits = {
min : 0,
max : 1000,
zoom_out : 1000,
zoom_in : 100
};
var dur = 500;
var drag_allowed = true;
var exports = {
ease : d3.ease("cubic-in-out"),
extend_canvas : {
left : 0,
right : 0
},
show_frame : true
// limits : function () {throw "The limits method should be defined"}
};
// The returned closure / object
var track_vis = function(div) {
div_id = d3.select(div).attr("id");
// The original div is classed with the tnt class
d3.select(div)
.classed("tnt", true);
// TODO: Move the styling to the scss?
var browserDiv = d3.select(div)
.append("div")
.attr("id", "tnt_" + div_id)
.style("position", "relative")
.classed("tnt_framed", exports.show_frame ? true : false)
.style("width", (width + cap_width*2 + exports.extend_canvas.right + exports.extend_canvas.left) + "px");
var groupDiv = browserDiv
.append("div")
.attr("class", "tnt_groupDiv");
// The SVG
svg = groupDiv
.append("svg")
.attr("class", "tnt_svg")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all");
svg_g = svg
.append("g")
.attr("transform", "translate(0,20)")
.append("g")
.attr("class", "tnt_g");
// caps
caps.left = svg_g
.append("rect")
.attr("id", "tnt_" + div_id + "_5pcap")
.attr("x", 0)
.attr("y", 0)
.attr("width", 0)
.attr("height", height)
.attr("fill", "red");
caps.right = svg_g
.append("rect")
.attr("id", "tnt_" + div_id + "_3pcap")
.attr("x", width-cap_width)
.attr("y", 0)
.attr("width", 0)
.attr("height", height)
.attr("fill", "red");
// The Zooming/Panning Pane
pane = svg_g
.append("rect")
.attr("class", "tnt_pane")
.attr("id", "tnt_" + div_id + "_pane")
.attr("width", width)
.attr("height", height)
.style("fill", bgColor);
// ** TODO: Wouldn't be better to have these messages by track?
// var tooWide_text = svg_g
// .append("text")
// .attr("class", "tnt_wideOK_text")
// .attr("id", "tnt_" + div_id + "_tooWide")
// .attr("fill", bgColor)
// .text("Region too wide");
// TODO: I don't know if this is the best way (and portable) way
// of centering the text in the text area
// var bb = tooWide_text[0][0].getBBox();
// tooWide_text
// .attr("x", ~~(width/2 - bb.width/2))
// .attr("y", ~~(height/2 - bb.height/2));
};
// API
var api = apijs (track_vis)
.getset (exports)
.getset (limits)
.getset (loc);
api.transform (track_vis.extend_canvas, function (val) {
var prev_val = track_vis.extend_canvas();
val.left = val.left || prev_val.left;
val.right = val.right || prev_val.right;
return val;
});
// track_vis always starts on loc.from & loc.to
api.method ('start', function () {
// make sure that zoom_out is within the min-max range
if ((limits.max - limits.min) < limits.zoom_out) {
limits.zoom_out = limits.max - limits.min;
}
plot();
// Reset the tracks
for (var i=0; i<tracks.length; i++) {
if (tracks[i].g) {
// tracks[i].display().reset.call(tracks[i]);
tracks[i].g.remove();
}
_init_track(tracks[i]);
}
_place_tracks();
// The continuation callback
var cont = function () {
if ((loc.to - loc.from) < limits.zoom_in) {
if ((loc.from + limits.zoom_in) > limits.max) {
loc.to = limits.max;
} else {
loc.to = loc.from + limits.zoom_in;
}
}
for (var i=0; i<tracks.length; i++) {
_update_track(tracks[i], loc);
}
};
cont();
});
api.method ('update', function () {
for (var i=0; i<tracks.length; i++) {
_update_track (tracks[i]);
}
});
var _update_track = function (track, where) {
if (track.data()) {
var track_data = track.data();
var data_updater = track_data;
data_updater.call(track, {
'loc' : where,
'on_success' : function () {
track.display().update.call(track, where);
}
});
}
};
var plot = function() {
xScale = d3.scale.linear()
.domain([loc.from, loc.to])
.range([0, width]);
if (drag_allowed) {
svg_g.call( zoomEventHandler
.x(xScale)
.scaleExtent([(loc.to-loc.from)/(limits.zoom_out-1), (loc.to-loc.from)/limits.zoom_in])
.on("zoom", _move)
);
}
};
var _reorder = function (new_tracks) {
// TODO: This is defining a new height, but the global height is used to define the size of several
// parts. We should do this dynamically
var found_indexes = [];
for (var j=0; j<new_tracks.length; j++) {
var found = false;
for (var i=0; i<tracks.length; i++) {
if (tracks[i].id() === new_tracks[j].id()) {
found = true;
found_indexes[i] = true;
// tracks.splice(i,1);
break;
}
}
if (!found) {
_init_track(new_tracks[j]);
_update_track(new_tracks[j], {from : loc.from, to : loc.to});
}
}
for (var x=0; x<tracks.length; x++) {
if (!found_indexes[x]) {
tracks[x].g.remove();
}
}
tracks = new_tracks;
_place_tracks();
};
// right/left/zoom pans or zooms the track. These methods are exposed to allow external buttons, etc to interact with the tracks. The argument is the amount of panning/zooming (ie. 1.2 means 20% panning) With left/right only positive numbers are allowed.
api.method ('scroll', function (factor) {
var amount = Math.abs(factor);
if (factor > 0) {
_manual_move(amount, 1);
} else if (factor < 0){
_manual_move(amount, -1);
}
});
api.method ('zoom', function (factor) {
_manual_move(1/factor, 0);
});
api.method ('find_track', function (id) {
for (var i=0; i<tracks.length; i++) {
if (tracks[i].id() === id) {
return tracks[i];
}
}
});
api.method ('remove_track', function (track) {
track.g.remove();
});
api.method ('add_track', function (track) {
if (track instanceof Array) {
for (var i=0; i<track.length; i++) {
track_vis.add_track (track[i]);
}
return track_vis;
}
tracks.push(track);
return track_vis;
});
api.method('tracks', function (ts) {
if (!arguments.length) {
return tracks;
}
_reorder(ts);
return this;
});
//
api.method ('width', function (w) {
// TODO: Allow suffixes like "1000px"?
// TODO: Test wrong formats
if (!arguments.length) {
return width;
}
// At least min-width
if (w < min_width) {
w = min_width;
}
// We are resizing
if (div_id !== undefined) {
d3.select("#tnt_" + div_id).select("svg").attr("width", w);
// Resize the zooming/panning pane
d3.select("#tnt_" + div_id).style("width", (parseInt(w) + cap_width*2) + "px");
d3.select("#tnt_" + div_id + "_pane").attr("width", w);
caps.right
.attr("x", w-cap_width);
// Replot
width = w;
xScale.range([0, width]);
plot();
for (var i=0; i<tracks.length; i++) {
tracks[i].g.select("rect").attr("width", w);
tracks[i].display().scale(xScale);
tracks[i].display().reset.call(tracks[i]);
tracks[i].display().init.call(tracks[i], w);
tracks[i].display().update.call(tracks[i], loc);
}
} else {
width = w;
}
return track_vis;
});
api.method('allow_drag', function(b) {
if (!arguments.length) {
return drag_allowed;
}
drag_allowed = b;
if (drag_allowed) {
// When this method is called on the object before starting the simulation, we don't have defined xScale
if (xScale !== undefined) {
svg_g.call( zoomEventHandler.x(xScale)
// .xExtent([0, limits.right])
.scaleExtent([(loc.to-loc.from)/(limits.zoom_out-1), (loc.to-loc.from)/limits.zoom_in])
.on("zoom", _move) );
}
} else {
// We create a new dummy scale in x to avoid dragging the previous one
// TODO: There may be a cheaper way of doing this?
zoomEventHandler.x(d3.scale.linear()).on("zoom", null);
}
return track_vis;
});
var _place_tracks = function () {
var h = 0;
for (var i=0; i<tracks.length; i++) {
var track = tracks[i];
if (track.g.attr("transform")) {
track.g
.transition()
.duration(dur)
.attr("transform", "translate(" + exports.extend_canvas.left + "," + h + ")");
} else {
track.g
.attr("transform", "translate(" + exports.extend_canvas.left + "," + h + ")");
}
h += track.height();
}
// svg
svg.attr("height", h + height_offset);
// div
d3.select("#tnt_" + div_id)
.style("height", (h + 10 + height_offset) + "px");
// caps
d3.select("#tnt_" + div_id + "_5pcap")
.attr("height", h)
.each(function (d) {
move_to_front(this);
});
d3.select("#tnt_" + div_id + "_3pcap")
.attr("height", h)
.each (function (d) {
move_to_front(this);
});
// pane
pane
.attr("height", h + height_offset);
return track_vis;
};
var _init_track = function (track) {
track.g = svg.select("g").select("g")
.append("g")
.attr("class", "tnt_track")
.attr("height", track.height());
// Rect for the background color
track.g
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", track_vis.width())
.attr("height", track.height())
.style("fill", track.color())
.style("pointer-events", "none");
if (track.display()) {
track.display()
.scale(xScale)
.init.call(track, width);
}
return track_vis;
};
var _manual_move = function (factor, direction) {
var oldDomain = xScale.domain();
var span = oldDomain[1] - oldDomain[0];
var offset = (span * factor) - span;
var newDomain;
switch (direction) {
case 1 :
newDomain = [(~~oldDomain[0] - offset), ~~(oldDomain[1] - offset)];
break;
case -1 :
newDomain = [(~~oldDomain[0] + offset), ~~(oldDomain[1] - offset)];
break;
case 0 :
newDomain = [oldDomain[0] - ~~(offset/2), oldDomain[1] + (~~offset/2)];
}
var interpolator = d3.interpolateNumber(oldDomain[0], newDomain[0]);
var ease = exports.ease;
var x = 0;
d3.timer(function() {
var curr_start = interpolator(ease(x));
var curr_end;
switch (direction) {
case -1 :
curr_end = curr_start + span;
break;
case 1 :
curr_end = curr_start + span;
break;
case 0 :
curr_end = oldDomain[1] + oldDomain[0] - curr_start;
break;
}
var currDomain = [curr_start, curr_end];
xScale.domain(currDomain);
_move(xScale);
x+=0.02;
return x>1;
});
};
var _move_cbak = function () {
var currDomain = xScale.domain();
track_vis.from(~~currDomain[0]);
track_vis.to(~~currDomain[1]);
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
_update_track(track, loc);
}
};
// The deferred_cbak is deferred at least this amount of time or re-scheduled if deferred is called before
var _deferred = deferCancel(_move_cbak, 300);
// api.method('update', function () {
// _move();
// });
var _move = function (new_xScale) {
if (new_xScale !== undefined && drag_allowed) {
zoomEventHandler.x(new_xScale);
}
// Show the red bars at the limits
var domain = xScale.domain();
if (domain[0] <= (limits.min + 5)) {
d3.select("#tnt_" + div_id + "_5pcap")
.attr("width", cap_width)
.transition()
.duration(200)
.attr("width", 0);
}
if (domain[1] >= (limits.max)-5) {
d3.select("#tnt_" + div_id + "_3pcap")
.attr("width", cap_width)
.transition()
.duration(200)
.attr("width", 0);
}
// Avoid moving past the limits
if (domain[0] < limits.min) {
zoomEventHandler.translate([zoomEventHandler.translate()[0] - xScale(limits.min) + xScale.range()[0], zoomEventHandler.translate()[1]]);
} else if (domain[1] > limits.max) {
zoomEventHandler.translate([zoomEventHandler.translate()[0] - xScale(limits.max) + xScale.range()[1], zoomEventHandler.translate()[1]]);
}
_deferred();
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
track.display().mover.call(track);
}
};
// api.method({
// allow_drag : api_allow_drag,
// width : api_width,
// add_track : api_add_track,
// reorder : api_reorder,
// zoom : api_zoom,
// left : api_left,
// right : api_right,
// start : api_start
// });
// Auxiliar functions
function move_to_front (elem) {
elem.parentNode.appendChild(elem);
}
return track_vis;
};
module.exports = exports = board;
},{"tnt.api":3,"tnt.utils":5}],11:[function(require,module,exports){
var apijs = require ("tnt.api");
var spinner = require ("./spinner.js")();
tnt_data = {};
tnt_data.sync = function() {
var update_track = function(obj) {
var track = this;
track.data().elements(update_track.retriever().call(track, obj.loc));
obj.on_success();
};
apijs (update_track)
.getset ('elements', [])
.getset ('retriever', function () {});
return update_track;
};
tnt_data.async = function () {
var update_track = function (obj) {
var track = this;
spinner.on.call(track);
update_track.retriever().call(track, obj.loc)
.then (function (resp) {
track.data().elements(resp);
obj.on_success();
spinner.off.call(track);
});
};
var api = apijs (update_track)
.getset ('elements', [])
.getset ('retriever');
return update_track;
};
// A predefined track displaying no external data
// it is used for location and axis tracks for example
tnt_data.empty = function () {
var updater = tnt_data.sync();
return updater;
};
module.exports = exports = tnt_data;
},{"./spinner.js":15,"tnt.api":3}],12:[function(require,module,exports){
var apijs = require ("tnt.api");
var layout = require("./layout.js");
// FEATURE VIS
// var board = {};
// board.track = {};
var tnt_feature = function () {
var dispatch = d3.dispatch ("click", "dblclick", "mouseover", "mouseout");
////// Vars exposed in the API
var config = {
create : function () {throw "create_elem is not defined in the base feature object";},
move : function () {throw "move_elem is not defined in the base feature object";},
distribute : function () {},
fixed : function () {},
//layout : function () {},
index : undefined,
layout : layout.identity(),
color : '#000',
scale : undefined
};
// The returned object
var feature = {};
var reset = function () {
var track = this;
track.g.selectAll(".tnt_elem").remove();
track.g.selectAll(".tnt_guider").remove();
track.g.selectAll(".tnt_fixed").remove();
};
var init = function (width) {
var track = this;
track.g
.append ("text")
.attr ("class", "tnt_fixed")
.attr ("x", 5)
.attr ("y", 12)
.attr ("font-size", 11)
.attr ("fill", "grey")
.text (track.label());
config.fixed.call(track, width);
};
var plot = function (new_elems, track, xScale) {
new_elems.on("click", function (d, i) {
if (d3.event.defaultPrevented) {
return;
}
dispatch.click.call(this, d, i);
});
new_elems.on("mouseover", function (d, i) {
if (d3.event.defaultPrevented) {
return;
}
dispatch.mouseover.call(this, d, i);
});
new_elems.on("dblclick", function (d, i) {
if (d3.event.defaultPrevented) {
return;
}
dispatch.dblclick.call(this, d, i);
});
new_elems.on("mouseout", function (d, i) {
if (d3.event.defaultPrevented) {
return;
}
dispatch.mouseout.call(this, d, i);
});
// new_elem is a g element the feature is inserted
config.create.call(track, new_elems, xScale);
};
var update = function (loc, field) {
var track = this;
var svg_g = track.g;
var elements = track.data().elements();
if (field !== undefined) {
elements = elements[field];
}
var data_elems = config.layout.call(track, elements);
if (data_elems === undefined) {
return;
}
var vis_sel;
var vis_elems;
if (field !== undefined) {
vis_sel = svg_g.selectAll(".tnt_elem_" + field);
} else {
vis_sel = svg_g.selectAll(".tnt_elem");
}
if (config.index) { // Indexing by field
vis_elems = vis_sel
.data(data_elems, function (d) {
if (d !== undefined) {
return config.index(d);
}
});
} else { // Indexing by position in array
vis_elems = vis_sel
.data(data_elems);
}
config.distribute.call(track, vis_elems, config.scale);
var new_elem = vis_elems
.enter();
new_elem
.append("g")
.attr("class", "tnt_elem")
.classed("tnt_elem_" + field, field)
.call(feature.plot, track, config.scale);
vis_elems
.exit()
.remove();
};
var mover = function (field) {
var track = this;
var svg_g = track.g;
var elems;
// TODO: Is selecting the elements to move too slow?
// It would be nice to profile
if (field !== undefined) {
elems = svg_g.selectAll(".tnt_elem_" + field);
} else {
elems = svg_g.selectAll(".tnt_elem");
}
config.move.call(this, elems);
};
var mtf = function (elem) {
elem.parentNode.appendChild(elem);
};
var move_to_front = function (field) {
if (field !== undefined) {
var track = this;
var svg_g = track.g;
svg_g.selectAll(".tnt_elem_" + field)
.each( function () {
mtf(this);
});
}
};
// API
apijs (feature)
.getset (config)
.method ({
reset : reset,
plot : plot,
update : update,
mover : mover,
init : init,
move_to_front : move_to_front
});
return d3.rebind(feature, dispatch, "on");
};
tnt_feature.composite = function () {
var displays = {};
var display_order = [];
var features = {};
var reset = function () {
var track = this;
for (var display in displays) {
if (displays.hasOwnProperty(display)) {
displays[display].reset.call(track);
}
}
};
var init = function (width) {
var track = this;
for (var display in displays) {
if (displays.hasOwnProperty(display)) {
displays[display].scale(features.scale());
displays[display].init.call(track, width);
}
}
};
var update = function () {
var track = this;
for (var i=0; i<display_order.length; i++) {
displays[display_order[i]].update.call(track, undefined, display_order[i]);
displays[display_order[i]].move_to_front.call(track, display_order[i]);
}
// for (var display in displays) {
// if (displays.hasOwnProperty(display)) {
// displays[display].update.call(track, xScale, display);
// }
// }
};
var mover = function () {
var track = this;
for (var display in displays) {
if (displays.hasOwnProperty(display)) {
displays[display].mover.call(track, display);
}
}
};
var add = function (key, display) {
displays[key] = display;
display_order.push(key);
return features;
};
var get_displays = function () {
var ds = [];
for (var i=0; i<display_order.length; i++) {
ds.push(displays[display_order[i]]);
}
return ds;
};
// API
apijs (features)
.getset("scale")
.method ({
reset : reset,
update : update,
mover : mover,
init : init,
add : add,
displays : get_displays
});
return features;
};
tnt_feature.area = function () {
var feature = tnt_feature.line();
var line = feature.line();
var area = d3.svg.area()
.interpolate(line.interpolate())
.tension(feature.tension());
var data_points;
var line_create = feature.create(); // We 'save' line creation
feature.create (function (points) {
var track = this;
var xScale = feature.scale();
if (data_points !== undefined) {
track.g.select("path").remove();
}
line_create.call(track, points, xScale);
area
.x(line.x())
.y1(line.y())
.y0(track.height());
data_points = points.data();
points.remove();
track.g
.append("path")
.attr("class", "tnt_area")
.classed("tnt_elem", true)
.datum(data_points)
.attr("d", area)
.attr("fill", d3.rgb(feature.color()).brighter());
});
var line_move = feature.move();
feature.move (function (path) {
var track = this;
var xScale = feature.scale();
line_move.call(track, path, xScale);
area.x(line.x());
track.g
.select(".tnt_area")
.datum(data_points)
.attr("d", area);
});
return feature;
};
tnt_feature.line = function () {
var feature = tnt_feature();
var x = function (d) {
return d.pos;
};
var y = function (d) {
return d.val;
};
var tension = 0.7;
var yScale = d3.scale.linear();
var line = d3.svg.line()
.interpolate("basis");
// line getter. TODO: Setter?
feature.line = function () {
return line;
};
feature.x = function (cbak) {
if (!arguments.length) {
return x;
}
x = cbak;
return feature;
};
feature.y = function (cbak) {
if (!arguments.length) {
return y;
}
y = cbak;
return feature;
};
feature.tension = function (t) {
if (!arguments.length) {
return tension;
}
tension = t;
return feature;
};
var data_points;
// For now, create is a one-off event
// TODO: Make it work with partial paths, ie. creating and displaying only the path that is being displayed
feature.create (function (points) {
var track = this;
var xScale = feature.scale();
if (data_points !== undefined) {
// return;
track.g.select("path").remove();
}
line
.tension(tension)
.x(function (d) {
return xScale(x(d));
})
.y(function (d) {
return track.height() - yScale(y(d));
});
data_points = points.data();
points.remove();
yScale
.domain([0, 1])
// .domain([0, d3.max(data_points, function (d) {
// return y(d);
// })])
.range([0, track.height() - 2]);
track.g
.append("path")
.attr("class", "tnt_elem")
.attr("d", line(data_points))
.style("stroke", feature.color())
.style("stroke-width", 4)
.style("fill", "none");
});
feature.move (function (path) {
var track = this;
var xScale = feature.scale();
line.x(function (d) {
return xScale(x(d));
});
track.g.select("path")
.attr("d", line(data_points));
});
return feature;
};
tnt_feature.conservation = function () {
// 'Inherit' from feature.area
var feature = tnt_feature.area();
var area_create = feature.create(); // We 'save' area creation
feature.create (function (points) {
var track = this;
var xScale = feature.scale();
area_create.call(track, d3.select(points[0][0]), xScale);
});
return feature;
};
tnt_feature.ensembl = function () {
// 'Inherit' from board.track.feature
var feature = tnt_feature();
var color2 = "#7FFF00";
var color3 = "#00BB00";
feature.fixed (function (width) {
var track = this;
var height_offset = ~~(track.height() - (track.height() * 0.8)) / 2;
track.g
.append("line")
.attr("class", "tnt_guider tnt_fixed")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height_offset)
.attr("y2", height_offset)
.style("stroke", feature.color())
.style("stroke-width", 1);
track.g
.append("line")
.attr("class", "tnt_guider tnt_fixed")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", track.height() - height_offset)
.attr("y2", track.height() - height_offset)
.style("stroke", feature.color())
.style("stroke-width", 1);
});
feature.create (function (new_elems) {
var track = this;
var xScale = feature.scale();
var height_offset = ~~(track.height() - (track.height() * 0.8)) / 2;
new_elems
.append("rect")
.attr("x", function (d) {
return xScale (d.start);
})
.attr("y", height_offset)
// .attr("rx", 3)
// .attr("ry", 3)
.attr("width", function (d) {
return (xScale(d.end) - xScale(d.start));
})
.attr("height", track.height() - ~~(height_offset * 2))
.attr("fill", track.color())
.transition()
.duration(500)
.attr("fill", function (d) {
if (d.type === 'high') {
return d3.rgb(feature.color());
}
if (d.type === 'low') {
return d3.rgb(feature.color2());
}
return d3.rgb(feature.color3());
});
});
feature.distribute (function (blocks) {
var xScale = feature.scale();
blocks
.select("rect")
.attr("width", function (d) {
return (xScale(d.end) - xScale(d.start));
});
});
feature.move (function (blocks) {
var xScale = feature.scale();
blocks
.select("rect")
.attr("x", function (d) {
return xScale(d.start);
})
.attr("width", function (d) {
return (xScale(d.end) - xScale(d.start));
});
});
feature.color2 = function (col) {
if (!arguments.length) {
return color2;
}
color2 = col;
return feature;
};
feature.color3 = function (col) {
if (!arguments.length) {
return color3;
}
color3 = col;
return feature;
};
return feature;
};
tnt_feature.vline = function () {
// 'Inherit' from feature
var feature = tnt_feature();
feature.create (function (new_elems) {
var xScale = feature.scale();
var track = this;
new_elems
.append ("line")
.attr("x1", function (d) {
return xScale(feature.index()(d));
})
.attr("x2", function (d) {
return xScale(feature.index()(d));
})
.attr("y1", 0)
.attr("y2", track.height())
.attr("stroke", feature.color())
.attr("stroke-width", 1);
});
feature.move (function (vlines) {
var xScale = feature.scale();
vlines
.select("line")
.attr("x1", function (d) {
return xScale(feature.index()(d));
})
.attr("x2", function (d) {
return xScale(feature.index()(d));
});
});
return feature;
};
tnt_feature.pin = function () {
// 'Inherit' from board.track.feature
var feature = tnt_feature();
var yScale = d3.scale.linear()
.domain([0,0])
.range([0,0]);
var opts = {
pos : d3.functor("pos"),
val : d3.functor("val"),
domain : [0,1]
};
var pin_ball_r = 5; // the radius of the circle in the pin
apijs(feature)
.getset(opts);
feature.create (function (new_pins) {
var track = this;
var xScale = feature.scale();
yScale
.domain(feature.domain())
.range([pin_ball_r, track.height()-pin_ball_r-10]); // 10 for labelling
// pins are composed of lines, circles and labels
new_pins
.append("line")
.attr("x1", function (d, i) {
return xScale(d[opts.pos(d, i)]);
})
.attr("y1", function (d) {
return track.height();
})
.attr("x2", function (d,i) {
return xScale(d[opts.pos(d, i)]);
})
.attr("y2", function (d, i) {
return track.height() - yScale(d[opts.val(d, i)]);
})
.attr("stroke", function (d) {
return d3.functor(feature.color())(d);
});
new_pins
.append("circle")
.attr("cx", function (d, i) {
return xScale(d[o