Skip to content

Commit

Permalink
Implement cluster label support
Browse files Browse the repository at this point in the history
This commit does two major things:

(1) In case cluster labels are long, we first expand the margins
of all enclosed nodes such that we can add our labels later with
adequate space. Unfortunately this needs to be done early enough
for us to position the nodes accordingly.

(2) We must make enough space for the label at each level of the
cluster. This requires recursively computing the necessary padding.
  • Loading branch information
Andrew Or committed May 14, 2015
1 parent c1ecccc commit 6db19e9
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 12 deletions.
60 changes: 52 additions & 8 deletions lib/create-clusters.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
var util = require("./util");
var _ = require("./lodash"),
addLabel = require("./label/add-label"),
util = require("./util");

module.exports = createClusters;

Expand All @@ -7,15 +9,42 @@ function createClusters(selection, g) {
svgClusters = selection.selectAll("g.cluster")
.data(clusters, function(v) { return v; });

// Clusters created from DOT subgraphs are prefixed with "cluster"
// strip this prefix if it exists and use our own (i.e. "cluster_")
var makeClusterIdentifier = function(v) {
return "cluster_" + v.replace(/^cluster/, "");
};

svgClusters.enter()
.append("g")
// clusters created from DOT subgraphs are prefixed with "cluster"
// strip this prefix if it exists and use our own (i.e. "cluster_")
.attr("class", function(v) { return "cluster_" + v.replace(/^cluster/, ""); })
.attr("name", function(v) { return g.node(v).label; })
.classed("cluster", true)
.style("opacity", 0)
.append("rect");
.attr("class", makeClusterIdentifier)
.attr("name", function(v) { return g.node(v).label; })
.classed("cluster", true)
.style("opacity", 0)
.append("rect");

// Draw the label for each cluster and adjust the padding for it.
// We position the labels later because the dimensions and the positions
// of the enclosing rectangles are still subject to change. Note that
// the ordering here is important because we build the parents' padding
// based on the children's.
var sortedClusters = util.orderByRank(g, svgClusters.data());
for (var i = 0; i < sortedClusters.length; i++) {
var v = sortedClusters[i];
var node = g.node(v);
if (node.label) {
var thisGroup = selection.select("g.cluster." + makeClusterIdentifier(v));
labelGroup = thisGroup.append("g").attr("class", "label"),
labelDom = addLabel(labelGroup, node),
bbox = _.pick(labelDom.node().getBBox(), "width", "height");
// Add some padding for the label
// Do this recursively to account for our descendants' labels.
// To avoid double counting, we must start from the leaves.
node.paddingTop += bbox.height;
node.paddingTop += util.getMaxChildPaddingTop(g, v);
}
}

util.applyTransition(svgClusters.exit(), g)
.style("opacity", 0)
.remove();
Expand All @@ -40,4 +69,19 @@ function createClusters(selection, g) {
var node = g.node(v);
return node.y - node.height / 2 - node.paddingTop;
});

// Position the labels
svgClusters.each(function() {
var cluster = d3.select(this),
label = cluster.select("g.label"),
rect = cluster.select("rect"),
bbox = label.node().getBBox(),
labelW = bbox.width,
labelH = bbox.height;
var num = function(x) { return parseFloat(x.toString().replace(/px$/, "")); };
var labelX = num(rect.attr("x")) + num(rect.attr("width")) - labelH / 2 + labelW / 2;
var labelY = num(rect.attr("y")) + labelH;
label.attr("text-anchor", "end")
.attr("transform", "translate(" + labelX + "," + labelY + ")");
});
}
28 changes: 24 additions & 4 deletions lib/create-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ function createNodes(selection, g, shapes) {
svgNodes.selectAll("*").remove();
svgNodes.enter()
.append("g")
.attr("class", function(v) { return "node_" + v; })
.attr("name", function(v) { return g.node(v).label; })
.classed("node", true)
.style("opacity", 0);
.attr("class", function(v) { return "node_" + v; })
.attr("name", function(v) { return g.node(v).label; })
.classed("node", true)
.style("opacity", 0);
svgNodes.each(function(v) {
var node = g.node(v),
thisGroup = d3.select(this),
Expand Down Expand Up @@ -47,7 +47,27 @@ function createNodes(selection, g, shapes) {
var shapeSvg = shape(d3.select(this), bbox, node);
util.applyStyle(shapeSvg, node.style);

// Stretch this node horizontally a little to account for ancestor cluster
// labels. We must do this here because by the time we create the clusters,
// we have already positioned all the nodes.
var requiredWidth = 0,
requiredHeight = 0;
var nextNode = g.node(g.parent(v));
while (nextNode) {
var tempGroup = thisGroup.append("g");
var tempLabel = addLabel(tempGroup, nextNode);
var tempBBox = tempLabel.node().getBBox();
// WARNING: this uses a hard-coded value of nodesep
tempBBox.width -= 50;
requiredWidth = Math.max(requiredWidth, tempBBox.width);
requiredHeight = Math.max(requiredHeight, tempBBox.height);
tempLabel.remove();
nextNode = g.node(g.parent(nextNode.label));
}

var shapeBBox = shapeSvg.node().getBBox();
shapeBBox.width = Math.max(shapeBBox.width, requiredWidth);
shapeBBox.height = Math.max(shapeBBox.height, requiredHeight);
node.width = shapeBBox.width;
node.height = shapeBBox.height;
});
Expand Down
41 changes: 41 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ var _ = require("./lodash");
// Public utility functions
module.exports = {
isSubgraph: isSubgraph,
getMaxChildPaddingTop: getMaxChildPaddingTop,
orderByRank: orderByRank,
edgeToId: edgeToId,
applyStyle: applyStyle,
applyClass: applyClass,
Expand All @@ -17,6 +19,45 @@ function isSubgraph(g, v) {
return !!g.children(v).length;
}

/*
* Returns the max "paddingTop" property among the specified node's children.
* A return value of 0 means this node has no children.
*/
function getMaxChildPaddingTop(g, v) {
var maxPadding = 0;
var children = g.children(v);
for (var i = 0; i < children.length; i++) {
var child = g.node(children[i]);
if (child.paddingTop && child.paddingTop > maxPadding) {
maxPadding = child.paddingTop;
}
}
return maxPadding;
}

/* Return the rank of the specified node. A rank of 0 means the node has no children. */
function getRank(g, v) {
var maxRank = 0;
var children = g.children(v);
for (var i = 0; i < children.length; i++) {
var thisRank = getRank(g, children[i]) + 1;
if (thisRank > maxRank) {
maxRank = thisRank;
}
}
return maxRank;
}

/*
* Order the following nodes by rank, from the leaves to the roots.
* This mutates the list of nodes in place while sorting them.
*/
function orderByRank(g, nodes) {
return nodes.sort(function(x, y) {
return getRank(g, x) - getRank(g, y);
});
}

function edgeToId(e) {
return escapeId(e.v) + ":" + escapeId(e.w) + ":" + escapeId(e.name);
}
Expand Down

0 comments on commit 6db19e9

Please sign in to comment.