Links

In the first part of the tutorial, we saw a working example of a simple JointJS application:

In the previous section, we focused on making the elements look much more interesting:

In this section, we are going to understand the other crucial building block of JointJS diagrams - links. This is an introduction to links as they appear in the Hello, World! application; more advanced topics are covered later in the tutorial series. We will continue with the code we modified in the previous section:

<!DOCTYPE html>
<html>
<body>
    <!-- content -->
    <div id="myholder"></div>

    <!-- dependencies -->
    <script src="node_modules/@joint/core/dist/joint.js"></script>

    <!-- code -->
    <script type="text/javascript">

        var namespace = joint.shapes;

        var graph = new joint.dia.Graph({}, { cellNamespace: namespace });

        var paper = new joint.dia.Paper({
            el: document.getElementById('myholder'),
            model: graph,
            width: 600,
            height: 300, // height had to be increased
            gridSize: 10,
            drawGrid: true,
            background: {
                color: 'rgba(0, 255, 0, 0.3)'
            },
            cellViewNamespace: namespace
        });

        var rect = new joint.shapes.standard.Rectangle();
        rect.position(100, 30);
        rect.resize(100, 40);
        rect.attr({
            body: {
                fill: 'blue'
            },
            label: {
                text: 'Hello',
                fill: 'white'
            }
        });
        rect.addTo(graph);

        var rect2 = new joint.shapes.standard.Rectangle();
        rect2.position(400, 30);
        rect2.resize(100, 40);
        rect2.attr({
            body: {
                fill: '#2C3E50',
                rx: 5,
                ry: 5,
                strokeWidth: 2
            },
            label: {
                text: 'World!',
                fill: '#3498DB',
                fontSize: 18,
                fontWeight: 'bold',
                fontVariant: 'small-caps'
            }
        });
        rect2.addTo(graph);

        var link = new joint.shapes.standard.Link();
        link.source(rect);
        link.target(rect2);
        link.addTo(graph);

        var rect3 = new joint.shapes.standard.Rectangle();
        rect3.position(100, 130);
        rect3.resize(100, 40);
        rect3.attr({
            body: {
                fill: '#E74C3C',
                rx: 20,
                ry: 20,
                strokeWidth: 0
            },
            label: {
                text: 'Hello',
                fill: '#ECF0F1',
                fontSize: 11,
                fontVariant: 'small-caps'
            }
        });
        rect3.addTo(graph);

        var rect4 = new joint.shapes.standard.Rectangle();
        rect4.position(400, 130);
        rect4.resize(100, 40);
        rect4.attr({
            body: {
                fill: '#8E44AD',
                strokeWidth: 0
            },
            label: {
                text: 'World!',
                fill: 'white',
                fontSize: 13
            }
        });
        rect4.addTo(graph);

        var link2 = new joint.shapes.standard.Link();
        link2.source(rect3);
        link2.target(rect4);
        link2.addTo(graph);

        var rect5 = new joint.shapes.standard.Rectangle();
        rect5.position(100, 230);
        rect5.resize(100, 40);
        rect5.attr({
            body: {
                fill: '#2ECC71',
                strokeDasharray: '10,2'
            },
            label: {
                text: 'Hello',
                fill: 'black',
                fontSize: 13
            }
        });
        rect5.addTo(graph);

        var rect6 = new joint.shapes.standard.Rectangle();
        rect6.position(400, 230);
        rect6.resize(100, 40);
        rect6.attr({
            body: {
                fill: '#F39C12',
                rx: 20,
                ry: 20,
                strokeDasharray: '1,1'
            },
            label: {
                text: 'World!',
                fill: 'gray',
                fontSize: 18,
                fontWeight: 'bold',
                fontVariant: 'small-caps',
                textShadow: '1px 1px 1px black'
            }
        });
        rect6.addTo(graph);

        var link3 = new joint.shapes.standard.Link();
        link3.source(rect5);
        link3.target(rect6);
        link3.addTo(graph);

    </script>
</body>
</html>

Working with links is similar to working with elements:

