Community Edition 4.x Beta Release Community Edition 4.x Beta Release
20 Jun 2020
World Cup 2018, Part 2 World Cup 2018, Part 2
26 Jun 2018
World Cup 2018, Part 1 World Cup 2018, Part 1
25 Jun 2018
Community Edition 2.2.2 Release Community Edition 2.2.2 Release
20 Oct 2016
Connecting SVG Shapes (Raphael, Highcharts etc) (update) Connecting SVG Shapes (Raphael, Highcharts etc) (update)
20 Oct 2016
Writing a Layout Decorator Writing a Layout Decorator
25 Jul 2015
Toolkit Edition 1.0.0 Toolkit Edition 1.0.0
23 Jul 2015
Community Edition 1.7.6 Community Edition 1.7.6
23 Jul 2015
Connecting SVG Shapes (Raphael, Highcharts etc) Connecting SVG Shapes (Raphael, Highcharts etc)
22 Jul 2015
The beforeDrag Interceptor The beforeDrag Interceptor
15 Jan 2015
Integrating Jekyll and YUIDoc Integrating Jekyll and YUIDoc
28 Jan 2014
Dragging Multiple Elements Dragging Multiple Elements
17 Jan 2014
Custom Connectors Custom Connectors
22 Dec 2013

Connecting SVG Shapes (Raphael, Highcharts etc) (update)

Note: This was originally posted on the 22nd of July 2015, with the caveat that "the code in this page will not run in Firefox, due to the underlying issues discussed here.", which discussed the fact that Firefox does not have offsetLeft and offsetTop properties on SVG elements.

Fast forward a year and a bit and the demo does not work anywhere now, because of the situation discussed here.. No browser has offsetLeft or offsetTop properties any longer. The upshot is that jsPlumb can no longer 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>

jsPlumb cannot connect these two svg elements. To connect the svg elements, we need to wrap them in divs:

<div style="position:relative;width:100%;height:200px;" id="demo1">
    <div id="red" style="position:absolute;left:0;top:0;">
        <svg width="150" height="150" fill="red">
            <rect x="0" y="0" width="150" height="150"/>
        </svg>
    </div>

    <div style="position:absolute;left:250px;top:0;" id="blue">
        <svg width="150" height="150" fill="blue">
            <rect x="0" y="0" width="150" height="150"/>
        </svg>
    </div>
</div>
var j = jsPlumb.getInstance({
    Container:document.getElementById("demo1")
});
j.connect({
    source:"red", 
    target:"blue", 
    anchor:"Continuous",
    connector:"Straight",
    paintStyle:{ lineWidth:2, strokeStyle:"aliceblue" }
});

The result is this:



One of the key things about this is that the svg elements are at position [0,0] of their parent elements, which are "standard" DOM elements. This is currently a requirement if you wish to connect SVG elements.

THE (MAIN) PROBLEM

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%">
  <div id="red2" style="position:absolute;">
      <svg width="100%" height="200" fill="red" 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>
</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

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) {
    var cx = parseInt(el.getAttribute("cx"), 10),
        cy = parseInt(el.getAttribute("cy"), 10),
        r = parseInt(el.getAttribute("r"), 10);
    return {
        left: (cx - r),
        top:(cy - r)
    };
},
"ELLIPSE":function(el) {
    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: (cx - rx),
        top:(cy - ry)
    };
},
"RECT":function(el) {
    var x = parseInt(el.getAttribute("x"), 10),
        y = parseInt(el.getAttribute("y"), 10);
    return {
        left:x,
        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]) {
       // here we rely on the fact that the SVG element is at 0,0 of its parent div. If it was not, we'd also want to adjust 
       // the offset for each shape by the offset of its parent.
       // note that a previous version of this blog post used to take into account the offset of the SVG's parent, but 
       // more recent versions of jsPlumb are strict about the necessity of working inside a "container" element, so this is
       // not a consideration any longer.
        return offsetCalculators[tn](el);
    }
    else
        return originalOffset.apply(this, [el]);
};

jsPlumbInstance.prototype.getSize = function(el) {
    var tn = el.tagName.toUpperCase();
    if (sizeCalculators[tn]) {
        return sizeCalculators[tn](el);
    }
    else
        return originalSize.apply(this, [el]);
};

And then tell jsPlumb to connect these two rectangles:

var j2 = jsPlumb.getInstance({
    Container:document.getElementById("demo2")
});
j2.connect({
 source:"rect1",
 target:"rect2",
 anchor:"Continuous",
 connector:"Bezier",
 paintStyle:{ lineWidth:2, strokeStyle:"#456" },
 endpoint:["Dot", { radius:2 }]
});

The result is this:

AND SO...RAPHAEL?

The original version of this post demonstrated a way to connect shapes drawn by Raphael. Unfortunately that no longer
works. We're hoping to revisit this whole post at some point in the future.