tnt.board
Version:
TnT track-based board display
866 lines (736 loc) • 21.7 kB
JavaScript
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[opts.pos(d, i)]);
})
.attr("cy", function (d, i) {
return track.height() - yScale(d[opts.val(d, i)]);
})
.attr("r", pin_ball_r)
.attr("fill", function (d) {
return d3.functor(feature.color())(d);
});
new_pins
.append("text")
.attr("font-size", "13")
.attr("x", function (d, i) {
return xScale(d[opts.pos(d, i)]);
})
.attr("y", function (d, i) {
return 10;
})
.style("text-anchor", "middle")
.style("fill", function (d) {
return d3.functor(feature.color())(d);
})
.text(function (d) {
return d.label || "";
});
});
feature.distribute (function (pins) {
pins
.select("text")
.text(function (d) {
return d.label || "";
});
});
feature.move(function (pins) {
var track = this;
var xScale = feature.scale();
pins
//.each(position_pin_line)
.select("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)]);
});
pins
.select("circle")
.attr("cx", function (d, i) {
return xScale(d[opts.pos(d, i)]);
})
.attr("cy", function (d, i) {
return track.height() - yScale(d[opts.val(d, i)]);
});
pins
.select("text")
.attr("x", function (d, i) {
return xScale(d[opts.pos(d, i)]);
})
.text(function (d) {
return d.label || "";
});
});
feature.fixed (function (width) {
var track = this;
track.g
.append("line")
.attr("class", "tnt_fixed")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", track.height())
.attr("y2", track.height())
.style("stroke", "black")
.style("stroke-with", "1px");
});
return feature;
};
tnt_feature.block = function () {
// 'Inherit' from board.track.feature
var feature = tnt_feature();
apijs(feature)
.getset('from', function (d) {
return d.start;
})
.getset('to', function (d) {
return d.end;
});
feature.create(function (new_elems) {
var track = this;
var xScale = feature.scale();
new_elems
.append("rect")
.attr("x", function (d, i) {
// TODO: start, end should be adjustable via the tracks API
return xScale(feature.from()(d, i));
})
.attr("y", 0)
.attr("width", function (d, i) {
return (xScale(feature.to()(d, i)) - xScale(feature.from()(d, i)));
})
.attr("height", track.height())
.attr("fill", track.color())
.transition()
.duration(500)
.attr("fill", function (d) {
if (d.color === undefined) {
return feature.color();
} else {
return d.color;
}
});
});
feature.distribute(function (elems) {
var xScale = feature.scale();
elems
.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));
});
});
return feature;
};
tnt_feature.axis = function () {
var xAxis;
var orientation = "top";
var xScale;
// Axis doesn't inherit from feature
var feature = {};
feature.reset = function () {
xAxis = undefined;
var track = this;
track.g.selectAll(".tick").remove();
};
feature.plot = function () {};
feature.mover = function () {
var track = this;
var svg_g = track.g;
svg_g.call(xAxis);
};
feature.init = function () {
xAxis = undefined;
};
feature.update = function () {
// Create Axis if it doesn't exist
if (xAxis === undefined) {
xAxis = d3.svg.axis()
.scale(xScale)
.orient(orientation);
}
var track = this;
var svg_g = track.g;
svg_g.call(xAxis);
};
feature.orientation = function (pos) {
if (!arguments.length) {
return orientation;
}
orientation = pos;
return this;
};
feature.scale = function (s) {
if (!arguments.length) {
return xScale;
}
xScale = s;
return this;
};
return feature;
};
tnt_feature.location = function () {
var row;
var xScale;
var feature = {};
feature.reset = function () {
row = undefined;
};
feature.plot = function () {};
feature.init = function () {
row = undefined;
var track = this;
track.g.select("text").remove();
};
feature.mover = function() {
var domain = xScale.domain();
row.select("text")
.text("Location: " + ~~domain[0] + "-" + ~~domain[1]);
};
feature.scale = function (sc) {
if (!arguments.length) {
return xScale;
}
xScale = sc;
return this;
};
feature.update = function (loc) {
var track = this;
var svg_g = track.g;
var domain = xScale.domain();
if (row === undefined) {
row = svg_g;
row
.append("text")
.text("Location: " + Math.round(domain[0]) + "-" + Math.round(domain[1]));
}
};
return feature;
};
module.exports = exports = tnt_feature;