In our case, the three links are instances of joint.shapes.standard.Link. The standard shape library contains several other ready-made link definitions (e.g. DoubleLink and ShadowLink) that can be used in your document. Moreover, advanced users may create their own link definitions instead, by extending the basic joint.dia.Link class.

Our demo showcases the two required Link methods:

Other important methods include:

The shape of the link can be set with three optional methods:

Vertices are user-defined points on the paper that the link should pass through on its way from source to target. The default is an empty array (meaning that the link navigates from source to target with no detour).

The router is a function that takes the array of link vertices and adds additional points to it as necessary to create a route with desired characteristics; for example, the orthogonal router creates a route consisting of vertical and horizontal lines attached at right angles. JointJS comes with a collection of pre-defined routers in the joint.routers namespace. Two of the provided routers are smart routers that are able to navigate around obstacles in their path. The default is the normal router, which returns the array of vertices as route points. Advanced users may provide their own routers to support custom routing strategies.

The connector is a function in charge of rendering the link's path on the paper. It takes the array of route points provided by a router and constructs a series of SVGPathElement path data commands to create a path with desired characteristics; for example, the rounded connector creates straight line segments with rounded corners around route points. JointJS comes with a collection of pre-defined connectors in the joint.connectors namespace. The default is the normal connector, which connects route points with simple straight lines. Advanced users may provide their own connectors to support custom connecting strategies.

Link geometry is also affected by the anchor and connectionPoint methods applied to link source and target, as well as the connectionStrategy set on the paper.

Link styling works analogously to element styling:

When link.attr() is called with one object as an argument, the keys of the object are selectors that correspond to SVGElements of the link's markup; each of those can have one or more attributes defined alongside the value to be set. If you only need to change one value, you can also call link.attr() with two arguments; the first is the path of the attribute in the form 'selector/attribute' and the second is the value to be assigned.

If you are completely new to SVG, you may want to have a look at, for example, the Fills and Strokes tutorial on MDN. JointJS is able to handle all standard SVG attributes, however, please note that we strongly encourage everyone to use camelCase versions of attribute names for consistency and in order to avoid the need for quotation marks in attribute name keys. In addition, JointJS provides an extensive set of non-standard special JointJS attributes; these allow you to specify attributes relatively to other shape selectors, among other things. Special attributes are discussed in detail later in the tutorial.

The joint.shapes.standard.Link shape used in our example has two selectors defined: line (the visible <path> SVGElement of the link), and wrapper (a wider, transparent <path> SVGElement under the line that makes it easier to interact with the link). Other link shapes have their own selector names (although consistency was preserved where applicable, e.g. with line); please refer to the joint.shapes.standard documentation for detailed information.

For example, we can change the color of a link by changing the stroke color attribute on the line selector:

link.attr('line/stroke', 'orange');

JointJS source code: links-attr.js

The same effect can be achieved by passing a single object argument to link.attr:

link.attr({
    line: { // selector for the visible <path> SVGElement
        stroke: 'orange' // SVG attribute and value
    }
});

Two special attributes are in charge of link arrowheads; sourceMarker and targetMarker. (In the case of joint.shapes.standard.Link, they are defined on the line selector.) The 'type' of the arrowhead can be any valid SVGElement type; we will look at path and image arrowheads specifically, but we will mention the others in more advanced sections of the tutorial.

The following example shows how to create a link with two simple path arrowheads. Notice that if the 'fill' and 'stroke' colors are not specified, they are adopted from the line.stroke attribute. The two arrowheads have the same path data commands, despite pointing in opposite directions; this is because all targetMarker values are automatically rotated by 180 degrees. The path commands' coordinate system has origin at the tip of the link and is rotated according to the slope of the link at that point. Together, these characteristics mean that you can design all your arrowheads as if they were pointing left and towards the point 0,0 in local coordinates; JointJS will take care of the rest. Let's illustrate with a simple clock:

link.attr({
    line: {
        sourceMarker: { // hour hand
            'type': 'path',
            'd': 'M 20 -10 0 0 20 10 Z'
        },
        targetMarker: { // minute hand
            'type': 'path',
            'stroke': 'green',
            'stroke-width': 2
            'fill': 'yellow',
            'd': 'M 20 -10 0 0 20 10 Z'
        }
    }
});

