Links and patterns

Disclaimer - The following tutorial was created with a past version of JointJS. The tutorial is still provided for those who may face a similar problem, but it may no longer show the best practices of JointJS. You may encounter the following issues:

  • Use of outdated/deprecated API calls, or inheritance from superseded shape collections.
  • Use of SVG string markup for custom Element/Link shape definitions; we have since started recommending using JSON markup instead.
  • The Element and Link types defined may not serialize properly.
  • Other unexpected problems.

Our current recommendations on best practices can be found in the appropriate sections of the basic and intermediate tutorials.

TL;DR: Jump right into the demo below.

This tutorial shows another way how to style JointJS links, especially if only changing the stroke color and width of your links is not sufficient to meet your requirements. What gives us more flexibility is SVG gradients and SVG patterns. SVG gradients as filling for strokes would be fine enough if we were using only straight links with no vertices. This is because the gradients are applied on the link SVG path as a whole. Therefore, applying the gradient on the link SVG path stroke would give different parts of the links different colors depending on how the link is broken and where it is positioned. This gives us an unpredictable result that we most likely don't want. Meet SVG patterns!

+ =

The idea is simple. We create and render a link. We create an HTML 5 canvas element of the size of the bounding box of the link. We draw onto the canvas anything we'd like the link to look like. We generate a Data URI image from the canvas and tell the link to use this image as an SVG pattern for the link stroke. And we repeat all this every time the link gets changed.

About the LinkView

A link view is responsible for rendering and updating a link, it manipulates the DOM elements and it is the right place to implement a link interaction or customize the link appearance.

Each time we change a link attribute, source or target, we add a vertex or we move an element that is connected to the link - the link has to be updated. Normally, the joint.dia.LinkView takes care of this. It inherits from the Backbone.View and extends it by new methods and properties. We're going to introduce some of them.

The linkView.render() method creates SVG elements from the defined link markup and appends them to the DOM. It's usually called only once when link is created. Also, it calls update() internally.
The linkView.update() method applies all the attributes to the DOM elements, finds the route, positions the link tools and arrowheads and so on. It is called everytime the link is changed.
The linkView.remove() cleans up SVG elements from the DOM and removes event handlers. Called once the view is removed.
The linkView.sourcePoint and linkView.targetPoint are cached coordinates of a point where the link connects to an element or point.
The linkView.route is a cached array of points calculated from the vertices by a router.
The linkView.paper is a reference to the paper the view is rendered into.

The joint.dia.LinkView can be extended and used, for example, in the following way:

joint.dia.LinkView.extend({

    render: function() {

        // call parent's render
        joint.dia.LinkView.prototype.render.apply(this, arguments);

        // here we create and append the pattern into the paper SVG <defs> element
        // and tell the link to use it

        // it is a good convetion to return `this` to enable chaining
        return this;
    },

    remove: function() {

        // call parent's remove first
        joint.dia.LinkView.prototype.remove.apply(this, arguments);

        // here we remove the pattern from the paper SVG <defs> element

        return this;
    },

    update: function() {

        // call parent's update first
        joint.dia.LinkView.prototype.update.apply(this, arguments);

        // here we generate an image and set it as a pattern

        return this;
    }
});

Creating a pattern

First of all we have to create an SVG pattern with an image element inside and append it to the DOM (specifically into the paper SVG <defs> element - this is a good place in SVG documents where referenced elements are defined). The perfect place for this is the linkView.render() method. Here we can also cache some important elements we will be using during the updates in order to minimize DOM traversal.

render: function() {

    joint.dia.LinkView.prototype.render.apply(this, arguments);

    // make sure that pattern doesn't already exist
    if (!this.pattern) {

        // create the pattern and the image element
        this.pattern = V('<pattern id="pattern-' + this.id + '" patternUnits="userSpaceOnUse"><image/></pattern>');

        // cache the image element for a quicker access
        this.patternImage = this.pattern.findOne('image');

        // append the pattern to the paper's defs
        V(this.paper.svg).defs().append(this.pattern);
    }

    // tell the '.connection' path to use the pattern
    var connection = V(this.el).findOne('.connection').attr({
        stroke: 'url(#pattern-' + this.id + ')'
    });

    // cache the stroke width
    this.strokeWidth = connection.attr('stroke-width') || 1;

    return this;
}

Note that we're using the built-in Vectorizer library for creating SVG.

Using the pattern

Once we are able to get the link's bounding box (the one without transformations), we can create an HTML 5 canvas of the size of the bounding box of the link. We know the points which the link goes through (sourcePoint, targetPoint and route) so the next thing is to transform them into the link coordinate system (the coordinates of the link top-left corner are obtained from its bounding box).
For example if we have a bounding box { x: 100, y: 30, width: 200, height: 200 } and a vertex with coordinates { x: 150, y: 150 } the position of that vertex on the canvas is { x: 50, y: 120 }. Now we have all we need to be able to draw our pattern into the canvas (more info on how to draw into the canvas can be found here).

