Special Attributes

This is the first article of the intermediate section of the JointJS tutorial. See index of basic and intermediate articles.

Special attributes are JointJS-specific attributes that offer functionality beyond that of native SVG attributes. We have already mentioned them when talking about element styling, and seen them in action when adding link arrowheads. They become much more important when we talk about creating custom elements, links and link labels.

The main way to define the styling of diagram elements, links, and labels in JointJS is through attrs objects. If the attributes passed are standard SVG attributes, they are merely passed down to the individual SVGElements of the shape; then it is the job of the browser to apply them to the elements and render the shapes in the requested manner when asked to do so by JointJS View classes. However, if JointJS encounters one of its special attributes, it takes over with custom logic in order to offer advanced functionality; the results are then encoded back as standard SVG attributes.

All JointJS special attributes use camelCase naming. For consistency, it is thus very strongly recommended that you make use JointJS's ability to translate camelCase into native SVG's kebab-case, and use camelCase when setting native SVG attributes as well (i.e. strokeWidth instead of native 'stroke-width').

A list of native SVG attributes can be found elsewhere on the Internet; have a look, for example, at MDN's SVG Attribute reference. In this section of the tutorial, we want to show you what additional things JointJS allows you to do.

Relative Dimensions

One of the most common requests when working with SVG is to set the dimensions of SVGElements relatively. JointJS provides a calc() function which lets you perform calculations when specifying SVG attributes values. This allows you to size subelements relative to the size of the shape's model. You can view the documentation for calc() in our attributes section here. Moreover, since all the calculations are programmatic and do not rely on the browser's bbox measurements, using this function does not impact performance of your app. (There is also a view-based method if you need to work with text; it is explained here in more detail.)

As calc() works with normal SVG attributes, you can perform relative sizing easily using the SVG attributes you are familiar with. For example, 'calc(w)' and 'calc(h)' performs calculations using the shape model width and height respectively. If we wanted to set the x coordinate of the top-left corner of a subelement relative to the top-left corner of the shape model bbox, we could use x: 'calc(w)' on the subelement.

JointJS also contains a suite of ref attributes to work with relative dimensions. However, we now recommend using calc() for relative sizing.

Ref attributes

Let's see relative sizing with calc() in action. We define a custom element type named CustomElement as a subtype of joint.dia.Element. We want it to have three SVGElements - a red-tinted ellipse named e, a green-tinted rect named r, and a blue-tinted circle named c, respectively. The outline SVGRectElement shows us the reference bbox of the element model. In the example, we use JointJS transitions to vary the dimensions of element from (40,40) to (270,100). (We also adjust position to make sure the element stays in the visible area of our paper.) Notice that the subelements of the shape adjust automatically as the size of the reference model changes:

var CustomElement = joint.dia.Element.define('examples.CustomElement', {
    attrs: {
        e: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(255,0,0,0.3)'
        },
        r: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(0,255,0,0.3)'
        },
        c: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(0,0,255,0.3)'
        },
        outline: {
            x: 0,
            y: 0,
            width: 'calc(w)',
            height: 'calc(h)',
            strokeWidth: 1,
            stroke: '#000000',
            strokeDasharray: '5 5',
            strokeDashoffset: 2.5,
            fill: 'none'
        }
    }
}, {
    markup: [{
        tagName: 'ellipse',
        selector: 'e'
    }, {
        tagName: 'rect',
        selector: 'r'
    }, {
        tagName: 'circle',
        selector: 'c'
    }, {
        tagName: 'rect',
        selector: 'outline'
    }]
});

var element = new CustomElement();
element.attr({
    e: {
        rx: 'calc(0.5*w)',
        ry: 'calc(0.25*h)',
        cx: 0,
        cy: 'calc(0.25*h)'
    },
    r: {
        // additional x offset
        x: 'calc(w-10)',
        // additional y offset
        y: 'calc(h-10)',
        width: 'calc(0.5*w)',
        height: 'calc(0.5*h)'
    },
    c: {
        r: 'calc(0.5*d)',
        cx: 'calc(0.5*w)',
        cy: 'calc(0.5*h)'
    }
});