JointJS source code: links-arrowheads-path.js

Creating image arrowheads is similarly straightforward. You just need to provide a url with the path to your image to the 'xlink:href' property, and then specify the desired 'width' and 'height'. The image for targetMarker will be automatically rotated by 180 degrees, as expected. Remember to reposition the image in the 'y' axis if you need the arrowhead to be centered. Let's return to our clock example:

link.attr({
    line: {
        sourceMarker: {
            'type': 'image',
            'xlink:href': 'https://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png',
            'width': 24,
            'height': 24,
            'y': -12
        },
        targetMarker: {
            'type': 'image',
            'xlink:href': 'https://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png',
            'width': 24,
            'height': 24,
            'y': -12
        }
    }
});

JointJS source code: links-arrowheads-image.js

JointJS also supports link labels:

Link labels are explained in more detail in a separate section of this tutorial. A simple label definition (including markup, attrs and position) is built into the joint.dia.Link class, from which the joint.shapes.standard.Link type inherits it. The built-in default label markup contains two subelements: <text> SVGElement ('text' selector) for label text, and <rect> SVGElement ('rect'selector) for label background. Meanwhile, the built-in default attributes specify that the label should look like a simple vertically centered text on a white rounded rectangle. Finally, the built-in default position places the label at the midpoint of the link. Thus, adding a label can be as simple as passing a value for the text/text attribute:

link.labels([{
    attrs: {
        text: {
            text: 'Hello, World'
        }
    }
}]);

JointJS source code: links-label-builtin.js

Note that since we were only adding one label, we could have also used the link.appendLabel() convenience function:

link.appendLabel({
    attrs: {
        text: {
            text: 'Hello, World'
        }
    }
});

More advanced topics about link labels, including custom markup, size, styling and position, are explained in a separate section of this tutorial.

Example

Now, let's use what we learned to have some fun with our links:

