Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented "arbitrary" node shapes #69

Merged
merged 3 commits into from
Jul 30, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions demo/user-defined-nodes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!doctype html>

<meta charset="utf-8">
<title>Dagre D3 User-defined Node Shapes Demo</title>

<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="../build/dagre-d3.js"></script>
<style>

svg {
border: 1px solid #999;
overflow: hidden;
}

text {
font-weight: 300;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 12px;
}

.node rect {
stroke: #999;
stroke-width: 1px;
fill: #fff;
}

.edgeLabel rect {
fill: #fff;
}

.edgePath path {
stroke: #333;
stroke-width: 1.5px;
fill: none;
}
</style>

<body onLoad="draw();">

<svg x="0" y="0" width=350 height=800>

<g transform="translate(20,20)">

<defs>
<g id="def-N0" ><rect x=-25 y =-25 width=50 height=50 fill="steelblue"/><circle cx=0 cy=0 r=20 fill="yellow"/><text x=-7 y=5 fill="#000000">N0</text></g>
<g id="def-N1" transform="translate(-40,-20)"><polygon points="0,20 20,0 35,0 35,10, 20,10, 20,20 60,20 60,10 45,10 45,0 60,0 80,20 60,50 40,35 20,50" style="fill:lightgreen;stroke:black;stroke-width:2;"/><text x=35 y=33 fill="#FF">N1</text></g>
<g id="def-N2" ><ellipse cx=0 cy=0 rx=50 ry=30 fill="#FFC0C0"/><text x=-7 y=5 fill="#FFFFFF">N2</text></g>
<g id="def-N3" transform="translate(-40,-20)"><polygon points="0,20 20,0 35,0 35,10, 20,10, 20,20 60,20 60,10 45,10 45,0 60,0 80,20 60,50 40,35 20,50" style="fill:lightgray;stroke:black;stroke-width:2;"/><text x=35 y=33 fill="#FF">N3</text></g>
<g id="def-N4" ><circle cx=0 cy=0 r=30 fill="#F0B3FF"/><text x=-7 y=5 fill="#FFFFFF">N4</text></g>
<g id="def-N5" transform="translate(-98,-98)"><polygon points= "100,10 40,198 190,78 10,78 160,198" fill-rule="evenodd" style="fill:silver;"/>

</defs>

</g>
</svg>

<script>
function draw() {
var g = new dagreD3.Digraph();
g.addNode("N0", {label: "N0", use: "def-N0"});
g.addNode("N1", {label: "N1", use: "def-N1"});
g.addNode("N2", {label: "N2", use: "def-N2"});
g.addNode("N3", {label: "N3", use: "def-N3"});
g.addNode("N4", {label: "N4", use: "def-N4"});
g.addNode("N5", {label: "N5", use: "def-N5"});

g.addEdge(null, "N0", "N1", { label: "N0-N1" });
g.addEdge(null, "N0", "N2", { label: "N0-N2" });
g.addEdge(null, "N1", "N2", { label: "N1-N2" });
g.addEdge(null, "N2", "N3", { label: "N2-N3" });
g.addEdge(null, "N3", "N0", { label: "N3-N0" });
g.addEdge(null, "N3", "N4", { label: "N3-N4" });
g.addEdge(null, "N4", "N5", { label: "N4-N5" });
g.addEdge(null, "N5", "N0", { label: "N5-N0" });


var renderer = new dagreD3.Renderer();
var layout = dagreD3.layout()
.rankSep(70);
var oldDrawNodes = renderer.drawNodes();
renderer.drawNodes(function(graph, root) {
var svgNodes = oldDrawNodes(graph, root);
svgNodes.attr("id", function(u) { return "node-" + u; });
return svgNodes;
});


renderer.layout(layout).run(g, d3.select("svg g"));
}
</script>
207 changes: 196 additions & 11 deletions lib/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ Renderer.prototype.edgeTension = function(edgeTension) {
return this;
};