JointJS source code: special-attributes-relative-dimensions.js

Relative Dimensions Based on Text

An advanced application of relative sizing is setting the dimensions of shape subelements based on the dimensions of bboxes of rendered JointJS views. This is especially valuable when you need to base the position and size of shape components on <text> subelements since JointJS is not able to work with them programmatically. Note that because this method relies on browser measurements, it is noticeably slower and less precise than the model-based method mentioned above; you should use that method for subelements that can be modeled by JointJS.

The key is the ref special attribute:

We define a custom element type named CustomTextElement as a subtype of joint.dia.Element. It is very similar to CustomElement defined above, except this time, all subelements refer to a new text component named label. In the example, we use JointJS transitions to vary the text content of label. Notice that the subelements of the shape adjust automatically as the size of label changes due to the added characters:

var CustomTextElement = joint.dia.Element.define('examples.CustomTextElement', {
    attrs: {
        label: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fontSize: 48
        },
        e: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(255,0,0,0.3)'
        },
        r: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(0,255,0,0.3)'
        },
        c: {
            strokeWidth: 1,
            stroke: '#000000',
            fill: 'rgba(0,0,255,0.3)'
        },
        outline: {
            ref: 'label',
            x: '-calc(0.5*w)',
            y: '-calc(0.5*h)',
            width: 'calc(w)',
            height: 'calc(h)',
            strokeWidth: 1,
            stroke: '#000000',
            strokeDasharray: '5 5',
            strokeDashoffset: 2.5,
            fill: 'none'
        }
    }
}, {
    markup: [{
        tagName: 'ellipse',
        selector: 'e'
    }, {
        tagName: 'rect',
        selector: 'r'
    }, {
        tagName: 'circle',
        selector: 'c'
    }, {
        tagName: 'text',
        selector: 'label'
    }, {
        tagName: 'rect',
        selector: 'outline'
    }]
});

var element = new CustomTextElement();
element.attr({
    label: {
        text: 'Hello, World!'
    },
    e: {
        ref: 'label',
        rx: 'calc(0.5*w)',
        ry: 'calc(0.25*h)',
        cx: '-calc(0.5*w)',
        cy: '-calc(0.25*h)'
    },
    r: {
        ref: 'label',
        // additional x offset
        x: 'calc(0.5*w-10)',
        // additional y offset
        y: 'calc(0.5*h-10)',
        width: 'calc(0.5*w)',
        height: 'calc(0.5*h)'
    },
    c: {
        ref: 'label',
        r: 'calc(0.5*d)'
        // c is already centered at label anchor
    }
});

JointJS source code: special-attributes-text-relative-dimensions.js

Two special attributes are in charge of link arrowheads. We already mentioned them in the basic tutorial:

Link arrowhead special attributes expect an object with native SVG attributes. This means that they are not able to understand JointJS special attributes, and they cannot make use of JointJS's camelCase translation. In order to better communicate these restrictions to programmers, we strongly encourage the use of quotation marks around all arrowhead marker properties (i.e. 'type' and not type; 'stroke-width' and not strokeWidth).

The 'type' of the arrowhead can be any valid SVGElement type. The following example shows how to create a link with two simple <path> arrowheads.

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. This means that you can design all your arrowheads as if they were pointing left and towards the point 0,0 in local coordinates, and then rely on JointJS's automatic arrowhead rotation.

Note that, in general, if the 'fill' and 'stroke' colors are not specified, they are adopted from the line.stroke attribute.

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

To create an <image> arrowhead, you need to provide an URL with the path to your image to the 'xlink:href' property, and then specify the desired 'width' and 'height'. Keep in mind that both of your markers should reference images pointing left; the image for targetMarker will be automatically rotated by 180 degrees by JointJS, and then both markers will match the gradient of the link path. Remember to reposition the image in the 'y' axis (by -1/2 the value of 'height') if you need the arrowhead to be centered.

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