<!DOCTYPE html>
<html>
<body>
    <!-- content -->
    <div id="myholder"></div>

    <!-- dependencies -->
    <script src="node_modules/@joint/core/dist/joint.js"></script>

    <!-- code -->
    <script type="text/javascript">

        var namespace = joint.shapes;

        var graph = new joint.dia.Graph({}, { cellNamespace: namespace });

        var paper = new joint.dia.Paper({
            el: document.getElementById('myholder'),
            model: graph,
            width: 600,
            height: 300,
            gridSize: 10,
            drawGrid: true,
            background: {
                color: 'rgba(0, 255, 0, 0.3)'
            },
            cellViewNamespace: namespace
        });

        var rect = new joint.shapes.standard.Rectangle();
        rect.position(100, 30);
        rect.resize(100, 40);
        rect.attr({
            body: {
                fill: 'blue'
            },
            label: {
                text: 'Hello',
                fill: 'white'
            }
        });
        rect.addTo(graph);

        var rect2 = new joint.shapes.standard.Rectangle();
        rect2.position(400, 30);
        rect2.resize(100, 40);
        rect2.attr({
            body: {
                fill: '#2C3E50',
                rx: 5,
                ry: 5,
                strokeWidth: 2
            },
            label: {
                text: 'World!',
                fill: '#3498DB',
                fontSize: 18,
                fontWeight: 'bold',
                fontVariant: 'small-caps'
            }
        });
        rect2.addTo(graph);

        var link = new joint.shapes.standard.Link();
        link.source(rect);
        link.target(rect2);
        link.attr({
            line: {
                stroke: 'blue',
                strokeWidth: 1,
                sourceMarker: {
                    'type': 'path',
                    'stroke': 'black',
                    'fill': 'red',
                    'd': 'M 10 -5 0 0 10 5 Z'
                },
                targetMarker: {
                    'type': 'path',
                    'stroke': 'black',
                    'fill': 'yellow',
                    'd': 'M 10 -5 0 0 10 5 Z'
                }
            }
        });
        link.labels([{
            attrs: {
                text: {
                    text: 'Hello, World!'
                }
            }
        }]);
        link.addTo(graph);

        var rect3 = new joint.shapes.standard.Rectangle();
        rect3.position(100, 130);
        rect3.resize(100, 40);
        rect3.attr({
            body: {
                fill: '#E74C3C',
                rx: 20,
                ry: 20,
                strokeWidth: 0
            },
            label: {
                text: 'Hello',
                fill: '#ECF0F1',
                fontSize: 11,
                fontVariant: 'small-caps'
            }
        });
        rect3.addTo(graph);

        var rect4 = new joint.shapes.standard.Rectangle();
        rect4.position(400, 130);
        rect4.resize(100, 40);
        rect4.attr({
            body: {
                fill: '#8E44AD',
                strokeWidth: 0
            },
            label: {
                text: 'World!',
                fill: 'white',
                fontSize: 13
            }
        });
        rect4.addTo(graph);

        var link2 = new joint.shapes.standard.Link();
        link2.source(rect3);
        link2.target(rect4);
        link2.vertices([
            new g.Point(250, 100),
            new g.Point(300, 150),
            new g.Point(350, 200)
        ]);
        link2.router('orthogonal');
        link2.connector('rounded');
        link2.attr({
            line: {
                stroke: 'gray',
                strokeWidth: 4,
                strokeDasharray: '4 2',
                sourceMarker: {
                    'type': 'image',
                    'xlink:href': 'https://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png',
                    'width': 24,
                    'height': 24,
                    'y': -12
                },
                targetMarker: {
                    'type': 'image',
                    'xlink:href': 'https://cdn3.iconfinder.com/data/icons/49handdrawing/24x24/left.png',
                    'width': 24,
                    'height': 24,
                    'y': -12
                }
            }
        });
        link2.addTo(graph);

        var link3 = new joint.shapes.standard.Link();
        link3.source(rect3);
        link3.target(rect4);
        link3.connector('jumpover', { size: 10 });
        link3.addTo(graph);

        var rect5 = new joint.shapes.standard.Rectangle();
        rect5.position(100, 230);
        rect5.resize(100, 40);
        rect5.attr({
            body: {
                fill: '#2ECC71',
                strokeDasharray: '10,2'
            },
            label: {
                text: 'Hello',
                fill: 'black',
                fontSize: 13
            }
        });
        rect5.addTo(graph);

        var rect6 = new joint.shapes.standard.Rectangle();
        rect6.position(400, 230);
        rect6.resize(100, 40);
        rect6.attr({
            body: {
                fill: '#F39C12',
                rx: 20,
                ry: 20,
                strokeDasharray: '1,1'
            },
            label: {
                text: 'World!',
                fill: 'gray',
                fontSize: 18,
                fontWeight: 'bold',
                fontVariant: 'small-caps',
                textShadow: '1px 1px 1px black'
            }
        });
        rect6.addTo(graph);

        var link4 = new joint.shapes.standard.Link();
        link4.source(rect5);
        link4.target(rect6);
        link4.attr({
            line: {
                stroke: '#3498DB',
                strokeWidth: 3,
                strokeDasharray: '5 5',
                strokeDashoffset: 7.5,
                sourceMarker: {
                    'type': 'path',
                    'stroke': 'none',
                    'fill': '#3498DB',
                    'd': 'M 20 -10 0 0 20 10 Z \
                        M 40 -10 20 0 40 10 Z'
                },
                targetMarker: {
                    'type': 'path',
                    'stroke': 'none',
                    'fill': '#3498DB',
                    'd': 'M 7.5 -10 2.5 -10 2.5 10 7.5 10 Z \
                        M 17.5 -10 12.5 -10 12.5 10 17.5 10 Z \
                        M 40 -10 20 0 40 10 Z'
                }
            }
        });
        link4.addTo(graph);

    </script>
</body>
</html>

JointJS source code: links.js

This concludes the introductory section of the JointJS tutorial! Congratulations! We have come a long way from the initial Hello, World! application:

Now that you have seen the basics, you should be confident working with the most important building blocks of JointJS - Graphs, Papers, Elements, and Links.

The next step is to head over to the intermediate section of the tutorial, which explains some handy but more involved concepts.