tnt.tree
Version:
TnT tree display
479 lines (398 loc) • 14.6 kB
JavaScript
var apijs = require("tnt.api");
var tnt_tree_node = require("tnt.tree.node");
var tree = function () {
"use strict";
var dispatch = d3.dispatch ("click", "dblclick", "mouseover", "mouseout");
var conf = {
duration : 500, // Duration of the transitions
node_display : tree.node_display.circle(),
label : tree.label.text(),
layout : tree.layout.vertical(),
// on_click : function () {},
// on_dbl_click : function () {},
// on_mouseover : function () {},
branch_color : 'black',
id : function (d) {
return d._id;
}
};
// Keep track of the focused node
// TODO: Would it be better to have multiple focused nodes? (ie use an array)
var focused_node;
// Extra delay in the transitions (TODO: Needed?)
var delay = 0;
// Ease of the transitions
var ease = "cubic-in-out";
// By node data
var sp_counts = {};
var scale = false;
// The id of the tree container
var div_id;
// The tree visualization (svg)
var svg;
var vis;
var links_g;
var nodes_g;
// TODO: For now, counts are given only for leaves
// but it may be good to allow counts for internal nodes
var counts = {};
// The full tree
var base = {
tree : undefined,
data : undefined,
nodes : undefined,
links : undefined
};
// The curr tree. Needed to re-compute the links / nodes positions of subtrees
var curr = {
tree : undefined,
data : undefined,
nodes : undefined,
links : undefined
};
// The cbak returned
var t = function (div) {
div_id = d3.select(div).attr("id");
var tree_div = d3.select(div)
.append("div")
.style("width", (conf.layout.width() + "px"))
.attr("class", "tnt_groupDiv");
var cluster = conf.layout.cluster;
var n_leaves = curr.tree.get_all_leaves().length;
var max_leaf_label_length = function (tree) {
var max = 0;
var leaves = tree.get_all_leaves();
for (var i=0; i<leaves.length; i++) {
var label_width = conf.label.width()(leaves[i]) + d3.functor (conf.node_display.size())(leaves[i]);
if (label_width > max) {
max = label_width;
}
}
return max;
};
var max_leaf_node_height = function (tree) {
var max = 0;
var leaves = tree.get_all_leaves();
for (var i=0; i<leaves.length; i++) {
var node_height = d3.functor(conf.node_display.size())(leaves[i]) * 2;
var label_height = d3.functor(conf.label.height())(leaves[i]);
max = d3.max([max, node_height, label_height]);
}
return max;
};
var max_label_length = max_leaf_label_length(curr.tree);
conf.layout.max_leaf_label_width(max_label_length);
var max_node_height = max_leaf_node_height(curr.tree);
// Cluster size is the result of...
// total width of the vis - transform for the tree - max_leaf_label_width - horizontal transform of the label
// TODO: Substitute 15 by the horizontal transform of the nodes
var cluster_size_params = {
n_leaves : n_leaves,
label_height : max_node_height,
label_padding : 15
};
conf.layout.adjust_cluster_size(cluster_size_params);
var diagonal = conf.layout.diagonal();
var transform = conf.layout.transform_node;
svg = tree_div
.append("svg")
.attr("width", conf.layout.width())
.attr("height", conf.layout.height(cluster_size_params) + 30)
.attr("fill", "none");
vis = svg
.append("g")
.attr("id", "tnt_st_" + div_id)
.attr("transform",
"translate(" +
conf.layout.translate_vis()[0] +
"," +
conf.layout.translate_vis()[1] +
")");
curr.nodes = cluster.nodes(curr.data);
conf.layout.scale_branch_lengths(curr);
curr.links = cluster.links(curr.nodes);
// LINKS
// All the links are grouped in a g element
links_g = vis
.append("g")
.attr("class", "links");
nodes_g = vis
.append("g")
.attr("class", "nodes");
//var link = vis
var link = links_g
.selectAll("path.tnt_tree_link")
.data(curr.links, function(d){
return conf.id(d.target);
});
link
.enter()
.append("path")
.attr("class", "tnt_tree_link")
.attr("id", function(d) {
return "tnt_tree_link_" + div_id + "_" + conf.id(d.target);
})
.style("stroke", function (d) {
return d3.functor(conf.branch_color)(tnt_tree_node(d.source), tnt_tree_node(d.target));
})
.attr("d", diagonal);
// NODES
//var node = vis
var node = nodes_g
.selectAll("g.tnt_tree_node")
.data(curr.nodes, function(d) {
return conf.id(d);
});
var new_node = node
.enter().append("g")
.attr("class", function(n) {
if (n.children) {
if (n.depth === 0) {
return "root tnt_tree_node";
} else {
return "inner tnt_tree_node";
}
} else {
return "leaf tnt_tree_node";
}
})
.attr("id", function(d) {
return "tnt_tree_node_" + div_id + "_" + d._id;
})
.attr("transform", transform);
// display node shape
new_node
.each (function (d) {
conf.node_display.call(this, tnt_tree_node(d));
});
// display node label
new_node
.each (function (d) {
conf.label.call(this, tnt_tree_node(d), conf.layout.type, d3.functor(conf.node_display.size())(tnt_tree_node(d)));
});
new_node.on("click", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:click", my_node);
dispatch.click.call(this, my_node);
});
new_node.on("dblclick", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:dblclick", my_node);
dispatch.dblclick.call(this, my_node);
});
new_node.on("mouseover", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:hover", tnt_tree_node(node));
dispatch.mouseover.call(this, my_node);
});
new_node.on("mouseout", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:mouseout", tnt_tree_node(node));
dispatch.mouseout.call(this, my_node);
});
// Update plots an updated tree
api.method ('update', function() {
tree_div
.style("width", (conf.layout.width() + "px"));
svg.attr("width", conf.layout.width());
var cluster = conf.layout.cluster;
var diagonal = conf.layout.diagonal();
var transform = conf.layout.transform_node;
var max_label_length = max_leaf_label_length(curr.tree);
conf.layout.max_leaf_label_width(max_label_length);
var max_node_height = max_leaf_node_height(curr.tree);
// Cluster size is the result of...
// total width of the vis - transform for the tree - max_leaf_label_width - horizontal transform of the label
// TODO: Substitute 15 by the transform of the nodes (probably by selecting one node assuming all the nodes have the same transform
var n_leaves = curr.tree.get_all_leaves().length;
var cluster_size_params = {
n_leaves : n_leaves,
label_height : max_node_height,
label_padding : 15
};
conf.layout.adjust_cluster_size(cluster_size_params);
svg
.transition()
.duration(conf.duration)
.ease(ease)
.attr("height", conf.layout.height(cluster_size_params) + 30); // height is in the layout
vis
.transition()
.duration(conf.duration)
.attr("transform",
"translate(" +
conf.layout.translate_vis()[0] +
"," +
conf.layout.translate_vis()[1] +
")");
curr.nodes = cluster.nodes(curr.data);
conf.layout.scale_branch_lengths(curr);
curr.links = cluster.links(curr.nodes);
// LINKS
var link = links_g
.selectAll("path.tnt_tree_link")
.data(curr.links, function(d){
return conf.id(d.target);
});
// NODES
var node = nodes_g
.selectAll("g.tnt_tree_node")
.data(curr.nodes, function(d) {
return conf.id(d);
});
var exit_link = link
.exit()
.remove();
link
.enter()
.append("path")
.attr("class", "tnt_tree_link")
.attr("id", function (d) {
return "tnt_tree_link_" + div_id + "_" + conf.id(d.target);
})
.attr("stroke", function (d) {
return d3.functor(conf.branch_color)(tnt_tree_node(d.source), tnt_tree_node(d.target));
})
.attr("d", diagonal);
link
.transition()
.ease(ease)
.duration(conf.duration)
.attr("d", diagonal);
// Nodes
var new_node = node
.enter()
.append("g")
.attr("class", function(n) {
if (n.children) {
if (n.depth === 0) {
return "root tnt_tree_node";
} else {
return "inner tnt_tree_node";
}
} else {
return "leaf tnt_tree_node";
}
})
.attr("id", function (d) {
return "tnt_tree_node_" + div_id + "_" + d._id;
})
.attr("transform", transform);
// Exiting nodes are just removed
node
.exit()
.remove();
new_node.on("click", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:click", my_node);
dispatch.click.call(this, my_node);
});
new_node.on("dblclick", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:dblclick", my_node);
dispatch.dblclick.call(this, my_node);
});
new_node.on("mouseover", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:hover", tnt_tree_node(node));
dispatch.mouseover.call(this, my_node);
});
new_node.on("mouseout", function (node) {
var my_node = tnt_tree_node(node);
tree.trigger("node:mouseout", tnt_tree_node(node));
dispatch.mouseout.call(this, my_node);
});
// // We need to re-create all the nodes again in case they have changed lively (or the layout)
// node.selectAll("*").remove();
// new_node
// .each(function (d) {
// conf.node_display.call(this, tnt_tree_node(d));
// });
//
// // We need to re-create all the labels again in case they have changed lively (or the layout)
// new_node
// .each (function (d) {
// conf.label.call(this, tnt_tree_node(d), conf.layout.type, d3.functor(conf.node_display.size())(tnt_tree_node(d)));
// });
t.update_nodes();
node
.transition()
.ease(ease)
.duration(conf.duration)
.attr("transform", transform);
});
api.method('update_nodes', function () {
var node = nodes_g
.selectAll("g.tnt_tree_node");
// re-create all the nodes again
// node.selectAll("*").remove();
node
.each(function () {
conf.node_display.reset.call(this);
});
node
.each(function (d) {
//console.log(conf.node_display());
conf.node_display.call(this, tnt_tree_node(d));
});
// re-create all the labels again
node
.each (function (d) {
conf.label.call(this, tnt_tree_node(d), conf.layout.type, d3.functor(conf.node_display.size())(tnt_tree_node(d)));
});
});
};
// API
var api = apijs (t)
.getset (conf);
// TODO: Rewrite data using getset / finalizers & transforms
api.method ('data', function (d) {
if (!arguments.length) {
return base.data;
}
// The original data is stored as the base and curr data
base.data = d;
curr.data = d;
// Set up a new tree based on the data
var newtree = tnt_tree_node(base.data);
t.root(newtree);
tree.trigger("data:hasChanged", base.data);
return this;
});
// TODO: Rewrite tree using getset / finalizers & transforms
api.method ('root', function (myTree) {
if (!arguments.length) {
return curr.tree;
}
// The original tree is stored as the base, prev and curr tree
base.tree = myTree;
curr.tree = base.tree;
// prev.tree = base.tree;
return this;
});
api.method ('subtree', function (curr_nodes, keepSingletons) {
var subtree = base.tree.subtree(curr_nodes, keepSingletons);
curr.data = subtree.data();
curr.tree = subtree;
return this;
});
api.method ('focus_node', function (node, keepSingletons) {
// find
var found_node = t.root().find_node(function (n) {
return node.id() === n.id();
});
focused_node = found_node;
t.subtree(found_node.get_all_leaves(), keepSingletons);
return this;
});
api.method ('has_focus', function (node) {
return ((focused_node !== undefined) && (focused_node.id() === node.id()));
});
api.method ('release_focus', function () {
t.data (base.data);
focused_node = undefined;
return this;
});
return d3.rebind (t, dispatch, "on");
};
module.exports = exports = tree;