To create an arrowhead of arbitrary SVGElement type, simply specify the 'type' appropriately, and then provide native SVG attributes to style it. Keep in mind that 'circle' and 'ellipse' SVG elements have origin in their center; they need to be repositioned with the 'cx' attribute (by the value of 'r') if you do not want them to overflow the end of the link.

link.attr({
    line: {
        sourceMarker: {
            'type': 'rect',
            'width': 50,
            'height': 10,
            'y': -5,
            'fill': 'rgba(255,0,0,0.3)',
            'stroke': 'black'
        },
        targetMarker: {
            'type': 'circle',
            'r': 10,
            'cx': 10,
            'fill': 'rgba(0,255,0,0.3)',
            'stroke': 'black'
        }
    }
});

JointJS source code: special-attributes-link-arrowheads.js

Several special attributes on links allow you to position subelements relatively to the link's connection path:

These attributes are ideal for adding symbols and arrowheads on the connection path and have them rotate according to path slope. Let us illustrate with a custom link type:

var CustomLink = joint.dia.Link.define('examples.CustomLink', {
    attrs: {
        line: {
            connection: true,
            fill: 'none',
            stroke: 'orange',
            strokeWidth: 2,
            sourceMarker: {
                'type': 'circle',
                'r': 4,
                'fill': 'white',
                'stroke': 'orange',
                'stroke-width': '2'
            },
            targetMarker: {
                'type': 'circle',
                'r': 4,
                'fill': 'white',
                'stroke': 'orange',
                'stroke-width': '2'
            }
        },
        arrowhead: {
            d: 'M -20 -10 0 0 -20 10 Z',
            fill: 'orange',
            stroke: 'none'
        },
        symbol: {
            d: 'M -20 -20 20 20',
            stroke: 'black',
            targetMarker: {
                'type': 'path',
                'd': 'M 0 0 10 -5 10 5 Z',
                'fill': 'black',
                'stroke': 'none'
            }
        }
    }
}, {
    markup: [{
        tagName: 'path',
        selector: 'line'
    }, {
        tagName: 'path',
        selector: 'arrowhead'
    }, {
        tagName: 'path',
        selector: 'symbol'
    }]
});

var link = new CustomLink();
link.attr({
    symbol: {
        atConnectionRatio: 0.25
    },
    arrowhead: {
        atConnectionRatio: 0.75,
    }
});

JointJS source code: special-attributes-link-relative-position.js

Special attributes also allow us to create custom labeling subelements - both those that adjust their rotation according to the underlying link path and those that always keep the same angle. However, keep in mind that link subelements are more difficult to set up and use dynamically than link labels (JointJS does not come with a built-in API to do this), and that they have a very important limitation when used this way:

For this reason, we recommend everyone to use the link label API.

Link subelement labels that do not keep link connection gradient are enabled with the following two special attributes:

(The previously introduced atConnectionLength/atConnectionRatio attributes are actually aliases for more verbose names atConnectionLengthKeepGradient/atConnectionRatioKeepGradient.) In both cases (that is, whether we keep link gradient or not), offsetting of link subelement labels is achieved by clever use of the native x and y SVG attributes.

Let us illustrate with the following demo. A custom link type is defined with link label subelements positioned in such a way as to emulate the functionality shown in the link label position and offset examples. The red asterisk marks the reference point of the offset subelements on the link connection path.

