Note: the code in this page will not run in Firefox, due to the underlying issues discussed here.
jsPlumb can connect SVG elements in the same way as it does standard DOM elements. Consider the following markup
and code snippet:
<div style="position:relative;width:100%">
<svg width="150" height="150" fill="red" id="red" x="0" y="0">
<rect x="0" y="0" width="150" height="150"/>
</svg>
<svg width="150" height="150" fill="blue" style="position:absolute;left:250px" id="blue" x="200" y="200" >
<rect x="0" y="0" width="150" height="150"/>
</svg>
</div>
var j = jsPlumb.getInstance();
j.connect({
source:"red",
target:"blue",
anchor:"Continuous",
connector:"Straight",
paintStyle:{ lineWidth:2, strokeStyle:"aliceblue" }
});
The result is this:
<svg width="150" height="150" fill="blue" style="position:absolute;left:250px;" id="blue">
<rect x="0" y="0" width="150" height="150"/>
</svg>
You cannot connect two rect
elements (or any SVG shapes) directly. This is because the shapes inside an SVG element
do not have the same positioning information as all other DOM elements. So now consider you have this markup:
<div style="position:relative;width:100%">
<svg width="100%" height="200" fill="red" id="red2" x="0" y="0" style="border:1px solid gray;">
<rect x="10" y="10" width="50" height="50" id="rect1"/>
<circle cx="220" cy="130" r="20"/>
<rect x="290" y="60" width="100" height="50" id="rect2"/>
</svg>
</div>
which produces:
and you want to connect the two red rectangles. The key here is that both rect
elements share the same svg
parent;
this happens in quite a few libraries that render to SVG (the one that comes to mind immediately, which spurred this
post, is Raphael. Highcharts is also a well known library that does this. But there are probably a million or more,
and that may or may not be an exaggeration).
The solution here is that you need to override the methods that jsPlumb uses to determine the location and size of
things. First let's define handlers for getting the offset and size for a few known SVG shapes:
var offsetCalculators = {
"CIRCLE":function(el, parentOffset) {
var cx = parseInt(el.getAttribute("cx"), 10),
cy = parseInt(el.getAttribute("cy"), 10),
r = parseInt(el.getAttribute("r"), 10);
return {
left: parentOffset.left + (cx - r),
top:parentOffset.top + (cy - r)
};
},
"ELLIPSE":function(el, parentOffset) {
var cx = parseInt(el.getAttribute("cx"), 10),
cy = parseInt(el.getAttribute("cy"), 10),
rx = parseInt(el.getAttribute("rx"), 10),
ry = parseInt(el.getAttribute("ry"), 10);
return {
left: parentOffset.left + (cx - rx),
top:parentOffset.top + (cy - ry)
};
},
"RECT":function(el, parentOffset) {
var x = parseInt(el.getAttribute("x"), 10),
y = parseInt(el.getAttribute("y"), 10);
return {
left: parentOffset.left + x,
top:parentOffset.top + y
};
}
};
// custom size calculators for SVG shapes.
var sizeCalculators = {
"CIRCLE":function(el) {
var r = parseInt(el.getAttribute("r"), 10);
return [ r * 2, r * 2 ];
},
"ELLIPSE":function(el) {
var rx = parseInt(el.getAttribute("rx"), 10),
ry = parseInt(el.getAttribute("ry"), 10);
return [ rx * 2, ry * 2 ];
},
"RECT":function(el) {
var w = parseInt(el.getAttribute("width"), 10),
h = parseInt(el.getAttribute("height"), 10);
return [ w, h ];
}
};
Now we can override the methods we need to in jsPlumb, handing off control to these handlers when necessary:
// store original jsPlumb prototype methods for getOffset and size.
var originalOffset = jsPlumbInstance.prototype.getOffset;
var originalSize = jsPlumbInstance.prototype.getSize;
jsPlumbInstance.prototype.getOffset = function(el) {
var tn = el.tagName.toUpperCase();
if (offsetCalculators[tn]) {
var so = {left:el.parentNode.offset();
return offsetCalculators[tn]($(el), so);
}
else
return $(el).offset();
};
jsPlumbInstance.prototype.getSize = function(el) {
var tn = el.tagName.toUpperCase();
if (sizeCalculators[tn]) {
return sizeCalculators[tn]($(el));
}
else
return [ $(el).outerWidth(), $(el).outerHeight() ];
};
And then tell jsPlumb to connect these two rectangles:
var j2 = jsPlumb.getInstance();
j2.connect({
source:"rect1",
target:"rect2",
anchor:"Continuous",
connector:"Bezier",
paintStyle:{ lineWidth:2, strokeStyle:"#456" },
endpoint:["Dot", { radius:2 }]
});
The result is this:
To connect nodes created by Raphael, set the element used as Raphael's paper
to be the container
of your
jsPlumb instance:
<div id="paper" style="width:100%;height:480px;border:1px solid gray"></div>
var j3 = jsPlumb.getInstance();
j3.setContainer("paper");
var paper = Raphael("paper");
Then draw a few things:
var circle1 = paper.circle(140, 110, 90).attr({ fill: '#3D6AA2', stroke: '#000000', 'stroke-width': 8 });
var circle2 = paper.circle(400, 180, 90).attr({ fill: '#3D6AA2', stroke: '#000000', 'stroke-width': 8 });
var rect = paper.rect(50, 280, 90, 70).attr({ fill: '#3D6AA2', stroke: '#000000', 'stroke-width': 8 });
var ellipse = paper.ellipse(300, 420, 90, 70).attr({ fill: '#3D6AA2', stroke: '#000000', 'stroke-width': 8 });
and connect them up:
j3.connect({source:circle1.node, target:circle2.node, anchor:"Center", connector:"Straight"});
j3.connect({source:circle1.node, target:rect.node, anchors:["Center", "Top"], connector:"Straight"});
j3.connect({source:circle2.node, target:ellipse.node, anchor:"Center"});
Easy as falling off a log.
Yes! Go ahead and try. Raphael supports dragging via the following call:
paper.set(circle1, circle2, ellipse, rect).drag(moveFunction, startFunction, upFunction);
Since our shapes have different attributes (circle
and ellipse
are located via cx
and cy
, but rect
is
located via x
and y
), we created a helper class:
var RaphaelDragger = function() {
var attNames = {
"circle":["cx", "cy"],
"ellipse":["cx", "cy"],
"rect":["x", "y"]
};
return [
function(dx, dy) {
var args = {};
args[attNames[this.type][0]] = this.ox + dx;
args[attNames[this.type][1]] = this.oy + dy;
this.attr(args);
j3.revalidate(this.node);
},
function() {
this.ox = this.attr(attNames[this.type][0]);
this.oy = this.attr(attNames[this.type][1]);
},
function() {}
];
};
..which returns an array of three functions, one each for move, start and up. Then we do a little JS trick and
call the apply
method of the drag object to supply these as arguments:
paper.set(circle1, circle2, ellipse, rect).drag.apply(paper, new RaphaelDragger());
A good question. You could follow the general principles outlined here but you'd have to decide for yourself
how you wanted to treat the geometry of a path. Where does it start? How big is it? Given the arbitrary
nature of paths this is often unclear.
Get the source by clicking here.