UNPKG

tnt.board

Version:

TnT track-based board display

1,884 lines (1,592 loc) 164 kB
(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