var CustomLink = joint.dia.Link.define('examples.CustomLink', {
    attrs: {
        line: {
            connection: true,
            fill: 'none',
            stroke: '#333333',
            strokeWidth: 2,
            strokeLinejoin: 'round',
            targetMarker: {
                'type': 'path',
                'd': 'M 10 -5 0 0 10 5 z'
            }
        },
        relativeLabel: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        relativeLabelBody: {
            x: -15,
            y: -10,
            width: 30,
            height: 20,
            fill: 'white',
            stroke: 'black'
        },
        absoluteLabel: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        absoluteLabelBody: {
            x: -15,
            y: -10,
            width: 30,
            height: 20,
            fill: 'white',
            stroke: 'black'
        },
        absoluteReverseLabel: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        absoluteReverseLabelBody: {
            x: -15,
            y: -10,
            width: 30,
            height: 20,
            fill: 'white',
            stroke: 'black'
        },
        offsetLabelPositive: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        offsetLabelPositiveBody: {
            width: 120,
            height: 20,
            fill: 'white',
            stroke: 'black'
        },
        offsetLabelNegative: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        offsetLabelNegativeBody: {
            width: 120,
            height: 20,
            fill: 'white',
            stroke: 'black'
        },
        offsetLabelAbsolute: {
            textAnchor: 'middle',
            textVerticalAnchor: 'middle',
            fill: 'black',
            fontSize: 12
        },
        offsetLabelAbsoluteBody: {
            width: 140,
            height: 20,
            fill: 'white',
            stroke: 'black'
        }
    }
}, {
    markup: [{
        tagName: 'path',
        selector: 'line'
    }, {
        tagName: 'rect',
        selector: 'relativeLabelBody'
    }, {
        tagName: 'text',
        selector: 'relativeLabel'
    }, {
        tagName: 'rect',
        selector: 'absoluteLabelBody'
    }, {
        tagName: 'text',
        selector: 'absoluteLabel'
    }, {
        tagName: 'rect',
        selector: 'absoluteReverseLabelBody'
    }, {
        tagName: 'text',
        selector: 'absoluteReverseLabel'
    }, {
        tagName: 'rect',
        selector: 'offsetLabelPositiveBody'
    }, {
        tagName: 'text',
        selector: 'offsetLabelPositive'
    }, {
        tagName: 'rect',
        selector: 'offsetLabelNegativeBody'
    }, {
        tagName: 'text',
        selector: 'offsetLabelNegative'
    }, {
        tagName: 'rect',
        selector: 'offsetLabelAbsoluteBody'
    }, {
        tagName: 'text',
        selector: 'offsetLabelAbsolute'
    }]
});

var link = new CustomLink();
link.attr({
    relativeLabel: {
        atConnectionRatio: 0.25,
        text: '0.25'
    },
    relativeLabelBody: {
        atConnectionRatio: 0.25
    },
    absoluteLabel: {
        atConnectionLength: 150,
        text: '150'
    },
    absoluteLabelBody: {
        atConnectionLength: 150
    },
    absoluteReverseLabel: {
        atConnectionLength: -100,
        text: '-100'
    },
    absoluteReverseLabelBody: {
        atConnectionLength: -100
    },
    offsetLabelPositive: {
        atConnectionRatio: 0.66,
        y: 40,
        text: 'keepGradient: 0,40'
    },
    offsetLabelPositiveBody: {
        atConnectionRatio: 0.66,
        x: -60, // 0 + -60
        y: 30 // 40 + -10
    },
    offsetLabelNegative: {
        atConnectionRatio: 0.66,
        y: -40,
        text: 'keepGradient: 0,-40'
    },
    offsetLabelNegativeBody: {
        atConnectionRatio: 0.66,
        x: -60, // 0 + -60
        y: -50 // -40 + -10
    },
    offsetLabelAbsolute: {
        atConnectionRatioIgnoreGradient: 0.66,
        x: -40,
        y: 80,
        text: 'ignoreGradient: -40,80'
    },
    offsetLabelAbsoluteBody: {
        atConnectionRatioIgnoreGradient: 0.66,
        x: -110, // -40 + -70
        y: 70 // 80 + -10
    }
});

JointJS source code: special-attributes-link-subelement-labels.js

In the next section of the intermediate tutorial, we will find out how to make use of JointJS events to support user interaction.