When we finish with drawing we set the pattern's coordinates and the dimensions to reflect the link bounding box. We set the xlink:href attribute of the image element inside the pattern to the data URI containing a representation of the image in the PNG format (we can obtain it from the canvas by calling canvas.toDataURL('image/png')).
update: function() {

    joint.dia.LinkView.prototype.update.apply(this, arguments);

    var strokeWidth = this.strokeWidth;

    // we get the bounding box of the linkView without the transformations
    // and expand it to all 4 sides by the stroke width
    // (making sure there is always enough room for drawing,
    // even if the bounding box was tiny.
    // Note that the bounding box doesn't include the stroke.)
    var bbox = g.rect(V(this.el).bbox(true)).moveAndExpand({
        x: - strokeWidth,
        y: - strokeWidth,
        width: 2 * strokeWidth,
        height: 2 * strokeWidth
    });

    // create an array of all the points the link goes through
    // (route doesn't contain the connection points)
    var points = [].concat(this.sourcePoint, this.route, this.targetPoint);

    // transform all the points to the link coordinate system
    points = _.map(points, function(point) {
        return g.point(point.x - bbox.x, point.y - bbox.y);
    });

    // create a canvas of the size same as the link bounding box
    var canvas = document.createElement('canvas');
    canvas.width = bbox.width;
    canvas.height = bbox.height;

    var ctx = canvas.getContext('2d');
    ctx.lineWidth = strokeWidth;

    // iterate over the points and draw the link's new look into the canvas
    for (var i = 0, pointsCount = points.length - 1; i < pointsCount; i++) {

        var from = points[i];
        var to = point[i + 1];

        // draw something into the canvas
        // e.g a line from 'from.x','from.y' to 'to.x','to.y'
    }

    // generate data URI from the canvas
    var dataUri = canvas.toDataURL('image/png');

    // set the pattern's size to the size of the link bounding box
    this.pattern.attr(bbox);

    // update the pattern image and the dimensions
    this.patternImage.attr({
        width: bbox.width,
        height: bbox.height,
        'xlink:href': dataUri
    });

    return this;
}

Removing the pattern

It's a good practice to clean up when the link gets removed from the paper. We don't want to leave unreferenced pattern elements in the DOM.

remove: function() {

    joint.dia.LinkView.prototype.remove.apply(this, arguments);

    // remove the pattern from the DOM
    this.pattern.remove();
}

Pure vertical and horizontal lines

There is one more thing we have to deal with. According to the SVG specification it is not possible to apply patterns on elements with no width or no height.

Keyword objectBoundingBox should not be used when the geometry of the applicable element has no width or no height, such as the case of a horizontal or vertical line, even when the line has actual thickness when viewed due to having a non-zero stroke width since stroke width is ignored for bounding box calculations. When the geometry of the applicable element has no width or height and objectBoundingBox is specified, then the given effect (e.g., a gradient or a filter) will be ignored.

That means, in our case, that we can't use patterns for drawing pure vertical and pure horizontal links.

<!-- pure vertical path (height 0)-->
<path d="M 0 0 L 300 0"/>

<!-- pure horizontal path (width 0)-->
<path d="M 100 0 100 50 100 100"/>

To overcome this issue, we can offset one of the path points by a small number.

<!-- vertical path (height 0.01) -->
<path d="M 0 0 L 300 0.01"/>

<!-- horizontal path (width 0.01)-->
<path d="M 100 0 100 50 100.01 100"/>

The best place where to deal with this is the link connector (connectors are responsible for generating the link path by constructing its d attribute). Here we take joint.connectors.normal, change the method name to normalDimFix and add a very small number to x and y coordinates of the last point of the resulting path.

joint.connectors.normalDimFix = function(sourcePoint, targetPoint, vertices) {

    var dimensionFix = 1e-3;

    var d = ['M', sourcePoint.x, sourcePoint.y];

    _.each(vertices, function(vertex) { d.push(vertex.x, vertex.y); });

    d.push(targetPoint.x + dimensionFix, targetPoint.y + dimensionFix);

    return d.join(' ');
};

The pipes demo

Let's combine all this together and create a link which looks like a pipe. The demo below contains all that we described so far, plus it is adding some new features.
The LinkView draws into the canvas and updates the pattern asynchronously (using joint.util.nextFrame).
It automatically creates a gradient with a directions perpendicular to the path direction for each link segment. This way you can style your link from the 'border' to 'border' and draw an image like the one below.


It also separates the drawing function from the update() into a new linkView method drawPattern().

The source code to the demo.