Renderer.prototype.run = function(graph, svg) {
Renderer.prototype.run = function(graph, orgSvg) {
// First copy the input graph so that it is not changed by the rendering
// process.
graph = copyAndInitGraph(graph);

// Create zoom elements
svg = this._zoomSetup(graph, svg);
var svg = this._zoomSetup(graph, orgSvg);

// Create layers
svg
Expand Down Expand Up @@ -148,7 +148,7 @@ Renderer.prototype.run = function(graph, svg) {
// Apply the layout information to the graph
this._positionNodes(result, svgNodes);
this._positionEdgeLabels(result, svgEdgeLabels);
this._positionEdgePaths(result, svgEdgePaths);
this._positionEdgePaths(result, svgEdgePaths, orgSvg);

this._postRender(result, svg);

Expand Down Expand Up @@ -294,7 +294,35 @@ function defaultPositionEdgeLabels(g, svgEdgeLabels) {
.attr('transform', transform);
}

function defaultPositionEdgePaths(g, svgEdgePaths) {
function isEllipse(obj) {
return Object.prototype.toString.call(obj) === '[object SVGEllipseElement]';
}

function isCircle(obj) {
return Object.prototype.toString.call(obj) === '[object SVGCircleElement]';
}

function isPolygon(obj) {
return Object.prototype.toString.call(obj) === '[object SVGPolygonElement]';
}

function intersectNode(nd, p1, root){
if (nd.label.match(/^[a-zA-Z0-9]+$/)) {
var definedFig = root.select('defs #def-' + nd.label).node();
if (definedFig) {
var outerFig = definedFig.childNodes[0];
if (isCircle(outerFig) || isEllipse(outerFig)) {
return intersectEllipse(nd, outerFig, p1);
} else if (isPolygon(outerFig)) {
return intersectPolygon(nd, outerFig, p1);
}
}
}
// TODO: use bpodgursky's shortening algorithm here
return intersectRect(nd, p1);
}

function defaultPositionEdgePaths(g, svgEdgePaths, root) {
var interpolate = this._edgeInterpolate,
tension = this._edgeTension;

Expand All @@ -306,11 +334,13 @@ function defaultPositionEdgePaths(g, svgEdgePaths) {

var p0 = points.length === 0 ? target : points[0];
var p1 = points.length === 0 ? source : points[points.length - 1];

points.unshift(intersectRect(source, p0));
// TODO: use bpodgursky's shortening algorithm here
points.push(intersectRect(target, p1));


//points.unshift(source); // ultimately we want this:
console.log('intersect source -> p0');
points.unshift(intersectNode(source, p0, root));
console.log('intersect target -> p1');
points.push(intersectNode(target, p1, root));

return d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
Expand Down Expand Up @@ -390,6 +420,11 @@ function defaultPostRender(graph, root) {
}

function addLabel(node, root, addingNode, marginX, marginY) {
// If the node has 'use' meta data, we rely on that
if(node.use){
root.append('use').attr('xlink:href', '#' + node.use);
return;
}
// Add the rect first so that it appears behind the label
var label = node.label;
var rect = root.append('rect');
Expand Down Expand Up @@ -526,8 +561,6 @@ function intersectRect(rect, point) {
var x = rect.x;
var y = rect.y;

// For now we only support rectangles

// Rectangle intersection algorithm from:
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
var dx = point.x - x;
Expand Down Expand Up @@ -555,6 +588,158 @@ function intersectRect(rect, point) {
return {x: x + sx, y: y + sy};
}

function intersectEllipse(node, ellipseOrCircle, point) {
// Formulae from: http://mathworld.wolfram.com/Ellipse-LineIntersection.html

var cx = node.x;
var cy = node.y;
var rx, ry;

if(isCircle(ellipseOrCircle)){
rx = ry = ellipseOrCircle.r.baseVal.value;
} else {
rx = ellipseOrCircle.rx.baseVal.value;
ry = ellipseOrCircle.ry.baseVal.value;
}

var px = cx - point.x;
var py = cy - point.y;

var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);

var dx = Math.abs(rx * ry * px / det);
if(point.x < cx){
dx = -dx;
}
var dy = Math.abs(rx * ry * py / det);
if(point.y < cy){
dy = - dy;
}

return {x: cx + dx, y: cy +dy};
}

function same_sign(r1, r2) {
return r1 * r2 > 0;
}

// Add point to the found interesctions, but check first that it is unique.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a typo here: interesctions => intersections

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Your code is now available in dagre v0.2.6. Thanks again!


function add_point(x, y, intersections){
if (!intersections.some(function (elm){ return elm[0] === x && elm[1] === y; })) {
intersections.push([x, y]);
}
}

function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4, intersections){
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, p7 and p473.

var a1, a2, b1, b2, c1, c2;
var r1, r2 , r3, r4;
var denom, offset, num;
var x, y;

// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + b1 y + c1 = 0.
a1 = y2 - y1;
b1 = x1 - x2;
c1 = (x2 * y1) - (x1 * y2);

// Compute r3 and r4.
r3 = ((a1 * x3) + (b1 * y3) + c1);
r4 = ((a1 * x4) + (b1 * y4) + c1);

// Check signs of r3 and r4. If both point 3 and point 4 lie on
// same side of line 1, the line segments do not intersect.
if ((r3 !== 0) && (r4 !== 0) && same_sign(r3, r4)) {
return /*DONT_INTERSECT*/;
}

// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
a2 = y4 - y3;
b2 = x3 - x4;
c2 = (x4 * y3) - (x3 * y4);

// Compute r1 and r2
r1 = (a2 * x1) + (b2 * y1) + c2;
r2 = (a2 * x2) + (b2 * y2) + c2;

// Check signs of r1 and r2. If both point 1 and point 2 lie
// on same side of second line segment, the line segments do
// not intersect.
if ((r1 !== 0) && (r2 !== 0) && (same_sign(r1, r2))) {
return /*DONT_INTERSECT*/;
}

// Line segments intersect: compute intersection point.
denom = (a1 * b2) - (a2 * b1);
if (denom === 0) {
return /*COLLINEAR*/;
}

offset = Math.abs(denom / 2);

// The denom/2 is to get rounding instead of truncating. It
// is added or subtracted to the numerator, depending upon the
// sign of the numerator.
num = (b1 * c2) - (b2 * c1);
x = (num < 0) ? ((num - offset) / denom) : ((num + offset) / denom);

num = (a2 * c1) - (a1 * c2);
y = (num < 0) ? ((num - offset) / denom) : ((num + offset) / denom);

// lines_intersect
add_point(x, y, intersections);
return;
}

function intersectPolygon(node, polygon, point) {
var x1 = node.x;
var y1 = node.y;
var x2 = point.x;
var y2 = point.y;

var intersections = [];
var points = polygon.points;

var minx = 100000, miny = 100000;
for(var j = 0; j < points.numberOfItems; j++){
var p = points.getItem(j);
minx = Math.min(minx, p.x);
miny = Math.min(miny, p.y);
}

var left = x1 - node.width/2 - minx;
var top = y1 - node.height/2 - miny;

for(var i = 0; i < points.numberOfItems; i++){
var p1 = points.getItem(i);
var p2 = points.getItem(i < points.numberOfItems - 1 ? i + 1 : 0);
intersectLine(x1, y1, x2, y2, left + p1.x, top + p1.y, left + p2.x, top + p2.y, intersections);
}

if(intersections.length === 1){
return {x: intersections[0][0], y: intersections[0][1]};
}
if(intersections.length > 1){
// More intersections, find the one nearest to edge end point
intersections.sort(function(p, q){
var pdx = p[0] - point.x,
pdy = p[1] - point.y,
distp = Math.sqrt(pdx * pdx + pdy * pdy),

qdx = q[0] - point.x,
qdy = q[1] - point.y,
distq = Math.sqrt(qdx * qdx + qdy * qdy);

return (distp < distq) ? -1 : (distp === distq ? 0 : 1);
});
return {x: intersections[0][0], y: intersections[0][1]};
} else {
console.log('NO INTERSCTION FOUND, RETURN NODE CENTER', node);
return node;
}
}

function isComposite(g, u) {
return 'children' in g && g.children(u).length;
}
Expand Down